Compare commits
221 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31a22327f1 | ||
|
|
c1712bebc6 | ||
|
|
cd91541245 | ||
|
|
4c1fc83256 | ||
|
|
34c7a33576 | ||
|
|
23ecfeeb4f | ||
|
|
9703f83eb3 | ||
|
|
0f3cc03d00 | ||
|
|
6f7ba1f9fc | ||
|
|
e1b85e4a1b | ||
|
|
b308dd58cc | ||
|
|
9f4c0479ce | ||
|
|
2c57817dde | ||
|
|
ba85c54d7c | ||
|
|
a80e5c3a65 | ||
|
|
22e2c34da8 | ||
|
|
00a8e4c2c5 | ||
|
|
10d0a4079c | ||
|
|
589f7f3c22 | ||
|
|
d1126a7eb0 | ||
|
|
9f4e72a0e1 | ||
|
|
a024295379 | ||
|
|
dc2b2ec488 | ||
|
|
0c5f5975aa | ||
|
|
dc3f682d2d | ||
|
|
2db8876c66 | ||
|
|
8f6201b0f7 | ||
|
|
4b146c70ad | ||
|
|
0118034b4b | ||
|
|
39217053ca | ||
|
|
fba190c826 | ||
|
|
5e9d528e16 | ||
|
|
c5921d88fc | ||
|
|
eb980b0ea1 | ||
|
|
de5b4216f7 | ||
|
|
495ff57b19 | ||
|
|
57948cf6e3 | ||
|
|
1aebbbcabd | ||
|
|
25b4cb072d | ||
|
|
1cdacc3a08 | ||
|
|
34d9466d09 | ||
|
|
c182c4ce66 | ||
|
|
dbb9bd1282 | ||
|
|
8019d2d6cc | ||
|
|
459cdb2e0b | ||
|
|
a230cd9513 | ||
|
|
0c44a25e85 | ||
|
|
34f3d04370 | ||
|
|
1f3e6b7e16 | ||
|
|
47d49a200a | ||
|
|
e1767d6e52 | ||
|
|
0f8e343cd2 | ||
|
|
23ab487baf | ||
|
|
22e5d38ef5 | ||
|
|
5819ccb528 | ||
|
|
42a2fd77cf | ||
|
|
ab93a8b0b3 | ||
|
|
84437eafa6 | ||
|
|
0107d848e0 | ||
|
|
5eeac96a0d | ||
|
|
9351c115be | ||
|
|
f95a11096c | ||
|
|
4203d179e6 | ||
|
|
78dfc9cb1c | ||
|
|
0bef307d77 | ||
|
|
b0da806f7a | ||
|
|
badecd1d81 | ||
|
|
6418e8ee30 | ||
|
|
09115c9658 | ||
|
|
74e3866bd7 | ||
|
|
408de78c13 | ||
|
|
c0451c18b3 | ||
|
|
f303d26c1e | ||
|
|
1b58a34859 | ||
|
|
82ea416e67 | ||
|
|
efd4fbad70 | ||
|
|
01bd15121b | ||
|
|
a9c2495349 | ||
|
|
e7c50b50ed | ||
|
|
6e25b289d2 | ||
|
|
157267eaf7 | ||
|
|
a317f9137a | ||
|
|
5dad3d22ea | ||
|
|
be85df456b | ||
|
|
2e172a08c7 | ||
|
|
bb1069ca60 | ||
|
|
d8141a1628 | ||
|
|
de9f7c4baf | ||
|
|
fa9b3116f1 | ||
|
|
dcf9d52961 | ||
|
|
1da93e2cc7 | ||
|
|
1d1bab988e | ||
|
|
dcc6ad3af3 | ||
|
|
d57f266789 | ||
|
|
c3395e1eff | ||
|
|
ca59ec2dbe | ||
|
|
79788125f3 | ||
|
|
2154f20fa4 | ||
|
|
afe40b6a89 | ||
|
|
ba4b3bd6b8 | ||
|
|
e423b5d745 | ||
|
|
6de8eca7ea | ||
|
|
9d68cfcaf0 | ||
|
|
225de11e6a | ||
|
|
916581bbd0 | ||
|
|
1cbb35840f | ||
|
|
7a1d769e39 | ||
|
|
8254bf934c | ||
|
|
5e2f20542f | ||
|
|
551a707ee4 | ||
|
|
024b15b4f9 | ||
|
|
1935df4143 | ||
|
|
3f99f90076 | ||
|
|
53cb445dde | ||
|
|
6e46947220 | ||
|
|
9b65e1671b | ||
|
|
d5c741db35 | ||
|
|
11e0780b6e | ||
|
|
f153541570 | ||
|
|
f066af88e7 | ||
|
|
97e1eef799 | ||
|
|
1bcd902817 | ||
|
|
2484568b21 | ||
|
|
085cc47ea5 | ||
|
|
aac36a88f3 | ||
|
|
1f2ebc82b7 | ||
|
|
9781949064 | ||
|
|
b06ef3781a | ||
|
|
b32213cb7b | ||
|
|
ac4c7d2421 | ||
|
|
824a49b80f | ||
|
|
13efd50d80 | ||
|
|
6fb091d20f | ||
|
|
518ab85cae | ||
|
|
f5124ad8b5 | ||
|
|
6f80900aa8 | ||
|
|
06b80e9281 | ||
|
|
51b39d9365 | ||
|
|
f7d2d8fc95 | ||
|
|
f34fb94c1a | ||
|
|
3107224e50 | ||
|
|
e1c481c534 | ||
|
|
945a2dd3eb | ||
|
|
e318945eb1 | ||
|
|
926709568d | ||
|
|
da040e799c | ||
|
|
ad07d2b8b1 | ||
|
|
c5aef78085 | ||
|
|
522ebc8aa2 | ||
|
|
199b3761b5 | ||
|
|
daf7b45e45 | ||
|
|
fc71b953c7 | ||
|
|
74ccd4a001 | ||
|
|
3c09f6ca55 | ||
|
|
c7dbe8599d | ||
|
|
ca036b6676 | ||
|
|
5ae87c8e03 | ||
|
|
9774fc4f65 | ||
|
|
d0891fb652 | ||
|
|
e388e2a1c7 | ||
|
|
d9ab354338 | ||
|
|
9ea0a0e214 | ||
|
|
131ddf0355 | ||
|
|
8abe71a0fe | ||
|
|
43e27a7e68 | ||
|
|
b784d277e4 | ||
|
|
cb443157da | ||
|
|
c886015d92 | ||
|
|
3161247da9 | ||
|
|
743a250131 | ||
|
|
4fb4b21a9e | ||
|
|
e1a7d3ebc5 | ||
|
|
72b8b156ac | ||
|
|
134dafb608 | ||
|
|
d5102b6422 | ||
|
|
a2cfb9d423 | ||
|
|
bef70f94ab | ||
|
|
4233fffe74 | ||
|
|
81c214748d | ||
|
|
c6a61dc8c8 | ||
|
|
483092d40d | ||
|
|
88cb02f6bc | ||
|
|
9628188730 | ||
|
|
2e66134bf8 | ||
|
|
424fe4d1e9 | ||
|
|
2b6f9568de | ||
|
|
4b270bce8b | ||
|
|
6b077e67db | ||
|
|
4c79ea0679 | ||
|
|
8c4c4c25aa | ||
|
|
a37dbe2c06 | ||
|
|
5e10cb2d16 | ||
|
|
58316c5c1d | ||
|
|
55f092f161 | ||
|
|
ab5049127a | ||
|
|
5f99067e56 | ||
|
|
3a89e61bd8 | ||
|
|
06edfa2fee | ||
|
|
77bfd72458 | ||
|
|
5ddf19be4d | ||
|
|
6657b47746 | ||
|
|
5690efb07a | ||
|
|
05600cba08 | ||
|
|
e3b4120b2c | ||
|
|
1059245fd9 | ||
|
|
87c8d310b3 | ||
|
|
fdc4999556 | ||
|
|
107ae70651 | ||
|
|
04de19033e | ||
|
|
089ac70cd3 | ||
|
|
ae40a9ead9 | ||
|
|
152806b7f6 | ||
|
|
06beb8e704 | ||
|
|
64f2b94685 | ||
|
|
5a42eb98ab | ||
|
|
7d4baa7046 | ||
|
|
a24eaaed50 | ||
|
|
26813c582f | ||
|
|
6067ac73e2 | ||
|
|
b1d94b67f4 | ||
|
|
452f4e69fd |
31
build/includer.js
Normal file
31
build/includer.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
//пример в коде:
|
||||
// @@include('./test/testFile.inc');
|
||||
|
||||
function includeRecursive(self, parentFile, source, depth) {
|
||||
depth = (depth ? depth : 0);
|
||||
if (depth > 50)
|
||||
throw new Error('includer: stack too big');
|
||||
const lines = source.split('\n');
|
||||
let result = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
const m = trimmed.match(/^@@[\s]*?include[\s]*?\(['"](.*)['"]\)/);
|
||||
if (m) {
|
||||
const includedFile = path.resolve(path.dirname(parentFile), m[1]);
|
||||
self.addDependency(includedFile);
|
||||
|
||||
const fileContent = fs.readFileSync(includedFile, 'utf8');
|
||||
result = result.concat(includeRecursive(self, includedFile, fileContent, depth + 1));
|
||||
} else {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.default = function includer(source) {
|
||||
return includeRecursive(this, this.resourcePath, source).join('\n');
|
||||
}
|
||||
@@ -16,6 +16,11 @@ module.exports = {
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader"
|
||||
},
|
||||
{
|
||||
test: /\.includer$/,
|
||||
resourceQuery: /^\?vue/,
|
||||
use: path.resolve('build/includer.js')
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
|
||||
@@ -9,7 +9,7 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const CleanWebpackPlugin = require('clean-webpack-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
const AppCachePlugin = require('appcache-webpack-plugin');
|
||||
const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin');
|
||||
|
||||
const publicDir = path.resolve(__dirname, '../dist/tmp/public');
|
||||
const clientDir = path.resolve(__dirname, '../client');
|
||||
@@ -55,6 +55,12 @@ module.exports = merge(baseWpConfig, {
|
||||
filename: `${publicDir}/index.html`
|
||||
}),
|
||||
new CopyWebpackPlugin([{from: `${clientDir}/assets/*`, to: `${publicDir}/`, flatten: true}]),
|
||||
new AppCachePlugin({exclude: ['../index.html']})
|
||||
new SWPrecacheWebpackPlugin({
|
||||
cacheId: 'liberama',
|
||||
filepath: `${publicDir}/service-worker.js`,
|
||||
minify: true,
|
||||
navigateFallback: '/index.html',
|
||||
stripPrefix: publicDir,
|
||||
}),
|
||||
]
|
||||
});
|
||||
|
||||
@@ -14,7 +14,10 @@ class Misc {
|
||||
|
||||
try {
|
||||
await wsc.open();
|
||||
return await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||
if (config.error)
|
||||
throw new Error(config.error);
|
||||
return config;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -18,17 +18,22 @@ class Reader {
|
||||
if (!callback) callback = () => {};
|
||||
|
||||
let response = {};
|
||||
|
||||
try {
|
||||
await wsc.open();
|
||||
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
|
||||
|
||||
let prevResponse = false;
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
response = await wsc.message(requestId);
|
||||
callback(response);
|
||||
|
||||
if (!response.state)
|
||||
throw new Error('Неверный ответ api');
|
||||
if (!response.state && prevResponse !== false) {//экономия траффика
|
||||
callback(prevResponse);
|
||||
} else {//были изменения worker state
|
||||
if (!response.state)
|
||||
throw new Error('Неверный ответ api');
|
||||
callback(response);
|
||||
prevResponse = response;
|
||||
}
|
||||
|
||||
if (response.state == 'finish' || response.state == 'error') {
|
||||
break;
|
||||
@@ -127,6 +132,9 @@ class Reader {
|
||||
response = await api.post('/restore-cached-file', {path: url});
|
||||
response = response.data;
|
||||
}
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
const workerId = response.workerId;
|
||||
if (!workerId)
|
||||
@@ -215,6 +223,9 @@ class Reader {
|
||||
const state = response.state;
|
||||
if (!state)
|
||||
throw new Error('Неверный ответ api');
|
||||
if (response.state == 'error') {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -111,11 +111,7 @@ class WebSocketConnection {
|
||||
requestId,
|
||||
timeout,
|
||||
onMessage: (mes) => {
|
||||
if (mes.error) {
|
||||
reject(mes.error);
|
||||
} else {
|
||||
resolve(mes);
|
||||
}
|
||||
resolve(mes);
|
||||
},
|
||||
onError: (e) => {
|
||||
reject(e);
|
||||
|
||||
5
client/assets/sw-register.js
Normal file
5
client/assets/sw-register.js
Normal file
@@ -0,0 +1,5 @@
|
||||
(function() {
|
||||
if('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
}
|
||||
})();
|
||||
@@ -1,9 +1,19 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-aside v-if="showAsideBar" :width="asideWidth">
|
||||
<!--q-layout view="lhr lpr lfr">
|
||||
<q-drawer v-model="showAsideBar" :width="asideWidth">
|
||||
<div class="app-name"><span v-html="appName"></span></div>
|
||||
<el-button class="el-button-collapse" @click="toggleCollapse" :icon="buttonCollapseIcon"></el-button>
|
||||
<el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
|
||||
<q-btn class="el-button-collapse" @click="toggleCollapse"></q-btn>
|
||||
|
||||
<q-list>
|
||||
<q-item clickable v-ripple>
|
||||
<q-item-section avatar>
|
||||
<q-icon name="inbox" />
|
||||
</q-item-section>
|
||||
|
||||
<q-item-section>Inbox</q-item-section>
|
||||
</q-item>
|
||||
</q-list-->
|
||||
<!--el-menu class="el-menu-vertical" :default-active="rootRoute" :collapse="isCollapse" router>
|
||||
<el-menu-item index="/cardindex">
|
||||
<i class="el-icon-search"></i>
|
||||
<span :class="itemTitleClass('/cardindex')" slot="title">{{ this.itemRuText['/cardindex'] }}</span>
|
||||
@@ -32,24 +42,37 @@
|
||||
<i class="el-icon-question"></i>
|
||||
<span :class="itemTitleClass('/help')" slot="title">{{ this.itemRuText['/help'] }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
</el-menu-->
|
||||
<!--/q-drawer>
|
||||
|
||||
<el-main v-if="showMain" :style="{padding: (isReaderActive ? 0 : '5px')}">
|
||||
<q-page-container>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</q-page-container>
|
||||
</q-layout-->
|
||||
<div class="fit row">
|
||||
<Notify ref="notify"/>
|
||||
<StdDialog ref="stdDialog"/>
|
||||
<keep-alive>
|
||||
<router-view class="col"></router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import Notify from './share/Notify.vue';
|
||||
import StdDialog from './share/StdDialog.vue';
|
||||
import * as utils from '../share/utils';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Notify,
|
||||
StdDialog,
|
||||
},
|
||||
watch: {
|
||||
mode: function() {
|
||||
this.setAppTitle();
|
||||
@@ -75,6 +98,18 @@ class App extends Vue {
|
||||
this.uistate = this.$store.state.uistate;
|
||||
this.config = this.$store.state.config;
|
||||
|
||||
//root route
|
||||
let cachedRoute = '';
|
||||
let cachedPath = '';
|
||||
this.$root.rootRoute = () => {
|
||||
if (this.$route.path != cachedPath) {
|
||||
cachedPath = this.$route.path;
|
||||
const m = cachedPath.match(/^(\/[^/]*).*$/i);
|
||||
cachedRoute = (m ? m[1] : this.$route.path);
|
||||
}
|
||||
return cachedRoute;
|
||||
}
|
||||
|
||||
// set-app-title
|
||||
this.$root.$on('set-app-title', this.setAppTitle);
|
||||
|
||||
@@ -108,17 +143,16 @@ class App extends Vue {
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.$root.notify = this.$refs.notify;
|
||||
this.$root.stdDialog = this.$refs.stdDialog;
|
||||
|
||||
this.dispatch('config/loadConfig');
|
||||
this.$watch('apiError', function(newError) {
|
||||
if (newError) {
|
||||
let mes = newError.message;
|
||||
if (newError.response && newError.response.config)
|
||||
mes = newError.response.config.url + '<br>' + newError.response.statusText;
|
||||
this.$notify.error({
|
||||
title: 'Ошибка API',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: mes
|
||||
});
|
||||
this.$root.notify.error(mes, 'Ошибка API');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,9 +171,9 @@ class App extends Vue {
|
||||
|
||||
get asideWidth() {
|
||||
if (this.uistate.asideBarCollapse) {
|
||||
return '64px';
|
||||
return 64;
|
||||
} else {
|
||||
return '170px';
|
||||
return 170;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +197,7 @@ class App extends Vue {
|
||||
}
|
||||
|
||||
get rootRoute() {
|
||||
const m = this.$route.path.match(/^(\/[^/]*).*$/i);
|
||||
this.$root.rootRoute = (m ? m[1] : this.$route.path);
|
||||
|
||||
return this.$root.rootRoute;
|
||||
return this.$root.rootRoute();
|
||||
}
|
||||
|
||||
setAppTitle(title) {
|
||||
@@ -193,12 +224,11 @@ class App extends Vue {
|
||||
return (this.mode !== null && this.mode != 'reader' && this.mode != 'omnireader');
|
||||
}
|
||||
|
||||
get isReaderActive() {
|
||||
return this.rootRoute == '/reader';
|
||||
set showAsideBar(value) {
|
||||
}
|
||||
|
||||
get showMain() {
|
||||
return (this.showAsideBar || this.isReaderActive);
|
||||
get isReaderActive() {
|
||||
return this.rootRoute == '/reader';
|
||||
}
|
||||
|
||||
redirectIfNeeded() {
|
||||
@@ -228,68 +258,28 @@ class App extends Vue {
|
||||
line-height: 140%;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.bold-font {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.el-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-aside {
|
||||
line-height: 1;
|
||||
background-color: #ccc;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: 0;
|
||||
background-color: #E6EDF4;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.el-menu-vertical:not(.el-menu--collapse) {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.el-menu--collapse {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.el-button-collapse, .el-button-collapse:focus, .el-button-collapse:active, .el-button-collapse:hover {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
height: 64px;
|
||||
border: 0;
|
||||
}
|
||||
.el-menu-item {
|
||||
font-size: 85%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
body, html, #app {
|
||||
body, html, #app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font: normal 12pt ReaderDefault;
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
flex: 1;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
.dborder {
|
||||
border: 2px solid yellow !important;
|
||||
}
|
||||
|
||||
.icon-rotate {
|
||||
vertical-align: middle;
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
.notify-button-icon {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Book в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Card в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<template>
|
||||
<el-container direction="vertical">
|
||||
<el-tabs type="border-card" style="height: 100%;" v-model="selectedTab">
|
||||
<el-tab-pane label="Поиск"></el-tab-pane>
|
||||
<el-tab-pane label="Автор"></el-tab-pane>
|
||||
<el-tab-pane label="Книга"></el-tab-pane>
|
||||
<el-tab-pane label="История"></el-tab-pane>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</el-tabs>
|
||||
</el-container>
|
||||
<div>
|
||||
<keep-alive>
|
||||
<router-view></router-view>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -18,7 +12,7 @@ import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import _ from 'lodash';
|
||||
|
||||
const rootRoute = '/cardindex';
|
||||
const selfRoute = '/cardindex';
|
||||
const tab2Route = [
|
||||
'/cardindex/search',
|
||||
'/cardindex/card',
|
||||
@@ -51,7 +45,7 @@ class CardIndex extends Vue {
|
||||
if (t !== this.selectedTab)
|
||||
this.selectedTab = t.toString();
|
||||
} else {
|
||||
if (route == rootRoute && lastActiveTab !== null)
|
||||
if (route == selfRoute && lastActiveTab !== null)
|
||||
this.setRouteByTab(lastActiveTab);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел History в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Search в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Help в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Income в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Страница не найдена
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -91,7 +91,7 @@ class CopyTextPage extends Vue {
|
||||
|
||||
close() {
|
||||
this.stopInit = true;
|
||||
this.$emit('copy-text-toggle');
|
||||
this.$emit('do-action', {action: 'copyText'});
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Возможности читалки:</h4>
|
||||
<span class="text-h6 text-bold">Возможности читалки:</span>
|
||||
<ul>
|
||||
<li>загрузка любой страницы интернета</li>
|
||||
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
||||
<li>работа в автономном режиме (без связи)</li>
|
||||
<li>изменение цвета фона, текста, размер и тип шрифта и прочее</li>
|
||||
<li>установка и запоминание текущей позиции и настроек в браузере и на сервере</li>
|
||||
<li>синхронизация данных (настроек и читаемых книг) между различными устройствами</li>
|
||||
<li>кэширование файлов книг на клиенте и на сервере</li>
|
||||
<li>открытие книг с локального диска</li>
|
||||
<li>плавный скроллинг текста</li>
|
||||
@@ -25,10 +25,10 @@
|
||||
<div v-show="mode == 'omnireader'">
|
||||
<p>Вы можете добавить в свой браузер закладку, указав в ее свойствах вместо адреса следующий код:
|
||||
<br><strong>javascript:location.href='https://omnireader.ru/?url='+location.href;</strong>
|
||||
|
||||
<span class="clickable" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||
(скопировать)
|
||||
</span>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyText('javascript:location.href=\'https://omnireader.ru/?url=\'+location.href;', 'Код для адреса закладки успешно скопирован в буфер обмена')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
|
||||
<br>или перетащив на панель закладок следующую ссылку:
|
||||
<br><a style="margin-left: 50px" href="javascript:location.href='https://omnireader.ru/?url='+location.href;">Omni Reader</a>
|
||||
<br>Тогда, активировав получившуюся закладку на любой странице интернета, вы автоматически загрузите эту страницу в Omni Reader.
|
||||
@@ -60,9 +60,9 @@ class CommonHelpPage extends Vue {
|
||||
const result = await copyTextToClipboard(text);
|
||||
const msg = (result ? mes : 'Копирование не удалось');
|
||||
if (result)
|
||||
this.$notify.success({message: msg});
|
||||
this.$root.notify.success(msg);
|
||||
else
|
||||
this.$notify.error({message: msg});
|
||||
this.$root.notify.error(msg);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -70,20 +70,16 @@ class CommonHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
.copy-icon {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,62 +4,47 @@
|
||||
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/yandex.png">
|
||||
<el-button class="button" @click="donateYandexMoney">Пожертвовать</el-button><br>
|
||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
|
||||
<div class="para">{{ yandexAddress }}
|
||||
<el-tooltip :open-delay="500" effect="light">
|
||||
<template slot="content">
|
||||
Скопировать
|
||||
</template>
|
||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(yandexAddress, 'Яндекс кошелек')"></i>
|
||||
</el-tooltip>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/paypal.png">
|
||||
<div class="para">{{ paypalAddress }}
|
||||
<el-tooltip :open-delay="500" effect="light">
|
||||
<template slot="content">
|
||||
Скопировать
|
||||
</template>
|
||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(paypalAddress, 'Paypal-адрес')"></i>
|
||||
</el-tooltip>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(paypalAddress, 'Paypal-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/bitcoin.png">
|
||||
<div class="para">{{ bitcoinAddress }}
|
||||
<el-tooltip :open-delay="500" effect="light">
|
||||
<template slot="content">
|
||||
Скопировать
|
||||
</template>
|
||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')"></i>
|
||||
</el-tooltip>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(bitcoinAddress, 'Bitcoin-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/litecoin.png">
|
||||
<div class="para">{{ litecoinAddress }}
|
||||
<el-tooltip :open-delay="500" effect="light">
|
||||
<template slot="content">
|
||||
Скопировать
|
||||
</template>
|
||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')"></i>
|
||||
</el-tooltip>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(litecoinAddress, 'Litecoin-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="address">
|
||||
<img class="logo" src="./assets/monero.png">
|
||||
<div class="para">{{ moneroAddress }}
|
||||
<el-tooltip :open-delay="500" effect="light">
|
||||
<template slot="content">
|
||||
Скопировать
|
||||
</template>
|
||||
<i class="el-icon-copy-document copy-icon" @click="copyAddress(moneroAddress, 'Monero-адрес')"></i>
|
||||
</el-tooltip>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(moneroAddress, 'Monero-адрес')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,9 +76,9 @@ class DonateHelpPage extends Vue {
|
||||
async copyAddress(address, prefix) {
|
||||
const result = await copyTextToClipboard(address);
|
||||
if (result)
|
||||
this.$notify.success({message: `${prefix} ${address} успешно скопирован в буфер обмена`});
|
||||
this.$root.notify.success(`${prefix} ${address} успешно скопирован в буфер обмена`);
|
||||
else
|
||||
this.$notify.error({message: 'Копирование не удалось'});
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
@@ -101,12 +86,10 @@ class DonateHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.p {
|
||||
@@ -116,15 +99,10 @@ class DonateHelpPage extends Vue {
|
||||
}
|
||||
|
||||
.box {
|
||||
flex: 1;
|
||||
max-width: 550px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.address {
|
||||
padding-top: 10px;
|
||||
margin-top: 20px;
|
||||
@@ -134,10 +112,6 @@ h5 {
|
||||
margin: 10px 10px 10px 40px;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 130px;
|
||||
position: relative;
|
||||
@@ -148,5 +122,6 @@ h5 {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,23 +4,20 @@
|
||||
Справка
|
||||
</template>
|
||||
|
||||
<el-tabs type="border-card" v-model="selectedTab">
|
||||
<el-tab-pane class="tab" label="Общее">
|
||||
<CommonHelpPage></CommonHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Клавиатура">
|
||||
<HotkeysHelpPage></HotkeysHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Мышь/тачскрин">
|
||||
<MouseHelpPage></MouseHelpPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="История версий" name="releases">
|
||||
<VersionHistoryPage></VersionHistoryPage>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Помочь проекту" name="donate">
|
||||
<DonateHelpPage></DonateHelpPage>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div class="col column" style="min-width: 600px">
|
||||
<q-btn-toggle
|
||||
v-model="selectedTab"
|
||||
toggle-color="primary"
|
||||
no-caps unelevated
|
||||
:options="buttons"
|
||||
/>
|
||||
<div class="separator"></div>
|
||||
|
||||
<keep-alive>
|
||||
<component ref="page" class="col" :is="activePage"
|
||||
></component>
|
||||
</keep-alive>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
@@ -33,32 +30,54 @@ import Window from '../../share/Window.vue';
|
||||
import CommonHelpPage from './CommonHelpPage/CommonHelpPage.vue';
|
||||
import HotkeysHelpPage from './HotkeysHelpPage/HotkeysHelpPage.vue';
|
||||
import MouseHelpPage from './MouseHelpPage/MouseHelpPage.vue';
|
||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
import VersionHistoryPage from './VersionHistoryPage/VersionHistoryPage.vue';
|
||||
import DonateHelpPage from './DonateHelpPage/DonateHelpPage.vue';
|
||||
|
||||
const pages = {
|
||||
'CommonHelpPage': CommonHelpPage,
|
||||
'HotkeysHelpPage': HotkeysHelpPage,
|
||||
'MouseHelpPage': MouseHelpPage,
|
||||
'VersionHistoryPage': VersionHistoryPage,
|
||||
'DonateHelpPage': DonateHelpPage,
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
['CommonHelpPage', 'Общее'],
|
||||
['MouseHelpPage', 'Мышь/тачскрин'],
|
||||
['HotkeysHelpPage', 'Клавиатура'],
|
||||
['VersionHistoryPage', 'История версий'],
|
||||
['DonateHelpPage', 'Помочь проекту'],
|
||||
];
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
Window,
|
||||
CommonHelpPage,
|
||||
HotkeysHelpPage,
|
||||
MouseHelpPage,
|
||||
DonateHelpPage,
|
||||
VersionHistoryPage,
|
||||
},
|
||||
components: Object.assign({ Window }, pages),
|
||||
})
|
||||
class HelpPage extends Vue {
|
||||
selectedTab = null;
|
||||
selectedTab = 'CommonHelpPage';
|
||||
|
||||
close() {
|
||||
this.$emit('help-toggle');
|
||||
this.$emit('do-action', {action: 'help'});
|
||||
}
|
||||
|
||||
get activePage() {
|
||||
if (pages[this.selectedTab])
|
||||
return pages[this.selectedTab];
|
||||
return null;
|
||||
}
|
||||
|
||||
get buttons() {
|
||||
let result = [];
|
||||
for (const tab of tabs)
|
||||
result.push({label: tab[1], value: tab[0]});
|
||||
return result;
|
||||
}
|
||||
|
||||
activateDonateHelpPage() {
|
||||
this.selectedTab = 'donate';
|
||||
this.selectedTab = 'DonateHelpPage';
|
||||
}
|
||||
|
||||
activateVersionHistoryHelpPage() {
|
||||
this.selectedTab = 'releases';
|
||||
this.selectedTab = 'VersionHistoryPage';
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
@@ -72,16 +91,8 @@ class HelpPage extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-tab-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
.separator {
|
||||
height: 1px;
|
||||
background-color: #E0E0E0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Управление с помощью горячих клавиш:</h4>
|
||||
<ul>
|
||||
<li><b>F1, H</b> - открыть справку</li>
|
||||
<li><b>Escape</b> - показать/скрыть страницу загрузки</li>
|
||||
<li><b>Tab, Q</b> - показать/скрыть панель управления</li>
|
||||
<li><b>PageUp, Left, Shift+Space, Backspace</b> - страницу назад</li>
|
||||
<li><b>PageDown, Right, Space</b> - страницу вперед</li>
|
||||
<li><b>Home</b> - в начало книги</li>
|
||||
<li><b>End</b> - в конец книги</li>
|
||||
<li><b>Up</b> - строчку назад</li>
|
||||
<li><b>Down</b> - строчку вперёд</li>
|
||||
<li><b>A, Shift+A</b> - изменить размер шрифта</li>
|
||||
<li><b>Enter, F, F11, ` (апостроф)</b> - вкл./выкл. полный экран</li>
|
||||
<li><b>Z</b> - вкл./выкл. плавный скроллинг текста</li>
|
||||
<li><b>Shift+Down/Shift+Up</b> - увеличить/уменьшить скорость скроллинга
|
||||
<li><b>P</b> - установить страницу</li>
|
||||
<li><b>Ctrl+F</b> - найти в тексте</li>
|
||||
<li><b>Ctrl+C</b> - скопировать текст со страницы</li>
|
||||
<li><b>R</b> - принудительно обновить книгу в обход кэша</li>
|
||||
<li><b>X</b> - открыть недавние</li>
|
||||
<li><b>O</b> - автономный режим</li>
|
||||
<li><b>S</b> - открыть окно настроек</li>
|
||||
</ul>
|
||||
<div style="font-size: 120%">
|
||||
<div class="text-h6 text-bold">Доступны следующие клавиатурные команды:</div>
|
||||
<br>
|
||||
</div>
|
||||
<div class="q-mb-md" style="width: 550px">
|
||||
<div class="text-right text-italic" style="font-size: 80%">* Изменить сочетания клавиш можно в настройках</div>
|
||||
<UserHotKeys v-model="userHotKeys" readonly/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -31,25 +16,32 @@
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import UserHotKeys from '../../SettingsPage/UserHotKeys/UserHotKeys.vue';
|
||||
|
||||
export default @Component({
|
||||
components: {
|
||||
UserHotKeys,
|
||||
},
|
||||
})
|
||||
class HotkeysHelpPage extends Vue {
|
||||
created() {
|
||||
}
|
||||
|
||||
get userHotKeys() {
|
||||
return this.$store.state.reader.settings.userHotKeys;
|
||||
}
|
||||
|
||||
set userHotKeys(value) {
|
||||
//no setter
|
||||
}
|
||||
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<h4>Управление с помощью мыши/тачскрина:</h4>
|
||||
<span class="text-h6 text-bold">Управление с помощью мыши/тачскрина:</span>
|
||||
<ul>
|
||||
<li><b>ЛКМ/ТАЧ</b> по экрану в одну из областей - активация действия:</li>
|
||||
<div class="click-map-page">
|
||||
@@ -49,17 +49,12 @@ class MouseHelpPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.click-map-page {
|
||||
position: relative;
|
||||
width: 400px;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<div id="versionHistoryPage" class="page">
|
||||
<span class="text-h6 text-bold">История версий:</span>
|
||||
<br><br>
|
||||
|
||||
<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">
|
||||
@@ -58,15 +59,11 @@ class VersionHistoryPage extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex: 1;
|
||||
padding: 15px;
|
||||
overflow-y: auto;
|
||||
font-size: 120%;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
svgStyle: {
|
||||
fill: this.cornerColor,
|
||||
@@ -60,8 +60,13 @@
|
||||
},
|
||||
flipped: false,
|
||||
svgPath1: 'M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z',
|
||||
svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
|
||||
svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
|
||||
svgPath2: 'M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 ' +
|
||||
'123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2',
|
||||
svgPath3: 'M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 ' +
|
||||
'C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 ' +
|
||||
'176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 ' +
|
||||
'216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 ' +
|
||||
'C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -99,7 +104,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
<style scoped>
|
||||
#github-corner .octo-arm {
|
||||
transform-origin: 130px 106px
|
||||
}
|
||||
@@ -122,7 +127,8 @@
|
||||
top: 0;
|
||||
border: 0;
|
||||
}
|
||||
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
|
||||
transition: fill 1s ease;
|
||||
}
|
||||
|
||||
#github-corner-svg, #github-corner-svg .octo-arm, #github-corner-svg .octo-body {
|
||||
transition: fill 1s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
<template>
|
||||
<div ref="main" class="main">
|
||||
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F"></GithubCorner>
|
||||
<div class="part top">
|
||||
<span class="greeting bold-font">{{ title }}</span>
|
||||
<div class="space"></div>
|
||||
<div ref="main" class="column no-wrap" style="min-height: 500px">
|
||||
<div class="relative-position">
|
||||
<GithubCorner url="https://github.com/bookpauk/liberama" cornerColor="#1B695F" gitColor="#EBE2C9"></GithubCorner>
|
||||
</div>
|
||||
<div class="col column justify-center items-center no-wrap overflow-hidden" style="min-height: 230px">
|
||||
<span class="greeting"><b>{{ title }}</b></span>
|
||||
<div class="q-my-sm"></div>
|
||||
<span class="greeting">Добро пожаловать!</span>
|
||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
||||
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
||||
</div>
|
||||
|
||||
<div class="part center">
|
||||
<el-input ref="input" placeholder="URL книги" v-model="bookUrl">
|
||||
<el-button slot="append" icon="el-icon-check" @click="submitUrl"></el-button>
|
||||
</el-input>
|
||||
<div class="space"></div>
|
||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||
<q-input ref="input" class="full-width q-px-sm" style="max-width: 700px" outlined dense bg-color="white" v-model="bookUrl" placeholder="URL книги">
|
||||
<template v-slot:append>
|
||||
<q-btn rounded flat style="width: 40px" icon="la la-check" @click="submitUrl"/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<input type="file" id="file" ref="file" @change="loadFile" style='display: none;'/>
|
||||
|
||||
<el-button size="mini" @click="loadFileClick">
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadFileClick">
|
||||
Загрузить файл с диска
|
||||
</el-button>
|
||||
<div class="space"></div>
|
||||
<el-button size="mini" @click="loadBufferClick">
|
||||
</q-btn>
|
||||
|
||||
<div class="q-my-sm"></div>
|
||||
<q-btn no-caps dense class="q-px-sm" color="primary" size="13px" @click="loadBufferClick">
|
||||
Из буфера обмена
|
||||
</el-button>
|
||||
</q-btn>
|
||||
|
||||
<div class="space"></div>
|
||||
<div class="space"></div>
|
||||
<div class="q-my-md"></div>
|
||||
<div v-if="mode == 'omnireader'">
|
||||
<div ref="yaShare2" class="ya-share2"
|
||||
data-services="collections,vkontakte,facebook,odnoklassniki,twitter,telegram"
|
||||
@@ -34,12 +39,12 @@
|
||||
data-url="https://omnireader.ru">
|
||||
</div>
|
||||
</div>
|
||||
<div class="space"></div>
|
||||
<div class="q-my-sm"></div>
|
||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openComments">Отзывы о читалке</span>
|
||||
<span v-if="mode == 'omnireader'" class="bottom-span clickable" @click="openOldVersion">Старая версия</span>
|
||||
</div>
|
||||
|
||||
<div class="part bottom">
|
||||
<div class="col column justify-end items-center no-wrap overflow-hidden">
|
||||
<span class="bottom-span clickable" @click="openHelp">Справка</span>
|
||||
<span class="bottom-span clickable" @click="openDonate">Помочь проекту</span>
|
||||
|
||||
@@ -143,12 +148,12 @@ class LoaderPage extends Vue {
|
||||
this.pasteTextActive = !this.pasteTextActive;
|
||||
}
|
||||
|
||||
openHelp() {
|
||||
this.$emit('help-toggle');
|
||||
openHelp(event) {
|
||||
this.$emit('do-action', {action: 'help', event});
|
||||
}
|
||||
|
||||
openDonate() {
|
||||
this.$emit('donate-toggle');
|
||||
this.$emit('do-action', {action: 'donate'});
|
||||
}
|
||||
|
||||
openComments() {
|
||||
@@ -168,80 +173,37 @@ class LoaderPage extends Vue {
|
||||
const input = this.$refs.input.$refs.input;
|
||||
if (document.activeElement === input && event.type == 'keydown' && event.code == 'Enter') {
|
||||
this.submitUrl();
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && (event.code == 'F1' || (document.activeElement !== input && event.code == 'KeyH'))) {
|
||||
this.$emit('help-toggle');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && (document.activeElement !== input && event.code == 'KeyQ')) {
|
||||
this.$emit('tool-bar-toggle');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
return true;
|
||||
if (event.type == 'keydown' && document.activeElement !== input) {
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
switch (action) {
|
||||
case 'help':
|
||||
this.openHelp(event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
.main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 480px;
|
||||
}
|
||||
|
||||
.part {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.greeting {
|
||||
font-size: 120%;
|
||||
line-height: 160%;
|
||||
}
|
||||
|
||||
.bold-font {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.top {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.center {
|
||||
justify-content: flex-start;
|
||||
padding: 0 10px 0 10px;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bottom-span {
|
||||
font-size: 70%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.el-input {
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.space {
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -3,14 +3,12 @@
|
||||
<template slot="header">
|
||||
<span style="position: relative; top: -3px">
|
||||
Вставьте текст и нажмите
|
||||
<span class="clickable" style="font-size: 150%; position: relative; top: 1px" @click="loadBuffer">загрузить</span>
|
||||
<span class="clickable text-primary" 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>
|
||||
<q-input class="q-px-sm" dense borderless v-model="bookTitle" placeholder="Введите название текста"/>
|
||||
<hr/>
|
||||
<textarea ref="textArea" class="text" @paste="calcTitle"></textarea>
|
||||
</Window>
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
<template>
|
||||
<div v-show="visible" class="main">
|
||||
<div class="center">
|
||||
<el-progress type="circle" :width="100" :stroke-width="6" color="#0F9900" :percentage="percentage"></el-progress>
|
||||
<p class="text">{{ text }}</p>
|
||||
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
|
||||
<div class="column justify-start items-center" style="height: 250px">
|
||||
<q-circular-progress
|
||||
show-value
|
||||
instant-feedback
|
||||
font-size="13px"
|
||||
:value="percentage"
|
||||
size="100px"
|
||||
:thickness="0.11"
|
||||
color="green-7"
|
||||
track-color="grey-4"
|
||||
class="q-ma-md"
|
||||
>
|
||||
<span class="text-yellow">{{ percentage }}%</span>
|
||||
</q-circular-progress>
|
||||
|
||||
<div>
|
||||
<span class="text-yellow">{{ text }}</span>
|
||||
<q-icon :style="iconStyle" color="yellow" name="la la-slash" size="20px"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,6 +27,7 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import * as utils from '../../../share/utils';
|
||||
|
||||
const ruMessage = {
|
||||
'start': ' ',
|
||||
@@ -33,14 +50,15 @@ class ProgressPage extends Vue {
|
||||
step = 1;
|
||||
progress = 0;
|
||||
visible = false;
|
||||
iconStyle = '';
|
||||
|
||||
show() {
|
||||
this.$el.style.width = this.$parent.$el.offsetWidth + 'px';
|
||||
this.$el.style.height = this.$parent.$el.offsetHeight + 'px';
|
||||
this.text = '';
|
||||
this.totalSteps = 1;
|
||||
this.step = 1;
|
||||
this.progress = 0;
|
||||
this.iconAngle = 0;
|
||||
this.ani = false;
|
||||
|
||||
this.visible = true;
|
||||
}
|
||||
@@ -48,6 +66,7 @@ class ProgressPage extends Vue {
|
||||
hide() {
|
||||
this.visible = false;
|
||||
this.text = '';
|
||||
this.iconAngle = 0;
|
||||
}
|
||||
|
||||
setState(state) {
|
||||
@@ -61,46 +80,21 @@ class ProgressPage extends Vue {
|
||||
this.step = (state.step ? state.step : this.step);
|
||||
this.totalSteps = (state.totalSteps > this.totalSteps ? state.totalSteps : this.totalSteps);
|
||||
this.progress = state.progress || 0;
|
||||
|
||||
if (!this.ani) {
|
||||
(async() => {
|
||||
this.ani = true;
|
||||
this.iconAngle += 30;
|
||||
this.iconStyle = `transform: rotate(${this.iconAngle}deg); transition: 150ms linear`;
|
||||
await utils.sleep(150);
|
||||
this.ani = false;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
get percentage() {
|
||||
let circle = document.querySelector('path[class="el-progress-circle__path"]');
|
||||
if (circle)
|
||||
circle.style.transition = '';
|
||||
return Math.round(((this.step - 1)/this.totalSteps + this.progress/(100*this.totalSteps))*100);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
<style scoped>
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
z-index: 100;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
|
||||
position: absolute;
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
color: white;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: yellow;
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.el-progress__text {
|
||||
color: lightgreen !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,99 +1,106 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-header v-show="toolBarActive" height='50px'>
|
||||
<div ref="header" class="header">
|
||||
<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-tooltip>
|
||||
<div class="column no-wrap">
|
||||
<div ref="header" class="header" v-show="toolBarActive">
|
||||
<div ref="buttons" class="row justify-between no-wrap">
|
||||
<button ref="loader" class="tool-button" :class="buttonActiveClass('loader')" @click="buttonClick('loader')" v-ripple>
|
||||
<q-icon name="la la-arrow-left" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom right" content-style="font-size: 80%">{{ rstore.readerActions['loader'] }}</q-tooltip>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<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-tooltip>
|
||||
<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-tooltip>
|
||||
<button ref="undoAction" v-show="showToolButton['undoAction']" class="tool-button" :class="buttonActiveClass('undoAction')" @click="buttonClick('undoAction')" v-ripple>
|
||||
<q-icon name="la la-angle-left" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['undoAction'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="redoAction" v-show="showToolButton['redoAction']" class="tool-button" :class="buttonActiveClass('redoAction')" @click="buttonClick('redoAction')" v-ripple>
|
||||
<q-icon name="la la-angle-right" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['redoAction'] }}</q-tooltip>
|
||||
</button>
|
||||
<div class="space"></div>
|
||||
<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-tooltip>
|
||||
<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-tooltip>
|
||||
<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-tooltip>
|
||||
<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-tooltip>
|
||||
<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-tooltip>
|
||||
<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')">
|
||||
<i class="el-icon-refresh" :class="{clear: !showRefreshIcon}"></i>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<button ref="fullScreen" v-show="showToolButton['fullScreen']" class="tool-button" :class="buttonActiveClass('fullScreen')" @click="buttonClick('fullScreen')" v-ripple>
|
||||
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['fullScreen'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="scrolling" v-show="showToolButton['scrolling']" class="tool-button" :class="buttonActiveClass('scrolling')" @click="buttonClick('scrolling')" v-ripple>
|
||||
<q-icon name="la la-film" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['scrolling'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="setPosition" v-show="showToolButton['setPosition']" class="tool-button" :class="buttonActiveClass('setPosition')" @click="buttonClick('setPosition')" v-ripple>
|
||||
<q-icon name="la la-angle-double-right" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['setPosition'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="search" v-show="showToolButton['search']" class="tool-button" :class="buttonActiveClass('search')" @click="buttonClick('search')" v-ripple>
|
||||
<q-icon name="la la-search" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['search'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="copyText" v-show="showToolButton['copyText']" class="tool-button" :class="buttonActiveClass('copyText')" @click="buttonClick('copyText')" v-ripple>
|
||||
<q-icon name="la la-copy" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
|
||||
<q-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['refresh'] }}</q-tooltip>
|
||||
</button>
|
||||
<div class="space"></div>
|
||||
<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 v-show="showToolButton['recentBooks']" content="Открыть недавние" :open-delay="1000" effect="light">
|
||||
<el-button ref="recentBooks" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')"><i class="el-icon-document"></i></el-button>
|
||||
</el-tooltip>
|
||||
<button ref="offlineMode" v-show="showToolButton['offlineMode']" class="tool-button" :class="buttonActiveClass('offlineMode')" @click="buttonClick('offlineMode')" v-ripple>
|
||||
<q-icon name="la la-unlink" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['offlineMode'] }}</q-tooltip>
|
||||
</button>
|
||||
<button ref="recentBooks" v-show="showToolButton['recentBooks']" class="tool-button" :class="buttonActiveClass('recentBooks')" @click="buttonClick('recentBooks')" v-ripple>
|
||||
<q-icon name="la la-book-open" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['recentBooks'] }}</q-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<el-tooltip content="Настроить" :open-delay="1000" effect="light">
|
||||
<el-button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')"><i class="el-icon-setting"></i></el-button>
|
||||
</el-tooltip>
|
||||
<button ref="settings" class="tool-button" :class="buttonActiveClass('settings')" @click="buttonClick('settings')" v-ripple>
|
||||
<q-icon name="la la-cog" size="32px"/>
|
||||
<q-tooltip :delay="1500" anchor="bottom left" content-style="font-size: 80%">{{ rstore.readerActions['settings'] }}</q-tooltip>
|
||||
</button>
|
||||
</div>
|
||||
</el-header>
|
||||
</div>
|
||||
|
||||
<el-main>
|
||||
<div class="main col row relative-position">
|
||||
<keep-alive>
|
||||
<component ref="page" :is="activePage"
|
||||
<component ref="page" class="col" :is="activePage"
|
||||
@load-book="loadBook"
|
||||
@load-file="loadFile"
|
||||
@book-pos-changed="bookPosChanged"
|
||||
@tool-bar-toggle="toolBarToggle"
|
||||
@full-screen-toogle="fullScreenToggle"
|
||||
@stop-scrolling="stopScrolling"
|
||||
@scrolling-toggle="scrollingToggle"
|
||||
@help-toggle="helpToggle"
|
||||
@donate-toggle="donateToggle"
|
||||
@do-action="doAction"
|
||||
></component>
|
||||
</keep-alive>
|
||||
|
||||
<SetPositionPage v-if="setPositionActive" ref="setPositionPage" @set-position-toggle="setPositionToggle" @book-pos-changed="bookPosChanged"></SetPositionPage>
|
||||
<SearchPage v-show="searchActive" ref="searchPage"
|
||||
@search-toggle="searchToggle"
|
||||
@do-action="doAction"
|
||||
@book-pos-changed="bookPosChanged"
|
||||
@start-text-search="startTextSearch"
|
||||
@stop-text-search="stopTextSearch">
|
||||
</SearchPage>
|
||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @copy-text-toggle="copyTextToggle"></CopyTextPage>
|
||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-toggle="recentBooksToggle"></RecentBooksPage>
|
||||
<SettingsPage v-if="settingsActive" ref="settingsPage" @settings-toggle="settingsToggle"></SettingsPage>
|
||||
<HelpPage v-if="helpActive" ref="helpPage" @help-toggle="helpToggle"></HelpPage>
|
||||
<CopyTextPage v-if="copyTextActive" ref="copyTextPage" @do-action="doAction"></CopyTextPage>
|
||||
<RecentBooksPage v-show="recentBooksActive" ref="recentBooksPage" @load-book="loadBook" @recent-books-close="recentBooksClose"></RecentBooksPage>
|
||||
<SettingsPage v-show="settingsActive" ref="settingsPage" @do-action="doAction"></SettingsPage>
|
||||
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||
|
||||
<el-dialog
|
||||
title="Что нового:"
|
||||
:visible.sync="whatsNewVisible"
|
||||
width="80%">
|
||||
<Dialog ref="dialog1" v-model="whatsNewVisible">
|
||||
<template slot="header">
|
||||
Что нового:
|
||||
</template>
|
||||
|
||||
<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 slot="footer">
|
||||
<q-btn class="q-px-md" dense no-caps @click="whatsNewDisable">Больше не показывать</q-btn>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="dialog2" v-model="donationVisible">
|
||||
<template slot="header">
|
||||
Здравствуйте, уважаемые читатели!
|
||||
</template>
|
||||
|
||||
<el-dialog
|
||||
title="Здравствуйте, уважаемые читатели!"
|
||||
:visible.sync="donationVisible"
|
||||
width="90%">
|
||||
<div style="word-break: normal">
|
||||
Стартовала ежегодная акция "Оплатим хостинг вместе".<br><br>
|
||||
|
||||
@@ -110,12 +117,9 @@
|
||||
|
||||
Автор также обращается с просьбой о помощи в распространении
|
||||
<a href="https://omnireader.ru" target="_blank">ссылки</a>
|
||||
<el-tooltip :open-delay="500" effect="light">
|
||||
<template slot="content">
|
||||
Скопировать
|
||||
</template>
|
||||
<i class="el-icon-copy-document" style="cursor: pointer; font-size: 100%" @click="copyLink('https://omnireader.ru')"></i>
|
||||
</el-tooltip>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyLink('https://omnireader.ru')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
на читалку через тематические форумы, соцсети, мессенджеры и пр.
|
||||
Чем нас больше, тем легче оставаться на плаву и тем больше мотивации у разработчика, чтобы продолжать работать над проектом.
|
||||
|
||||
@@ -125,20 +129,20 @@
|
||||
P.S. При необходимости можно воспользоваться подходящим обменником на <a href="https://www.bestchange.ru" target="_blank">bestchange.ru</a>
|
||||
|
||||
<br><br>
|
||||
<el-row type="flex" justify="center">
|
||||
<el-button type="success" round @click="openDonate">Помочь проекту</el-button>
|
||||
</el-row>
|
||||
<div class="row justify-center">
|
||||
<q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">Помочь проекту</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<span class="clickable" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||
<br><br>
|
||||
<el-button @click="donationDialogRemind">Напомнить позже</el-button>
|
||||
<span slot="footer">
|
||||
<span class="clickable row justify-end" style="font-size: 60%; color: grey" @click="donationDialogDisable">Больше не показывать</span>
|
||||
<br>
|
||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</Dialog>
|
||||
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -160,8 +164,10 @@ import SettingsPage from './SettingsPage/SettingsPage.vue';
|
||||
import HelpPage from './HelpPage/HelpPage.vue';
|
||||
import ClickMapPage from './ClickMapPage/ClickMapPage.vue';
|
||||
import ServerStorage from './ServerStorage/ServerStorage.vue';
|
||||
import Dialog from '../share/Dialog.vue';
|
||||
|
||||
import bookManager from './share/bookManager';
|
||||
import rstore from '../../store/modules/reader';
|
||||
import readerApi from '../../api/reader';
|
||||
import * as utils from '../../share/utils';
|
||||
import {versionHistory} from './versionHistory';
|
||||
@@ -180,6 +186,7 @@ export default @Component({
|
||||
HelpPage,
|
||||
ClickMapPage,
|
||||
ServerStorage,
|
||||
Dialog,
|
||||
},
|
||||
watch: {
|
||||
bookPos: function(newValue) {
|
||||
@@ -221,6 +228,7 @@ export default @Component({
|
||||
},
|
||||
})
|
||||
class Reader extends Vue {
|
||||
rstore = {};
|
||||
loaderActive = false;
|
||||
progressActive = false;
|
||||
fullScreenActive = false;
|
||||
@@ -250,6 +258,7 @@ class Reader extends Vue {
|
||||
donationVisible = false;
|
||||
|
||||
created() {
|
||||
this.rstore = rstore;
|
||||
this.loading = true;
|
||||
this.commit = this.$store.commit;
|
||||
this.dispatch = this.$store.dispatch;
|
||||
@@ -292,8 +301,8 @@ class Reader extends Vue {
|
||||
(async() => {
|
||||
await bookManager.init(this.settings);
|
||||
bookManager.addEventListener(this.bookManagerEvent);
|
||||
|
||||
if (this.$root.rootRoute == '/reader') {
|
||||
|
||||
if (this.$root.rootRoute() == '/reader') {
|
||||
if (this.routeParamUrl) {
|
||||
await this.loadBook({url: this.routeParamUrl, bookPos: this.routeParamPos, force: this.routeParamRefresh});
|
||||
} else {
|
||||
@@ -325,13 +334,23 @@ class Reader extends Vue {
|
||||
this.showToolButton = settings.showToolButton;
|
||||
this.enableSitesFilter = settings.enableSitesFilter;
|
||||
|
||||
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||
this.$root.readerActionByKeyEvent = (event) => {
|
||||
return this.readerActionByKeyCode[utils.keyEventToCode(event)];
|
||||
}
|
||||
|
||||
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';
|
||||
if (this.$refs.buttons)
|
||||
this.$refs.buttons.style.minWidth = 65*showButtonCount + 'px';
|
||||
(async() => {
|
||||
await utils.sleep(1000);
|
||||
if (this.$refs.header)
|
||||
this.$refs.header.style.overflowX = 'auto';
|
||||
})();
|
||||
}
|
||||
|
||||
checkSetStorageAccessKey() {
|
||||
@@ -417,9 +436,9 @@ class Reader extends Vue {
|
||||
async copyLink(link) {
|
||||
const result = await utils.copyTextToClipboard(link);
|
||||
if (result)
|
||||
this.$notify.success({message: `Ссылка ${link} успешно скопирована в буфер обмена`});
|
||||
this.$root.notify.success(`Ссылка ${link} успешно скопирована в буфер обмена`);
|
||||
else
|
||||
this.$notify.error({message: 'Копирование не удалось'});
|
||||
this.$root.notify.error('Копирование не удалось');
|
||||
}
|
||||
|
||||
openVersionHistory() {
|
||||
@@ -562,22 +581,9 @@ class Reader extends Vue {
|
||||
fullScreenToggle() {
|
||||
this.fullScreenActive = !this.fullScreenActive;
|
||||
if (this.fullScreenActive) {
|
||||
const element = document.documentElement;
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen();
|
||||
} else if (element.webkitrequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
} else if (element.mozRequestFullscreen) {
|
||||
element.mozRequestFullScreen();
|
||||
}
|
||||
this.$q.fullscreen.request();
|
||||
} else {
|
||||
if (document.cancelFullScreen) {
|
||||
document.cancelFullScreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitCancelFullScreen) {
|
||||
document.webkitCancelFullScreen();
|
||||
}
|
||||
this.$q.fullscreen.exit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,7 +606,8 @@ class Reader extends Vue {
|
||||
|
||||
setPositionToggle() {
|
||||
this.setPositionActive = !this.setPositionActive;
|
||||
if (this.setPositionActive && this.activePage == 'TextPage' && this.mostRecentBook()) {
|
||||
const page = this.$refs.page;
|
||||
if (this.setPositionActive && this.activePage == 'TextPage' && page.parsed) {
|
||||
this.closeAllTextPages();
|
||||
this.setPositionActive = true;
|
||||
|
||||
@@ -680,6 +687,10 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
recentBooksClose() {
|
||||
this.recentBooksActive = false;
|
||||
}
|
||||
|
||||
recentBooksToggle() {
|
||||
this.recentBooksActive = !this.recentBooksActive;
|
||||
if (this.recentBooksActive) {
|
||||
@@ -742,66 +753,37 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
buttonClick(button) {
|
||||
const activeClass = this.buttonActiveClass(button);
|
||||
undoAction() {
|
||||
if (this.actionCur > 0) {
|
||||
this.actionCur--;
|
||||
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
|
||||
}
|
||||
}
|
||||
|
||||
this.$refs[button].$el.blur();
|
||||
redoAction() {
|
||||
if (this.actionCur < this.actionList.length - 1) {
|
||||
this.actionCur++;
|
||||
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
|
||||
}
|
||||
}
|
||||
|
||||
buttonClick(action) {
|
||||
const activeClass = this.buttonActiveClass(action);
|
||||
|
||||
this.$refs[action].blur();
|
||||
|
||||
if (activeClass['tool-button-disabled'])
|
||||
return;
|
||||
|
||||
switch (button) {
|
||||
case 'loader':
|
||||
this.loaderToggle();
|
||||
break;
|
||||
case 'undoAction':
|
||||
if (this.actionCur > 0) {
|
||||
this.actionCur--;
|
||||
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
|
||||
}
|
||||
break;
|
||||
case 'redoAction':
|
||||
if (this.actionCur < this.actionList.length - 1) {
|
||||
this.actionCur++;
|
||||
this.bookPosChanged({bookPos: this.actionList[this.actionCur]});
|
||||
}
|
||||
break;
|
||||
case 'fullScreen':
|
||||
this.fullScreenToggle();
|
||||
break;
|
||||
case 'setPosition':
|
||||
this.setPositionToggle();
|
||||
break;
|
||||
case 'scrolling':
|
||||
this.scrollingToggle();
|
||||
break;
|
||||
case 'search':
|
||||
this.searchToggle();
|
||||
break;
|
||||
case 'copyText':
|
||||
this.copyTextToggle();
|
||||
break;
|
||||
case 'refresh':
|
||||
this.refreshBook();
|
||||
break;
|
||||
case 'recentBooks':
|
||||
this.recentBooksToggle();
|
||||
break;
|
||||
case 'offlineMode':
|
||||
this.offlineModeToggle();
|
||||
break;
|
||||
case 'settings':
|
||||
this.settingsToggle();
|
||||
break;
|
||||
}
|
||||
this.doAction({action});
|
||||
}
|
||||
|
||||
buttonActiveClass(button) {
|
||||
buttonActiveClass(action) {
|
||||
const classActive = { 'tool-button-active': true, 'tool-button-active:hover': true };
|
||||
const classDisabled = { 'tool-button-disabled': true, 'tool-button-disabled:hover': true };
|
||||
let classResult = {};
|
||||
|
||||
switch (button) {
|
||||
switch (action) {
|
||||
case 'loader':
|
||||
case 'fullScreen':
|
||||
case 'setPosition':
|
||||
@@ -814,7 +796,7 @@ class Reader extends Vue {
|
||||
case 'settings':
|
||||
if (this.progressActive) {
|
||||
classResult = classDisabled;
|
||||
} else if (this[`${button}Active`]) {
|
||||
} else if (this[`${action}Active`]) {
|
||||
classResult = classActive;
|
||||
}
|
||||
break;
|
||||
@@ -829,7 +811,7 @@ class Reader extends Vue {
|
||||
}
|
||||
|
||||
if (this.activePage == 'LoaderPage' || !this.mostRecentBookReactive) {
|
||||
switch (button) {
|
||||
switch (action) {
|
||||
case 'undoAction':
|
||||
case 'redoAction':
|
||||
case 'setPosition':
|
||||
@@ -914,6 +896,8 @@ class Reader extends Vue {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeAllTextPages();
|
||||
|
||||
let url = encodeURI(decodeURI(opts.url));
|
||||
|
||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||
@@ -1020,7 +1004,7 @@ class Reader extends Vue {
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1044,7 +1028,7 @@ class Reader extends Vue {
|
||||
} catch (e) {
|
||||
progress.hide(); this.progressActive = false;
|
||||
this.loaderActive = true;
|
||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1076,8 +1060,119 @@ class Reader extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
doAction(opts) {
|
||||
let result = true;
|
||||
let {action = '', event = false} = opts;
|
||||
|
||||
switch (action) {
|
||||
case 'loader':
|
||||
this.loaderToggle();
|
||||
break;
|
||||
case 'help':
|
||||
this.helpToggle();
|
||||
break;
|
||||
case 'settings':
|
||||
this.settingsToggle();
|
||||
break;
|
||||
case 'undoAction':
|
||||
this.undoAction();
|
||||
break;
|
||||
case 'redoAction':
|
||||
this.redoAction();
|
||||
break;
|
||||
case 'fullScreen':
|
||||
this.fullScreenToggle();
|
||||
break;
|
||||
case 'scrolling':
|
||||
this.scrollingToggle();
|
||||
break;
|
||||
case 'stopScrolling':
|
||||
this.stopScrolling();
|
||||
break;
|
||||
case 'setPosition':
|
||||
this.setPositionToggle();
|
||||
break;
|
||||
case 'search':
|
||||
this.searchToggle();
|
||||
break;
|
||||
case 'copyText':
|
||||
this.copyTextToggle();
|
||||
break;
|
||||
case 'refresh':
|
||||
this.refreshBook();
|
||||
break;
|
||||
case 'offlineMode':
|
||||
this.offlineModeToggle();
|
||||
break;
|
||||
case 'recentBooks':
|
||||
this.recentBooksToggle();
|
||||
break;
|
||||
case 'switchToolbar':
|
||||
this.toolBarToggle();
|
||||
break;
|
||||
case 'donate':
|
||||
this.donateToggle();
|
||||
break;
|
||||
default:
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!result && this.activePage == 'TextPage' && this.$refs.page) {
|
||||
result = true;
|
||||
const textPage = this.$refs.page;
|
||||
|
||||
switch (action) {
|
||||
case 'bookBegin':
|
||||
textPage.doHome();
|
||||
break;
|
||||
case 'bookEnd':
|
||||
textPage.doEnd();
|
||||
break;
|
||||
case 'pageBack':
|
||||
textPage.doPageUp();
|
||||
break;
|
||||
case 'pageForward':
|
||||
textPage.doPageDown();
|
||||
break;
|
||||
case 'lineBack':
|
||||
textPage.doUp();
|
||||
break;
|
||||
case 'lineForward':
|
||||
textPage.doDown();
|
||||
break;
|
||||
case 'incFontSize':
|
||||
textPage.doFontSizeInc();
|
||||
break;
|
||||
case 'decFontSize':
|
||||
textPage.doFontSizeDec();
|
||||
break;
|
||||
case 'scrollingSpeedUp':
|
||||
textPage.doScrollingSpeedUp();
|
||||
break;
|
||||
case 'scrollingSpeedDown':
|
||||
textPage.doScrollingSpeedDown();
|
||||
break;
|
||||
default:
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (result && event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.$root.rootRoute == '/reader') {
|
||||
let result = false;
|
||||
if (this.$root.rootRoute() == '/reader') {
|
||||
if (this.$root.stdDialog.active || this.$refs.dialog1.active || this.$refs.dialog2.active)
|
||||
return result;
|
||||
|
||||
let handled = false;
|
||||
if (!handled && this.helpActive)
|
||||
handled = this.$refs.helpPage.keyHook(event);
|
||||
@@ -1101,92 +1196,40 @@ class Reader extends Vue {
|
||||
handled = this.$refs.page.keyHook(event);
|
||||
|
||||
if (!handled && event.type == 'keydown') {
|
||||
if (event.code == 'Escape')
|
||||
this.loaderToggle();
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
|
||||
if (this.activePage == 'TextPage') {
|
||||
switch (event.code) {
|
||||
case 'KeyH':
|
||||
case 'F1':
|
||||
this.helpToggle();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case 'KeyZ':
|
||||
this.scrollingToggle();
|
||||
break;
|
||||
case 'KeyP':
|
||||
this.setPositionToggle();
|
||||
break;
|
||||
case 'KeyF':
|
||||
if (event.ctrlKey) {
|
||||
this.searchToggle();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
break;
|
||||
case 'KeyC':
|
||||
if (event.ctrlKey) {
|
||||
this.copyTextToggle();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
break;
|
||||
case 'KeyR':
|
||||
this.refreshBook();
|
||||
break;
|
||||
case 'KeyX':
|
||||
this.recentBooksToggle();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
case 'KeyO':
|
||||
this.offlineModeToggle();
|
||||
break;
|
||||
case 'KeyS':
|
||||
this.settingsToggle();
|
||||
break;
|
||||
}
|
||||
if (action == 'loader') {
|
||||
result = this.doAction({action, event});
|
||||
}
|
||||
|
||||
if (!result && this.activePage == 'TextPage') {
|
||||
result = this.doAction({action, event});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-container {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-header {
|
||||
.header {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
background-color: #1B695F;
|
||||
color: #000;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
overflow: hidden;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.main {
|
||||
background-color: #EBE2C9;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tool-button {
|
||||
margin: 0 2px 0 2px;
|
||||
margin: 0px 2px 0 2px;
|
||||
padding: 0;
|
||||
color: #3E843E;
|
||||
background-color: #E6EDF4;
|
||||
@@ -1194,15 +1237,14 @@ class Reader extends Vue {
|
||||
height: 38px;
|
||||
width: 38px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 3px 3px 5px black;
|
||||
}
|
||||
|
||||
.tool-button + .tool-button {
|
||||
margin: 0 2px 0 2px;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.tool-button:hover {
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tool-button-active {
|
||||
@@ -1217,20 +1259,19 @@ class Reader extends Vue {
|
||||
.tool-button-active:hover {
|
||||
color: white;
|
||||
background-color: #81C581;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tool-button-disabled {
|
||||
color: lightgray;
|
||||
background-color: gray;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tool-button-disabled:hover {
|
||||
color: lightgray;
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 200%;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.space {
|
||||
@@ -1247,4 +1288,10 @@ i {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
cursor: pointer;
|
||||
font-size: 120%;
|
||||
color: blue;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,97 +1,84 @@
|
||||
<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>
|
||||
<span v-show="!loading">{{ header }}</span>
|
||||
<span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7"/>Список загружается</span>
|
||||
</template>
|
||||
|
||||
<a ref="download" style='display: none;'></a>
|
||||
<el-table
|
||||
|
||||
<q-table
|
||||
class="recent-books-table col"
|
||||
:data="tableData"
|
||||
style="width: 570px"
|
||||
size="mini"
|
||||
height="1px"
|
||||
stripe
|
||||
border
|
||||
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
||||
:header-cell-style = "headerCellStyle"
|
||||
:row-key = "rowKey"
|
||||
>
|
||||
:columns="columns"
|
||||
row-key="key"
|
||||
:pagination.sync="pagination"
|
||||
separator="cell"
|
||||
hide-bottom
|
||||
virtual-scroll
|
||||
dense
|
||||
>
|
||||
<template v-slot:header="props">
|
||||
<q-tr :props="props">
|
||||
<q-th class="td-mp" style="width: 25px" key="num" :props="props"><span v-html="props.cols[0].label"></span></q-th>
|
||||
<q-th class="td-mp break-word" style="width: 77px" key="date" :props="props"><span v-html="props.cols[1].label"></span></q-th>
|
||||
<q-th class="td-mp" style="width: 332px" key="desc" :props="props" colspan="4">
|
||||
<q-input ref="input" outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
|
||||
placeholder="Найти"
|
||||
v-model="search"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<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>
|
||||
<span v-html="props.cols[2].label"></span>
|
||||
</q-th>
|
||||
</q-tr>
|
||||
</template>
|
||||
|
||||
<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"
|
||||
/>
|
||||
<template v-slot:body="props">
|
||||
<q-tr :props="props">
|
||||
<q-td key="num" :props="props" class="td-mp" auto-width>
|
||||
<div class="break-word" style="width: 25px">
|
||||
{{ props.row.num }}
|
||||
</div>
|
||||
</template>
|
||||
</q-td>
|
||||
|
||||
<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>
|
||||
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||
<div class="break-word" style="width: 68px">
|
||||
{{ props.row.touchDate }}<br>
|
||||
{{ props.row.touchTime }}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</q-td>
|
||||
|
||||
<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" @click.prevent="downloadBook(scope.row.path)">Скачать FB2</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
||||
<div class="break-word" style="width: 332px; font-size: 90%">
|
||||
<div style="color: green">{{ props.row.desc.author }}</div>
|
||||
<div>{{ props.row.desc.title }}</div>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
<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>
|
||||
<q-td key="links" :props="props" class="td-mp" auto-width>
|
||||
<div class="break-word" style="width: 75px; font-size: 90%">
|
||||
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
|
||||
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
|
||||
</div>
|
||||
</q-td>
|
||||
|
||||
</el-table-column>
|
||||
<q-td key="close" :props="props" class="td-mp" auto-width>
|
||||
<div style="width: 38px">
|
||||
<q-btn
|
||||
dense
|
||||
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
||||
@click="handleDel(props.row.key)">
|
||||
<q-icon class="la la-times" size="14px" style="top: -6px"/>
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-td>
|
||||
<q-td key="last" :props="props" class="no-mp">
|
||||
</q-td>
|
||||
</q-tr>
|
||||
</template>
|
||||
</q-table>
|
||||
|
||||
</el-table>
|
||||
</Window>
|
||||
</template>
|
||||
|
||||
@@ -121,52 +108,90 @@ class RecentBooksPage extends Vue {
|
||||
loading = false;
|
||||
search = null;
|
||||
tableData = [];
|
||||
columns = [];
|
||||
pagination = {};
|
||||
|
||||
created() {
|
||||
this.pagination = {rowsPerPage: 0};
|
||||
|
||||
this.columns = [
|
||||
{
|
||||
name: 'num',
|
||||
label: '#',
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
field: 'num',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: 'Время<br>просм.',
|
||||
align: 'left',
|
||||
field: 'touchDateTime',
|
||||
sortable: true,
|
||||
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
|
||||
},
|
||||
{
|
||||
name: 'desc',
|
||||
label: 'Название',
|
||||
align: 'left',
|
||||
field: 'descString',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
name: 'links',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'close',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
name: 'last',
|
||||
label: '',
|
||||
align: 'left',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$refs.window.init();
|
||||
|
||||
this.$nextTick(() => {
|
||||
//this.$refs.input.focus();
|
||||
//this.$refs.input.focus();//плохо на планшетах
|
||||
});
|
||||
(async() => {//отбражение подгрузки списка, иначе тормозит
|
||||
(async() => {//подгрузка списка
|
||||
if (this.initing)
|
||||
return;
|
||||
this.initing = true;
|
||||
|
||||
await this.updateTableData(3);
|
||||
await utils.sleep(200);
|
||||
|
||||
if (bookManager.loaded) {
|
||||
const t = Date.now();
|
||||
if (!bookManager.loaded) {
|
||||
await this.updateTableData(10);
|
||||
if (bookManager.getSortedRecent().length > 10)
|
||||
await utils.sleep(10*(Date.now() - t));
|
||||
} else {
|
||||
//для отзывчивости
|
||||
await utils.sleep(100);
|
||||
let i = 0;
|
||||
let j = 5;
|
||||
while (i < 500 && !bookManager.loaded) {
|
||||
if (i % j == 0) {
|
||||
bookManager.sortedRecentCached = null;
|
||||
await this.updateTableData(100);
|
||||
await this.updateTableData(20);
|
||||
j *= 2;
|
||||
}
|
||||
|
||||
await utils.sleep(100);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
//для отзывчивости
|
||||
await utils.sleep(100);
|
||||
}
|
||||
await this.updateTableData();
|
||||
this.initing = false;
|
||||
})();
|
||||
}
|
||||
|
||||
rowKey(row) {
|
||||
return row.key;
|
||||
}
|
||||
|
||||
async updateTableData(limit) {
|
||||
while (this.updating) await utils.sleep(100);
|
||||
this.updating = true;
|
||||
@@ -175,11 +200,13 @@ class RecentBooksPage extends Vue {
|
||||
this.loading = !!limit;
|
||||
const sorted = bookManager.getSortedRecent();
|
||||
|
||||
let num = 0;
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
const book = sorted[i];
|
||||
if (book.deleted)
|
||||
continue;
|
||||
|
||||
num++;
|
||||
if (limit && result.length >= limit)
|
||||
break;
|
||||
|
||||
@@ -221,19 +248,19 @@ class RecentBooksPage extends Vue {
|
||||
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
||||
|
||||
result.push({
|
||||
num,
|
||||
touchDateTime: book.touchTime,
|
||||
touchDate: t[0],
|
||||
touchTime: t[1],
|
||||
desc: {
|
||||
title: `${title}${perc}${textLen}`,
|
||||
author,
|
||||
title: `${title}${perc}${textLen}`,
|
||||
},
|
||||
descString: `${author}${title}${perc}${textLen}`,
|
||||
url: book.url,
|
||||
path: book.path,
|
||||
key: book.key,
|
||||
});
|
||||
if (result.length >= 100)
|
||||
break;
|
||||
}
|
||||
|
||||
const search = this.search;
|
||||
@@ -245,29 +272,23 @@ class RecentBooksPage extends Vue {
|
||||
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;
|
||||
wordEnding(num) {
|
||||
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
|
||||
const deci = num % 100;
|
||||
if (deci > 10 && deci < 20) {
|
||||
return '';
|
||||
} else {
|
||||
return endings[num % 10];
|
||||
}
|
||||
if (cell.rowIndex > 0) {
|
||||
result.height = '0px';
|
||||
result['border-right'] = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get header() {
|
||||
const len = (this.tableData ? this.tableData.length : 0);
|
||||
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
||||
}
|
||||
|
||||
async downloadBook(fb2path) {
|
||||
@@ -282,7 +303,7 @@ class RecentBooksPage extends Vue {
|
||||
let errMes = e.message;
|
||||
if (errMes.indexOf('404') >= 0)
|
||||
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
|
||||
this.$alert(errMes, 'Ошибка', {type: 'error'});
|
||||
this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +317,7 @@ class RecentBooksPage extends Vue {
|
||||
|
||||
async handleDel(key) {
|
||||
await bookManager.delRecentBook({key});
|
||||
this.updateTableData();
|
||||
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
||||
|
||||
if (!bookManager.mostRecentBook())
|
||||
this.close();
|
||||
@@ -315,11 +336,11 @@ class RecentBooksPage extends Vue {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('recent-books-toggle');
|
||||
this.$emit('recent-books-close');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && event.code == 'Escape') {
|
||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.code == 'Escape') {
|
||||
this.close();
|
||||
}
|
||||
return true;
|
||||
@@ -329,7 +350,51 @@ class RecentBooksPage extends Vue {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.desc {
|
||||
.recent-books-table {
|
||||
width: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
.td-mp {
|
||||
margin: 0 !important;
|
||||
padding: 4px 4px 4px 4px !important;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.no-mp {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 0;
|
||||
border-left: 1px solid #ddd !important;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
line-height: 180%;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.recent-books-table .q-table__middle {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.recent-books-table thead tr:first-child th {
|
||||
position: sticky;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
background-color: #c1f4cd;
|
||||
}
|
||||
.recent-books-table tr:nth-child(even) {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,15 +8,19 @@
|
||||
<span v-show="initStep">{{ initPercentage }}%</span>
|
||||
|
||||
<div v-show="!initStep" class="input">
|
||||
<input ref="input" class="el-input__inner"
|
||||
<!--input ref="input"
|
||||
placeholder="что ищем"
|
||||
:value="needle" @input="needle = $event.target.value"/>
|
||||
:value="needle" @input="needle = $event.target.value"/-->
|
||||
<q-input ref="input" class="col" outlined dense
|
||||
placeholder="что ищем"
|
||||
v-model="needle" @keydown="inputKeyDown"
|
||||
/>
|
||||
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">{{ foundText }}</div>
|
||||
</div>
|
||||
<el-button-group v-show="!initStep" class="button-group">
|
||||
<el-button @click="showNext"><i class="el-icon-arrow-down"></i></el-button>
|
||||
<el-button @click="showPrev"><i class="el-icon-arrow-up"></i></el-button>
|
||||
</el-button-group>
|
||||
<q-btn-group v-show="!initStep" class="button-group row no-wrap">
|
||||
<q-btn class="button" dense stretch @click="showNext"><q-icon style="top: -6px" name="la la-angle-down" dense size="22px"/></q-btn>
|
||||
<q-btn class="button" dense stretch @click="showPrev"><q-icon style="top: -4px" class="icon" name="la la-angle-up" dense size="22px"/></q-btn>
|
||||
</q-btn-group>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
@@ -39,7 +43,10 @@ export default @Component({
|
||||
|
||||
},
|
||||
foundText: function(newValue) {
|
||||
this.$refs.input.style.paddingRight = (10 + newValue.length*12) + 'px';
|
||||
//недостатки сторонних ui
|
||||
const el = this.$refs.input.$el.querySelector('label div div div input');
|
||||
if (el)
|
||||
el.style.paddingRight = newValue.length*12 + 'px';
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -157,15 +164,16 @@ class SearchPage extends Vue {
|
||||
|
||||
close() {
|
||||
this.stopInit = true;
|
||||
this.$emit('search-toggle');
|
||||
this.$emit('do-action', {action: 'search'});
|
||||
}
|
||||
|
||||
inputKeyDown(event) {
|
||||
if (event.key == 'Enter') {
|
||||
this.showNext();
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
//недостатки сторонних ui
|
||||
if (document.activeElement === this.$refs.input && event.type == 'keydown' && event.key == 'Enter') {
|
||||
this.showNext();
|
||||
}
|
||||
|
||||
if (event.type == 'keydown' && (event.code == 'Escape')) {
|
||||
this.close();
|
||||
}
|
||||
@@ -194,17 +202,14 @@ class SearchPage extends Vue {
|
||||
}
|
||||
|
||||
.button-group {
|
||||
width: 150px;
|
||||
width: 100px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
.button {
|
||||
padding: 9px 17px 9px 17px;
|
||||
width: 55px;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -177,17 +177,17 @@ class ServerStorage extends Vue {
|
||||
|
||||
success(message) {
|
||||
if (this.showServerStorageMessages)
|
||||
this.$notify.success({message});
|
||||
this.$root.notify.success(message);
|
||||
}
|
||||
|
||||
warning(message) {
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$notify.warning({message});
|
||||
this.$root.notify.warning(message);
|
||||
}
|
||||
|
||||
error(message) {
|
||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
||||
this.$notify.error({message});
|
||||
this.$root.notify.error(message);
|
||||
}
|
||||
|
||||
async loadSettings(force = false, doNotifySuccess = true) {
|
||||
|
||||
@@ -4,8 +4,15 @@
|
||||
Установить позицию
|
||||
</template>
|
||||
|
||||
<div class="slider">
|
||||
<el-slider v-model="sliderValue" :max="sliderMax" :format-tooltip="formatTooltip"></el-slider>
|
||||
<div id="set-position-slider" class="slider q-px-md">
|
||||
<q-slider
|
||||
thumb-path="M 2, 10 a 8.5,8.5 0 1,0 17,0 a 8.5,8.5 0 1,0 -17,0"
|
||||
v-model="sliderValue"
|
||||
:max="sliderMax"
|
||||
label
|
||||
:label-value="(sliderMax ? (sliderValue/this.sliderMax*100).toFixed(2) + '%' : 0)"
|
||||
color="primary"
|
||||
/>
|
||||
</div>
|
||||
</Window>
|
||||
</template>
|
||||
@@ -46,20 +53,16 @@ class SetPositionPage extends Vue {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
formatTooltip(val) {
|
||||
if (this.sliderMax)
|
||||
return (val/this.sliderMax*100).toFixed(2) + '%';
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.$emit('set-position-toggle');
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (event.type == 'keydown' && (event.code == 'Escape' || event.code == 'KeyP')) {
|
||||
this.close();
|
||||
if (event.type == 'keydown') {
|
||||
const action = this.$root.readerActionByKeyEvent(event);
|
||||
if (event.code == 'Escape' || action == 'setPosition') {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -73,9 +76,13 @@ class SetPositionPage extends Vue {
|
||||
background-color: #efefef;
|
||||
border-radius: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
.el-slider {
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
<style>
|
||||
#set-position-slider .q-slider__thumb path {
|
||||
fill: white !important;
|
||||
stroke: blue !important;
|
||||
stroke-width: 2 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="table col column no-wrap">
|
||||
<!-- header -->
|
||||
<div class="table-row row">
|
||||
<div class="desc q-pa-sm bg-blue-2">Команда</div>
|
||||
<div class="hotKeys col q-pa-sm bg-blue-2 row no-wrap">
|
||||
<div style="width: 80px">Сочетание клавиш</div>
|
||||
<q-input ref="input" class="q-ml-sm col"
|
||||
outlined dense rounded
|
||||
bg-color="grey-4"
|
||||
placeholder="Найти"
|
||||
v-model="search"
|
||||
@click.stop
|
||||
/>
|
||||
<div v-show="!readonly" class="q-ml-sm column justify-center">
|
||||
<q-btn class="bg-grey-4 text-grey-6" style="height: 35px; width: 35px" rounded flat icon="la la-broom" @click="defaultHotKeyAll">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Установить все сочетания по умолчанию
|
||||
</q-tooltip>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- body -->
|
||||
<div class="table-row row" v-for="(action, index) in tableData" :key="index">
|
||||
<div class="desc q-pa-sm">{{ rstore.readerActions[action] }}</div>
|
||||
<div class="hotKeys col q-pa-sm">
|
||||
<q-chip
|
||||
:color="collisions[code] ? 'red' : 'grey-7'"
|
||||
:removable="!readonly" :clickable="collisions[code] ? true : false"
|
||||
text-color="white" v-for="(code, index) in value[action]" :key="index" @remove="removeCode(action, code)"
|
||||
@click="collisionWarning(code)"
|
||||
>
|
||||
{{ code }}
|
||||
</q-chip>
|
||||
</div>
|
||||
<div v-show="!readonly" class="column q-pa-xs">
|
||||
<q-icon
|
||||
name="la la-plus-circle"
|
||||
class="button bg-green-8 text-white"
|
||||
@click="addHotKey(action)"
|
||||
v-ripple
|
||||
:disabled="value[action].length >= maxCodesLength"
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Добавить сочетание клавиш
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
<q-icon
|
||||
name="la la-broom"
|
||||
class="button text-grey-5"
|
||||
@click="defaultHotKey(action)"
|
||||
v-ripple
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
По умолчанию
|
||||
</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import rstore from '../../../../store/modules/reader';
|
||||
//import * as utils from '../../share/utils';
|
||||
|
||||
const UserHotKeysProps = Vue.extend({
|
||||
props: {
|
||||
value: Object,
|
||||
readonly: Boolean,
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
search: function() {
|
||||
this.updateTableData();
|
||||
},
|
||||
value: function() {
|
||||
this.checkCollisions();
|
||||
this.updateTableData();
|
||||
}
|
||||
},
|
||||
})
|
||||
class UserHotKeys extends UserHotKeysProps {
|
||||
search = '';
|
||||
rstore = {};
|
||||
tableData = [];
|
||||
collisions = {};
|
||||
maxCodesLength = 10;
|
||||
|
||||
created() {
|
||||
this.rstore = rstore;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.checkCollisions();
|
||||
this.updateTableData();
|
||||
}
|
||||
|
||||
updateTableData() {
|
||||
let result = rstore.hotKeys.map(hk => hk.name);
|
||||
|
||||
const search = this.search.toLowerCase();
|
||||
const codesIncludeSearch = (action) => {
|
||||
for (const code of this.value[action]) {
|
||||
if (code.toLowerCase().includes(search))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
result = result.filter(item => {
|
||||
return !search ||
|
||||
rstore.readerActions[item].toLowerCase().includes(search) ||
|
||||
codesIncludeSearch(item)
|
||||
});
|
||||
|
||||
this.tableData = result;
|
||||
}
|
||||
|
||||
checkCollisions() {
|
||||
const cols = {};
|
||||
for (const [action, codes] of Object.entries(this.value)) {
|
||||
codes.forEach(code => {
|
||||
if (!cols[code])
|
||||
cols[code] = [];
|
||||
if (cols[code].indexOf(action) < 0)
|
||||
cols[code].push(action);
|
||||
});
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const [code, actions] of Object.entries(cols)) {
|
||||
if (actions.length > 1)
|
||||
result[code] = actions;
|
||||
}
|
||||
|
||||
this.collisions = result;
|
||||
}
|
||||
|
||||
collisionWarning(code) {
|
||||
if (this.collisions[code]) {
|
||||
const descs = this.collisions[code].map(action => `<b>${rstore.readerActions[action]}</b>`);
|
||||
this.$root.stdDialog.alert(`Сочетание '${code}' одновременно назначено<br>следующим командам:<br>${descs.join('<br>')}<br><br>
|
||||
Возможно неожиданное поведение.`, 'Предупреждение');
|
||||
}
|
||||
}
|
||||
|
||||
removeCode(action, code) {
|
||||
let codes = Array.from(this.value[action]);
|
||||
const index = codes.indexOf(code);
|
||||
if (index >= 0) {
|
||||
codes.splice(index, 1);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
}
|
||||
}
|
||||
|
||||
async addHotKey(action) {
|
||||
if (this.value[action].length >= this.maxCodesLength)
|
||||
return;
|
||||
try {
|
||||
const result = await this.$root.stdDialog.getHotKey(`Добавить сочетание для:<br><b>${rstore.readerActions[action]}</b>`, '');
|
||||
if (result) {
|
||||
let codes = Array.from(this.value[action]);
|
||||
if (codes.indexOf(result) < 0) {
|
||||
codes.push(result);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
this.$nextTick(() => {
|
||||
this.collisionWarning(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
async defaultHotKey(action) {
|
||||
try {
|
||||
if (await this.$root.stdDialog.confirm(`Подтвердите сброс сочетаний клавиш<br>в значения по умолчанию для команды:<br><b>${rstore.readerActions[action]}</b>`, ' ')) {
|
||||
const codes = Array.from(rstore.settingDefaults.userHotKeys[action]);
|
||||
const newValue = Object.assign({}, this.value, {[action]: codes});
|
||||
this.$emit('input', newValue);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
async defaultHotKeyAll() {
|
||||
try {
|
||||
if (await this.$root.stdDialog.confirm('Подтвердите сброс сочетаний клавиш<br>для ВСЕХ команд в значения по умолчанию:', ' ')) {
|
||||
const newValue = Object.assign({}, rstore.settingDefaults.userHotKeys);
|
||||
this.$emit('input', newValue);
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.table {
|
||||
border-left: 1px solid grey;
|
||||
border-top: 1px solid grey;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
border-right: 1px solid grey;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
.table-row:nth-child(even) {
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.desc {
|
||||
width: 130px;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.hotKeys {
|
||||
border-left: 1px solid grey;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 25px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
17
client/components/Reader/SettingsPage/defPalette.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const defPalette = [
|
||||
'rgb(255,204,204)', 'rgb(255,230,204)', 'rgb(255,255,204)', 'rgb(204,255,204)', 'rgb(204,255,230)',
|
||||
'rgb(204,255,255)', 'rgb(204,230,255)', 'rgb(204,204,255)', 'rgb(230,204,255)', 'rgb(255,204,255)',
|
||||
'rgb(255,153,153)', 'rgb(255,204,153)', 'rgb(255,255,153)', 'rgb(153,255,153)', 'rgb(153,255,204)',
|
||||
'rgb(153,255,255)', 'rgb(153,204,255)', 'rgb(153,153,255)', 'rgb(204,153,255)', 'rgb(255,153,255)',
|
||||
'rgb(255,102,102)', 'rgb(255,179,102)', 'rgb(255,255,102)', 'rgb(102,255,102)', 'rgb(102,255,179)',
|
||||
'rgb(102,255,255)', 'rgb(102,179,255)', 'rgb(102,102,255)', 'rgb(179,102,255)', 'rgb(255,102,255)',
|
||||
'rgb(255,51,51)', 'rgb(255,153,51)', 'rgb(255,255,51)', 'rgb(51,255,51)', 'rgb(51,255,153)', 'rgb(51,255,255)', 'rgb(51,153,255)', 'rgb(51,51,255)', 'rgb(153,51,255)', 'rgb(255,51,255)',
|
||||
'rgb(255,0,0)', 'rgb(255,128,0)', 'rgb(255,255,0)', 'rgb(0,255,0)', 'rgb(0,255,128)', 'rgb(0,255,255)', 'rgb(0,128,255)', 'rgb(0,0,255)', 'rgb(128,0,255)', 'rgb(255,0,255)',
|
||||
'rgb(245,0,0)', 'rgb(245,123,0)', 'rgb(245,245,0)', 'rgb(0,245,0)', 'rgb(0,245,123)', 'rgb(0,245,245)', 'rgb(0,123,245)', 'rgb(0,0,245)', 'rgb(123,0,245)', 'rgb(245,0,245)',
|
||||
'rgb(214,0,0)', 'rgb(214,108,0)', 'rgb(214,214,0)', 'rgb(0,214,0)', 'rgb(0,214,108)', 'rgb(0,214,214)', 'rgb(0,108,214)', 'rgb(0,0,214)', 'rgb(108,0,214)', 'rgb(214,0,214)',
|
||||
'rgb(163,0,0)', 'rgb(163,82,0)', 'rgb(163,163,0)', 'rgb(0,163,0)', 'rgb(0,163,82)', 'rgb(0,163,163)', 'rgb(0,82,163)', 'rgb(0,0,163)', 'rgb(82,0,163)', 'rgb(163,0,163)',
|
||||
'rgb(92,0,0)', 'rgb(92,46,0)', 'rgb(92,92,0)', 'rgb(0,92,0)', 'rgb(0,92,46)', 'rgb(0,92,92)', 'rgb(0,46,92)', 'rgb(0,0,92)', 'rgb(46,0,92)', 'rgb(92,0,92)',
|
||||
'rgb(255,255,255)', 'rgb(205,205,205)', 'rgb(178,178,178)', 'rgb(153,153,153)', 'rgb(127,127,127)', 'rgb(102,102,102)', 'rgb(76,76,76)', 'rgb(51,51,51)', 'rgb(25,25,25)', 'rgb(0,0,0)'
|
||||
];
|
||||
|
||||
export default defPalette;
|
||||
@@ -0,0 +1,8 @@
|
||||
<div class="part-header">Показывать кнопки панели</div>
|
||||
|
||||
<div class="item row" v-for="item in toolButtons" :key="item.name">
|
||||
<div class="label-3"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox size="xs" @input="changeShowToolButton(item.name)" :value="showToolButton[item.name]" :label="rstore.readerActions[item.name]" />
|
||||
</div>
|
||||
</div>
|
||||
33
client/components/Reader/SettingsPage/include/KeysTab.inc
Normal file
33
client/components/Reader/SettingsPage/include/KeysTab.inc
Normal file
@@ -0,0 +1,33 @@
|
||||
<div class="bg-grey-3 row">
|
||||
<q-tabs
|
||||
v-model="selectedKeysTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="mouse" label="Мышь/тачскрин" />
|
||||
<q-tab name="keyboard" label="Клавиатура" />
|
||||
</q-tabs>
|
||||
</div>
|
||||
|
||||
<div class="q-mb-sm"/>
|
||||
|
||||
<div class="col tab-panel">
|
||||
<div v-if="selectedKeysTab == 'mouse'">
|
||||
<div class="item row">
|
||||
<div class="label-4"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox size="xs" v-model="clickControl" label="Включить управление кликом" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedKeysTab == 'keyboard'">
|
||||
<div class="item row">
|
||||
<UserHotKeys v-model="userHotKeys" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
107
client/components/Reader/SettingsPage/include/OthersTab.inc
Normal file
@@ -0,0 +1,107 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Подсказки, уведомления</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-6">Подсказка</div>
|
||||
<q-checkbox size="xs" v-model="showClickMapPage" label="Показывать области управления кликом" :disable="!clickControl" >
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать или нет подсказку при каждой загрузке книги
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Подсказка</div>
|
||||
<q-checkbox size="xs" v-model="blinkCachedLoad" label="Предупреждать о загрузке из кэша">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Мерцать сообщением в строке статуса и на кнопке<br>
|
||||
обновления при загрузке книги из кэша
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row no-wrap">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showServerStorageMessages" label="Показывать сообщения синхронизации">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомления и ошибки от<br>
|
||||
синхронизатора данных с сервером
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showWhatsNewDialog">
|
||||
Показывать уведомление "Что нового"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомления "Что нового"<br>
|
||||
при каждом выходе новой версии читалки
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Уведомление</div>
|
||||
<q-checkbox size="xs" v-model="showDonationDialog2020">
|
||||
Показывать "Оплатим хостинг вместе"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Показывать уведомление "Оплатим хостинг вместе"
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="enableSitesFilter" @input="needTextReload" size="xs" label="Включить html-фильтр для сайтов">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Html-фильтр вырезает лишние элементы со<br>
|
||||
страницы для определенных сайтов, таких как:<br>
|
||||
samlib.ru<br>
|
||||
www.fanfiction.net<br>
|
||||
archiveofourown.org<br>
|
||||
и других
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Обработка</div>
|
||||
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Включение этой опции позволяет делать предварительную<br>
|
||||
подготовку всего текста в ленивом режиме сразу после<br>
|
||||
загрузки книги. Это может повысить отзывчивость читалки,<br>
|
||||
но нагружает процессор каждый раз при открытии книги.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Парам. в URL</div>
|
||||
<q-checkbox size="xs" v-model="allowUrlParamBookPos">
|
||||
Добавлять параметр "__p"
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Добавление параметра "__p" в строке браузера<br>
|
||||
позволяет передавать ссылку на книгу в читалке<br>
|
||||
без потери текущей позиции. Однако в этом случае<br>
|
||||
при листании забивается история браузера, т.к. на<br>
|
||||
каждое изменение позиции происходит смена URL.
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-6">Копирование</div>
|
||||
<q-checkbox size="xs" v-model="copyFullText" label="Загружать весь текст">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Загружать весь текст в окно<br>
|
||||
копирования текста со страницы
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Анимация</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Тип</div>
|
||||
<q-select class="col-left" v-model="pageChangeAnimation" :options="pageChangeAnimationOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Скорость</div>
|
||||
<NumInput class="col-left" v-model="pageChangeAnimationSpeed" :min="0" :max="100" :disable="pageChangeAnimation == ''"/>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Другое</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-5">Страница</div>
|
||||
<q-checkbox v-model="keepLastToFirst" size="xs" label="Переносить последнюю строку">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Переносить последнюю строку страницы<br>
|
||||
в начало следующей при листании
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
101
client/components/Reader/SettingsPage/include/ProfilesTab.inc
Normal file
101
client/components/Reader/SettingsPage/include/ProfilesTab.inc
Normal file
@@ -0,0 +1,101 @@
|
||||
<div class="part-header">Управление синхронизацией данных</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-checkbox class="col" v-model="serverSyncEnabled" size="xs" label="Включить синхронизацию с сервером" />
|
||||
</div>
|
||||
|
||||
<div v-show="serverSyncEnabled">
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Профили устройств</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div class="text col">
|
||||
Выберите или добавьте профиль устройства, чтобы начать синхронизацию настроек с сервером.
|
||||
<br>При выборе "Нет" синхронизация настроек (но не книг) отключается.
|
||||
</div>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1">Устройство</div>
|
||||
<div class="col">
|
||||
<q-select v-model="currentProfile" :options="currentProfileOptions"
|
||||
style="width: 275px"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" dense no-caps @click="addProfile">Добавить</q-btn>
|
||||
<q-btn class="button" dense no-caps @click="delProfile">Удалить</q-btn>
|
||||
<q-btn class="button" dense no-caps @click="delAllProfiles">Удалить все</q-btn>
|
||||
</div>
|
||||
|
||||
<!---------------------------------------------->
|
||||
<div class="part-header">Ключ доступа</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div class="text col">
|
||||
Ключ доступа позволяет восстановить профили с настройками и список читаемых книг.
|
||||
Для этого необходимо передать ключ на новое устройство через почту, мессенджер или другим способом.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" style="width: 250px" dense no-caps @click="showServerStorageKey">
|
||||
<span v-show="serverStorageKeyVisible">Скрыть</span>
|
||||
<span v-show="!serverStorageKeyVisible">Показать</span>
|
||||
ключ доступа
|
||||
</q-btn>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div v-if="!serverStorageKeyVisible" class="col">
|
||||
<hr/>
|
||||
<b>{{ partialStorageKey }}</b> (часть вашего ключа)
|
||||
<hr/>
|
||||
</div>
|
||||
<div v-else class="col" style="line-height: 100%">
|
||||
<hr/>
|
||||
<div style="width: 300px; padding-top: 5px; overflow-wrap: break-word;">
|
||||
<b>{{ serverStorageKey }}</b>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(serverStorageKey, 'Ключ')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
<div v-if="mode == 'omnireader'">
|
||||
<br>Переход по ссылке позволит автоматически ввести ключ доступа:
|
||||
<br><div class="text-center" style="margin-top: 5px">
|
||||
<a :href="setStorageKeyLink" target="_blank">Ссылка для ввода ключа</a>
|
||||
<q-icon class="copy-icon" name="la la-copy" @click="copyToClip(setStorageKeyLink, 'Ссылка')">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||
</q-icon>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" style="width: 250px" dense no-caps @click="enterServerStorageKey">Ввести ключ доступа</q-btn>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<q-btn class="button" style="width: 250px" dense no-caps @click="generateServerStorageKey">Сгенерировать новый ключ</q-btn>
|
||||
</div>
|
||||
<div class="item row">
|
||||
<div class="label-1"></div>
|
||||
<div class="text col">
|
||||
Рекомендуется сохранить ключ в надежном месте, чтобы всегда иметь возможность восстановить настройки,
|
||||
например, после переустановки ОС или чистки/смены браузера.<br>
|
||||
<b>ПРЕДУПРЕЖДЕНИЕ!</b> При утере ключа, НИКТО не сможет восстановить ваши данные, т.к. они сжимаются
|
||||
и шифруются ключом доступа перед отправкой на сервер.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="item row">
|
||||
<q-btn class="col q-ma-sm" dense no-caps @click="setDefaults">Установить по умолчанию</q-btn>
|
||||
</div>
|
||||
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
34
client/components/Reader/SettingsPage/include/ViewTab.inc
Normal file
@@ -0,0 +1,34 @@
|
||||
<q-tabs
|
||||
v-model="selectedViewTab"
|
||||
active-color="black"
|
||||
active-bg-color="white"
|
||||
indicator-color="white"
|
||||
dense
|
||||
no-caps
|
||||
class="no-mp bg-grey-4 text-grey-7"
|
||||
>
|
||||
<q-tab name="color" label="Цвет" />
|
||||
<q-tab name="font" label="Шрифт" />
|
||||
<q-tab name="text" label="Текст" />
|
||||
<q-tab name="status" label="Строка статуса" />
|
||||
</q-tabs>
|
||||
|
||||
<div class="q-mb-sm"/>
|
||||
|
||||
<div class="col tab-panel">
|
||||
<div v-if="selectedViewTab == 'color'">
|
||||
@@include('./ViewTab/Color.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'font'">
|
||||
@@include('./ViewTab/Font.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'text'">
|
||||
@@include('./ViewTab/Text.inc');
|
||||
</div>
|
||||
|
||||
<div v-if="selectedViewTab == 'status'">
|
||||
@@include('./ViewTab/Status.inc');
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Цвет</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Текст</div>
|
||||
<div class="col row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="textColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('text')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="textColor"
|
||||
no-header default-view="palette" :palette="predefineTextColors"
|
||||
/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mt-md"/>
|
||||
<div class="item row">
|
||||
<div class="label-2">Фон</div>
|
||||
<div class="col row">
|
||||
<q-input class="col-left no-mp"
|
||||
outlined dense
|
||||
v-model="bgColorFiltered"
|
||||
:rules="['hexColor']"
|
||||
style="max-width: 150px"
|
||||
:disable="wallpaper != ''"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
||||
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||
<div>
|
||||
<q-color v-model="backgroundColor" no-header default-view="palette" :palette="predefineBackgroundColors"/>
|
||||
</div>
|
||||
</q-popup-proxy>
|
||||
</q-icon>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="wallpaper" :options="wallpaperOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Шрифт</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Локальный/веб</div>
|
||||
<div class="col row">
|
||||
<q-select class="col-left" v-model="fontName" :options="fontsOptions" :disable="webFontName != ''"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="webFontName" :options="webFontsOptions"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Веб шрифты дают большое разнообразие,<br>
|
||||
однако есть шанс, что шрифт будет загружаться<br>
|
||||
очень медленно или вовсе не загрузится
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Размер</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="fontSize" :min="5" :max="200"/>
|
||||
|
||||
<div class="col q-pt-xs text-right">
|
||||
<a href="https://fonts.google.com/?subset=cyrillic" target="_blank">Примеры</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Сдвиг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="vertShift" :min="-100" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сдвиг шрифта по вертикали в процентах от размера.<br>
|
||||
Отрицательное значение сдвигает вверх, положительное -<br>
|
||||
вниз. Значение зависит от метрики шрифта.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Стиль</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="fontBold" size="xs" label="Жирный" />
|
||||
<q-checkbox class="q-ml-sm" v-model="fontItalic" size="xs" label="Курсив" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,36 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Строка статуса</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Статус</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
|
||||
<q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Высота</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Прозрачность</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
По клику на автора-название в строке статуса<br>
|
||||
открывать оригинал произведения в новой вкладке
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
144
client/components/Reader/SettingsPage/include/ViewTab/Text.inc
Normal file
144
client/components/Reader/SettingsPage/include/ViewTab/Text.inc
Normal file
@@ -0,0 +1,144 @@
|
||||
<!---------------------------------------------->
|
||||
<div class="hidden part-header">Текст</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Интервал</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="lineInterval" :min="0" :max="200"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Параграф</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="p" :min="0" :max="2000"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Отступ</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="indentLR" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Слева/справа
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="indentTB" :min="0" :max="2000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сверху/снизу
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Сдвиг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="textVertShift" :min="-100" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Сдвиг текста по вертикали в процентах от размера шрифта.<br>
|
||||
Отрицательное значение сдвигает вверх, положительное -<br>
|
||||
вниз.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Скроллинг</div>
|
||||
<div class="col row">
|
||||
<NumInput class="col-left" v-model="scrollingDelay" :min="1" :max="10000">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Замедление скроллинга в миллисекундах.<br>
|
||||
Определяет время, за которое текст<br>
|
||||
прокручивается на одну строку.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
|
||||
<div class="q-px-sm"/>
|
||||
<q-select class="col" v-model="scrollingType" :options="['linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out']"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
>
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Вид скроллинга: линейный,<br>
|
||||
ускорение-замедление и пр.
|
||||
</q-tooltip>
|
||||
</q-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Выравнивание</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="textAlignJustify" size="xs" label="По ширине" />
|
||||
<q-checkbox class="q-ml-sm" v-model="wordWrap" size="xs" label="Перенос по слогам" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Компактность
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="compactTextPerc" :min="0" :max="100">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Степень компактности текста в процентах.<br>
|
||||
Чем больше компактность, тем хуже выравнивание<br>
|
||||
по правому краю.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Обработка</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="cutEmptyParagraphs" size="xs" label="Убирать пустые строки" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Добавлять пустые
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="addEmptyParagraphs" :min="0" :max="2"/>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2">Изображения</div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="showImages" size="xs" label="Показывать" />
|
||||
<q-checkbox class="q-ml-sm" v-model="showInlineImagesInCenter" @input="needReload" :disable="!showImages" size="xs" label="Инлайн в центр">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Выносить все изображения в центр экрана
|
||||
</q-tooltip>
|
||||
</q-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col row">
|
||||
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item row">
|
||||
<div class="label-2"></div>
|
||||
<div class="col-left column justify-center text-right">
|
||||
Высота не более
|
||||
</div>
|
||||
<div class="q-px-sm"/>
|
||||
<NumInput class="col" v-model="imageHeightLines" :min="1" :max="100" :disable="!showImages">
|
||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||
Определяет высоту изображения количеством строк.<br>
|
||||
В случае превышения высоты, изображение будет<br>
|
||||
уменьшено с сохранением пропорций так, чтобы<br>
|
||||
помещаться в указанное количество строк.
|
||||
</q-tooltip>
|
||||
</NumInput>
|
||||
</div>
|
||||
@@ -21,11 +21,12 @@
|
||||
@wheel.prevent.stop="onMouseWheel"
|
||||
@touchstart.stop="onTouchStart" @touchend.stop="onTouchEnd" @touchmove.stop="onTouchMove" @touchcancel.prevent.stop="onTouchCancel"
|
||||
oncontextmenu="return false;">
|
||||
<div v-show="showStatusBar" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
<div v-show="showStatusBar && statusBarClickOpen" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
</div>
|
||||
<div v-show="!clickControl && showStatusBar" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick"></div>
|
||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||
@click.prevent.stop="onStatusBarClick">
|
||||
</div>
|
||||
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
|
||||
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
||||
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
||||
@@ -38,8 +39,8 @@ import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
import {loadCSS} from 'fg-loadcss';
|
||||
import _ from 'lodash';
|
||||
import {sleep} from '../../../share/utils';
|
||||
|
||||
import {sleep} from '../../../share/utils';
|
||||
import bookManager from '../share/bookManager';
|
||||
import DrawHelper from './DrawHelper';
|
||||
import rstore from '../../../store/modules/reader';
|
||||
@@ -131,7 +132,11 @@ class TextPage extends Vue {
|
||||
await this.doPageAnimation();
|
||||
}, 10);
|
||||
|
||||
this.$root.$on('resize', () => {this.$nextTick(this.onResize)});
|
||||
this.$root.$on('resize', async() => {
|
||||
this.$nextTick(this.onResize);
|
||||
await sleep(500);
|
||||
this.$nextTick(this.onResize);
|
||||
});
|
||||
}
|
||||
|
||||
mounted() {
|
||||
@@ -242,6 +247,9 @@ class TextPage extends Vue {
|
||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
||||
this.parsed.imageFitWidth = this.imageFitWidth;
|
||||
this.parsed.compactTextPerc = this.compactTextPerc;
|
||||
|
||||
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
|
||||
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
|
||||
}
|
||||
|
||||
//scrolling page
|
||||
@@ -268,25 +276,18 @@ class TextPage extends Vue {
|
||||
async checkLoadedFonts() {
|
||||
let loaded = await Promise.all(this.fontList.map(font => document.fonts.check(font)));
|
||||
if (loaded.some(r => !r)) {
|
||||
loaded = await Promise.all(this.fontList.map(font => document.fonts.load(font)));
|
||||
if (loaded.some(r => !r.length))
|
||||
throw new Error('some font not loaded');
|
||||
await Promise.all(this.fontList.map(font => document.fonts.load(font)));
|
||||
}
|
||||
}
|
||||
|
||||
async loadFonts() {
|
||||
this.fontsLoading = true;
|
||||
|
||||
let inst = null;
|
||||
let close = null;
|
||||
(async() => {
|
||||
await sleep(500);
|
||||
if (this.fontsLoading)
|
||||
inst = this.$notify({
|
||||
title: '',
|
||||
dangerouslyUseHTMLString: true,
|
||||
message: 'Загрузка шрифта <i class="el-icon-loading"></i>',
|
||||
duration: 0
|
||||
});
|
||||
close = this.$root.notify.info('Загрузка шрифта <i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
|
||||
})();
|
||||
|
||||
if (!this.fontsLoaded)
|
||||
@@ -298,29 +299,15 @@ class TextPage extends Vue {
|
||||
this.fontsLoaded[this.fontCssUrl] = 1;
|
||||
}
|
||||
|
||||
const waitingTime = 10*1000;
|
||||
const delay = 100;
|
||||
let i = 0;
|
||||
//ждем шрифты
|
||||
while (i < waitingTime/delay) {
|
||||
i++;
|
||||
try {
|
||||
await this.checkLoadedFonts();
|
||||
i = waitingTime;
|
||||
} catch (e) {
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
if (i !== waitingTime) {
|
||||
this.$notify.error({
|
||||
title: 'Ошибка загрузки',
|
||||
message: 'Некоторые шрифты не удалось загрузить'
|
||||
});
|
||||
try {
|
||||
await this.checkLoadedFonts();
|
||||
} catch (e) {
|
||||
this.$root.notify.error('Некоторые шрифты не удалось загрузить', 'Ошибка загрузки');
|
||||
}
|
||||
|
||||
this.fontsLoading = false;
|
||||
if (inst)
|
||||
inst.close();
|
||||
if (close)
|
||||
close();
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
@@ -351,11 +338,15 @@ class TextPage extends Vue {
|
||||
// ширина шрифта некоторое время выдается неверно, поэтому
|
||||
if (!omitLoadFonts) {
|
||||
const parsed = this.parsed;
|
||||
await sleep(100);
|
||||
|
||||
let i = 0;
|
||||
const t = this.parsed.testText;
|
||||
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
|
||||
await sleep(100);
|
||||
|
||||
if (this.parsed === parsed) {
|
||||
parsed.force = true;
|
||||
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
||||
this.draw();
|
||||
parsed.force = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -432,7 +423,7 @@ class TextPage extends Vue {
|
||||
if (this.lazyParseEnabled)
|
||||
this.lazyParsePara();
|
||||
} catch (e) {
|
||||
this.$alert(e.message, 'Ошибка', {type: 'error'});
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||
}
|
||||
})();
|
||||
}
|
||||
@@ -457,13 +448,13 @@ class TextPage extends Vue {
|
||||
}
|
||||
|
||||
async onResize() {
|
||||
/*this.page1 = null;
|
||||
this.page2 = null;
|
||||
this.statusBar = null;*/
|
||||
|
||||
this.calcDrawProps();
|
||||
this.setBackground();
|
||||
this.draw();
|
||||
try {
|
||||
this.calcDrawProps();
|
||||
this.setBackground();
|
||||
this.draw();
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
get settings() {
|
||||
@@ -513,7 +504,7 @@ class TextPage extends Vue {
|
||||
async startTextScrolling() {
|
||||
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
|
||||
this.linesDown.length <= this.pageLineCount) {
|
||||
this.$emit('stop-scrolling');
|
||||
this.doStopScrolling();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -554,7 +545,7 @@ class TextPage extends Vue {
|
||||
}
|
||||
this.resolveTransition1Finish = null;
|
||||
this.doingScrolling = false;
|
||||
this.$emit('stop-scrolling');
|
||||
this.doStopScrolling();
|
||||
this.draw();
|
||||
}
|
||||
|
||||
@@ -893,22 +884,26 @@ class TextPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
doToolBarToggle() {
|
||||
this.$emit('tool-bar-toggle');
|
||||
doToolBarToggle(event) {
|
||||
this.$emit('do-action', {action: 'switchToolbar', event});
|
||||
}
|
||||
|
||||
doScrollingToggle() {
|
||||
this.$emit('scrolling-toggle');
|
||||
this.$emit('do-action', {action: 'scrolling', event});
|
||||
}
|
||||
|
||||
doFullScreenToggle() {
|
||||
this.$emit('full-screen-toogle');
|
||||
this.$emit('do-action', {action: 'fullScreen', event});
|
||||
}
|
||||
|
||||
doStopScrolling() {
|
||||
this.$emit('do-action', {action: 'stopScrolling', event});
|
||||
}
|
||||
|
||||
async doFontSizeInc() {
|
||||
if (!this.settingsChanging) {
|
||||
this.settingsChanging = true;
|
||||
const newSize = (this.settings.fontSize + 1 < 100 ? this.settings.fontSize + 1 : 100);
|
||||
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
|
||||
const newSettings = Object.assign({}, this.settings, {fontSize: newSize});
|
||||
this.commit('reader/setSettings', newSettings);
|
||||
await sleep(50);
|
||||
@@ -949,69 +944,6 @@ class TextPage extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
let result = false;
|
||||
if (event.type == 'keydown' && !event.ctrlKey && !event.altKey) {
|
||||
result = true;
|
||||
switch (event.code) {
|
||||
case 'ArrowDown':
|
||||
if (event.shiftKey)
|
||||
this.doScrollingSpeedUp();
|
||||
else
|
||||
this.doDown();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (event.shiftKey)
|
||||
this.doScrollingSpeedDown();
|
||||
else
|
||||
this.doUp();
|
||||
break;
|
||||
case 'PageDown':
|
||||
case 'ArrowRight':
|
||||
this.doPageDown();
|
||||
break;
|
||||
case 'Space':
|
||||
if (event.shiftKey)
|
||||
this.doPageUp();
|
||||
else
|
||||
this.doPageDown();
|
||||
break;
|
||||
case 'PageUp':
|
||||
case 'ArrowLeft':
|
||||
case 'Backspace':
|
||||
this.doPageUp();
|
||||
break;
|
||||
case 'Home':
|
||||
this.doHome();
|
||||
break;
|
||||
case 'End':
|
||||
this.doEnd();
|
||||
break;
|
||||
case 'KeyA':
|
||||
if (event.shiftKey)
|
||||
this.doFontSizeDec();
|
||||
else
|
||||
this.doFontSizeInc();
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Backquote'://`
|
||||
case 'KeyF':
|
||||
this.doFullScreenToggle();
|
||||
break;
|
||||
case 'Tab':
|
||||
case 'KeyQ':
|
||||
this.doToolBarToggle();
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
break;
|
||||
default:
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async startClickRepeat(pointX, pointY) {
|
||||
this.repX = pointX;
|
||||
this.repY = pointY;
|
||||
@@ -1089,7 +1021,7 @@ class TextPage extends Vue {
|
||||
//движение вправо
|
||||
this.doScrollingSpeedUp();
|
||||
} else if (Math.abs(dy) < touchDelta && Math.abs(dx) < touchDelta) {
|
||||
this.doToolBarToggle();
|
||||
this.doToolBarToggle(event);
|
||||
}
|
||||
|
||||
this.startTouch = null;
|
||||
@@ -1116,7 +1048,7 @@ class TextPage extends Vue {
|
||||
} else if (event.button == 1) {
|
||||
this.doScrollingToggle();
|
||||
} else if (event.button == 2) {
|
||||
this.doToolBarToggle();
|
||||
this.doToolBarToggle(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1141,7 +1073,7 @@ class TextPage extends Vue {
|
||||
if (url && url.indexOf('file://') != 0) {
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
this.$alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска', '', {type: 'warning'});
|
||||
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -605,6 +605,7 @@ export default class BookParser {
|
||||
|
||||
if (!this.force &&
|
||||
para.parsed &&
|
||||
para.parsed.testWidth === this.testWidth &&
|
||||
para.parsed.w === this.w &&
|
||||
para.parsed.p === this.p &&
|
||||
para.parsed.wordWrap === this.wordWrap &&
|
||||
@@ -620,6 +621,7 @@ export default class BookParser {
|
||||
return para.parsed;
|
||||
|
||||
const parsed = {
|
||||
testWidth: this.testWidth,
|
||||
w: this.w,
|
||||
p: this.p,
|
||||
wordWrap: this.wordWrap,
|
||||
|
||||
@@ -1,4 +1,41 @@
|
||||
export const versionHistory = [
|
||||
{
|
||||
showUntil: '2020-04-25',
|
||||
header: '0.9.2 (2020-03-15)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>в настройки добавлена возможность назначать сочетания клавиш на команды в читалке</li>
|
||||
<li>переход на Service Worker вместо AppCache для автономного режима работы</li>
|
||||
<li>исправления багов</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-03-02',
|
||||
header: '0.9.1 (2020-03-03)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>улучшение работы серверной части</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-02-25',
|
||||
header: '0.9.0 (2020-02-26)',
|
||||
content:
|
||||
`
|
||||
<ul>
|
||||
<li>переход на UI-фреймфорк Quasar</li>
|
||||
<li>незначительные изменения интерфейса</li>
|
||||
</ul>
|
||||
`
|
||||
},
|
||||
|
||||
{
|
||||
showUntil: '2020-02-05',
|
||||
header: '0.8.4 (2020-02-06)',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Settings в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<div>
|
||||
Раздел Sources в разработке
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
64
client/components/share/Dialog.vue
Normal file
64
client/components/share/Dialog.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<q-dialog v-model="active">
|
||||
<div class="column bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col q-mx-md">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<div class="row justify-end q-pa-md">
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
const DialogProps = Vue.extend({
|
||||
props: {
|
||||
value: Boolean,
|
||||
}
|
||||
})
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Dialog extends DialogProps {
|
||||
get active() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
set active(value) {
|
||||
this.$emit('input', value);
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 110%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
||||
58
client/components/share/Notify.vue
Normal file
58
client/components/share/Notify.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="hidden"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
export default @Component({
|
||||
})
|
||||
class Notify extends Vue {
|
||||
notify(opts) {
|
||||
let {
|
||||
caption = null,
|
||||
captionColor = 'black',
|
||||
color = 'positive',
|
||||
icon = '',
|
||||
iconColor = 'white',
|
||||
message = '',
|
||||
messageColor = 'black',
|
||||
} = opts;
|
||||
|
||||
caption = (caption ? `<div style="font-size: 120%; color: ${captionColor}"><b>${caption}</b></div><br>` : '');
|
||||
return this.$q.notify({
|
||||
position: 'top-right',
|
||||
color,
|
||||
textColor: iconColor,
|
||||
icon,
|
||||
actions: [{icon: 'la la-times notify-button-icon', color: 'black'}],
|
||||
html: true,
|
||||
|
||||
message:
|
||||
`<div style="max-width: 350px;">
|
||||
${caption}
|
||||
<div style="color: ${messageColor}; overflow-wrap: break-word; word-wrap: break-word;">${message}</div>
|
||||
</div>`
|
||||
});
|
||||
}
|
||||
|
||||
success(message, caption) {
|
||||
this.notify({color: 'positive', icon: 'la la-check-circle', message, caption});
|
||||
}
|
||||
|
||||
warning(message, caption) {
|
||||
this.notify({color: 'warning', icon: 'la la-exclamation-circle', message, caption});
|
||||
}
|
||||
|
||||
error(message, caption) {
|
||||
this.notify({color: 'negative', icon: 'la la-exclamation-circle', messageColor: 'yellow', captionColor: 'white', message, caption});
|
||||
}
|
||||
|
||||
info(message, caption) {
|
||||
this.notify({color: 'info', icon: 'la la-bell', message, caption});
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
185
client/components/share/NumInput.vue
Normal file
185
client/components/share/NumInput.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<q-input outlined dense
|
||||
v-model="filteredValue"
|
||||
input-style="text-align: center"
|
||||
class="no-mp"
|
||||
:class="(error ? 'error' : '')"
|
||||
:disable="disable"
|
||||
>
|
||||
<slot></slot>
|
||||
<template v-slot:prepend>
|
||||
<q-icon :class="(validate(value - step) ? '' : 'disable')"
|
||||
name="la la-minus-circle"
|
||||
class="button"
|
||||
v-ripple="validate(value - step)"
|
||||
@click="minus"
|
||||
@mousedown.prevent.stop="onMouseDown($event, 'minus')"
|
||||
@mouseup.prevent.stop="onMouseUp"
|
||||
@mouseout.prevent.stop="onMouseUp"
|
||||
@touchstart.stop="onTouchStart($event, 'minus')"
|
||||
@touchend.stop="onTouchEnd"
|
||||
@touchcancel.prevent.stop="onTouchEnd"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<q-icon :class="(validate(value + step) ? '' : 'disable')"
|
||||
name="la la-plus-circle"
|
||||
class="button"
|
||||
v-ripple="validate(value + step)"
|
||||
@click="plus"
|
||||
@mousedown.prevent.stop="onMouseDown($event, 'plus')"
|
||||
@mouseup.prevent.stop="onMouseUp"
|
||||
@mouseout.prevent.stop="onMouseUp"
|
||||
@touchstart.stop="onTouchStart($event, 'plus')"
|
||||
@touchend.stop="onTouchEnd"
|
||||
@touchcancel.prevent.stop="onTouchEnd"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
const NumInputProps = Vue.extend({
|
||||
props: {
|
||||
value: Number,
|
||||
min: { type: Number, default: -Number.MAX_VALUE },
|
||||
max: { type: Number, default: Number.MAX_VALUE },
|
||||
step: { type: Number, default: 1 },
|
||||
digits: { type: Number, default: 0 },
|
||||
disable: Boolean
|
||||
}
|
||||
});
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
filteredValue: function(newValue) {
|
||||
if (this.validate(newValue)) {
|
||||
this.error = false;
|
||||
this.$emit('input', this.string2number(newValue));
|
||||
} else {
|
||||
this.error = true;
|
||||
}
|
||||
},
|
||||
value: function(newValue) {
|
||||
this.filteredValue = newValue;
|
||||
},
|
||||
}
|
||||
})
|
||||
class NumInput extends NumInputProps {
|
||||
filteredValue = 0;
|
||||
error = false;
|
||||
|
||||
created() {
|
||||
this.filteredValue = this.value;
|
||||
}
|
||||
|
||||
string2number(value) {
|
||||
return Number.parseFloat(Number.parseFloat(value).toFixed(this.digits));
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
let n = this.string2number(value);
|
||||
if (isNaN(n))
|
||||
return false;
|
||||
if (n < this.min)
|
||||
return false;
|
||||
if (n > this.max)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
plus() {
|
||||
const newValue = this.value + this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
|
||||
minus() {
|
||||
const newValue = this.value - this.step;
|
||||
if (this.validate(newValue))
|
||||
this.filteredValue = newValue;
|
||||
}
|
||||
|
||||
onMouseDown(event, way) {
|
||||
this.startClickRepeat = true;
|
||||
this.clickRepeat = false;
|
||||
|
||||
if (event.button == 0) {
|
||||
(async() => {
|
||||
await utils.sleep(300);
|
||||
if (this.startClickRepeat) {
|
||||
this.clickRepeat = true;
|
||||
while (this.clickRepeat) {
|
||||
if (way == 'plus') {
|
||||
this.plus();
|
||||
} else {
|
||||
this.minus();
|
||||
}
|
||||
await utils.sleep(50);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp() {
|
||||
if (this.inTouch)
|
||||
return;
|
||||
this.startClickRepeat = false;
|
||||
this.clickRepeat = false;
|
||||
}
|
||||
|
||||
onTouchStart(event, way) {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
if (event.touches.length == 1) {
|
||||
this.inTouch = true;
|
||||
this.onMouseDown({button: 0}, way);
|
||||
}
|
||||
}
|
||||
|
||||
onTouchEnd() {
|
||||
if (!this.$isMobileDevice)
|
||||
return;
|
||||
this.inTouch = false;
|
||||
this.onMouseUp();
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.no-mp {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-size: 130%;
|
||||
border-radius: 20px;
|
||||
color: #bbb;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
color: #616161;
|
||||
background-color: #efebe9;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #ffabab;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.disable, .disable:hover {
|
||||
cursor: not-allowed;
|
||||
color: #bbb;
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
324
client/components/share/StdDialog.vue
Normal file
324
client/components/share/StdDialog.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
|
||||
<slot></slot>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'alert'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md" dense no-caps @click="okClick">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'confirm'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'prompt'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
<q-input ref="input" class="q-mt-xs" outlined dense v-model="inputValue"/>
|
||||
<div class="error"><span v-show="error != ''">{{ error }}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--------------------------------------------------->
|
||||
<div v-show="type == 'hotKey'" class="bg-white no-wrap">
|
||||
<div class="header row">
|
||||
<div class="caption col row items-center q-ml-md">
|
||||
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" name="las la-exclamation-circle" size="28px"></q-icon>
|
||||
<div v-html="caption"></div>
|
||||
</div>
|
||||
<div class="close-icon column justify-center items-center">
|
||||
<q-btn flat round dense v-close-popup>
|
||||
<q-icon name="la la-times" size="18px"></q-icon>
|
||||
</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="q-mx-md">
|
||||
<div v-html="message"></div>
|
||||
<div class="q-my-md text-center">
|
||||
<div v-show="hotKeyCode == ''" class="text-grey-5">Нет</div>
|
||||
<div>{{ hotKeyCode }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="buttons row justify-end q-pa-md">
|
||||
<q-btn class="q-px-md q-ml-sm" dense no-caps v-close-popup>Отмена</q-btn>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick" :disabled="hotKeyCode == ''">OK</q-btn>
|
||||
</div>
|
||||
</div>
|
||||
</q-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import Vue from 'vue';
|
||||
import Component from 'vue-class-component';
|
||||
|
||||
import * as utils from '../../share/utils';
|
||||
|
||||
export default @Component({
|
||||
watch: {
|
||||
inputValue: function(newValue) {
|
||||
this.validate(newValue);
|
||||
},
|
||||
}
|
||||
})
|
||||
class StdDialog extends Vue {
|
||||
caption = '';
|
||||
message = '';
|
||||
active = false;
|
||||
type = '';
|
||||
inputValue = '';
|
||||
error = '';
|
||||
iconColor = '';
|
||||
hotKeyCode = '';
|
||||
|
||||
created() {
|
||||
if (this.$root.addKeyHook) {
|
||||
this.$root.addKeyHook(this.keyHook);
|
||||
}
|
||||
}
|
||||
|
||||
init(message, caption, opts) {
|
||||
this.caption = caption;
|
||||
this.message = message;
|
||||
|
||||
this.ok = false;
|
||||
this.type = '';
|
||||
this.inputValidator = null;
|
||||
this.inputValue = '';
|
||||
this.error = '';
|
||||
|
||||
this.iconColor = 'text-warning';
|
||||
if (opts && opts.color) {
|
||||
this.iconColor = `text-${opts.color}`;
|
||||
}
|
||||
|
||||
this.hotKeyCode = '';
|
||||
if (opts && opts.hotKeyCode) {
|
||||
this.hotKeyCode = opts.hotKeyCode;
|
||||
}
|
||||
}
|
||||
|
||||
onHide() {
|
||||
if (this.hideTrigger) {
|
||||
this.hideTrigger();
|
||||
this.hideTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
onShow() {
|
||||
if (this.type == 'prompt') {
|
||||
this.enableValidator = true;
|
||||
if (this.inputValue)
|
||||
this.validate(this.inputValue);
|
||||
this.$refs.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
if (!this.enableValidator)
|
||||
return false;
|
||||
|
||||
if (this.inputValidator) {
|
||||
const result = this.inputValidator(value);
|
||||
if (result !== true) {
|
||||
this.error = result;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.error = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
okClick() {
|
||||
if (this.type == 'prompt' && !this.validate(this.inputValue)) {
|
||||
this.$refs.dialog.shake();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type == 'hotKey' && this.hotKeyCode == '') {
|
||||
this.$refs.dialog.shake();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ok = true;
|
||||
this.$refs.dialog.hide();
|
||||
}
|
||||
|
||||
alert(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'alert';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
confirm(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'confirm';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
prompt(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.enableValidator = false;
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve({value: this.inputValue});
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'prompt';
|
||||
if (opts) {
|
||||
this.inputValidator = opts.inputValidator || null;
|
||||
this.inputValue = opts.inputValue || '';
|
||||
}
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
getHotKey(message, caption, opts) {
|
||||
return new Promise((resolve) => {
|
||||
this.init(message, caption, opts);
|
||||
|
||||
this.hideTrigger = () => {
|
||||
if (this.ok) {
|
||||
resolve(this.hotKeyCode);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
this.type = 'hotKey';
|
||||
this.active = true;
|
||||
});
|
||||
}
|
||||
|
||||
keyHook(event) {
|
||||
if (this.active) {
|
||||
if (this.type == 'hotKey') {
|
||||
if (event.type == 'keydown') {
|
||||
this.hotKeyCode = utils.keyEventToCode(event);
|
||||
}
|
||||
} else {
|
||||
if (event.code == 'Enter')
|
||||
this.okClick();
|
||||
if (event.code == 'Escape') {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dialog.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.caption {
|
||||
font-size: 110%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.error {
|
||||
height: 20px;
|
||||
font-size: 80%;
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div ref="main" class="main" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div ref="windowBox" class="windowBox" @click.stop>
|
||||
<div class="window">
|
||||
<div ref="header" class="header" @mousedown.prevent.stop="onMouseDown"
|
||||
<div ref="main" class="main xyfit absolute" @click="close" @mouseup="onMouseUp" @mousemove="onMouseMove">
|
||||
<div ref="windowBox" class="xyfit absolute flex no-wrap" @click.stop>
|
||||
<div class="window flexfit column no-wrap">
|
||||
<div ref="header" class="header row justify-end" @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>
|
||||
<span class="header-text col"><slot name="header"></slot></span>
|
||||
<span class="close-button row justify-center items-center" @mousedown.stop @click="close"><q-icon name="la la-times" size="16px"/></span>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
@@ -116,23 +117,20 @@ class Window extends Vue {
|
||||
|
||||
<style scoped>
|
||||
.main {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: transparent !important;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.windowBox {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
.xyfit {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.window {
|
||||
.flexfit {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.window {
|
||||
margin: 10px;
|
||||
background-color: #ffffff;
|
||||
border: 3px double black;
|
||||
@@ -141,23 +139,21 @@ class Window extends Vue {
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
background-color: #59B04F;
|
||||
background: linear-gradient(to bottom right, green, #59B04F);
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
color: yellow;
|
||||
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
@@ -166,4 +162,5 @@ class Window extends Vue {
|
||||
.close-button:hover {
|
||||
background-color: #69C05F;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,69 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
/*
|
||||
import ElementUI from 'element-ui';
|
||||
import './theme/index.css';
|
||||
import locale from 'element-ui/lib/locale/lang/ru-RU';
|
||||
|
||||
Vue.use(ElementUI, { locale });
|
||||
*/
|
||||
|
||||
//------------------------------------------------------
|
||||
import './theme/index.css';
|
||||
|
||||
import ElMenu from 'element-ui/lib/menu';
|
||||
import ElMenuItem from 'element-ui/lib/menu-item';
|
||||
import ElButton from 'element-ui/lib/button';
|
||||
import ElButtonGroup from 'element-ui/lib/button-group';
|
||||
import ElCheckbox from 'element-ui/lib/checkbox';
|
||||
import ElTabs from 'element-ui/lib/tabs';
|
||||
import ElTabPane from 'element-ui/lib/tab-pane';
|
||||
import ElTooltip from 'element-ui/lib/tooltip';
|
||||
import ElRow from 'element-ui/lib/row';
|
||||
import ElCol from 'element-ui/lib/col';
|
||||
import ElContainer from 'element-ui/lib/container';
|
||||
import ElAside from 'element-ui/lib/aside';
|
||||
import ElHeader from 'element-ui/lib/header';
|
||||
import ElMain from 'element-ui/lib/main';
|
||||
import ElInput from 'element-ui/lib/input';
|
||||
import ElInputNumber from 'element-ui/lib/input-number';
|
||||
import ElSelect from 'element-ui/lib/select';
|
||||
import ElOption from 'element-ui/lib/option';
|
||||
import ElTable from 'element-ui/lib/table';
|
||||
import ElTableColumn from 'element-ui/lib/table-column';
|
||||
import ElProgress from 'element-ui/lib/progress';
|
||||
import ElSlider from 'element-ui/lib/slider';
|
||||
import ElForm from 'element-ui/lib/form';
|
||||
import ElFormItem from 'element-ui/lib/form-item';
|
||||
import ElColorPicker from 'element-ui/lib/color-picker';
|
||||
import ElDialog from 'element-ui/lib/dialog';
|
||||
|
||||
import Notification from 'element-ui/lib/notification';
|
||||
import Loading from 'element-ui/lib/loading';
|
||||
import MessageBox from 'element-ui/lib/message-box';
|
||||
|
||||
const components = {
|
||||
ElMenu, ElMenuItem, ElButton, ElButtonGroup, ElCheckbox, ElTabs, ElTabPane, ElTooltip,
|
||||
ElRow, ElCol, ElContainer, ElAside, ElMain, ElHeader,
|
||||
ElInput, ElInputNumber, ElSelect, ElOption, ElTable, ElTableColumn,
|
||||
ElProgress, ElSlider, ElForm, ElFormItem,
|
||||
ElColorPicker, ElDialog,
|
||||
};
|
||||
|
||||
for (let name in components) {
|
||||
Vue.component(name, components[name]);
|
||||
}
|
||||
|
||||
//Vue.use(Loading.directive);
|
||||
|
||||
Vue.prototype.$loading = Loading.service;
|
||||
Vue.prototype.$msgbox = MessageBox;
|
||||
Vue.prototype.$alert = MessageBox.alert;
|
||||
Vue.prototype.$confirm = MessageBox.confirm;
|
||||
Vue.prototype.$prompt = MessageBox.prompt;
|
||||
Vue.prototype.$notify = Notification;
|
||||
//Vue.prototype.$message = Message;
|
||||
|
||||
import lang from 'element-ui/lib/locale/lang/ru-RU';
|
||||
import locale from 'element-ui/lib/locale';
|
||||
locale.use(lang);
|
||||
@@ -1,11 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html manifest="/app/manifest.appcache">
|
||||
<html>
|
||||
<head>
|
||||
<title></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<meta name="description" content="Браузерная онлайн-читалка книг. Поддерживаются форматы: fb2, html, txt, rtf, doc, docx, pdf, epub, mobi.">
|
||||
<meta name="keywords" content="онлайн,читалка,fb2,книги,читать,браузер,интернет">
|
||||
<title></title>
|
||||
<script src="/sw-register.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import Vue from 'vue';
|
||||
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './element';
|
||||
import './quasar';
|
||||
|
||||
import App from './components/App.vue';
|
||||
//Vue.config.productionTip = false;
|
||||
|
||||
89
client/quasar.js
Normal file
89
client/quasar.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import 'quasar/dist/quasar.css';
|
||||
import Quasar from 'quasar/src/vue-plugin.js'
|
||||
|
||||
//config
|
||||
const config = {};
|
||||
|
||||
//components
|
||||
//import {QLayout} from 'quasar/src/components/layout';
|
||||
//import {QPageContainer, QPage} from 'quasar/src/components/page';
|
||||
//import {QDrawer} from 'quasar/src/components/drawer';
|
||||
|
||||
import {QCircularProgress} from 'quasar/src/components/circular-progress';
|
||||
import {QInput} from 'quasar/src/components/input';
|
||||
import {QBtn} from 'quasar/src/components/btn';
|
||||
import {QBtnGroup} from 'quasar/src/components/btn-group';
|
||||
import {QBtnToggle} from 'quasar/src/components/btn-toggle';
|
||||
import {QIcon} from 'quasar/src/components/icon';
|
||||
import {QSlider} from 'quasar/src/components/slider';
|
||||
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
||||
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
||||
import {QSeparator} from 'quasar/src/components/separator';
|
||||
import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||
import {QTooltip} from 'quasar/src/components/tooltip';
|
||||
import {QSpinner} from 'quasar/src/components/spinner';
|
||||
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
||||
import {QCheckbox} from 'quasar/src/components/checkbox';
|
||||
import {QSelect} from 'quasar/src/components/select';
|
||||
import {QColor} from 'quasar/src/components/color';
|
||||
import {QPopupProxy} from 'quasar/src/components/popup-proxy';
|
||||
import {QDialog} from 'quasar/src/components/dialog';
|
||||
import {QChip} from 'quasar/src/components/chip';
|
||||
|
||||
const components = {
|
||||
//QLayout,
|
||||
//QPageContainer, QPage,
|
||||
//QDrawer,
|
||||
|
||||
QCircularProgress,
|
||||
QInput,
|
||||
QBtn,
|
||||
QBtnGroup,
|
||||
QBtnToggle,
|
||||
QIcon,
|
||||
QSlider,
|
||||
QTabs, QTab,
|
||||
//QTabPanels, QTabPanel,
|
||||
QSeparator,
|
||||
QList, QItem, QItemSection, QItemLabel,
|
||||
QTooltip,
|
||||
QSpinner,
|
||||
QTable, QTh, QTr, QTd,
|
||||
QCheckbox,
|
||||
QSelect,
|
||||
QColor,
|
||||
QPopupProxy,
|
||||
QDialog,
|
||||
QChip,
|
||||
};
|
||||
|
||||
//directives
|
||||
import Ripple from 'quasar/src/directives/Ripple';
|
||||
import ClosePopup from 'quasar/src/directives/ClosePopup';
|
||||
|
||||
const directives = {Ripple, ClosePopup};
|
||||
|
||||
//plugins
|
||||
import AppFullscreen from 'quasar/src/plugins/AppFullscreen';
|
||||
import Notify from 'quasar/src/plugins/Notify';
|
||||
|
||||
const plugins = {
|
||||
AppFullscreen,
|
||||
Notify,
|
||||
};
|
||||
|
||||
//use
|
||||
Vue.use(Quasar, { config, components, directives, plugins });
|
||||
|
||||
//icons
|
||||
//import '@quasar/extras/material-icons/material-icons.css';
|
||||
//import '@quasar/extras/material-icons-outlined/material-icons-outlined.css';
|
||||
//import '@quasar/extras/fontawesome-v5/fontawesome-v5.css';
|
||||
|
||||
import '@quasar/extras/line-awesome/line-awesome.css';
|
||||
|
||||
//import fontawesomeV5 from 'quasar/icon-set/fontawesome-v5.js'
|
||||
import lineAwesome from 'quasar/icon-set/line-awesome.js'
|
||||
Quasar.iconSet.set(lineAwesome);
|
||||
@@ -202,4 +202,38 @@ export function escapeXml(str) {
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
export function keyEventToCode(event) {
|
||||
let result = [];
|
||||
let code = event.code;
|
||||
|
||||
const modCode = code.substring(0, 3);
|
||||
if (event.metaKey && modCode != 'Met')
|
||||
result.push('Meta');
|
||||
if (event.ctrlKey && modCode != 'Con')
|
||||
result.push('Ctrl');
|
||||
if (event.shiftKey && modCode != 'Shi')
|
||||
result.push('Shift');
|
||||
if (event.altKey && modCode != 'Alt')
|
||||
result.push('Alt');
|
||||
|
||||
if (modCode == 'Dig') {
|
||||
code = code.substring(5, 6);
|
||||
} else if (modCode == 'Key') {
|
||||
code = code.substring(3, 4);
|
||||
}
|
||||
result.push(code);
|
||||
|
||||
return result.join('+');
|
||||
}
|
||||
|
||||
export function userHotKeysObjectSwap(userHotKeys) {
|
||||
let result = {};
|
||||
for (const [name, codes] of Object.entries(userHotKeys)) {
|
||||
for (const code of codes) {
|
||||
result[code] = name;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ Vue.use(Vuex);
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export default new Vuex.Store(Object.assign({}, root, {
|
||||
modules: {
|
||||
uistate,
|
||||
config,
|
||||
reader,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: [createPersistedState()]
|
||||
modules: {
|
||||
uistate,
|
||||
config,
|
||||
reader,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: [createPersistedState()]
|
||||
}));
|
||||
|
||||
@@ -1,15 +1,73 @@
|
||||
//занчение toolButtons.name не должно совпадать с settingDefaults-propertyName
|
||||
const readerActions = {
|
||||
'help': 'Вызвать cправку',
|
||||
'loader': 'На страницу загрузки',
|
||||
'settings': 'Настроить',
|
||||
'undoAction': 'Действие назад',
|
||||
'redoAction': 'Действие вперед',
|
||||
'fullScreen': 'На весь экран',
|
||||
'scrolling': 'Плавный скроллинг',
|
||||
'stopScrolling': '',
|
||||
'setPosition': 'Установить позицию',
|
||||
'search': 'Найти в тексте',
|
||||
'copyText': 'Скопировать текст со страницы',
|
||||
'refresh': 'Принудительно обновить книгу',
|
||||
'offlineMode': 'Автономный режим (без интернета)',
|
||||
'recentBooks': 'Открыть недавние',
|
||||
'switchToolbar': 'Показать/скрыть панель управления',
|
||||
'donate': '',
|
||||
'bookBegin': 'В начало книги',
|
||||
'bookEnd': 'В конец книги',
|
||||
'pageBack': 'Страницу назад',
|
||||
'pageForward': 'Страницу вперед',
|
||||
'lineBack': 'Строчку назад',
|
||||
'lineForward': 'Строчку вперед',
|
||||
'incFontSize': 'Увеличить размер шрифта',
|
||||
'decFontSize': 'Уменьшить размер шрифта',
|
||||
'scrollingSpeedUp': 'Увеличить скорость скроллинга',
|
||||
'scrollingSpeedDown': 'Уменьшить скорость скроллинга',
|
||||
};
|
||||
|
||||
//readerActions[name]
|
||||
const toolButtons = [
|
||||
{name: 'undoAction', show: true, text: 'Действие назад'},
|
||||
{name: 'redoAction', show: true, text: 'Действие вперед'},
|
||||
{name: 'fullScreen', show: true, text: 'На весь экран'},
|
||||
{name: 'scrolling', show: false, text: 'Плавный скроллинг'},
|
||||
{name: 'setPosition', show: true, text: 'На страницу'},
|
||||
{name: 'search', show: true, text: 'Найти в тексте'},
|
||||
{name: 'copyText', show: false, text: 'Скопировать текст со страницы'},
|
||||
{name: 'refresh', show: true, text: 'Принудительно обновить книгу'},
|
||||
{name: 'offlineMode', show: false, text: 'Автономный режим (без интернета)'},
|
||||
{name: 'recentBooks', show: true, text: 'Открыть недавние'},
|
||||
{name: 'undoAction', show: true},
|
||||
{name: 'redoAction', show: true},
|
||||
{name: 'fullScreen', show: true},
|
||||
{name: 'scrolling', show: false},
|
||||
{name: 'setPosition', show: true},
|
||||
{name: 'search', show: true},
|
||||
{name: 'copyText', show: false},
|
||||
{name: 'refresh', show: true},
|
||||
{name: 'offlineMode', show: false},
|
||||
{name: 'recentBooks', show: true},
|
||||
];
|
||||
|
||||
//readerActions[name]
|
||||
const hotKeys = [
|
||||
{name: 'help', codes: ['F1', 'H']},
|
||||
{name: 'loader', codes: ['Escape']},
|
||||
{name: 'settings', codes: ['S']},
|
||||
{name: 'undoAction', codes: ['Ctrl+BracketLeft']},
|
||||
{name: 'redoAction', codes: ['Ctrl+BracketRight']},
|
||||
{name: 'fullScreen', codes: ['Enter', 'Backquote', 'F']},
|
||||
{name: 'scrolling', codes: ['Z']},
|
||||
{name: 'setPosition', codes: ['P']},
|
||||
{name: 'search', codes: ['Ctrl+F']},
|
||||
{name: 'copyText', codes: ['Ctrl+C']},
|
||||
{name: 'refresh', codes: ['R']},
|
||||
{name: 'offlineMode', codes: ['O']},
|
||||
{name: 'recentBooks', codes: ['X']},
|
||||
|
||||
{name: 'switchToolbar', codes: ['Tab', 'Q']},
|
||||
{name: 'bookBegin', codes: ['Home']},
|
||||
{name: 'bookEnd', codes: ['End']},
|
||||
{name: 'pageBack', codes: ['PageUp', 'ArrowLeft', 'Backspace', 'Shift+Space']},
|
||||
{name: 'pageForward', codes: ['PageDown', 'ArrowRight', 'Space']},
|
||||
{name: 'lineBack', codes: ['ArrowUp']},
|
||||
{name: 'lineForward', codes: ['ArrowDown']},
|
||||
{name: 'incFontSize', codes: ['A']},
|
||||
{name: 'decFontSize', codes: ['Shift+A']},
|
||||
{name: 'scrollingSpeedUp', codes: ['Shift+ArrowDown']},
|
||||
{name: 'scrollingSpeedDown', codes: ['Shift+ArrowUp']},
|
||||
];
|
||||
|
||||
const fonts = [
|
||||
@@ -136,6 +194,7 @@ const webFonts = [
|
||||
|
||||
];
|
||||
|
||||
//----------------------------------------------------------------------------------------------------------
|
||||
const settingDefaults = {
|
||||
textColor: '#000000',
|
||||
backgroundColor: '#EBE2C9',
|
||||
@@ -160,11 +219,12 @@ const settingDefaults = {
|
||||
statusBarTop: false,// top, bottom
|
||||
statusBarHeight: 19,// px
|
||||
statusBarColorAlpha: 0.4,
|
||||
statusBarClickOpen: true,
|
||||
|
||||
scrollingDelay: 3000,// замедление, ms
|
||||
scrollingType: 'ease-in-out', //linear, ease, ease-in, ease-out, ease-in-out
|
||||
|
||||
pageChangeAnimation: 'flip',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||
pageChangeAnimation: 'blink',// '' - нет, downShift, rightShift, thaw - протаивание, blink - мерцание, rotate - вращение, flip - листание
|
||||
pageChangeAnimationSpeed: 80, //0-100%
|
||||
|
||||
allowUrlParamBookPos: false,
|
||||
@@ -187,6 +247,7 @@ const settingDefaults = {
|
||||
|
||||
fontShifts: {},
|
||||
showToolButton: {},
|
||||
userHotKeys: {},
|
||||
};
|
||||
|
||||
for (const font of fonts)
|
||||
@@ -195,6 +256,8 @@ for (const font of webFonts)
|
||||
settingDefaults.fontShifts[font.name] = font.fontVertShift;
|
||||
for (const button of toolButtons)
|
||||
settingDefaults.showToolButton[button.name] = button.show;
|
||||
for (const hotKey of hotKeys)
|
||||
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
@@ -255,7 +318,9 @@ const mutations = {
|
||||
};
|
||||
|
||||
export default {
|
||||
readerActions,
|
||||
toolButtons,
|
||||
hotKeys,
|
||||
fonts,
|
||||
webFonts,
|
||||
settingDefaults,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
48
docs/beta.omnireader/beta.omnireader
Normal file
48
docs/beta.omnireader/beta.omnireader
Normal file
@@ -0,0 +1,48 @@
|
||||
server {
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/beta.omnireader.ru/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/beta.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 beta.omnireader.ru;
|
||||
|
||||
client_max_body_size 50m;
|
||||
proxy_read_timeout 1h;
|
||||
|
||||
gzip on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types *;
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:34081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
location / {
|
||||
root /home/beta.liberama/public;
|
||||
|
||||
location /tmp {
|
||||
add_header Content-Type text/xml;
|
||||
add_header Content-Encoding gzip;
|
||||
}
|
||||
|
||||
location ~* \.(?:manifest|appcache|html)$ {
|
||||
expires -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name beta.omnireader.ru;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
4
docs/beta.omnireader/deploy.sh
Executable file
4
docs/beta.omnireader/deploy.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
npm run build:linux
|
||||
sudo -u www-data cp -r ../../dist/linux/* /home/beta.liberama
|
||||
11
docs/beta.omnireader/run_server.sh
Executable file
11
docs/beta.omnireader/run_server.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
sudo -H -u www-data bash -c "\
|
||||
while true; do\
|
||||
trap '' 2;\
|
||||
cd /var/www;\
|
||||
/home/beta.liberama/liberama;\
|
||||
trap 2;\
|
||||
echo \"Restart after 5 sec. Press Ctrl+C to exit.\";\
|
||||
sleep 5;\
|
||||
done;"
|
||||
2520
package-lock.json
generated
2520
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Liberama",
|
||||
"version": "0.8.4",
|
||||
"version": "0.9.2",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/liberama",
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.22.1",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-loader": "^7.1.1",
|
||||
"babel-plugin-component": "^1.1.1",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
@@ -32,7 +32,6 @@
|
||||
"clean-webpack-plugin": "^1.0.1",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "^1.0.0",
|
||||
"element-theme-chalk": "^2.12.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-html": "^5.0.5",
|
||||
"eslint-plugin-node": "^8.0.0",
|
||||
@@ -41,26 +40,26 @@
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.3",
|
||||
"pkg": "^4.4.2",
|
||||
"pkg": "^4.4.4",
|
||||
"sw-precache-webpack-plugin": "^1.0.0",
|
||||
"terser-webpack-plugin": "^1.4.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"vue-class-component": "^6.3.2",
|
||||
"vue-loader": "^15.7.1",
|
||||
"vue-loader": "^15.9.0",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.6.10",
|
||||
"webpack": "^4.39.3",
|
||||
"webpack-cli": "^3.3.7",
|
||||
"webpack-dev-middleware": "^3.7.1",
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"webpack-dev-middleware": "^3.7.2",
|
||||
"webpack-hot-middleware": "^2.25.0",
|
||||
"webpack-merge": "^4.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"appcache-webpack-plugin": "^1.4.0",
|
||||
"@quasar/extras": "^1.5.2",
|
||||
"axios": "^0.18.1",
|
||||
"base-x": "^3.0.6",
|
||||
"base-x": "^3.0.8",
|
||||
"chardet": "^0.7.0",
|
||||
"compression": "^1.7.4",
|
||||
"element-ui": "^2.12.0",
|
||||
"express": "^4.17.1",
|
||||
"fg-loadcss": "^2.1.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
@@ -71,20 +70,21 @@
|
||||
"lodash": "^4.17.15",
|
||||
"minimist": "^1.2.0",
|
||||
"multer": "^1.4.2",
|
||||
"pako": "^1.0.10",
|
||||
"pako": "^1.0.11",
|
||||
"path-browserify": "^1.0.0",
|
||||
"quasar": "^1.9.7",
|
||||
"safe-buffer": "^5.2.0",
|
||||
"sjcl": "^1.0.8",
|
||||
"sql-template-strings": "^2.2.2",
|
||||
"sqlite": "^3.0.3",
|
||||
"tar-fs": "^2.0.0",
|
||||
"unbzip2-stream": "^1.3.3",
|
||||
"vue": "github:paulkamer/vue#fix_palemoon_clickhandlers_dist",
|
||||
"vue-router": "^3.1.3",
|
||||
"vuex": "^3.1.1",
|
||||
"vuex-persistedstate": "^2.5.4",
|
||||
"webdav": "^2.10.1",
|
||||
"vue": "github:bookpauk/vue",
|
||||
"vue-router": "^3.1.6",
|
||||
"vuex": "^3.1.2",
|
||||
"vuex-persistedstate": "^2.7.1",
|
||||
"webdav": "^2.10.2",
|
||||
"ws": "^7.2.1",
|
||||
"zip-stream": "^2.1.2"
|
||||
"zip-stream": "^2.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ module.exports = {
|
||||
maxTempPublicDirSize: 512*1024*1024,//512Мб
|
||||
maxUploadPublicDirSize: 200*1024*1024,//100Мб
|
||||
|
||||
useExternalBookConverter: false,
|
||||
useExternalBookConverter: false,
|
||||
webConfigParams: ['name', 'version', 'mode', 'maxUploadFileSize', 'useExternalBookConverter', 'branch'],
|
||||
|
||||
db: [
|
||||
{
|
||||
|
||||
@@ -3,8 +3,11 @@ const _ = require('lodash');
|
||||
|
||||
class MiscController extends BaseController {
|
||||
async getConfig(req, res) {
|
||||
if (Array.isArray(req.body.params))
|
||||
return _.pick(this.config, req.body.params);
|
||||
if (Array.isArray(req.body.params)) {
|
||||
const paramsSet = new Set(req.body.params);
|
||||
|
||||
return _.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x)));
|
||||
}
|
||||
//bad request
|
||||
res.status(400).send({error: 'params is not an array'});
|
||||
return false;
|
||||
|
||||
@@ -98,7 +98,9 @@ class WebSocketController {
|
||||
|
||||
async getConfig(req, ws) {
|
||||
if (Array.isArray(req.params)) {
|
||||
this.send(_.pick(this.config, req.params), req, ws);
|
||||
const paramsSet = new Set(req.params);
|
||||
|
||||
this.send(_.pick(this.config, this.config.webConfigParams.filter(x => paramsSet.has(x))), req, ws);
|
||||
} else {
|
||||
throw new Error('params is not an array');
|
||||
}
|
||||
@@ -122,9 +124,10 @@ class WebSocketController {
|
||||
while (1) {// eslint-disable-line no-constant-condition
|
||||
const prevProgress = state.progress || -1;
|
||||
const prevState = state.state || '';
|
||||
const lastModified = state.lastModified || 0;
|
||||
state = this.workerState.getState(req.workerId);
|
||||
|
||||
this.send((state ? state : {}), req, ws);
|
||||
this.send((state && lastModified != state.lastModified ? state : {}), req, ws);
|
||||
if (!state) break;
|
||||
|
||||
if (state.state != 'finish' && state.state != 'error')
|
||||
|
||||
@@ -241,27 +241,29 @@ class FileDecompressor {
|
||||
});
|
||||
}
|
||||
|
||||
async gzipFileIfNotExists(filename, outDir) {
|
||||
async gzipFileIfNotExists(filename, outDir, isMaxCompression) {
|
||||
const hash = await utils.getFileHash(filename, 'sha256', 'hex');
|
||||
|
||||
const outFilename = `${outDir}/${hash}`;
|
||||
|
||||
if (!await fs.pathExists(outFilename)) {
|
||||
await this.gzipFile(filename, outFilename, 1);
|
||||
await this.gzipFile(filename, outFilename, (isMaxCompression ? 9 : 1));
|
||||
|
||||
// переупакуем через некоторое время на максималках
|
||||
const filenameCopy = `${filename}.copy`;
|
||||
await fs.copy(filename, filenameCopy);
|
||||
// переупакуем через некоторое время на максималках, если упаковали плохо
|
||||
if (!isMaxCompression) {
|
||||
const filenameCopy = `${filename}.copy`;
|
||||
await fs.copy(filename, filenameCopy);
|
||||
|
||||
(async() => {
|
||||
await utils.sleep(5000);
|
||||
const filenameGZ = `${filename}.gz`;
|
||||
await this.gzipFile(filenameCopy, filenameGZ, 9);
|
||||
(async() => {
|
||||
await utils.sleep(5000);
|
||||
const filenameGZ = `${filename}.gz`;
|
||||
await this.gzipFile(filenameCopy, filenameGZ, 9);
|
||||
|
||||
await fs.move(filenameGZ, outFilename, {overwrite: true});
|
||||
await fs.move(filenameGZ, outFilename, {overwrite: true});
|
||||
|
||||
await fs.remove(filenameCopy);
|
||||
})().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
|
||||
await fs.remove(filenameCopy);
|
||||
})().catch((e) => { if (appLogger.inited) appLogger.log(LM_ERR, `FileDecompressor.gzipFileIfNotExists: ${e.message}`) });
|
||||
}
|
||||
} else {
|
||||
await utils.touchFile(outFilename);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const signatures = require('./signatures.json');
|
||||
class FileDetector {
|
||||
detectFile(filename) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.fromFile(filename, 2000, (err, result) => {
|
||||
this.fromFile(filename, 10000, (err, result) => {
|
||||
if (err) reject(err);
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
@@ -662,31 +662,6 @@
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "html",
|
||||
"ext": "html",
|
||||
"mime": "text/html",
|
||||
"rules": [
|
||||
{ "type": "or", "rules":
|
||||
[
|
||||
{ "type": "contains", "bytes": "3c68746d6c" },
|
||||
{ "type": "contains", "bytes": "3c00680074006d006c00" },
|
||||
{ "type": "equal", "end": 5, "bytes": "3c68746d6c" },
|
||||
{ "type": "equal", "end": 10, "bytes": "3c00680074006d006c00" },
|
||||
{ "type": "equal", "end": 9, "bytes": "3c21646f6374797065" },
|
||||
{ "type": "equal", "end": 5, "bytes": "3c626f6479" },
|
||||
{ "type": "equal", "end": 5, "bytes": "3c68656164" },
|
||||
{ "type": "equal", "end": 7, "bytes": "3c696672616d65" },
|
||||
{ "type": "equal", "end": 4, "bytes": "3c696d67" },
|
||||
{ "type": "equal", "end": 7, "bytes": "3c6f626a656374" },
|
||||
{ "type": "equal", "end": 7, "bytes": "3c736372697074" },
|
||||
{ "type": "equal", "end": 6, "bytes": "3c7461626c65" },
|
||||
{ "type": "equal", "end": 6, "bytes": "3c7469746c65" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "docx",
|
||||
"ext": "docx",
|
||||
@@ -721,6 +696,35 @@
|
||||
"rules": [
|
||||
{ "type": "equal", "start": 64, "end": 68, "bytes": "4d4f4249" }
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "html",
|
||||
"ext": "html",
|
||||
"mime": "text/html",
|
||||
"rules": [
|
||||
{ "type": "or", "rules":
|
||||
[
|
||||
{ "type": "contains", "bytes": "3c68746d6c" },
|
||||
{ "type": "contains", "bytes": "3c00680074006d006c00" },
|
||||
{ "type": "contains", "bytes": "3c48544d4c" },
|
||||
{ "type": "contains", "bytes": "3c00480054004d004c00" },
|
||||
|
||||
{ "type": "equal", "end": 5, "bytes": "3c68746d6c" },
|
||||
{ "type": "equal", "end": 10, "bytes": "3c00680074006d006c00" },
|
||||
{ "type": "equal", "end": 9, "bytes": "3c21646f6374797065" },
|
||||
{ "type": "equal", "end": 9, "bytes": "3c21444f4354595045" },
|
||||
{ "type": "equal", "end": 5, "bytes": "3c626f6479" },
|
||||
{ "type": "equal", "end": 5, "bytes": "3c68656164" },
|
||||
{ "type": "equal", "end": 7, "bytes": "3c696672616d65" },
|
||||
{ "type": "equal", "end": 4, "bytes": "3c696d67" },
|
||||
{ "type": "equal", "end": 7, "bytes": "3c6f626a656374" },
|
||||
{ "type": "equal", "end": 7, "bytes": "3c736372697074" },
|
||||
{ "type": "equal", "end": 6, "bytes": "3c7461626c65" },
|
||||
{ "type": "equal", "end": 6, "bytes": "3c7469746c65" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
@@ -100,7 +100,7 @@ function getEncodingLite(buf, returnAll) {
|
||||
}
|
||||
|
||||
function checkIfText(buf) {
|
||||
const enc = getEncoding(buf, true);
|
||||
const enc = getEncodingLite(buf, true);
|
||||
if (enc[0].c > enc[0].totalChecked*0.9)
|
||||
return true;
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ class ReaderWorker {
|
||||
let decompDir = '';
|
||||
let downloadedFilename = '';
|
||||
let isUploaded = false;
|
||||
let isRestored = false;
|
||||
let convertFilename = '';
|
||||
|
||||
const overLoadMes = 'Слишком большая очередь загрузки. Пожалуйста, попробуйте позже.';
|
||||
@@ -88,9 +89,17 @@ class ReaderWorker {
|
||||
downloadedFilename = `${this.config.tempDownloadDir}/${tempFilename}`;
|
||||
await fs.writeFile(downloadedFilename, downdata);
|
||||
} else {//uploaded file
|
||||
downloadedFilename = `${this.config.uploadDir}/${url.substr(7)}`;
|
||||
if (!await fs.pathExists(downloadedFilename))
|
||||
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
||||
const fileHash = url.substr(7);
|
||||
downloadedFilename = `${this.config.uploadDir}/${fileHash}`;
|
||||
if (!await fs.pathExists(downloadedFilename)) {
|
||||
//если удалено из upload, попробуем восстановить из удаленного хранилища
|
||||
try {
|
||||
downloadedFilename = await this.restoreRemoteFile(fileHash);
|
||||
isRestored = true;
|
||||
} catch(e) {
|
||||
throw new Error('Файл не найден на сервере (возможно был удален как устаревший). Пожалуйста, загрузите файл с диска на сервер заново.');
|
||||
}
|
||||
}
|
||||
await utils.touchFile(downloadedFilename);
|
||||
isUploaded = true;
|
||||
}
|
||||
@@ -146,6 +155,20 @@ class ReaderWorker {
|
||||
})();
|
||||
}
|
||||
|
||||
//лениво сохраним downloadedFilename в tmp и в удаленном хранилище в случае isUploaded
|
||||
if (this.remoteWebDavStorage && isUploaded && !isRestored) {
|
||||
(async() => {
|
||||
await utils.sleep(30*1000);
|
||||
try {
|
||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||
const compDownloadedFilename = await this.decomp.gzipFileIfNotExists(downloadedFilename, this.config.tempPublicDir, true);
|
||||
await this.remoteWebDavStorage.putFile(compDownloadedFilename);
|
||||
} catch (e) {
|
||||
log(LM_ERR, e.stack);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log(LM_ERR, e.stack);
|
||||
if (e.message == 'abort')
|
||||
@@ -188,6 +211,24 @@ class ReaderWorker {
|
||||
return `file://${hash}`;
|
||||
}
|
||||
|
||||
async restoreRemoteFile(filename) {
|
||||
const basename = path.basename(filename);
|
||||
const targetName = `${this.config.tempPublicDir}/${basename}`;
|
||||
|
||||
if (!await fs.pathExists(targetName)) {
|
||||
let found = false;
|
||||
if (this.remoteWebDavStorage) {
|
||||
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
throw new Error('404 Файл не найден');
|
||||
}
|
||||
}
|
||||
|
||||
return targetName;
|
||||
}
|
||||
|
||||
restoreCachedFile(filename) {
|
||||
const workerId = this.workerState.generateWorkerId();
|
||||
const wState = this.workerState.getControl(workerId);
|
||||
@@ -197,21 +238,10 @@ class ReaderWorker {
|
||||
try {
|
||||
wState.set({state: 'download', step: 1, totalSteps: 1, path: filename, progress: 0});
|
||||
|
||||
const basename = path.basename(filename);
|
||||
const targetName = `${this.config.tempPublicDir}/${basename}`;
|
||||
|
||||
if (!await fs.pathExists(targetName)) {
|
||||
let found = false;
|
||||
if (this.remoteWebDavStorage) {
|
||||
found = await this.remoteWebDavStorage.getFileSuccess(targetName);
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
throw new Error('404 Файл не найден');
|
||||
}
|
||||
}
|
||||
|
||||
const targetName = await this.restoreRemoteFile(filename);
|
||||
const stat = await fs.stat(targetName);
|
||||
|
||||
const basename = path.basename(filename);
|
||||
wState.finish({path: `/tmp/${basename}`, size: stat.size, progress: 100});
|
||||
} catch (e) {
|
||||
if (e.message.indexOf('404') < 0)
|
||||
|
||||
Reference in New Issue
Block a user