Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6e534b9db | ||
|
|
032ab6a85d | ||
|
|
21716163cb | ||
|
|
ca924148a5 | ||
|
|
37aa9b84ae | ||
|
|
c7bd7d4d7d | ||
|
|
d81a50e696 | ||
|
|
dda9943dbe | ||
|
|
2b4b9f24a1 | ||
|
|
2af77f22d6 | ||
|
|
f142e5812d | ||
|
|
ed901fc181 | ||
|
|
87a068899a | ||
|
|
115f683128 | ||
|
|
111568fc2e | ||
|
|
825136b5ff | ||
|
|
eae34b1121 | ||
|
|
b9d7a6a3bb | ||
|
|
1e5375f8f9 | ||
|
|
f597c603bf | ||
|
|
b93dd0a59e | ||
|
|
a5740e4349 | ||
|
|
dacbd05911 | ||
|
|
65c66e0feb | ||
|
|
52f9131f99 | ||
|
|
cfc946ad12 | ||
|
|
a207a0554c | ||
|
|
675e898163 | ||
|
|
d2167d8605 | ||
|
|
de849d3447 | ||
|
|
6c20b0b83e | ||
|
|
a09b70a991 | ||
|
|
2427a3e08b | ||
|
|
1104f9b850 | ||
|
|
dc48700e9e | ||
|
|
f0b0c39328 | ||
|
|
aad74cf682 | ||
|
|
d449478204 | ||
|
|
d4f6536caa | ||
|
|
1eac00f71c | ||
|
|
ca1170a9f0 | ||
|
|
79dda03bac | ||
|
|
6c8e0b8573 | ||
|
|
17c14722fd | ||
|
|
48612ee118 | ||
|
|
205c676999 | ||
|
|
54e0dd0478 | ||
|
|
2de8d7515e | ||
|
|
a251d16432 | ||
|
|
599caba912 | ||
|
|
3477c43465 | ||
|
|
200dac7946 | ||
|
|
e60829946d | ||
|
|
ef12a84285 | ||
|
|
6a18ae3f27 | ||
|
|
a250e95950 | ||
|
|
b174ae452b | ||
|
|
0b63bce357 | ||
|
|
de0d10e792 | ||
|
|
b358b340b4 | ||
|
|
455aba7f4f | ||
|
|
fde0437157 | ||
|
|
480c95bd63 | ||
|
|
972f957685 | ||
|
|
40ff04e5dc | ||
|
|
b3c028bd7a | ||
|
|
51ec6a54fa | ||
|
|
7a29b16ee8 | ||
|
|
7af6fd8248 | ||
|
|
e1c93169b5 | ||
|
|
f4716d5a1e | ||
|
|
f5c06ce420 | ||
|
|
9492f85d80 | ||
|
|
b1303a3ba2 | ||
|
|
5c9cfe5e6f | ||
|
|
b89b5322b8 | ||
|
|
945feba6b2 | ||
|
|
c8af4b907b | ||
|
|
298e8928cf | ||
|
|
8cb67d2976 | ||
|
|
32b8382641 | ||
|
|
007e97463b | ||
|
|
e4f190698d | ||
|
|
b3be07b17e | ||
|
|
72f8977071 | ||
|
|
3dbf00344e | ||
|
|
ffdf0b12cd | ||
|
|
a51150c729 | ||
|
|
37e14b397c | ||
|
|
e48af7ee7d | ||
|
|
3eb3dd371a | ||
|
|
8ef6551560 | ||
|
|
b1f5f3dd28 | ||
|
|
6074c4b7bd | ||
|
|
9906dd43c7 | ||
|
|
17699f66f8 | ||
|
|
80a29e654d | ||
|
|
4184fda247 | ||
|
|
7460ff7055 | ||
|
|
3137b86cee | ||
|
|
b2ca84bb7e | ||
|
|
7d692dd730 | ||
|
|
8850a89aa7 | ||
|
|
57b01dd204 | ||
|
|
8aa1da36b6 | ||
|
|
2dbe29d632 | ||
|
|
7fa891b4fc | ||
|
|
6cb7412cf3 | ||
|
|
157322834b | ||
|
|
1a13a0fee1 | ||
|
|
37256255bf | ||
|
|
75e01c899e | ||
|
|
ef0d6eab89 | ||
|
|
5d54b1b0f4 | ||
|
|
522f953b4f | ||
|
|
15f02c7115 | ||
|
|
174c877eee | ||
|
|
fd9ec736d7 | ||
|
|
2c94025ba3 | ||
|
|
bfadf35c40 | ||
|
|
f3b69caa12 | ||
|
|
18a83a5b0b | ||
|
|
bd9669b782 | ||
|
|
e05713aa7f | ||
|
|
bc3e1f0a6f | ||
|
|
063d01b5ca | ||
|
|
81c38d7749 | ||
|
|
a29842b084 | ||
|
|
bb5adcdaf6 | ||
|
|
537e17a219 | ||
|
|
03ce50153e | ||
|
|
15d01ad7fc | ||
|
|
e2b29e2c2f | ||
|
|
ce7ae84e0f | ||
|
|
01eb545f15 | ||
|
|
706738c7f1 | ||
|
|
6afa78cde9 | ||
|
|
71f5710bba | ||
|
|
0d87043f91 | ||
|
|
e25375fb7a | ||
|
|
41822999c8 | ||
|
|
07444bc7c2 | ||
|
|
ec48e5b0b7 | ||
|
|
e8e2e9297f | ||
|
|
4f871dd5ca | ||
|
|
f5f07a591a | ||
|
|
4c11e6918f | ||
|
|
403b9c0508 | ||
|
|
ee8ba75371 | ||
|
|
a2773fb180 | ||
|
|
ca36d588fc | ||
|
|
1e65707b7f | ||
|
|
eddf34ce55 | ||
|
|
0fb43aa33c | ||
|
|
b273b02da4 | ||
|
|
0b997f9673 | ||
|
|
bdb2ae57a8 | ||
|
|
b5e563679a | ||
|
|
992c104262 | ||
|
|
555154031e | ||
|
|
acb083e429 | ||
|
|
4a527d192d | ||
|
|
39c3bf17dd | ||
|
|
afc8c84f41 | ||
|
|
a085e04c4d | ||
|
|
2f82b0db34 | ||
|
|
0124c2b17d | ||
|
|
d2cfbbc9f3 | ||
|
|
c59f48822c | ||
|
|
b2d6584c4a | ||
|
|
8f7cafb240 | ||
|
|
08fd0f15ff | ||
|
|
dbb1bfe587 | ||
|
|
fe4b7a5a85 | ||
|
|
d8df5d76e5 | ||
|
|
b65dcc5ade | ||
|
|
a5c387a19e | ||
|
|
07c38d9a9f | ||
|
|
20ac8a444b | ||
|
|
7b601c9c7f | ||
|
|
8d2f74daa4 | ||
|
|
01e82dca5f | ||
|
|
094bb407ed | ||
|
|
338baa55ec | ||
|
|
d06d20a33e | ||
|
|
d46ba6b92b | ||
|
|
ec2639039d | ||
|
|
3a211ded2e | ||
|
|
c2131e3654 | ||
|
|
594fb59395 | ||
|
|
f44378ec84 | ||
|
|
0f6b366f62 | ||
|
|
8d0a5997ee | ||
|
|
347cb3417e | ||
|
|
337fcebd10 | ||
|
|
e057b130e9 | ||
|
|
19a0765a1a | ||
|
|
b81cd3240b | ||
|
|
1e6105b076 | ||
|
|
d8d89b3463 | ||
|
|
459564cb2d | ||
|
|
084df35184 | ||
|
|
81912babeb |
@@ -13,8 +13,7 @@ class Misc {
|
|||||||
]};
|
]};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await wsc.open();
|
const config = await wsc.message(await wsc.send(Object.assign({action: 'get-config'}, query)));
|
||||||
const config = await wsc.message(wsc.send(Object.assign({action: 'get-config'}, query)));
|
|
||||||
if (config.error)
|
if (config.error)
|
||||||
throw new Error(config.error);
|
throw new Error(config.error);
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ class Reader {
|
|||||||
|
|
||||||
let response = {};
|
let response = {};
|
||||||
try {
|
try {
|
||||||
await wsc.open();
|
const requestId = await wsc.send({action: 'worker-get-state-finish', workerId});
|
||||||
const requestId = wsc.send({action: 'worker-get-state-finish', workerId});
|
|
||||||
|
|
||||||
let prevResponse = false;
|
let prevResponse = false;
|
||||||
while (1) {// eslint-disable-line no-constant-condition
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
@@ -66,7 +65,7 @@ class Reader {
|
|||||||
await utils.sleep(refreshPause);
|
await utils.sleep(refreshPause);
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
if (i > 120*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
if (i > 180*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||||
throw new Error('Слишком долгое время ожидания');
|
throw new Error('Слишком долгое время ожидания');
|
||||||
}
|
}
|
||||||
//проверка воркера
|
//проверка воркера
|
||||||
@@ -124,8 +123,7 @@ class Reader {
|
|||||||
let response = null
|
let response = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await wsc.open();
|
response = await wsc.message(await wsc.send({action: 'reader-restore-cached-file', path: url}));
|
||||||
response = await wsc.message(wsc.send({action: 'reader-restore-cached-file', path: url}));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
//если с WebSocket проблема, работаем по http
|
//если с WebSocket проблема, работаем по http
|
||||||
@@ -181,9 +179,8 @@ class Reader {
|
|||||||
maxUploadFileSize = 10*1024*1024;
|
maxUploadFileSize = 10*1024*1024;
|
||||||
if (file.size > maxUploadFileSize)
|
if (file.size > maxUploadFileSize)
|
||||||
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
throw new Error(`Размер файла превышает ${maxUploadFileSize} байт`);
|
||||||
|
|
||||||
let formData = new FormData();
|
let formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file, file.name);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -211,8 +208,7 @@ class Reader {
|
|||||||
async storage(request) {
|
async storage(request) {
|
||||||
let response = null;
|
let response = null;
|
||||||
try {
|
try {
|
||||||
await wsc.open();
|
response = await wsc.message(await wsc.send({action: 'reader-storage', body: request}));
|
||||||
response = await wsc.message(wsc.send({action: 'reader-storage', body: request}));
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
//если с WebSocket проблема, работаем по http
|
//если с WebSocket проблема, работаем по http
|
||||||
|
|||||||
@@ -1,185 +1,3 @@
|
|||||||
import * as utils from '../share/utils';
|
import WebSocketConnection from '../../server/core/WebSocketConnection';
|
||||||
|
|
||||||
const cleanPeriod = 60*1000;//1 минута
|
|
||||||
|
|
||||||
class WebSocketConnection {
|
|
||||||
//messageLifeTime в минутах (cleanPeriod)
|
|
||||||
constructor(messageLifeTime = 5) {
|
|
||||||
this.ws = null;
|
|
||||||
this.timer = null;
|
|
||||||
this.listeners = [];
|
|
||||||
this.messageQueue = [];
|
|
||||||
this.messageLifeTime = messageLifeTime;
|
|
||||||
this.requestId = 0;
|
|
||||||
|
|
||||||
this.connecting = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
addListener(listener) {
|
|
||||||
if (this.listeners.indexOf(listener) < 0)
|
|
||||||
this.listeners.push(Object.assign({regTime: Date.now()}, listener));
|
|
||||||
}
|
|
||||||
|
|
||||||
//рассылаем сообщение и удаляем те обработчики, которые его получили
|
|
||||||
emit(mes, isError) {
|
|
||||||
const len = this.listeners.length;
|
|
||||||
if (len > 0) {
|
|
||||||
let newListeners = [];
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
let emitted = false;
|
|
||||||
if (isError) {
|
|
||||||
if (listener.onError)
|
|
||||||
listener.onError(mes);
|
|
||||||
emitted = true;
|
|
||||||
} else {
|
|
||||||
if (listener.onMessage) {
|
|
||||||
if (listener.requestId) {
|
|
||||||
if (listener.requestId === mes.requestId) {
|
|
||||||
listener.onMessage(mes);
|
|
||||||
emitted = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
listener.onMessage(mes);
|
|
||||||
emitted = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
emitted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!emitted)
|
|
||||||
newListeners.push(listener);
|
|
||||||
}
|
|
||||||
this.listeners = newListeners;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.listeners.length != len;
|
|
||||||
}
|
|
||||||
|
|
||||||
open(url) {
|
|
||||||
return new Promise((resolve, reject) => { (async() => {
|
|
||||||
//Ожидаем окончания процесса подключения, если open уже был вызван
|
|
||||||
let i = 0;
|
|
||||||
while (this.connecting && i < 200) {//10 сек
|
|
||||||
await utils.sleep(50);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
if (i >= 200)
|
|
||||||
this.connecting = false;
|
|
||||||
|
|
||||||
//проверим подключение, и если нет, то подключимся заново
|
|
||||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
|
||||||
resolve(this.ws);
|
|
||||||
} else {
|
|
||||||
this.connecting = true;
|
|
||||||
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
|
|
||||||
|
|
||||||
url = url || `${protocol}//${window.location.host}/ws`;
|
|
||||||
|
|
||||||
this.ws = new WebSocket(url);
|
|
||||||
|
|
||||||
if (this.timer) {
|
|
||||||
clearTimeout(this.timer);
|
|
||||||
}
|
|
||||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
|
||||||
|
|
||||||
this.ws.onopen = (e) => {
|
|
||||||
this.connecting = false;
|
|
||||||
resolve(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onmessage = (e) => {
|
|
||||||
try {
|
|
||||||
const mes = JSON.parse(e.data);
|
|
||||||
this.messageQueue.push({regTime: Date.now(), mes});
|
|
||||||
|
|
||||||
let newMessageQueue = [];
|
|
||||||
for (const message of this.messageQueue) {
|
|
||||||
if (!this.emit(message.mes)) {
|
|
||||||
newMessageQueue.push(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.messageQueue = newMessageQueue;
|
|
||||||
} catch (e) {
|
|
||||||
this.emit(e.message, true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.ws.onerror = (e) => {
|
|
||||||
this.emit(e.message, true);
|
|
||||||
if (this.connecting) {
|
|
||||||
this.connecting = false;
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
})() });
|
|
||||||
}
|
|
||||||
|
|
||||||
//timeout в минутах (cleanPeriod)
|
|
||||||
message(requestId, timeout = 2) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.addListener({
|
|
||||||
requestId,
|
|
||||||
timeout,
|
|
||||||
onMessage: (mes) => {
|
|
||||||
resolve(mes);
|
|
||||||
},
|
|
||||||
onError: (e) => {
|
|
||||||
reject(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
send(req) {
|
|
||||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
|
||||||
const requestId = ++this.requestId;
|
|
||||||
this.ws.send(JSON.stringify(Object.assign({requestId}, req)));
|
|
||||||
return requestId;
|
|
||||||
} else {
|
|
||||||
throw new Error('WebSocket connection is not ready');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
if (this.ws && this.ws.readyState == WebSocket.OPEN) {
|
|
||||||
this.ws.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
periodicClean() {
|
|
||||||
try {
|
|
||||||
this.timer = null;
|
|
||||||
|
|
||||||
const now = Date.now();
|
|
||||||
//чистка listeners
|
|
||||||
let newListeners = [];
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
if (now - listener.regTime < listener.timeout*cleanPeriod - 50) {
|
|
||||||
newListeners.push(listener);
|
|
||||||
} else {
|
|
||||||
if (listener.onError)
|
|
||||||
listener.onError('Время ожидания ответа истекло');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.listeners = newListeners;
|
|
||||||
|
|
||||||
//чистка messageQueue
|
|
||||||
let newMessageQueue = [];
|
|
||||||
for (const message of this.messageQueue) {
|
|
||||||
if (now - message.regTime < this.messageLifeTime*cleanPeriod - 50) {
|
|
||||||
newMessageQueue.push(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.messageQueue = newMessageQueue;
|
|
||||||
} finally {
|
|
||||||
if (this.ws.readyState == WebSocket.OPEN) {
|
|
||||||
this.timer = setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new WebSocketConnection();
|
export default new WebSocketConnection();
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="fit row">
|
<div class="fit row">
|
||||||
<Notify ref="notify"/>
|
<Notify ref="notify"/>
|
||||||
<StdDialog ref="stdDialog"/>
|
<StdDialog ref="stdDialog"/>
|
||||||
<keep-alive>
|
<keep-alive v-if="showPage">
|
||||||
<router-view class="col"></router-view>
|
<router-view class="col"></router-view>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,8 +12,11 @@
|
|||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
|
|
||||||
import Notify from './share/Notify.vue';
|
import Notify from './share/Notify.vue';
|
||||||
import StdDialog from './share/StdDialog.vue';
|
import StdDialog from './share/StdDialog.vue';
|
||||||
|
|
||||||
|
import miscApi from '../api/misc';
|
||||||
import * as utils from '../share/utils';
|
import * as utils from '../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
@@ -30,6 +33,8 @@ export default @Component({
|
|||||||
|
|
||||||
})
|
})
|
||||||
class App extends Vue {
|
class App extends Vue {
|
||||||
|
showPage = false;
|
||||||
|
|
||||||
itemRuText = {
|
itemRuText = {
|
||||||
'/cardindex': 'Картотека',
|
'/cardindex': 'Картотека',
|
||||||
'/reader': 'Читалка',
|
'/reader': 'Читалка',
|
||||||
@@ -42,7 +47,6 @@ class App extends Vue {
|
|||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.dispatch = this.$store.dispatch;
|
|
||||||
this.state = this.$store.state;
|
this.state = this.$store.state;
|
||||||
this.uistate = this.$store.state.uistate;
|
this.uistate = this.$store.state.uistate;
|
||||||
this.config = this.$store.state.config;
|
this.config = this.$store.state.config;
|
||||||
@@ -116,18 +120,28 @@ class App extends Vue {
|
|||||||
this.$root.notify = this.$refs.notify;
|
this.$root.notify = this.$refs.notify;
|
||||||
this.$root.stdDialog = this.$refs.stdDialog;
|
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.$root.notify.error(mes, 'Ошибка API');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setAppTitle();
|
this.setAppTitle();
|
||||||
(async() => {
|
(async() => {
|
||||||
|
//загрузим конфиг сревера
|
||||||
|
try {
|
||||||
|
const config = await miscApi.loadConfig();
|
||||||
|
this.commit('config/setConfig', config);
|
||||||
|
this.showPage = true;
|
||||||
|
} catch(e) {
|
||||||
|
//проверим, не получен ли конфиг ранее
|
||||||
|
if (!this.mode) {
|
||||||
|
this.$root.notify.error(e.message, 'Ошибка API');
|
||||||
|
} else {
|
||||||
|
//вероятно, работаем в оффлайне
|
||||||
|
this.showPage = true;
|
||||||
|
}
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
//запросим persistent storage
|
||||||
|
if (navigator.storage && navigator.storage.persist) {
|
||||||
|
navigator.storage.persist();
|
||||||
|
}
|
||||||
await this.routerReady();
|
await this.routerReady();
|
||||||
this.redirectIfNeeded();
|
this.redirectIfNeeded();
|
||||||
})();
|
})();
|
||||||
@@ -256,6 +270,14 @@ body, html, #app {
|
|||||||
animation: rotating 2s linear infinite;
|
animation: rotating 2s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes rotating {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
} to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.notify-button-icon {
|
.notify-button-icon {
|
||||||
font-size: 16px !important;
|
font-size: 16px !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,7 +408,7 @@ class ExternalLibs extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get header() {
|
get header() {
|
||||||
let result = (this.ready ? 'Библиотека' : 'Загрузка...');
|
let result = (this.ready ? 'Сетевая библиотека' : 'Загрузка...');
|
||||||
if (this.ready && this.selectedLink) {
|
if (this.ready && this.selectedLink) {
|
||||||
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
|
result += ` | ${(this.libs.comment ? this.libs.comment + ' ': '') + lu.removeProtocol(this.libs.startLink)}`;
|
||||||
}
|
}
|
||||||
@@ -787,12 +787,17 @@ class ExternalLibs extends Vue {
|
|||||||
|
|
||||||
showHelp() {
|
showHelp() {
|
||||||
this.$root.stdDialog.alert(`
|
this.$root.stdDialog.alert(`
|
||||||
<p>Окно 'Библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
|
<p>Окно 'Сетевая библиотека' позволяет открывать ссылки в читалке без переключения между окнами,
|
||||||
что особенно актуально для мобильных устройств.</p>
|
что особенно актуально для мобильных устройств. Имеется возможность управлять закладками
|
||||||
|
|
||||||
<p>'Библиотека' разрешает свободный доступ к сайту flibusta.is. Имеется возможность управлять закладками
|
|
||||||
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
|
на понравившиеся ресурсы, книги или страницы авторов. Открытие ссылок и навигация происходят во фрейме, но,
|
||||||
к сожалению, в нем открываются не все страницы.
|
к сожалению, в нем открываются не все страницы.</p>
|
||||||
|
|
||||||
|
<p>Доступ к сайтам <span style="color: blue">http://flibusta.is</span> и <span style="color: blue">http://fantasy-worlds.org</span> работает через прокси.
|
||||||
|
|
||||||
|
<br><span style="color: red"><b>ПРЕДУПРЕЖДЕНИЕ!</b></span>
|
||||||
|
Доступ предназначен только для просмотра и скачивания книг. Авторизоваться на этих сайтах
|
||||||
|
из фрейма категорически не рекомендуется, т.к. ваше подключение не защищено и данные могут попасть
|
||||||
|
к третьим лицам.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
|
<p>Из-за проблем с безопасностью, навигация 'вперед-назад' во фрейме осуществляется с помощью контекстного меню правой кнопкой мыши.
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
class="no-mp bg-grey-4 text-grey-7"
|
class="no-mp bg-grey-4 text-grey-7"
|
||||||
>
|
>
|
||||||
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
<q-tab name="contents" icon="la la-list" label="Оглавление" />
|
||||||
|
<q-tab name="images" icon="la la-image" label="Изображения" />
|
||||||
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
<q-tab name="bookmarks" icon="la la-bookmark" label="Закладки" />
|
||||||
</q-tabs>
|
</q-tabs>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,30 +26,61 @@
|
|||||||
<div class="tab-panel" v-show="selectedTab == 'contents'">
|
<div class="tab-panel" v-show="selectedTab == 'contents'">
|
||||||
<div>
|
<div>
|
||||||
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
<div v-for="item in contents" :key="item.key" class="column" style="width: 540px">
|
||||||
<div class="row item q-px-sm no-wrap">
|
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||||
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
<div v-if="item.list.length" class="row justify-center items-center expand-button clickable" @click="expandClick(item.key)">
|
||||||
<q-icon name="la la-arrow-circle-down" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px"/>
|
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
<div v-else class="no-expand-button clickable" @click="setBookPos(item.offset)">
|
||||||
|
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="20px"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="col row clickable" @click="setBookPos(item.offset)">
|
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||||
<div :style="item.indentStyle"></div>
|
<div :style="item.indentStyle"></div>
|
||||||
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="item.label"></div>
|
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||||
<div class="column justify-center">{{ item.perc }}%</div>
|
<div class="column justify-center">{{ item.perc }}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
|
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
|
||||||
<div v-for="subitem in item.list" :key="subitem.key" class="row subitem q-px-sm no-wrap">
|
<div v-for="subitem in item.list" :key="subitem.key" class="row q-px-sm no-wrap" :class="{'subitem': !subitem.isBookPos, 'subitem-book-pos': subitem.isBookPos}">
|
||||||
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
<div class="col row clickable" @click="setBookPos(subitem.offset)">
|
||||||
<div class="no-expand-button"></div>
|
<div class="no-expand-button"></div>
|
||||||
<div :style="subitem.indentStyle"></div>
|
<div :style="subitem.indentStyle"></div>
|
||||||
<div class="q-mr-sm col overflow-hidden column justify-center" v-html="subitem.label"></div>
|
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="subitem.label"></div>
|
||||||
<div class="column justify-center">{{ subitem.perc }}%</div>
|
<div class="column justify-center">{{ subitem.perc }}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="!contents.length" class="column justify-center items-center" style="height: 100px">
|
||||||
|
Оглавление отсутствует
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-panel" v-show="selectedTab == 'images'">
|
||||||
|
<div>
|
||||||
|
<div v-for="item in images" :key="item.key" class="column" style="width: 540px">
|
||||||
|
<div class="row q-px-sm no-wrap" :class="{'item': !item.isBookPos, 'item-book-pos': item.isBookPos}">
|
||||||
|
<div class="col row clickable" @click="setBookPos(item.offset)">
|
||||||
|
<div class="image-thumb-box row justify-center items-center">
|
||||||
|
<div v-show="!imageLoaded[item.id]" class="image-thumb column justify-center"><i class="loading-img-icon la la-images"></i></div>
|
||||||
|
<img v-show="imageLoaded[item.id]" class="image-thumb" :src="imageSrc[item.id]"/>
|
||||||
|
</div>
|
||||||
|
<div class="no-expand-button column justify-center items-center">
|
||||||
|
<div class="image-num">{{ item.num }}</div>
|
||||||
|
<div v-show="item.type == 'image/jpeg'" class="image-type it-jpg-color row justify-center">JPG</div>
|
||||||
|
<div v-show="item.type == 'image/png'" class="image-type it-png-color row justify-center">PNG</div>
|
||||||
|
<div v-show="!item.local" class="image-type it-net-color row justify-center">INET</div>
|
||||||
|
</div>
|
||||||
|
<div :style="item.indentStyle"></div>
|
||||||
|
<div class="q-mr-sm col overflow-hidden column justify-center" :style="item.labelStyle" v-html="item.label"></div>
|
||||||
|
<div class="column justify-center">{{ item.perc }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!images.length" class="column justify-center items-center" style="height: 100px">
|
||||||
|
Изображения отсутствуют
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,16 +102,29 @@ import Component from 'vue-class-component';
|
|||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
|
const ContentsPageProps = Vue.extend({
|
||||||
|
props: {
|
||||||
|
bookPos: Number,
|
||||||
|
isVisible: Boolean,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
components: {
|
components: {
|
||||||
Window,
|
Window,
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
bookPos: function() {
|
||||||
|
this.updateBookPosSelection();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
class ContentsPage extends Vue {
|
class ContentsPage extends ContentsPageProps {
|
||||||
selectedTab = 'contents';
|
selectedTab = 'contents';
|
||||||
contents = [];
|
contents = [];
|
||||||
|
images = [];
|
||||||
|
imageSrc = [];
|
||||||
|
imageLoaded = [];
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
@@ -89,71 +134,177 @@ class ContentsPage extends Vue {
|
|||||||
|
|
||||||
//закладки
|
//закладки
|
||||||
|
|
||||||
//далее формаирование оглавления
|
//проверим, надо ли обновлять списки
|
||||||
if (this.parsed == parsed)
|
if (this.parsed == parsed) {
|
||||||
|
this.updateBookPosSelection();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//далее формирование оглавления
|
||||||
this.parsed = parsed;
|
this.parsed = parsed;
|
||||||
this.contents = [];
|
this.contents = [];
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
|
const pc = parsed.contents;
|
||||||
|
const ims = parsed.images;
|
||||||
|
const newpc = [];
|
||||||
|
if (pc.length) {//если есть оглавление
|
||||||
|
//преобразуем все, кроме первого, разделы body в title-subtitle
|
||||||
|
let curSubtitles = [];
|
||||||
|
let prevBodyIndex = -1;
|
||||||
|
for (let i = 0; i < pc.length; i++) {
|
||||||
|
const cont = pc[i];
|
||||||
|
if (prevBodyIndex != cont.bodyIndex)
|
||||||
|
curSubtitles = [];
|
||||||
|
|
||||||
|
prevBodyIndex = cont.bodyIndex;
|
||||||
|
|
||||||
|
if (cont.bodyIndex > 1) {
|
||||||
|
if (cont.inset < 1) {
|
||||||
|
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
|
||||||
|
} else {
|
||||||
|
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newpc.push(cont);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {//попробуем вытащить из images
|
||||||
|
for (let i = 0; i < ims.length; i++) {
|
||||||
|
const image = ims[i];
|
||||||
|
|
||||||
|
if (image.alt) {
|
||||||
|
newpc.push({paraIndex: image.paraIndex, title: image.alt, inset: 1, bodyIndex: 0, subtitles: []});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const prepareLabel = (title, bolder = false) => {
|
const prepareLabel = (title, bolder = false) => {
|
||||||
let titleParts = title.split('<p>');
|
let titleParts = title.split('<p>');
|
||||||
const textParts = titleParts.filter(v => v).map(v => `<div>${v.replace(/(<([^>]+)>)/ig, '')}</div>`);
|
const textParts = titleParts.filter(v => v).map(v => `<div>${utils.removeHtmlTags(v)}</div>`);
|
||||||
if (bolder && textParts.length > 1)
|
if (bolder && textParts.length > 1)
|
||||||
textParts[0] = `<b>${textParts[0]}</b>`;
|
textParts[0] = `<b>${textParts[0]}</b>`;
|
||||||
return textParts.join('');
|
return textParts.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
const insetStyle = inset => `width: ${inset*20}px`;
|
const getIndentStyle = inset => `width: ${inset*20}px`;
|
||||||
const pc = parsed.contents;
|
|
||||||
const newpc = [];
|
|
||||||
|
|
||||||
//преобразуем не первые разделы body в title-subtitle
|
const getLabelStyle = (inset) => {
|
||||||
let curSubtitles = [];
|
const fontSizes = ['110%', '100%', '90%', '85%'];
|
||||||
let prevBodyIndex = -1;
|
inset = (inset > 3 ? 3 : inset);
|
||||||
for (let i = 0; i < pc.length; i++) {
|
return `font-size: ${fontSizes[inset]}`;
|
||||||
const cont = pc[i];
|
};
|
||||||
if (prevBodyIndex != cont.bodyIndex)
|
|
||||||
curSubtitles = [];
|
|
||||||
|
|
||||||
prevBodyIndex = cont.bodyIndex;
|
|
||||||
|
|
||||||
if (cont.bodyIndex > 1) {
|
|
||||||
if (cont.inset < 1) {
|
|
||||||
newpc.push(Object.assign({}, cont, {subtitles: curSubtitles}));
|
|
||||||
} else {
|
|
||||||
curSubtitles.push(Object.assign({}, cont, {inset: cont.inset - 1}));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
newpc.push(cont);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//формируем newContents
|
//формируем newContents
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const newContents = [];
|
const newContents = [];
|
||||||
newpc.forEach((cont) => {
|
newpc.forEach((cont) => {
|
||||||
const label = prepareLabel(cont.title, true);
|
const label = prepareLabel(cont.title, true);
|
||||||
const indentStyle = insetStyle(cont.inset);
|
const indentStyle = getIndentStyle(cont.inset);
|
||||||
|
const labelStyle = getLabelStyle(cont.inset);
|
||||||
|
|
||||||
let j = 0;
|
let j = 0;
|
||||||
const list = [];
|
const list = [];
|
||||||
cont.subtitles.forEach((sub) => {
|
cont.subtitles.forEach((sub) => {
|
||||||
const l = prepareLabel(sub.title);
|
const l = prepareLabel(sub.title);
|
||||||
const s = insetStyle(sub.inset + 1);
|
const s = getIndentStyle(sub.inset + 1);
|
||||||
|
const ls = getLabelStyle(cont.inset + 1);
|
||||||
const p = parsed.para[sub.paraIndex];
|
const p = parsed.para[sub.paraIndex];
|
||||||
list[j] = {perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, indentStyle: s};
|
list[j] = {perc: (p.offset/parsed.textLength*100).toFixed(2), label: l, key: j, offset: p.offset, indentStyle: s, labelStyle: ls};
|
||||||
j++;
|
j++;
|
||||||
});
|
});
|
||||||
|
|
||||||
const p = parsed.para[cont.paraIndex];
|
const p = parsed.para[cont.paraIndex];
|
||||||
newContents[i] = {perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, indentStyle, expanded: false, list};
|
newContents[i] = {perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset, indentStyle, labelStyle, expanded: false, list};
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.contents = newContents;
|
this.contents = newContents;
|
||||||
|
|
||||||
|
//формируем newImages
|
||||||
|
const newImages = [];
|
||||||
|
for (i = 0; i < ims.length; i++) {
|
||||||
|
const image = ims[i];
|
||||||
|
const bin = parsed.binary[image.id];
|
||||||
|
const type = (bin ? bin.type : '');
|
||||||
|
|
||||||
|
const label = (image.alt ? image.alt : '<span style="font-size: 90%; color: #dddddd"><i>Без названия</i></span>');
|
||||||
|
const indentStyle = getIndentStyle(1);
|
||||||
|
const labelStyle = getLabelStyle(1);
|
||||||
|
|
||||||
|
const p = parsed.para[image.paraIndex];
|
||||||
|
newImages.push({perc: (p.offset/parsed.textLength*100).toFixed(0), label, key: i, offset: p.offset,
|
||||||
|
indentStyle, labelStyle, type, num: image.num, id: image.id, local: image.local});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.images = newImages;
|
||||||
|
|
||||||
|
if (this.selectedTab == 'contents' && !this.contents.length && this.images.length)
|
||||||
|
this.selectedTab = 'images';
|
||||||
|
|
||||||
|
//выделим на bookPos
|
||||||
|
this.updateBookPosSelection();
|
||||||
|
|
||||||
|
//асинхронная загрузка изображений
|
||||||
|
this.imageSrc = [];
|
||||||
|
this.imageLoaded = [];
|
||||||
|
await utils.sleep(50);
|
||||||
|
(async() => {
|
||||||
|
for (i = 0; i < ims.length; i++) {
|
||||||
|
const {id, local} = ims[i];
|
||||||
|
const bin = this.parsed.binary[id];
|
||||||
|
if (local)
|
||||||
|
this.$set(this.imageSrc, id, (bin ? `data:${bin.type};base64,${bin.data}` : ''));
|
||||||
|
else
|
||||||
|
this.$set(this.imageSrc, id, id);
|
||||||
|
this.imageLoaded[id] = true;
|
||||||
|
await utils.sleep(5);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBookPosSelection() {
|
||||||
|
if (!this.isVisible)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await utils.sleep(50);
|
||||||
|
const bp = this.bookPos;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.contents.length; i++) {
|
||||||
|
const item = this.contents[i];
|
||||||
|
const nextOffset = (i < this.contents.length - 1 ? this.contents[i + 1].offset : this.parsed.textLength);
|
||||||
|
|
||||||
|
for (let j = 0; j < item.list.length; j++) {
|
||||||
|
const subitem = item.list[j];
|
||||||
|
const nextSubOffset = (j < item.list.length - 1 ? item.list[j + 1].offset : nextOffset);
|
||||||
|
|
||||||
|
if (bp >= subitem.offset && bp < nextSubOffset) {
|
||||||
|
subitem.isBookPos = true;
|
||||||
|
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
|
||||||
|
} else if (subitem.isBookPos) {
|
||||||
|
subitem.isBookPos = false;
|
||||||
|
this.$set(this.contents, i, Object.assign(item, {list: item.list}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bp >= item.offset && bp < nextOffset) {
|
||||||
|
this.$set(this.contents, i, Object.assign(item, {isBookPos: true}));
|
||||||
|
} else if (item.isBookPos) {
|
||||||
|
this.$set(this.contents, i, Object.assign(item, {isBookPos: false}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.images.length; i++) {
|
||||||
|
const img = this.images[i];
|
||||||
|
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
|
||||||
|
|
||||||
|
if (bp >= img.offset && bp < nextOffset) {
|
||||||
|
this.$set(this.images, i, Object.assign(img, {isBookPos: true}));
|
||||||
|
} else if (img.isBookPos) {
|
||||||
|
this.$set(this.images, i, Object.assign(img, {isBookPos: false}));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async expandClick(key) {
|
async expandClick(key) {
|
||||||
@@ -177,7 +328,6 @@ class ContentsPage extends Vue {
|
|||||||
|
|
||||||
async setBookPos(newValue) {
|
async setBookPos(newValue) {
|
||||||
this.$emit('book-pos-changed', {bookPos: newValue});
|
this.$emit('book-pos-changed', {bookPos: newValue});
|
||||||
await this.$nextTick();
|
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,10 +355,10 @@ class ContentsPage extends Vue {
|
|||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
padding: 10px 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.item, .subitem {
|
.item, .subitem, .item-book-pos, .subitem-book-pos {
|
||||||
height: 40px;
|
|
||||||
border-bottom: 1px solid #e0e0e0;
|
border-bottom: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,14 +366,24 @@ class ContentsPage extends Vue {
|
|||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-button, .no-expand-button {
|
.item-book-pos {
|
||||||
width: 40px;
|
background-color: #b0f0b0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.expand-button:hover {
|
.subitem-book-pos {
|
||||||
background-color: white;
|
background-color: #d0f5d0;
|
||||||
border-radius: 15px;
|
}
|
||||||
border: 1px solid #d0d0d0;
|
|
||||||
|
.item-book-pos:hover {
|
||||||
|
background-color: #b0e0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subitem-book-pos:hover {
|
||||||
|
background-color: #d0f0d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-button, .no-expand-button {
|
||||||
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subitems-transition {
|
.subitems-transition {
|
||||||
@@ -237,6 +397,40 @@ class ContentsPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.expanded-icon {
|
.expanded-icon {
|
||||||
transform: rotate(180deg);
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-num {
|
||||||
|
font-size: 120%;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
}
|
||||||
|
.image-type {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 80%;
|
||||||
|
padding: 2px 0 2px 0;
|
||||||
|
width: 34px;
|
||||||
|
}
|
||||||
|
.it-jpg-color {
|
||||||
|
background: linear-gradient(to right, #fabc3d, #ffec6d);
|
||||||
|
}
|
||||||
|
.it-png-color {
|
||||||
|
background: linear-gradient(to right, #4bc4e5, #6bf4ff);
|
||||||
|
}
|
||||||
|
.it-net-color {
|
||||||
|
background: linear-gradient(to right, #00c400, #00f400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumb-box {
|
||||||
|
width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-thumb {
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-img-icon {
|
||||||
|
font-size: 250%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<div class="box">
|
<div class="box">
|
||||||
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
|
<p class="p">Вы можете пожертвовать на развитие проекта любую сумму:</p>
|
||||||
<div class="address">
|
<div class="address">
|
||||||
<img class="logo" src="./assets/yandex.png">
|
<img class="logo" src="./assets/yoomoney.png">
|
||||||
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYandexMoney">Пожертвовать</q-btn><br>
|
<q-btn class="q-ml-sm q-px-sm" dense no-caps @click="donateYooMoney">Пожертвовать</q-btn><br>
|
||||||
<div class="para">{{ yandexAddress }}
|
<div class="para">{{ yooAddress }}
|
||||||
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yandexAddress, 'Яндекс кошелек')">
|
<q-icon class="copy-icon" name="la la-copy" @click="copyAddress(yooAddress, 'Кошелёк ЮMoney')">
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
<q-tooltip :delay="1000" anchor="top middle" self="center middle" content-style="font-size: 80%">Скопировать</q-tooltip>
|
||||||
</q-icon>
|
</q-icon>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +60,7 @@ import {copyTextToClipboard} from '../../../../share/utils';
|
|||||||
export default @Component({
|
export default @Component({
|
||||||
})
|
})
|
||||||
class DonateHelpPage extends Vue {
|
class DonateHelpPage extends Vue {
|
||||||
yandexAddress = '410018702323056';
|
yooAddress = '410018702323056';
|
||||||
paypalAddress = 'bookpauk@gmail.com';
|
paypalAddress = 'bookpauk@gmail.com';
|
||||||
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
bitcoinAddress = '3EbgZ7MK1UVaN38Gty5DCBtS4PknM4Ut85';
|
||||||
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
litecoinAddress = 'MP39Riec4oSNB3XMjiquKoLWxbufRYNXxZ';
|
||||||
@@ -69,8 +69,8 @@ class DonateHelpPage extends Vue {
|
|||||||
created() {
|
created() {
|
||||||
}
|
}
|
||||||
|
|
||||||
donateYandexMoney() {
|
donateYooMoney() {
|
||||||
window.open(`https://money.yandex.ru/to/${this.yandexAddress}`, '_blank');
|
window.open(`https://yoomoney.ru/to/${this.yooAddress}`, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyAddress(address, prefix) {
|
async copyAddress(address, prefix) {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
@@ -7,8 +7,8 @@
|
|||||||
<span class="greeting"><b>{{ title }}</b></span>
|
<span class="greeting"><b>{{ title }}</b></span>
|
||||||
<div class="q-my-sm"></div>
|
<div class="q-my-sm"></div>
|
||||||
<span class="greeting">Добро пожаловать!</span>
|
<span class="greeting">Добро пожаловать!</span>
|
||||||
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz</b></span>
|
<span class="greeting">Поддерживаются форматы: <b>fb2, html, txt</b> и сжатие: <b>zip, bz2, gz<span v-if="isExternalConverter">, rar</span></b></span>
|
||||||
<span v-if="isExternalConverter" class="greeting">...а также форматы: <b>rtf, doc, docx, pdf, epub, mobi</b></span>
|
<span v-if="isExternalConverter" class="greeting">...а также частично форматы: <b>epub, mobi, rtf, doc, docx, pdf, djvu</b></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
<div class="col-auto column justify-start items-center no-wrap overflow-hidden">
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class PasteTextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadBuffer() {
|
loadBuffer() {
|
||||||
this.$emit('load-buffer', {buffer: `<buffer><cut-title>${utils.escapeXml(this.bookTitle)}</cut-title>${this.$refs.textArea.value}</buffer>`});
|
this.$emit('load-buffer', {buffer: `<buffer><fb2-title>${utils.escapeXml(this.bookTitle)}</fb2-title>${utils.escapeXml(this.$refs.textArea.value)}</buffer>`});
|
||||||
this.close();
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,9 @@
|
|||||||
<q-icon name="la la-copy" size="32px"/>
|
<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>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['copyText'] }}</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button ref="splitToPara" v-show="showToolButton['splitToPara']" class="tool-button" :class="buttonActiveClass('splitToPara')" @click="buttonClick('splitToPara')" v-ripple>
|
<button ref="convOptions" v-show="showToolButton['convOptions']" class="tool-button" :class="buttonActiveClass('convOptions')" @click="buttonClick('convOptions')" v-ripple>
|
||||||
<q-icon name="la la-retweet" size="32px"/>
|
<q-icon name="la la-magic" size="32px"/>
|
||||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['splitToPara'] }}</q-tooltip>
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">{{ rstore.readerActions['convOptions'] }}</q-tooltip>
|
||||||
</button>
|
</button>
|
||||||
<button ref="refresh" v-show="showToolButton['refresh']" class="tool-button" :class="buttonActiveClass('refresh')" @click="buttonClick('refresh')" v-ripple>
|
<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-icon name="la la-sync" size="32px" :class="{clear: !showRefreshIcon}"/>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
<HelpPage v-if="helpActive" ref="helpPage" @do-action="doAction"></HelpPage>
|
||||||
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
<ClickMapPage v-show="clickMapActive" ref="clickMapPage"></ClickMapPage>
|
||||||
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
<ServerStorage v-show="hidden" ref="serverStorage"></ServerStorage>
|
||||||
<ContentsPage v-show="contentsActive" ref="contentsPage" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
<ContentsPage v-show="contentsActive" ref="contentsPage" :book-pos="bookPos" :is-visible="contentsActive" @do-action="doAction" @book-pos-changed="bookPosChanged"></ContentsPage>
|
||||||
|
|
||||||
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
|
<ReaderDialogs ref="dialogs" @donate-toggle="donateToggle" @version-history-toggle="versionHistoryToggle"></ReaderDialogs>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,8 +131,14 @@ import ContentsPage from './ContentsPage/ContentsPage.vue';
|
|||||||
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
|
import ReaderDialogs from './ReaderDialogs/ReaderDialogs.vue';
|
||||||
|
|
||||||
import bookManager from './share/bookManager';
|
import bookManager from './share/bookManager';
|
||||||
|
import wallpaperStorage from './share/wallpaperStorage';
|
||||||
|
import dynamicCss from '../../share/dynamicCss';
|
||||||
|
|
||||||
import rstore from '../../store/modules/reader';
|
import rstore from '../../store/modules/reader';
|
||||||
import readerApi from '../../api/reader';
|
import readerApi from '../../api/reader';
|
||||||
|
import miscApi from '../../api/misc';
|
||||||
|
|
||||||
|
import {versionHistory} from './versionHistory';
|
||||||
import * as utils from '../../share/utils';
|
import * as utils from '../../share/utils';
|
||||||
|
|
||||||
export default @Component({
|
export default @Component({
|
||||||
@@ -191,6 +197,10 @@ export default @Component({
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
|
dualPageMode(newValue) {
|
||||||
|
if (newValue)
|
||||||
|
this.stopScrolling();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
class Reader extends Vue {
|
class Reader extends Vue {
|
||||||
@@ -224,12 +234,12 @@ class Reader extends Vue {
|
|||||||
whatsNewVisible = false;
|
whatsNewVisible = false;
|
||||||
whatsNewContent = '';
|
whatsNewContent = '';
|
||||||
donationVisible = false;
|
donationVisible = false;
|
||||||
|
dualPageMode = false;
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.rstore = rstore;
|
this.rstore = rstore;
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
this.dispatch = this.$store.dispatch;
|
|
||||||
this.reader = this.$store.state.reader;
|
this.reader = this.$store.state.reader;
|
||||||
this.config = this.$store.state.config;
|
this.config = this.$store.state.config;
|
||||||
|
|
||||||
@@ -250,11 +260,11 @@ class Reader extends Vue {
|
|||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
this.paramPosIgnore = false;
|
this.paramPosIgnore = false;
|
||||||
}
|
}
|
||||||
}, 500, {'maxWait':5000});
|
}, 500, {maxWait: 5000});
|
||||||
|
|
||||||
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
this.scrollingSetRecentBook = _.debounce((newValue) => {
|
||||||
this.debouncedSetRecentBook(newValue);
|
this.debouncedSetRecentBook(newValue);
|
||||||
}, 15000, {'maxWait':20000});
|
}, 15000, {maxWait: 20000});
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', () => {
|
document.addEventListener('fullscreenchange', () => {
|
||||||
this.fullScreenActive = (document.fullscreenElement !== null);
|
this.fullScreenActive = (document.fullscreenElement !== null);
|
||||||
@@ -267,6 +277,7 @@ class Reader extends Vue {
|
|||||||
this.updateHeaderMinWidth();
|
this.updateHeaderMinWidth();
|
||||||
|
|
||||||
(async() => {
|
(async() => {
|
||||||
|
await wallpaperStorage.init();
|
||||||
await bookManager.init(this.settings);
|
await bookManager.init(this.settings);
|
||||||
bookManager.addEventListener(this.bookManagerEvent);
|
bookManager.addEventListener(this.bookManagerEvent);
|
||||||
|
|
||||||
@@ -293,6 +304,16 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
await this.$refs.dialogs.init();
|
await this.$refs.dialogs.init();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
(async() => {
|
||||||
|
this.isFirstNeedUpdateNotify = true;
|
||||||
|
//вечный цикл, запрашиваем периодически конфиг для проверки выхода новой версии читалки
|
||||||
|
while (true) {// eslint-disable-line no-constant-condition
|
||||||
|
await this.checkNewVersionAvailable();
|
||||||
|
await utils.sleep(3600*1000); //каждый час
|
||||||
|
}
|
||||||
|
//дальше кода нет
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSettings() {
|
loadSettings() {
|
||||||
@@ -304,6 +325,13 @@ class Reader extends Vue {
|
|||||||
this.blinkCachedLoad = settings.blinkCachedLoad;
|
this.blinkCachedLoad = settings.blinkCachedLoad;
|
||||||
this.showToolButton = settings.showToolButton;
|
this.showToolButton = settings.showToolButton;
|
||||||
this.enableSitesFilter = settings.enableSitesFilter;
|
this.enableSitesFilter = settings.enableSitesFilter;
|
||||||
|
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
|
||||||
|
this.splitToPara = settings.splitToPara;
|
||||||
|
this.djvuQuality = settings.djvuQuality;
|
||||||
|
this.pdfAsText = settings.pdfAsText;
|
||||||
|
this.pdfQuality = settings.pdfQuality;
|
||||||
|
this.dualPageMode = settings.dualPageMode;
|
||||||
|
this.userWallpapers = settings.userWallpapers;
|
||||||
|
|
||||||
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
this.readerActionByKeyCode = utils.userHotKeysObjectSwap(settings.userHotKeys);
|
||||||
this.$root.readerActionByKeyEvent = (event) => {
|
this.$root.readerActionByKeyEvent = (event) => {
|
||||||
@@ -311,6 +339,54 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.updateHeaderMinWidth();
|
this.updateHeaderMinWidth();
|
||||||
|
|
||||||
|
this.loadWallpapers();//no await
|
||||||
|
}
|
||||||
|
|
||||||
|
//wallpaper css
|
||||||
|
async loadWallpapers() {
|
||||||
|
const wallpaperDataLength = await wallpaperStorage.getLength();
|
||||||
|
if (wallpaperDataLength !== this.wallpaperDataLength) {//оптимизация
|
||||||
|
this.wallpaperDataLength = wallpaperDataLength;
|
||||||
|
|
||||||
|
let newCss = '';
|
||||||
|
for (const wp of this.userWallpapers) {
|
||||||
|
const data = await wallpaperStorage.getData(wp.cssClass);
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
//здесь будем восстанавливать данные с сервера
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
newCss += `.${wp.cssClass} {background: url(${data}) center; background-size: 100% 100%;}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dynamicCss.replace('wallpapers', newCss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkNewVersionAvailable() {
|
||||||
|
if (!this.checkingNewVersion && this.showNeedUpdateNotify) {
|
||||||
|
this.checkingNewVersion = true;
|
||||||
|
try {
|
||||||
|
await utils.sleep(15*1000); //подождем 15 секунд, чтобы прогрузился ServiceWorker при выходе новой версии
|
||||||
|
const config = await miscApi.loadConfig();
|
||||||
|
this.commit('config/setConfig', config);
|
||||||
|
|
||||||
|
let againMes = '';
|
||||||
|
if (this.isFirstNeedUpdateNotify) {
|
||||||
|
againMes = ' еще один раз';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.version != this.clientVersion)
|
||||||
|
this.$root.notify.info(`Вышла новая версия (v${this.version}) читалки.<br>Пожалуйста, обновите страницу${againMes}.`, 'Обновление');
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
this.checkingNewVersion = false;
|
||||||
|
}
|
||||||
|
this.isFirstNeedUpdateNotify = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateHeaderMinWidth() {
|
updateHeaderMinWidth() {
|
||||||
@@ -394,6 +470,16 @@ class Reader extends Vue {
|
|||||||
return this.$store.state.config.mode;
|
return this.$store.state.config.mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get version() {
|
||||||
|
return this.$store.state.config.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
get clientVersion() {
|
||||||
|
let v = versionHistory[0].header;
|
||||||
|
v = v.split(' ')[0];
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
get routeParamUrl() {
|
get routeParamUrl() {
|
||||||
let result = '';
|
let result = '';
|
||||||
const path = this.$route.fullPath;
|
const path = this.$route.fullPath;
|
||||||
@@ -593,12 +679,6 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshBookSplitToPara() {
|
|
||||||
if (this.mostRecentBook()) {
|
|
||||||
this.loadBook({url: this.mostRecentBook().url, skipCheck: true, isText: true, force: true});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recentBooksClose() {
|
recentBooksClose() {
|
||||||
this.recentBooksActive = false;
|
this.recentBooksActive = false;
|
||||||
}
|
}
|
||||||
@@ -662,6 +742,12 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convOptionsToggle() {
|
||||||
|
this.settingsToggle();
|
||||||
|
if (this.settingsActive)
|
||||||
|
this.$refs.settingsPage.selectedTab = 'convert';
|
||||||
|
}
|
||||||
|
|
||||||
helpToggle() {
|
helpToggle() {
|
||||||
this.helpActive = !this.helpActive;
|
this.helpActive = !this.helpActive;
|
||||||
if (this.helpActive) {
|
if (this.helpActive) {
|
||||||
@@ -689,9 +775,8 @@ class Reader extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshBook() {
|
refreshBook() {
|
||||||
if (this.mostRecentBook()) {
|
const mrb = this.mostRecentBook();
|
||||||
this.loadBook({url: this.mostRecentBook().url, force: true});
|
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
undoAction() {
|
undoAction() {
|
||||||
@@ -728,10 +813,9 @@ class Reader extends Vue {
|
|||||||
case 'loader':
|
case 'loader':
|
||||||
case 'fullScreen':
|
case 'fullScreen':
|
||||||
case 'setPosition':
|
case 'setPosition':
|
||||||
case 'scrolling':
|
|
||||||
case 'search':
|
case 'search':
|
||||||
case 'copyText':
|
case 'copyText':
|
||||||
case 'splitToPara':
|
case 'convOptions':
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
case 'contents':
|
case 'contents':
|
||||||
case 'libs':
|
case 'libs':
|
||||||
@@ -744,6 +828,13 @@ class Reader extends Vue {
|
|||||||
classResult = classActive;
|
classResult = classActive;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'scrolling':
|
||||||
|
if (this.progressActive || this.dualPageMode) {
|
||||||
|
classResult = classDisabled;
|
||||||
|
} else if (this[`${action}Active`]) {
|
||||||
|
classResult = classActive;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'undoAction':
|
case 'undoAction':
|
||||||
if (this.actionCur <= 0)
|
if (this.actionCur <= 0)
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
@@ -765,7 +856,6 @@ class Reader extends Vue {
|
|||||||
case 'contents':
|
case 'contents':
|
||||||
classResult = classDisabled;
|
classResult = classDisabled;
|
||||||
break;
|
break;
|
||||||
case 'splitToPara':
|
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
case 'recentBooks':
|
case 'recentBooks':
|
||||||
if (!this.mostRecentBookReactive)
|
if (!this.mostRecentBookReactive)
|
||||||
@@ -846,8 +936,12 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
let url = encodeURI(decodeURI(opts.url));
|
let url = encodeURI(decodeURI(opts.url));
|
||||||
|
|
||||||
|
//TODO: убрать конвертирование 'file://' после 06.2021
|
||||||
|
if (url.length == 71 && url.indexOf('file://') == 0)
|
||||||
|
url = url.replace(/^file/, 'disk');
|
||||||
|
|
||||||
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
if ((url.indexOf('http://') != 0) && (url.indexOf('https://') != 0) &&
|
||||||
(url.indexOf('file://') != 0))
|
(url.indexOf('disk://') != 0))
|
||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
|
|
||||||
// уже просматривается сейчас
|
// уже просматривается сейчас
|
||||||
@@ -878,6 +972,7 @@ class Reader extends Vue {
|
|||||||
wasOpened = (wasOpened ? wasOpened : {});
|
wasOpened = (wasOpened ? wasOpened : {});
|
||||||
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
|
||||||
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
|
||||||
|
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
|
||||||
|
|
||||||
let book = null;
|
let book = null;
|
||||||
|
|
||||||
@@ -922,9 +1017,13 @@ class Reader extends Vue {
|
|||||||
if (!book) {
|
if (!book) {
|
||||||
book = await readerApi.loadBook({
|
book = await readerApi.loadBook({
|
||||||
url,
|
url,
|
||||||
skipCheck: (opts.skipCheck ? true : false),
|
uploadFileName,
|
||||||
isText: (opts.isText ? true : false),
|
enableSitesFilter: this.enableSitesFilter,
|
||||||
enableSitesFilter: this.enableSitesFilter
|
skipHtmlCheck: (this.splitToPara ? true : false),
|
||||||
|
isText: (this.splitToPara ? true : false),
|
||||||
|
djvuQuality: this.djvuQuality,
|
||||||
|
pdfAsText: this.pdfAsText,
|
||||||
|
pdfQuality: this.pdfQuality,
|
||||||
},
|
},
|
||||||
(state) => {
|
(state) => {
|
||||||
progress.setState(state);
|
progress.setState(state);
|
||||||
@@ -940,7 +1039,7 @@ class Reader extends Vue {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// добавляем в историю
|
// добавляем в историю
|
||||||
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, addedBook));
|
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
|
||||||
this.mostRecentBook();
|
this.mostRecentBook();
|
||||||
this.addAction(bookPos);
|
this.addAction(bookPos);
|
||||||
this.updateRoute(true);
|
this.updateRoute(true);
|
||||||
@@ -958,6 +1057,8 @@ class Reader extends Vue {
|
|||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
|
||||||
|
} finally {
|
||||||
|
this.checkNewVersionAvailable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,7 +1078,7 @@ class Reader extends Vue {
|
|||||||
|
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
|
|
||||||
await this.loadBook({url});
|
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
progress.hide(); this.progressActive = false;
|
progress.hide(); this.progressActive = false;
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
@@ -1048,8 +1149,8 @@ class Reader extends Vue {
|
|||||||
case 'copyText':
|
case 'copyText':
|
||||||
this.copyTextToggle();
|
this.copyTextToggle();
|
||||||
break;
|
break;
|
||||||
case 'splitToPara':
|
case 'convOptions':
|
||||||
this.refreshBookSplitToPara();
|
this.convOptionsToggle();
|
||||||
break;
|
break;
|
||||||
case 'refresh':
|
case 'refresh':
|
||||||
this.refreshBook();
|
this.refreshBook();
|
||||||
|
|||||||
@@ -57,37 +57,6 @@
|
|||||||
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
<q-btn class="q-px-sm" dense no-caps @click="donationDialogRemind">Напомнить позже</q-btn>
|
||||||
</span>
|
</span>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog ref="dialog3" v-model="liberamaTopVisible">
|
|
||||||
<template slot="header">
|
|
||||||
Здравствуйте, уважаемые читатели!
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div style="word-break: normal">
|
|
||||||
Создан новый ресурс:<br><br>
|
|
||||||
|
|
||||||
<a href="https://liberama.top" target="_blank">https://liberama.top</a>
|
|
||||||
<br><br>
|
|
||||||
Это клон читалки Omni Reader, но с некоторыми дополнениями, ориентированными в сторону более свободного обмена книгами:
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>добавлено новое окно "Библиотека" для свободного доступа к Флибусте и другим ресурсам по желанию читателя</li>
|
|
||||||
<li>планируется добавить возможность создания подборок книг и обмена ими между пользователями</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
Легко мигрировать на новый сайт можно с помощью синхронизации с сервером.
|
|
||||||
О багах и предложениях просьба сообщать на почту <a href="mailto:bookpauk@gmail.com">bookpauk@gmail.com</a><br><br>
|
|
||||||
Спасибо, что вы с нами!
|
|
||||||
<br><br>
|
|
||||||
<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">
|
|
||||||
<q-btn class="q-px-sm" dense no-caps @click="liberamaTopDialogDisable">Больше не показывать</q-btn>
|
|
||||||
</span>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -114,7 +83,6 @@ class ReaderDialogs extends Vue {
|
|||||||
whatsNewVisible = false;
|
whatsNewVisible = false;
|
||||||
whatsNewContent = '';
|
whatsNewContent = '';
|
||||||
donationVisible = false;
|
donationVisible = false;
|
||||||
liberamaTopVisible = false;
|
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.commit = this.$store.commit;
|
this.commit = this.$store.commit;
|
||||||
@@ -127,14 +95,12 @@ class ReaderDialogs extends Vue {
|
|||||||
async init() {
|
async init() {
|
||||||
await this.showWhatsNew();
|
await this.showWhatsNew();
|
||||||
await this.showDonation();
|
await this.showDonation();
|
||||||
await this.showLiberamaTop();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSettings() {
|
loadSettings() {
|
||||||
const settings = this.settings;
|
const settings = this.settings;
|
||||||
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
this.showWhatsNewDialog = settings.showWhatsNewDialog;
|
||||||
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
this.showDonationDialog2020 = settings.showDonationDialog2020;
|
||||||
this.showLiberamaTopDialog2020 = settings.showLiberamaTopDialog2020;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async showWhatsNew() {
|
async showWhatsNew() {
|
||||||
@@ -171,7 +137,6 @@ class ReaderDialogs extends Vue {
|
|||||||
|
|
||||||
openDonate() {
|
openDonate() {
|
||||||
this.donationVisible = false;
|
this.donationVisible = false;
|
||||||
this.liberamaTopVisible = false;
|
|
||||||
this.$emit('donate-toggle');
|
this.$emit('donate-toggle');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,24 +175,8 @@ class ReaderDialogs extends Vue {
|
|||||||
return this.$store.state.reader.donationRemindDate;
|
return this.$store.state.reader.donationRemindDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
async showLiberamaTop() {
|
|
||||||
const today = utils.formatDate(new Date(), 'coDate');
|
|
||||||
|
|
||||||
if (this.mode == 'omnireader' && today < '2020-12-01' && this.showLiberamaTopDialog2020) {
|
|
||||||
await utils.sleep(3000);
|
|
||||||
this.liberamaTopVisible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
liberamaTopDialogDisable() {
|
|
||||||
this.liberamaTopVisible = false;
|
|
||||||
if (this.showLiberamaTopDialog2020) {
|
|
||||||
this.commit('reader/setSettings', { showLiberamaTopDialog2020: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keyHook() {
|
keyHook() {
|
||||||
if (this.$refs.dialog1.active || this.$refs.dialog2.active || this.$refs.dialog3.active)
|
if (this.$refs.dialog1.active || this.$refs.dialog2.active)
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,11 @@
|
|||||||
placeholder="Найти"
|
placeholder="Найти"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
@click.stop
|
@click.stop
|
||||||
/>
|
>
|
||||||
|
<template v-slot:append>
|
||||||
|
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch"/>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
<span v-html="props.cols[2].label"></span>
|
<span v-html="props.cols[2].label"></span>
|
||||||
</q-th>
|
</q-th>
|
||||||
</q-tr>
|
</q-tr>
|
||||||
@@ -53,13 +56,14 @@
|
|||||||
<div class="break-word" style="width: 332px; font-size: 90%">
|
<div class="break-word" style="width: 332px; font-size: 90%">
|
||||||
<div style="color: green">{{ props.row.desc.author }}</div>
|
<div style="color: green">{{ props.row.desc.author }}</div>
|
||||||
<div>{{ props.row.desc.title }}</div>
|
<div>{{ props.row.desc.title }}</div>
|
||||||
|
<div class="read-bar" :style="`width: ${332*props.row.readPart}px`"></div>
|
||||||
</div>
|
</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
<q-td key="links" :props="props" class="td-mp" auto-width>
|
<q-td key="links" :props="props" class="td-mp" auto-width>
|
||||||
<div class="break-word" style="width: 75px; font-size: 90%">
|
<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 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>
|
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
|
||||||
</div>
|
</div>
|
||||||
</q-td>
|
</q-td>
|
||||||
|
|
||||||
@@ -87,7 +91,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Component from 'vue-class-component';
|
import Component from 'vue-class-component';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import _ from 'lodash';
|
//import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
@@ -106,12 +110,13 @@ export default @Component({
|
|||||||
})
|
})
|
||||||
class RecentBooksPage extends Vue {
|
class RecentBooksPage extends Vue {
|
||||||
loading = false;
|
loading = false;
|
||||||
search = null;
|
search = '';
|
||||||
tableData = [];
|
tableData = [];
|
||||||
columns = [];
|
columns = [];
|
||||||
pagination = {};
|
pagination = {};
|
||||||
|
|
||||||
created() {
|
created() {
|
||||||
|
this.firstInit = true;
|
||||||
this.pagination = {rowsPerPage: 0};
|
this.pagination = {rowsPerPage: 0};
|
||||||
|
|
||||||
this.columns = [
|
this.columns = [
|
||||||
@@ -167,26 +172,11 @@ class RecentBooksPage extends Vue {
|
|||||||
this.initing = true;
|
this.initing = true;
|
||||||
|
|
||||||
|
|
||||||
if (!bookManager.loaded) {
|
if (this.firstInit) {//для отзывчивости
|
||||||
await this.updateTableData(10);
|
await this.updateTableData(20);
|
||||||
//для отзывчивости
|
this.firstInit = false;
|
||||||
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(20);
|
|
||||||
j *= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.sleep(100);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//для отзывчивости
|
|
||||||
await utils.sleep(100);
|
|
||||||
}
|
}
|
||||||
|
await utils.sleep(50);
|
||||||
await this.updateTableData();
|
await this.updateTableData();
|
||||||
this.initing = false;
|
this.initing = false;
|
||||||
})();
|
})();
|
||||||
@@ -214,38 +204,21 @@ class RecentBooksPage extends Vue {
|
|||||||
d.setTime(book.touchTime);
|
d.setTime(book.touchTime);
|
||||||
const t = utils.formatDate(d).split(' ');
|
const t = utils.formatDate(d).split(' ');
|
||||||
|
|
||||||
|
let readPart = 0;
|
||||||
let perc = '';
|
let perc = '';
|
||||||
let textLen = '';
|
let textLen = '';
|
||||||
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
||||||
if (book.textLength) {
|
if (book.textLength) {
|
||||||
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
|
readPart = p/book.textLength;
|
||||||
|
perc = ` [${(readPart*100).toFixed(2)}%]`;
|
||||||
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fb2 = (book.fb2 ? book.fb2 : {});
|
const bt = utils.getBookTitle(book.fb2);
|
||||||
|
|
||||||
let title = fb2.bookTitle;
|
let title = bt.bookTitle;
|
||||||
if (title)
|
title = (title ? `"${title}"`: '');
|
||||||
title = `"${title}"`;
|
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
|
||||||
else
|
|
||||||
title = '';
|
|
||||||
|
|
||||||
let author = '';
|
|
||||||
if (fb2.author) {
|
|
||||||
const authorNames = fb2.author.map(a => _.compact([
|
|
||||||
a.lastName,
|
|
||||||
a.firstName,
|
|
||||||
a.middleName
|
|
||||||
]).join(' '));
|
|
||||||
author = authorNames.join(', ');
|
|
||||||
} else {//TODO: убрать в будущем
|
|
||||||
author = _.compact([
|
|
||||||
fb2.lastName,
|
|
||||||
fb2.firstName,
|
|
||||||
fb2.middleName
|
|
||||||
]).join(' ');
|
|
||||||
}
|
|
||||||
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
|
||||||
|
|
||||||
result.push({
|
result.push({
|
||||||
num,
|
num,
|
||||||
@@ -256,9 +229,11 @@ class RecentBooksPage extends Vue {
|
|||||||
author,
|
author,
|
||||||
title: `${title}${perc}${textLen}`,
|
title: `${title}${perc}${textLen}`,
|
||||||
},
|
},
|
||||||
descString: `${author}${title}${perc}${textLen}`,
|
readPart,
|
||||||
|
descString: `${author}${title}${perc}${textLen}`,//для сортировки
|
||||||
url: book.url,
|
url: book.url,
|
||||||
path: book.path,
|
path: book.path,
|
||||||
|
fullTitle: bt.fullTitle,
|
||||||
key: book.key,
|
key: book.key,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -276,6 +251,11 @@ class RecentBooksPage extends Vue {
|
|||||||
this.updating = false;
|
this.updating = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetSearch() {
|
||||||
|
this.search = '';
|
||||||
|
this.$refs.input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
wordEnding(num) {
|
wordEnding(num) {
|
||||||
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
|
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
|
||||||
const deci = num % 100;
|
const deci = num % 100;
|
||||||
@@ -291,13 +271,18 @@ class RecentBooksPage extends Vue {
|
|||||||
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadBook(fb2path) {
|
async downloadBook(fb2path, fullTitle) {
|
||||||
try {
|
try {
|
||||||
await readerApi.checkCachedBook(fb2path);
|
await readerApi.checkCachedBook(fb2path);
|
||||||
|
|
||||||
const d = this.$refs.download;
|
const d = this.$refs.download;
|
||||||
d.href = fb2path;
|
d.href = fb2path;
|
||||||
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
try {
|
||||||
|
const fn = utils.makeValidFilename(fullTitle);
|
||||||
|
d.download = fn.substring(0, 100) + '.fb2';
|
||||||
|
} catch(e) {
|
||||||
|
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
||||||
|
}
|
||||||
|
|
||||||
d.click();
|
d.click();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -308,14 +293,6 @@ class RecentBooksPage extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openOriginal(url) {
|
|
||||||
window.open(url, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
openFb2(path) {
|
|
||||||
window.open(path, '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleDel(key) {
|
async handleDel(key) {
|
||||||
await bookManager.delRecentBook({key});
|
await bookManager.delRecentBook({key});
|
||||||
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
//this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
|
||||||
@@ -331,7 +308,7 @@ class RecentBooksPage extends Vue {
|
|||||||
|
|
||||||
isUrl(url) {
|
isUrl(url) {
|
||||||
if (url)
|
if (url)
|
||||||
return (url.indexOf('file://') != 0);
|
return (url.indexOf('disk://') != 0);
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -381,6 +358,10 @@ class RecentBooksPage extends Vue {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.read-bar {
|
||||||
|
height: 3px;
|
||||||
|
background-color: #aaaaaa;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -216,8 +216,15 @@ class ServerStorage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
error(message) {
|
error(message) {
|
||||||
if (this.showServerStorageMessages && !this.offlineModeActive)
|
if (this.showServerStorageMessages && !this.offlineModeActive) {
|
||||||
this.$root.notify.error(message);
|
this.errorMessageCounter = (this.errorMessageCounter ? this.errorMessageCounter + 1 : 1);
|
||||||
|
const hint = (this.errorMessageCounter < 2 ? '' :
|
||||||
|
'<div><br>Надоело это сообщение? Добавьте в настройках кнопку "Автономный режим" ' +
|
||||||
|
'<i class="la la-unlink" style="font-size: 20px; color: white"></i> на панель инструментов и активируйте ее.</div>'
|
||||||
|
);
|
||||||
|
|
||||||
|
this.$root.notify.error(message + hint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadSettings(force = false, doNotifySuccess = true) {
|
async loadSettings(force = false, doNotifySuccess = true) {
|
||||||
@@ -507,10 +514,10 @@ class ServerStorage extends Vue {
|
|||||||
if (md.key && result[md.key])
|
if (md.key && result[md.key])
|
||||||
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
|
result[md.key] = utils.applyObjDiff(result[md.key], md.mod, {isAddChanged: true});
|
||||||
|
|
||||||
if (!bookManager.loaded) {
|
/*if (!bookManager.loaded) {
|
||||||
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
this.warning('Ожидание загрузки списка книг перед синхронизацией');
|
||||||
while (!bookManager.loaded) await utils.sleep(100);
|
while (!bookManager.loaded) await utils.sleep(100);
|
||||||
}
|
}*/
|
||||||
|
|
||||||
if (newRecent.rev != this.cachedRecent.rev)
|
if (newRecent.rev != this.cachedRecent.rev)
|
||||||
await this.setCachedRecent(newRecent);
|
await this.setCachedRecent(newRecent);
|
||||||
@@ -580,8 +587,8 @@ class ServerStorage extends Vue {
|
|||||||
let newRecent = {};
|
let newRecent = {};
|
||||||
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
if (!itemKey || (needSaveRecentPatch && Object.keys(newRecentPatch.data).length > 10)) {
|
||||||
//ждем весь bm.recent
|
//ждем весь bm.recent
|
||||||
while (!bookManager.loaded)
|
/*while (!bookManager.loaded)
|
||||||
await utils.sleep(100);
|
await utils.sleep(100);*/
|
||||||
|
|
||||||
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
newRecent = {rev: this.cachedRecent.rev + 1, data: _.cloneDeep(bm.recent)};
|
||||||
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
newRecentPatch = {rev: this.cachedRecentPatch.rev + 1, data: {}};
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
|
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
|
||||||
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
<q-tab class="tab" name="keys" icon="la la-gamepad" label="Управление" />
|
||||||
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
<q-tab class="tab" name="pagemove" icon="la la-school" label="Листание" />
|
||||||
|
<q-tab class="tab" name="convert" icon="la la-magic" label="Конвертир." />
|
||||||
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
<q-tab class="tab" name="others" icon="la la-list-ul" label="Прочее" />
|
||||||
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
<q-tab class="tab" name="reset" icon="la la-broom" label="Сброс" />
|
||||||
<div v-show="tabsScrollable" class="q-pt-lg"/>
|
<div v-show="tabsScrollable" class="q-pt-lg"/>
|
||||||
@@ -53,6 +54,10 @@
|
|||||||
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
|
<div v-if="selectedTab == 'pagemove'" class="fit tab-panel">
|
||||||
@@include('./include/PageMoveTab.inc');
|
@@include('./include/PageMoveTab.inc');
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Конвертирование ------------------------------------------------------------->
|
||||||
|
<div v-if="selectedTab == 'convert'" class="fit tab-panel">
|
||||||
|
@@include('./include/ConvertTab.inc');
|
||||||
|
</div>
|
||||||
<!-- Прочее ---------------------------------------------------------------------->
|
<!-- Прочее ---------------------------------------------------------------------->
|
||||||
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
<div v-if="selectedTab == 'others'" class="fit tab-panel">
|
||||||
@@include('./include/OthersTab.inc');
|
@@include('./include/OthersTab.inc');
|
||||||
@@ -74,9 +79,11 @@ import Component from 'vue-class-component';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
|
import * as cryptoUtils from '../../../share/cryptoUtils';
|
||||||
import Window from '../../share/Window.vue';
|
import Window from '../../share/Window.vue';
|
||||||
import NumInput from '../../share/NumInput.vue';
|
import NumInput from '../../share/NumInput.vue';
|
||||||
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
import UserHotKeys from './UserHotKeys/UserHotKeys.vue';
|
||||||
|
import wallpaperStorage from '../share/wallpaperStorage';
|
||||||
|
|
||||||
import rstore from '../../../store/modules/reader';
|
import rstore from '../../../store/modules/reader';
|
||||||
import defPalette from './defPalette';
|
import defPalette from './defPalette';
|
||||||
@@ -108,7 +115,7 @@ export default @Component({
|
|||||||
},
|
},
|
||||||
vertShift: function(newValue) {
|
vertShift: function(newValue) {
|
||||||
const font = (this.webFontName ? this.webFontName : this.fontName);
|
const font = (this.webFontName ? this.webFontName : this.fontName);
|
||||||
if (this.fontShifts[font] != newValue) {
|
if (this.fontShifts[font] != newValue || this.fontVertShift != newValue) {
|
||||||
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
|
this.fontShifts = Object.assign({}, this.fontShifts, {[font]: newValue});
|
||||||
this.fontVertShift = newValue;
|
this.fontVertShift = newValue;
|
||||||
}
|
}
|
||||||
@@ -125,6 +132,10 @@ export default @Component({
|
|||||||
if (newValue != '' && this.pageChangeAnimation == 'flip')
|
if (newValue != '' && this.pageChangeAnimation == 'flip')
|
||||||
this.pageChangeAnimation = '';
|
this.pageChangeAnimation = '';
|
||||||
},
|
},
|
||||||
|
dualPageMode(newValue) {
|
||||||
|
if (newValue && this.pageChangeAnimation == 'flip' || this.pageChangeAnimation == 'rightShift')
|
||||||
|
this.pageChangeAnimation = '';
|
||||||
|
},
|
||||||
textColor: function(newValue) {
|
textColor: function(newValue) {
|
||||||
this.textColorFiltered = newValue;
|
this.textColorFiltered = newValue;
|
||||||
},
|
},
|
||||||
@@ -139,11 +150,25 @@ export default @Component({
|
|||||||
if (hex.test(newValue))
|
if (hex.test(newValue))
|
||||||
this.backgroundColor = newValue;
|
this.backgroundColor = newValue;
|
||||||
},
|
},
|
||||||
|
dualDivColor(newValue) {
|
||||||
|
this.dualDivColorFiltered = newValue;
|
||||||
|
},
|
||||||
|
dualDivColorFiltered(newValue) {
|
||||||
|
if (hex.test(newValue))
|
||||||
|
this.dualDivColor = newValue;
|
||||||
|
},
|
||||||
|
statusBarColor(newValue) {
|
||||||
|
this.statusBarColorFiltered = newValue;
|
||||||
|
},
|
||||||
|
statusBarColorFiltered(newValue) {
|
||||||
|
if (hex.test(newValue))
|
||||||
|
this.statusBarColor = newValue;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
class SettingsPage extends Vue {
|
class SettingsPage extends Vue {
|
||||||
selectedTab = 'profiles';
|
selectedTab = 'profiles';
|
||||||
selectedViewTab = 'color';
|
selectedViewTab = 'mode';
|
||||||
selectedKeysTab = 'mouse';
|
selectedKeysTab = 'mouse';
|
||||||
form = {};
|
form = {};
|
||||||
fontBold = false;
|
fontBold = false;
|
||||||
@@ -152,6 +177,7 @@ class SettingsPage extends Vue {
|
|||||||
tabsScrollable = false;
|
tabsScrollable = false;
|
||||||
textColorFiltered = '';
|
textColorFiltered = '';
|
||||||
bgColorFiltered = '';
|
bgColorFiltered = '';
|
||||||
|
dualDivColorFiltered = '';
|
||||||
|
|
||||||
webFonts = [];
|
webFonts = [];
|
||||||
fonts = [];
|
fonts = [];
|
||||||
@@ -212,12 +238,18 @@ class SettingsPage extends Vue {
|
|||||||
this.vertShift = this.fontShifts[font] || 0;
|
this.vertShift = this.fontShifts[font] || 0;
|
||||||
this.textColorFiltered = this.textColor;
|
this.textColorFiltered = this.textColor;
|
||||||
this.bgColorFiltered = this.backgroundColor;
|
this.bgColorFiltered = this.backgroundColor;
|
||||||
|
this.dualDivColorFiltered = this.dualDivColor;
|
||||||
|
this.statusBarColorFiltered = this.statusBarColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
get mode() {
|
get mode() {
|
||||||
return this.$store.state.config.mode;
|
return this.$store.state.config.mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isExternalConverter() {
|
||||||
|
return this.$store.state.config.useExternalBookConverter;
|
||||||
|
}
|
||||||
|
|
||||||
get settings() {
|
get settings() {
|
||||||
return this.$store.state.reader.settings;
|
return this.$store.state.reader.settings;
|
||||||
}
|
}
|
||||||
@@ -247,9 +279,19 @@ class SettingsPage extends Vue {
|
|||||||
|
|
||||||
get wallpaperOptions() {
|
get wallpaperOptions() {
|
||||||
let result = [{label: 'Нет', value: ''}];
|
let result = [{label: 'Нет', value: ''}];
|
||||||
for (let i = 1; i < 10; i++) {
|
|
||||||
|
const userWallpapers = _.cloneDeep(this.userWallpapers);
|
||||||
|
userWallpapers.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
|
|
||||||
|
for (const wp of userWallpapers) {
|
||||||
|
if (wallpaperStorage.keyExists(wp.cssClass))
|
||||||
|
result.push({label: wp.label, value: wp.cssClass});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= 17; i++) {
|
||||||
result.push({label: i, value: `paper${i}`});
|
result.push({label: i, value: `paper${i}`});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,13 +315,15 @@ class SettingsPage extends Vue {
|
|||||||
let result = [
|
let result = [
|
||||||
{label: 'Нет', value: ''},
|
{label: 'Нет', value: ''},
|
||||||
{label: 'Вверх-вниз', value: 'downShift'},
|
{label: 'Вверх-вниз', value: 'downShift'},
|
||||||
{label: 'Вправо-влево', value: 'rightShift'},
|
(!this.dualPageMode ? {label: 'Вправо-влево', value: 'rightShift'} : null),
|
||||||
{label: 'Протаивание', value: 'thaw'},
|
{label: 'Протаивание', value: 'thaw'},
|
||||||
{label: 'Мерцание', value: 'blink'},
|
{label: 'Мерцание', value: 'blink'},
|
||||||
{label: 'Вращение', value: 'rotate'},
|
{label: 'Вращение', value: 'rotate'},
|
||||||
|
(this.wallpaper == '' && !this.dualPageMode ? {label: 'Листание', value: 'flip'} : null),
|
||||||
];
|
];
|
||||||
if (this.wallpaper == '')
|
|
||||||
result.push({label: 'Листание', value: 'flip'});
|
result = result.filter(v => v);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +386,12 @@ class SettingsPage extends Vue {
|
|||||||
case 'bg':
|
case 'bg':
|
||||||
result += `background-color: ${this.backgroundColor};`
|
result += `background-color: ${this.backgroundColor};`
|
||||||
break;
|
break;
|
||||||
|
case 'div':
|
||||||
|
result += `background-color: ${this.dualDivColor};`
|
||||||
|
break;
|
||||||
|
case 'statusbar':
|
||||||
|
result += `background-color: ${this.statusBarColor};`
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -508,6 +558,71 @@ class SettingsPage extends Vue {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadWallpaperFileClick() {
|
||||||
|
this.$refs.file.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWallpaperFile() {
|
||||||
|
const file = this.$refs.file.files[0];
|
||||||
|
if (file.size > 10*1024*1024) {
|
||||||
|
this.$root.stdDialog.alert('Файл обоев не должен превышать в размере 10Mb', 'Ошибка');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type != 'image/png' && file.type != 'image/jpeg') {
|
||||||
|
this.$root.stdDialog.alert('Файл обоев должен иметь тип PNG или JPEG', 'Ошибка');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.userWallpapers.length >= 100) {
|
||||||
|
this.$root.stdDialog.alert('Превышено максимальное количество пользовательских обоев.', 'Ошибка');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.file.value = '';
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
(async() => {
|
||||||
|
const data = e.target.result;
|
||||||
|
const key = utils.toHex(cryptoUtils.sha256(data));
|
||||||
|
const label = `#${key.substring(0, 4)}`;
|
||||||
|
const cssClass = `user-paper${key}`;
|
||||||
|
|
||||||
|
const newUserWallpapers = _.cloneDeep(this.userWallpapers);
|
||||||
|
const index = _.findIndex(newUserWallpapers, (item) => (item.cssClass == cssClass));
|
||||||
|
|
||||||
|
if (index < 0)
|
||||||
|
newUserWallpapers.push({label, cssClass});
|
||||||
|
if (!wallpaperStorage.keyExists(cssClass))
|
||||||
|
await wallpaperStorage.setData(cssClass, data);
|
||||||
|
|
||||||
|
this.userWallpapers = newUserWallpapers;
|
||||||
|
this.wallpaper = cssClass;
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delWallpaper() {
|
||||||
|
if (this.wallpaper.indexOf('user-paper') == 0) {
|
||||||
|
const newUserWallpapers = [];
|
||||||
|
for (const wp of this.userWallpapers) {
|
||||||
|
if (wp.cssClass != this.wallpaper) {
|
||||||
|
newUserWallpapers.push(wp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await wallpaperStorage.removeData(this.wallpaper);
|
||||||
|
|
||||||
|
this.userWallpapers = newUserWallpapers;
|
||||||
|
this.wallpaper = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keyHook(event) {
|
keyHook(event) {
|
||||||
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
|
||||||
this.close();
|
this.close();
|
||||||
@@ -544,7 +659,7 @@ class SettingsPage extends Vue {
|
|||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-1 {
|
.label-1, .label-7 {
|
||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,7 +671,7 @@ class SettingsPage extends Vue {
|
|||||||
width: 100px;
|
width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6 {
|
.label-1, .label-2, .label-3, .label-4, .label-5, .label-6, .label-7 {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
87
client/components/Reader/SettingsPage/include/ConvertTab.inc
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="q-mt-sm column items-center">
|
||||||
|
<span>Настройки конвертирования применяются ко всем</span>
|
||||||
|
<span>вновь загружаемым или обновляемым файлам</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="part-header">HTML, XML, TXT</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-7">Текст</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="splitToPara" size="xs" label="Попытаться разбить текст на параграфы">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Опция принудительно включает эвристику разбиения текста на<br>
|
||||||
|
параграфы в случае, если формат файла определен как html,<br>
|
||||||
|
xml или txt. Возможна нечитабельная разметка текста.
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-7">Сайты</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="enableSitesFilter" 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 v-if="isExternalConverter">
|
||||||
|
<div class="part-header">PDF</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-7">Формат</div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="pdfAsText" size="xs" label="Извлекать текст из PDF">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Пытается извлечь текст из pdf-файла и переразбить на параграфы.<br>
|
||||||
|
Размер получаемого fb2-файла при этом относительно небольшой.<br>
|
||||||
|
При отключении этой опции, pdf будет представлен как набор<br>
|
||||||
|
изображений (аналогично ковертированию djvu).
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-7">Качество</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-5" v-model="pdfQuality" :min="10" :max="100" :disable="pdfAsText" >
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Качество конвертирования Pdf в Fb2. Чем значение выше, тем больше<br>
|
||||||
|
размер итогового файла. Если сервер отказывается конвертировать<br>
|
||||||
|
слишком большой файл, то попробуйте понизить качество.
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!---------------------------------------------->
|
||||||
|
<div v-if="isExternalConverter">
|
||||||
|
<div class="part-header">DJVU</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-7">Качество</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-5" v-model="djvuQuality" :min="10" :max="100">
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Качество конвертирования Djvu в Fb2. Чем значение выше, тем больше<br>
|
||||||
|
размер итогового файла. Если сервер отказывается конвертировать<br>
|
||||||
|
слишком большой файл, то попробуйте понизить качество.
|
||||||
|
</q-tooltip>
|
||||||
|
</NumInput>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -36,7 +36,18 @@
|
|||||||
Показывать уведомление "Что нового"
|
Показывать уведомление "Что нового"
|
||||||
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
Показывать уведомления "Что нового"<br>
|
Показывать уведомления "Что нового"<br>
|
||||||
при каждом выходе новой версии читалки
|
при появлении новой версии читалки
|
||||||
|
</q-tooltip>
|
||||||
|
</q-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-6">Уведомление</div>
|
||||||
|
<q-checkbox size="xs" v-model="showNeedUpdateNotify">
|
||||||
|
Показывать уведомление о новой версии
|
||||||
|
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
|
||||||
|
Напоминать о необходимости обновления страницы<br>
|
||||||
|
при появлении новой версии читалки
|
||||||
</q-tooltip>
|
</q-tooltip>
|
||||||
</q-checkbox>
|
</q-checkbox>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,22 +65,6 @@
|
|||||||
<!---------------------------------------------->
|
<!---------------------------------------------->
|
||||||
<div class="part-header">Другое</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="item row">
|
||||||
<div class="label-6">Обработка</div>
|
<div class="label-6">Обработка</div>
|
||||||
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
<q-checkbox size="xs" v-model="lazyParseEnabled" label="Предварительная подготовка текста">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
no-caps
|
no-caps
|
||||||
class="no-mp bg-grey-4 text-grey-7"
|
class="no-mp bg-grey-4 text-grey-7"
|
||||||
>
|
>
|
||||||
|
<q-tab name="mode" label="Режим" />
|
||||||
<q-tab name="color" label="Цвет" />
|
<q-tab name="color" label="Цвет" />
|
||||||
<q-tab name="font" label="Шрифт" />
|
<q-tab name="font" label="Шрифт" />
|
||||||
<q-tab name="text" label="Текст" />
|
<q-tab name="text" label="Текст" />
|
||||||
@@ -16,6 +17,10 @@
|
|||||||
<div class="q-mb-sm"/>
|
<div class="q-mb-sm"/>
|
||||||
|
|
||||||
<div class="col tab-panel">
|
<div class="col tab-panel">
|
||||||
|
<div v-if="selectedViewTab == 'mode'">
|
||||||
|
@@include('./ViewTab/Mode.inc');
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedViewTab == 'color'">
|
<div v-if="selectedViewTab == 'color'">
|
||||||
@@include('./ViewTab/Color.inc');
|
@@include('./ViewTab/Color.inc');
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
</q-icon>
|
</q-icon>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</q-input>
|
||||||
|
|
||||||
<span class="col" style="position: relative; top: 35px; left: 15px;">Обои:</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -36,7 +34,6 @@
|
|||||||
v-model="bgColorFiltered"
|
v-model="bgColorFiltered"
|
||||||
:rules="['hexColor']"
|
:rules="['hexColor']"
|
||||||
style="max-width: 150px"
|
style="max-width: 150px"
|
||||||
:disable="wallpaper != ''"
|
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('bg')">
|
||||||
@@ -48,11 +45,51 @@
|
|||||||
</q-icon>
|
</q-icon>
|
||||||
</template>
|
</template>
|
||||||
</q-input>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mt-md"/>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Обои</div>
|
||||||
|
<div class="col row items-center">
|
||||||
|
<q-select class="col-left no-mp" v-model="wallpaper" :options="wallpaperOptions"
|
||||||
|
dropdown-icon="la la-angle-down la-sm"
|
||||||
|
outlined dense emit-value map-options
|
||||||
|
>
|
||||||
|
<template v-slot:selected-item="scope">
|
||||||
|
<div >{{ scope.opt.label }}</div>
|
||||||
|
<div v-show="scope.opt.value" class="q-ml-sm" :class="scope.opt.value" style="width: 40px; height: 28px;"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:option="scope">
|
||||||
|
<q-item
|
||||||
|
v-bind="scope.itemProps"
|
||||||
|
v-on="scope.itemEvents"
|
||||||
|
>
|
||||||
|
<q-item-section style="min-width: 50px;">
|
||||||
|
<q-item-label v-html="scope.opt.label" />
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section v-show="scope.opt.value" :class="scope.opt.value" style="min-width: 70px; min-height: 50px;"/>
|
||||||
|
</q-item>
|
||||||
|
</template>
|
||||||
|
</q-select>
|
||||||
|
|
||||||
|
<div class="q-px-xs"/>
|
||||||
|
<q-btn class="q-ml-sm" round dense color="blue" icon="la la-plus" @click.stop="loadWallpaperFileClick">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Добавить файл обоев</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
<q-btn v-show="wallpaper.indexOf('user-paper') === 0" class="q-ml-sm" round dense color="blue" icon="la la-minus" @click.stop="delWallpaper">
|
||||||
|
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Удалить выбранные обои</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-mt-sm"/>
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2"></div>
|
||||||
|
<div class="col row items-center">
|
||||||
|
<q-checkbox v-model="wallpaperIgnoreStatusBar" size="xs" label="Не включать строку статуса в обои" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="file" ref="file" @change="loadWallpaperFile" style='display: none;'/>
|
||||||
|
|||||||
124
client/components/Reader/SettingsPage/include/ViewTab/Mode.inc
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<!---------------------------------------------->
|
||||||
|
<div class="hidden part-header">Режим</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2"></div>
|
||||||
|
<div class="col row">
|
||||||
|
<q-checkbox v-model="dualPageMode" size="xs" label="Двухстраничный режим" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="part-header">Страницы</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 v-show="dualPageMode" class="item row">
|
||||||
|
<div class="label-2">Отступ внутри</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="dualIndentLR" :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 v-show="dualPageMode">
|
||||||
|
<div class="part-header">Разделитель</div>
|
||||||
|
|
||||||
|
<div class="item row no-wrap">
|
||||||
|
<div class="label-2">Цвет</div>
|
||||||
|
<div class="col-left row">
|
||||||
|
<q-input class="col-left no-mp"
|
||||||
|
outlined dense
|
||||||
|
v-model="dualDivColorFiltered"
|
||||||
|
:rules="['hexColor']"
|
||||||
|
style="max-width: 150px"
|
||||||
|
:disable="dualDivColorAsText"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('div')">
|
||||||
|
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||||
|
<div>
|
||||||
|
<q-color v-model="dualDivColor"
|
||||||
|
no-header default-view="palette" :palette="predefineTextColors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="q-px-xs"/>
|
||||||
|
<q-checkbox v-model="dualDivColorAsText" size="xs" label="Как у текста" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Прозрачность</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="dualDivColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item row">
|
||||||
|
<div class="label-2">Ширина (px)</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="dualDivWidth" :min="0" :max="100">
|
||||||
|
<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="dualDivHeight" :min="0" :max="100">
|
||||||
|
<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="dualDivStrokeFill" :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="dualDivStrokeGap" :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="dualDivShadowWidth" :min="0" :max="100"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -5,25 +5,53 @@
|
|||||||
<div class="label-2">Статус</div>
|
<div class="label-2">Статус</div>
|
||||||
<div class="col row">
|
<div class="col row">
|
||||||
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
|
<q-checkbox v-model="showStatusBar" size="xs" label="Показывать" />
|
||||||
<q-checkbox class="q-ml-sm" v-model="statusBarTop" size="xs" :disable="!showStatusBar" label="Вверху/внизу" />
|
<q-checkbox v-show="showStatusBar" class="q-ml-sm" v-model="statusBarTop" size="xs" label="Вверху/внизу" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item row">
|
<div v-show="showStatusBar" class="item row no-wrap">
|
||||||
<div class="label-2">Высота</div>
|
<div class="label-2">Цвет</div>
|
||||||
<div class="col row">
|
<div class="col-left row">
|
||||||
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100" :disable="!showStatusBar"/>
|
<q-input class="col-left no-mp"
|
||||||
|
outlined dense
|
||||||
|
v-model="statusBarColorFiltered"
|
||||||
|
:rules="['hexColor']"
|
||||||
|
style="max-width: 150px"
|
||||||
|
:disable="statusBarColorAsText"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<q-icon name="la la-angle-down la-xs" class="cursor-pointer text-white" :style="colorPanStyle('statusbar')">
|
||||||
|
<q-popup-proxy anchor="bottom middle" self="top middle">
|
||||||
|
<div>
|
||||||
|
<q-color v-model="statusBarColor"
|
||||||
|
no-header default-view="palette" :palette="predefineTextColors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-popup-proxy>
|
||||||
|
</q-icon>
|
||||||
|
</template>
|
||||||
|
</q-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="q-px-xs"/>
|
||||||
|
<q-checkbox v-model="statusBarColorAsText" size="xs" label="Как у текста"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item row">
|
<div v-show="showStatusBar" class="item row">
|
||||||
<div class="label-2">Прозрачность</div>
|
<div class="label-2">Прозрачность</div>
|
||||||
<div class="col row">
|
<div class="col row">
|
||||||
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1" :disable="!showStatusBar"/>
|
<NumInput class="col-left" v-model="statusBarColorAlpha" :min="0" :max="1" :digits="2" :step="0.1"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item row">
|
<div v-show="showStatusBar" class="item row">
|
||||||
|
<div class="label-2">Высота</div>
|
||||||
|
<div class="col row">
|
||||||
|
<NumInput class="col-left" v-model="statusBarHeight" :min="5" :max="100"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="showStatusBar" class="item row">
|
||||||
<div class="label-2"></div>
|
<div class="label-2"></div>
|
||||||
<div class="col row">
|
<div class="col row">
|
||||||
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
|
<q-checkbox v-model="statusBarClickOpen" size="xs" label="Открывать оригинал по клику">
|
||||||
|
|||||||
@@ -15,23 +15,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="item row">
|
||||||
<div class="label-2">Сдвиг</div>
|
<div class="label-2">Сдвиг</div>
|
||||||
<div class="col row">
|
<div class="col row">
|
||||||
@@ -123,7 +106,7 @@
|
|||||||
<div class="item row">
|
<div class="item row">
|
||||||
<div class="label-2"></div>
|
<div class="label-2"></div>
|
||||||
<div class="col row">
|
<div class="col row">
|
||||||
<q-checkbox v-model="imageFitWidth" :disable="!showImages" size="xs" label="Ширина не более размера экрана" />
|
<q-checkbox v-model="imageFitWidth" size="xs" label="Ширина не более размера страницы" :disable="!showImages || dualPageMode"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import {sleep} from '../../../share/utils';
|
|||||||
|
|
||||||
export default class DrawHelper {
|
export default class DrawHelper {
|
||||||
fontBySize(size) {
|
fontBySize(size) {
|
||||||
return `${size}px ${this.fontName}`;
|
return `${size}px '${this.fontName}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
fontByStyle(style) {
|
fontByStyle(style) {
|
||||||
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px ${this.fontName}`;
|
return `${style.italic ? 'italic' : this.fontStyle} ${style.bold ? 'bold' : this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
measureText(text, style) {// eslint-disable-line no-unused-vars
|
measureText(text, style) {// eslint-disable-line no-unused-vars
|
||||||
@@ -19,6 +19,109 @@ export default class DrawHelper {
|
|||||||
return this.context.measureText(text).width;
|
return this.context.measureText(text).width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawLine(line, lineIndex, baseLineIndex, sel, imageDrawn) {
|
||||||
|
/* line:
|
||||||
|
{
|
||||||
|
begin: Number,
|
||||||
|
end: Number,
|
||||||
|
first: Boolean,
|
||||||
|
last: Boolean,
|
||||||
|
parts: array of {
|
||||||
|
style: {bold: Boolean, italic: Boolean, center: Boolean},
|
||||||
|
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
|
||||||
|
text: String,
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
let out = '<div>';
|
||||||
|
|
||||||
|
let lineText = '';
|
||||||
|
let center = false;
|
||||||
|
let space = 0;
|
||||||
|
let j = 0;
|
||||||
|
//формируем строку
|
||||||
|
for (const part of line.parts) {
|
||||||
|
let tOpen = '';
|
||||||
|
tOpen += (part.style.bold ? '<b>' : '');
|
||||||
|
tOpen += (part.style.italic ? '<i>' : '');
|
||||||
|
tOpen += (part.style.sup ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: -0.3em">' : '');
|
||||||
|
tOpen += (part.style.sub ? '<span style="vertical-align: baseline; position: relative; line-height: 0; top: 0.3em">' : '');
|
||||||
|
let tClose = '';
|
||||||
|
tClose += (part.style.sub ? '</span>' : '');
|
||||||
|
tClose += (part.style.sup ? '</span>' : '');
|
||||||
|
tClose += (part.style.italic ? '</i>' : '');
|
||||||
|
tClose += (part.style.bold ? '</b>' : '');
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
if (lineIndex == 0 && this.searching) {
|
||||||
|
for (let k = 0; k < part.text.length; k++) {
|
||||||
|
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
text = part.text;
|
||||||
|
|
||||||
|
if (text && text.trim() == '')
|
||||||
|
text = `<span style="white-space: pre">${text}</span>`;
|
||||||
|
|
||||||
|
lineText += `${tOpen}${text}${tClose}`;
|
||||||
|
|
||||||
|
center = center || part.style.center;
|
||||||
|
space = (part.style.space > space ? part.style.space : space);
|
||||||
|
|
||||||
|
//избражения
|
||||||
|
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
|
||||||
|
const img = part.image;
|
||||||
|
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
|
||||||
|
const bin = this.parsed.binary[img.id];
|
||||||
|
if (bin) {
|
||||||
|
let resize = '';
|
||||||
|
if (bin.h > img.h) {
|
||||||
|
resize = `height: ${img.h}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = (this.w - img.w)/2;
|
||||||
|
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (lineIndex - baseLineIndex - img.imageLine)*this.lineHeight;
|
||||||
|
if (img.local) {
|
||||||
|
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||||
|
} else {
|
||||||
|
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imageDrawn.add(img.paraIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (img && img.id && img.inline) {
|
||||||
|
if (img.local) {
|
||||||
|
const bin = this.parsed.binary[img.id];
|
||||||
|
if (bin) {
|
||||||
|
let resize = '';
|
||||||
|
if (bin.h > this.fontSize) {
|
||||||
|
resize = `height: ${this.fontSize - 3}px`;
|
||||||
|
}
|
||||||
|
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
|
||||||
|
if ((line.first || space) && !center) {
|
||||||
|
let p = (line.first ? this.p : 0);
|
||||||
|
p = (space ? p + this.p*space : p);
|
||||||
|
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.last || center)
|
||||||
|
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
|
||||||
|
|
||||||
|
out += lineText + '</div>';
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
drawPage(lines, isScrolling) {
|
drawPage(lines, isScrolling) {
|
||||||
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
|
if (!this.lastBook || this.pageLineCount < 1 || !this.book || !lines || !this.parsed.textLength)
|
||||||
return '';
|
return '';
|
||||||
@@ -26,140 +129,78 @@ export default class DrawHelper {
|
|||||||
const font = this.fontByStyle({});
|
const font = this.fontByStyle({});
|
||||||
const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : '');
|
const justify = (this.textAlignJustify ? 'text-align: justify; text-align-last: justify;' : '');
|
||||||
|
|
||||||
let out = `<div style="width: ${this.w}px; height: ${this.h + (isScrolling ? this.lineHeight : 0)}px;` +
|
const boxH = this.h + (isScrolling ? this.lineHeight : 0);
|
||||||
|
let out = `<div class="row no-wrap" style="width: ${this.boxW}px; height: ${boxH}px;` +
|
||||||
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
|
` position: absolute; top: ${this.fontSize*this.textShift}px; color: ${this.textColor}; font: ${font}; ${justify}` +
|
||||||
` line-height: ${this.lineHeight}px; white-space: nowrap;">`;
|
` line-height: ${this.lineHeight}px; white-space: nowrap;">`;
|
||||||
|
|
||||||
let imageDrawn = new Set();
|
let imageDrawn1 = new Set();
|
||||||
|
let imageDrawn2 = new Set();
|
||||||
let len = lines.length;
|
let len = lines.length;
|
||||||
const lineCount = this.pageLineCount + (isScrolling ? 1 : 0);
|
const lineCount = this.pageLineCount + (isScrolling ? 1 : 0);
|
||||||
len = (len > lineCount ? lineCount : len);
|
len = (len > lineCount ? lineCount : len);
|
||||||
|
|
||||||
for (let i = 0; i < len; i++) {
|
//поиск
|
||||||
const line = lines[i];
|
let sel = new Set();
|
||||||
/* line:
|
if (len > 0 && this.searching) {
|
||||||
{
|
const line = lines[0];
|
||||||
begin: Number,
|
let pureText = '';
|
||||||
end: Number,
|
for (const part of line.parts) {
|
||||||
first: Boolean,
|
pureText += part.text;
|
||||||
last: Boolean,
|
|
||||||
parts: array of {
|
|
||||||
style: {bold: Boolean, italic: Boolean, center: Boolean},
|
|
||||||
image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number},
|
|
||||||
text: String,
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
let sel = new Set();
|
|
||||||
//поиск
|
|
||||||
if (i == 0 && this.searching) {
|
|
||||||
let pureText = '';
|
|
||||||
for (const part of line.parts) {
|
|
||||||
pureText += part.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
pureText = pureText.toLowerCase();
|
|
||||||
let j = 0;
|
|
||||||
while (1) {// eslint-disable-line no-constant-condition
|
|
||||||
j = pureText.indexOf(this.needle, j);
|
|
||||||
if (j >= 0) {
|
|
||||||
for (let k = 0; k < this.needle.length; k++) {
|
|
||||||
sel.add(j + k);
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
break;
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let lineText = '';
|
pureText = pureText.toLowerCase();
|
||||||
let center = false;
|
|
||||||
let space = 0;
|
|
||||||
let j = 0;
|
let j = 0;
|
||||||
//формируем строку
|
while (1) {// eslint-disable-line no-constant-condition
|
||||||
for (const part of line.parts) {
|
j = pureText.indexOf(this.needle, j);
|
||||||
let tOpen = (part.style.bold ? '<b>' : '');
|
if (j >= 0) {
|
||||||
tOpen += (part.style.italic ? '<i>' : '');
|
for (let k = 0; k < this.needle.length; k++) {
|
||||||
let tClose = (part.style.italic ? '</i>' : '');
|
sel.add(j + k);
|
||||||
tClose += (part.style.bold ? '</b>' : '');
|
|
||||||
|
|
||||||
let text = '';
|
|
||||||
if (i == 0 && this.searching) {
|
|
||||||
for (let k = 0; k < part.text.length; k++) {
|
|
||||||
text += (sel.has(j) ? `<ins>${part.text[k]}</ins>` : part.text[k]);
|
|
||||||
j++;
|
|
||||||
}
|
}
|
||||||
} else
|
} else
|
||||||
text = part.text;
|
break;
|
||||||
|
j++;
|
||||||
if (text && text.trim() == '')
|
|
||||||
text = `<span style="white-space: pre">${text}</span>`;
|
|
||||||
|
|
||||||
lineText += `${tOpen}${text}${tClose}`;
|
|
||||||
|
|
||||||
center = center || part.style.center;
|
|
||||||
space = (part.style.space > space ? part.style.space : space);
|
|
||||||
|
|
||||||
//избражения
|
|
||||||
//image: {local: Boolean, inline: Boolean, id: String, imageLine: Number, lineCount: Number, paraIndex: Number, w: Number, h: Number},
|
|
||||||
const img = part.image;
|
|
||||||
if (img && img.id && !img.inline && !imageDrawn.has(img.paraIndex)) {
|
|
||||||
const bin = this.parsed.binary[img.id];
|
|
||||||
if (bin) {
|
|
||||||
let resize = '';
|
|
||||||
if (bin.h > img.h) {
|
|
||||||
resize = `height: ${img.h}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const left = (this.w - img.w)/2;
|
|
||||||
const top = ((img.lineCount*this.lineHeight - img.h)/2) + (i - img.imageLine)*this.lineHeight;
|
|
||||||
if (img.local) {
|
|
||||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
|
||||||
} else {
|
|
||||||
lineText += `<img src="${img.id}" style="position: absolute; left: ${left}px; top: ${top}px; ${resize}"/>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
imageDrawn.add(img.paraIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (img && img.id && img.inline) {
|
|
||||||
if (img.local) {
|
|
||||||
const bin = this.parsed.binary[img.id];
|
|
||||||
if (bin) {
|
|
||||||
let resize = '';
|
|
||||||
if (bin.h > this.fontSize) {
|
|
||||||
resize = `height: ${this.fontSize - 3}px`;
|
|
||||||
}
|
|
||||||
lineText += `<img src="data:${bin.type};base64,${bin.data}" style="${resize}"/>`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const centerStyle = (center ? `text-align: center; text-align-last: center; width: ${this.w}px` : '')
|
//отрисовка строк
|
||||||
if ((line.first || space) && !center) {
|
if (!this.dualPageMode) {
|
||||||
let p = (line.first ? this.p : 0);
|
out += `<div class="fit">`;
|
||||||
p = (space ? p + this.p*space : p);
|
for (let i = 0; i < len; i++) {
|
||||||
lineText = `<span style="display: inline-block; margin-left: ${p}px"></span>${lineText}`;
|
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
|
||||||
}
|
}
|
||||||
|
out += `</div>`;
|
||||||
|
} else {
|
||||||
|
//левая страница
|
||||||
|
out += `<div style="width: ${this.w}px; margin-left: ${this.dualIndentLR}px; position: relative;">`;
|
||||||
|
const l2 = (this.pageRowsCount > len ? len : this.pageRowsCount);
|
||||||
|
for (let i = 0; i < l2; i++) {
|
||||||
|
out += this.drawLine(lines[i], i, 0, sel, imageDrawn1);
|
||||||
|
}
|
||||||
|
out += '</div>';
|
||||||
|
|
||||||
if (line.last || center)
|
//разделитель
|
||||||
lineText = `<span style="display: inline-block; ${centerStyle}">${lineText}</span>`;
|
out += `<div style="width: ${this.dualIndentLR*2}px;"></div>`;
|
||||||
|
|
||||||
out += (i > 0 ? '<br>' : '') + lineText;
|
//правая страница
|
||||||
|
out += `<div style="width: ${this.w}px; margin-right: ${this.dualIndentLR}px; position: relative;">`;
|
||||||
|
for (let i = l2; i < len; i++) {
|
||||||
|
out += this.drawLine(lines[i], i, l2, sel, imageDrawn2);
|
||||||
|
}
|
||||||
|
out += '</div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
out += '</div>';
|
out += '</div>';
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength) {
|
drawPercentBar(x, y, w, h, font, fontSize, bookPos, textLength, imageNum, imageLength) {
|
||||||
const pad = 3;
|
const pad = 3;
|
||||||
const fh = h - 2*pad;
|
const fh = h - 2*pad;
|
||||||
const fh2 = fh/2;
|
const fh2 = fh/2;
|
||||||
|
|
||||||
const t1 = `${Math.floor((bookPos + 1)/1000)}k/${Math.floor(textLength/1000)}k`;
|
const tImg = (imageNum > 0 ? ` (${imageNum}/${imageLength})` : '');
|
||||||
|
const t1 = `${Math.floor((bookPos + 1)/1000)}/${Math.floor(textLength/1000)}${tImg}`;
|
||||||
const w1 = this.measureTextFont(t1, font) + fh2;
|
const w1 = this.measureTextFont(t1, font) + fh2;
|
||||||
const read = (bookPos + 1)/textLength;
|
const read = (bookPos + 1)/textLength;
|
||||||
const t2 = `${(read*100).toFixed(2)}%`;
|
const t2 = `${(read*100).toFixed(2)}%`;
|
||||||
@@ -172,8 +213,8 @@ export default class DrawHelper {
|
|||||||
|
|
||||||
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
|
if (w1 + w2 + w3 <= w && w3 > (10 + fh2)) {
|
||||||
const barWidth = w - w1 - w2 - fh2;
|
const barWidth = w - w1 - w2 - fh2;
|
||||||
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarColor);
|
out += this.strokeRect(x + w1, y + pad, barWidth, fh - 2, this.statusBarRgbaColor);
|
||||||
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarColor);
|
out += this.fillRect(x + w1 + 2, y + pad + 2, (barWidth - 4)*read, fh - 6, this.statusBarRgbaColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (w1 <= w)
|
if (w1 <= w)
|
||||||
@@ -182,16 +223,16 @@ export default class DrawHelper {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title) {
|
drawStatusBar(statusBarTop, statusBarHeight, bookPos, textLength, title, imageNum, imageLength) {
|
||||||
|
|
||||||
let out = `<div class="layout" style="` +
|
let out = `<div class="layout" style="` +
|
||||||
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
`width: ${this.realWidth}px; height: ${statusBarHeight}px; ` +
|
||||||
`color: ${this.statusBarColor}">`;
|
`color: ${this.statusBarRgbaColor}">`;
|
||||||
|
|
||||||
const fontSize = statusBarHeight*0.75;
|
const fontSize = statusBarHeight*0.75;
|
||||||
const font = 'bold ' + this.fontBySize(fontSize);
|
const font = 'bold ' + this.fontBySize(fontSize);
|
||||||
|
|
||||||
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarColor);
|
out += this.fillRect(0, (statusBarTop ? statusBarHeight : 0), this.realWidth, 1, this.statusBarRgbaColor);
|
||||||
|
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
const time = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
||||||
@@ -200,7 +241,7 @@ export default class DrawHelper {
|
|||||||
|
|
||||||
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
|
out += this.fillTextShift(this.fittingString(title, this.realWidth/2 - fontSize - 3, font), fontSize, 2, font, fontSize);
|
||||||
|
|
||||||
out += this.drawPercentBar(this.realWidth/2, 2, this.realWidth/2 - timeW - 2*fontSize, statusBarHeight, font, fontSize, bookPos, textLength);
|
out += this.drawPercentBar(this.realWidth/2 + fontSize, 2, this.realWidth/2 - timeW - 3*fontSize, statusBarHeight, font, fontSize, bookPos, textLength, imageNum, imageLength);
|
||||||
|
|
||||||
out += '</div>';
|
out += '</div>';
|
||||||
return out;
|
return out;
|
||||||
@@ -267,7 +308,7 @@ export default class DrawHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
|
async doPageAnimationRightShift(page1, page2, duration, isDown, animation1Finish) {
|
||||||
const s = this.w + this.fontSize;
|
const s = this.boxW + this.fontSize;
|
||||||
|
|
||||||
if (isDown) {
|
if (isDown) {
|
||||||
page1.style.transform = `translateX(${s}px)`;
|
page1.style.transform = `translateX(${s}px)`;
|
||||||
|
|||||||
93
client/components/Reader/TextPage/TextPage.css
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
@keyframes page1-animation-thaw {
|
||||||
|
0% { opacity: 0; }
|
||||||
|
100% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes page2-animation-thaw {
|
||||||
|
0% { opacity: 1; }
|
||||||
|
100% { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper1 {
|
||||||
|
background: url("images/paper1.jpg") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper2 {
|
||||||
|
background: url("images/paper2.jpg") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper3 {
|
||||||
|
background: url("images/paper3.jpg") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper4 {
|
||||||
|
background: url("images/paper4.jpg") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper5 {
|
||||||
|
background: url("images/paper5.jpg") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper6 {
|
||||||
|
background: url("images/paper6.jpg") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper7 {
|
||||||
|
background: url("images/paper7.jpg") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper8 {
|
||||||
|
background: url("images/paper8.jpg") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper9 {
|
||||||
|
background: url("images/paper9.jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper10 {
|
||||||
|
background: url("images/paper10.png") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper11 {
|
||||||
|
background: url("images/paper11.png") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper12 {
|
||||||
|
background: url("images/paper12.png") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper13 {
|
||||||
|
background: url("images/paper13.png") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper14 {
|
||||||
|
background: url("images/paper14.png") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper15 {
|
||||||
|
background: url("images/paper15.png") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper16 {
|
||||||
|
background: url("images/paper16.png") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper17 {
|
||||||
|
background: url("images/paper17.png") center;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="main" class="main">
|
<div ref="main" class="main">
|
||||||
<div class="layout back" @wheel.prevent.stop="onMouseWheel">
|
<div class="layout back" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div v-html="background"></div>
|
<div class="absolute" v-html="background"></div>
|
||||||
<!-- img -->
|
<div class="absolute" v-html="pageDivider"></div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
<div ref="scrollBox1" class="layout over-hidden" @wheel.prevent.stop="onMouseWheel">
|
||||||
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
<div ref="scrollingPage1" class="layout over-hidden" @transitionend="onPage1TransitionEnd" @animationend="onPage1AnimationEnd">
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
<div v-show="!clickControl && showStatusBar && statusBarClickOpen" class="layout" v-html="statusBarClickable" @mousedown.prevent.stop @touchstart.stop
|
||||||
@click.prevent.stop="onStatusBarClick">
|
@click.prevent.stop="onStatusBarClick">
|
||||||
</div>
|
</div>
|
||||||
<!-- невидимым делать нельзя, вовремя не подгружаютя шрифты -->
|
<!-- невидимым делать нельзя (display: none), вовремя не подгружаютя шрифты -->
|
||||||
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
<canvas ref="offscreenCanvas" class="layout" style="visibility: hidden"></canvas>
|
||||||
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
<div ref="measureWidth" style="position: absolute; visibility: hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,10 @@ import Component from 'vue-class-component';
|
|||||||
import {loadCSS} from 'fg-loadcss';
|
import {loadCSS} from 'fg-loadcss';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
import {sleep} from '../../../share/utils';
|
import './TextPage.css';
|
||||||
|
|
||||||
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
import bookManager from '../share/bookManager';
|
import bookManager from '../share/bookManager';
|
||||||
import DrawHelper from './DrawHelper';
|
import DrawHelper from './DrawHelper';
|
||||||
import rstore from '../../../store/modules/reader';
|
import rstore from '../../../store/modules/reader';
|
||||||
@@ -74,6 +77,7 @@ class TextPage extends Vue {
|
|||||||
clickControl = true;
|
clickControl = true;
|
||||||
|
|
||||||
background = null;
|
background = null;
|
||||||
|
pageDivider = null;
|
||||||
page1 = null;
|
page1 = null;
|
||||||
page2 = null;
|
page2 = null;
|
||||||
statusBar = null;
|
statusBar = null;
|
||||||
@@ -112,6 +116,10 @@ class TextPage extends Vue {
|
|||||||
this.drawStatusBar();
|
this.drawStatusBar();
|
||||||
}, 60);
|
}, 60);
|
||||||
|
|
||||||
|
this.debouncedDrawPageDividerAndOrnament = _.throttle(() => {
|
||||||
|
this.drawPageDividerAndOrnament();
|
||||||
|
}, 65);
|
||||||
|
|
||||||
this.debouncedLoadSettings = _.debounce(() => {
|
this.debouncedLoadSettings = _.debounce(() => {
|
||||||
this.loadSettings();
|
this.loadSettings();
|
||||||
}, 50);
|
}, 50);
|
||||||
@@ -134,7 +142,7 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
this.$root.$on('resize', async() => {
|
this.$root.$on('resize', async() => {
|
||||||
this.$nextTick(this.onResize);
|
this.$nextTick(this.onResize);
|
||||||
await sleep(500);
|
await utils.sleep(500);
|
||||||
this.$nextTick(this.onResize);
|
this.$nextTick(this.onResize);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,7 +160,7 @@ class TextPage extends Vue {
|
|||||||
const wideLetter = 'Щ';
|
const wideLetter = 'Щ';
|
||||||
|
|
||||||
//preloaded fonts
|
//preloaded fonts
|
||||||
this.fontList = [`12px ${this.fontName}`];
|
this.fontList = [`12px '${this.fontName}'`];
|
||||||
|
|
||||||
//widths
|
//widths
|
||||||
this.realWidth = this.$refs.main.clientWidth;
|
this.realWidth = this.$refs.main.clientWidth;
|
||||||
@@ -161,14 +169,16 @@ class TextPage extends Vue {
|
|||||||
this.$refs.layoutEvents.style.width = this.realWidth + 'px';
|
this.$refs.layoutEvents.style.width = this.realWidth + 'px';
|
||||||
this.$refs.layoutEvents.style.height = this.realHeight + 'px';
|
this.$refs.layoutEvents.style.height = this.realHeight + 'px';
|
||||||
|
|
||||||
this.w = this.realWidth - 2*this.indentLR;
|
const dual = (this.dualPageMode ? 2 : 1);
|
||||||
|
this.boxW = this.realWidth - 2*this.indentLR;
|
||||||
|
this.w = this.boxW/dual - (this.dualPageMode ? 2*this.dualIndentLR : 0);
|
||||||
|
|
||||||
this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0);
|
this.scrollHeight = this.realHeight - (this.showStatusBar ? this.statusBarHeight : 0);
|
||||||
this.h = this.scrollHeight - 2*this.indentTB;
|
this.h = this.scrollHeight - 2*this.indentTB;
|
||||||
this.lineHeight = this.fontSize + this.lineInterval;
|
|
||||||
this.pageLineCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
|
|
||||||
|
|
||||||
this.$refs.scrollingPage1.style.width = this.w + 'px';
|
this.lineHeight = this.fontSize + this.lineInterval;
|
||||||
this.$refs.scrollingPage2.style.width = this.w + 'px';
|
this.pageRowsCount = 1 + Math.floor((this.h - this.lineHeight + this.lineInterval/2)/this.lineHeight);
|
||||||
|
this.pageLineCount = (this.dualPageMode ? this.pageRowsCount*2 : this.pageRowsCount)
|
||||||
|
|
||||||
//stuff
|
//stuff
|
||||||
this.currentAnimation = '';
|
this.currentAnimation = '';
|
||||||
@@ -180,7 +190,10 @@ class TextPage extends Vue {
|
|||||||
this.$refs.statusBar.style.left = '0px';
|
this.$refs.statusBar.style.left = '0px';
|
||||||
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
|
this.$refs.statusBar.style.top = (this.statusBarTop ? 1 : this.realHeight - this.statusBarHeight) + 'px';
|
||||||
|
|
||||||
this.statusBarColor = this.hex2rgba(this.textColor || '#000000', this.statusBarColorAlpha);
|
const sbColor = (this.statusBarColorAsText ? this.textColor : this.statusBarColor);
|
||||||
|
this.statusBarRgbaColor = this.hex2rgba(sbColor || '#000000', this.statusBarColorAlpha);
|
||||||
|
const ddColor = (this.dualDivColorAsText ? this.textColor : this.dualDivColor);
|
||||||
|
this.dualDivRgbaColor = this.hex2rgba(ddColor || '#000000', this.dualDivColorAlpha);
|
||||||
|
|
||||||
//drawHelper
|
//drawHelper
|
||||||
this.drawHelper.realWidth = this.realWidth;
|
this.drawHelper.realWidth = this.realWidth;
|
||||||
@@ -188,10 +201,20 @@ class TextPage extends Vue {
|
|||||||
this.drawHelper.lastBook = this.lastBook;
|
this.drawHelper.lastBook = this.lastBook;
|
||||||
this.drawHelper.book = this.book;
|
this.drawHelper.book = this.book;
|
||||||
this.drawHelper.parsed = this.parsed;
|
this.drawHelper.parsed = this.parsed;
|
||||||
|
this.drawHelper.pageRowsCount = this.pageRowsCount;
|
||||||
this.drawHelper.pageLineCount = this.pageLineCount;
|
this.drawHelper.pageLineCount = this.pageLineCount;
|
||||||
|
|
||||||
|
this.drawHelper.dualPageMode = this.dualPageMode;
|
||||||
|
this.drawHelper.dualIndentLR = this.dualIndentLR;
|
||||||
|
/*this.drawHelper.dualDivWidth = this.dualDivWidth;
|
||||||
|
this.drawHelper.dualDivHeight = this.dualDivHeight;
|
||||||
|
this.drawHelper.dualDivRgbaColor = this.dualDivRgbaColor;
|
||||||
|
this.drawHelper.dualDivStrokeFill = this.dualDivStrokeFill;
|
||||||
|
this.drawHelper.dualDivStrokeGap = this.dualDivStrokeGap;
|
||||||
|
this.drawHelper.dualDivShadowWidth = this.dualDivShadowWidth;*/
|
||||||
|
|
||||||
this.drawHelper.backgroundColor = this.backgroundColor;
|
this.drawHelper.backgroundColor = this.backgroundColor;
|
||||||
this.drawHelper.statusBarColor = this.statusBarColor;
|
this.drawHelper.statusBarRgbaColor = this.statusBarRgbaColor;
|
||||||
this.drawHelper.fontStyle = this.fontStyle;
|
this.drawHelper.fontStyle = this.fontStyle;
|
||||||
this.drawHelper.fontWeight = this.fontWeight;
|
this.drawHelper.fontWeight = this.fontWeight;
|
||||||
this.drawHelper.fontSize = this.fontSize;
|
this.drawHelper.fontSize = this.fontSize;
|
||||||
@@ -200,6 +223,7 @@ class TextPage extends Vue {
|
|||||||
this.drawHelper.textColor = this.textColor;
|
this.drawHelper.textColor = this.textColor;
|
||||||
this.drawHelper.textShift = this.textShift;
|
this.drawHelper.textShift = this.textShift;
|
||||||
this.drawHelper.p = this.p;
|
this.drawHelper.p = this.p;
|
||||||
|
this.drawHelper.boxW = this.boxW;
|
||||||
this.drawHelper.w = this.w;
|
this.drawHelper.w = this.w;
|
||||||
this.drawHelper.h = this.h;
|
this.drawHelper.h = this.h;
|
||||||
this.drawHelper.indentLR = this.indentLR;
|
this.drawHelper.indentLR = this.indentLR;
|
||||||
@@ -228,32 +252,33 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
//parsed
|
//parsed
|
||||||
if (this.parsed) {
|
if (this.parsed) {
|
||||||
this.parsed.p = this.p;
|
let wideLine = wideLetter;
|
||||||
this.parsed.w = this.w;// px, ширина текста
|
if (!this.drawHelper.measureText(wideLine, {}))
|
||||||
this.parsed.font = this.font;
|
|
||||||
this.parsed.fontSize = this.fontSize;
|
|
||||||
this.parsed.wordWrap = this.wordWrap;
|
|
||||||
this.parsed.cutEmptyParagraphs = this.cutEmptyParagraphs;
|
|
||||||
this.parsed.addEmptyParagraphs = this.addEmptyParagraphs;
|
|
||||||
let t = wideLetter;
|
|
||||||
if (!this.drawHelper.measureText(t, {}))
|
|
||||||
throw new Error('Ошибка measureText');
|
throw new Error('Ошибка measureText');
|
||||||
while (this.drawHelper.measureText(t, {}) < this.w) t += wideLetter;
|
while (this.drawHelper.measureText(wideLine, {}) < this.w) wideLine += wideLetter;
|
||||||
this.parsed.maxWordLength = t.length - 1;
|
|
||||||
this.parsed.measureText = this.drawHelper.measureText.bind(this.drawHelper);
|
|
||||||
this.parsed.lineHeight = this.lineHeight;
|
|
||||||
this.parsed.showImages = this.showImages;
|
|
||||||
this.parsed.showInlineImagesInCenter = this.showInlineImagesInCenter;
|
|
||||||
this.parsed.imageHeightLines = this.imageHeightLines;
|
|
||||||
this.parsed.imageFitWidth = this.imageFitWidth;
|
|
||||||
this.parsed.compactTextPerc = this.compactTextPerc;
|
|
||||||
|
|
||||||
this.parsed.testText = 'Это тестовый текст. Его ширина выдается системой неверно некоторое время.';
|
this.parsed.setSettings({
|
||||||
this.parsed.testWidth = this.drawHelper.measureText(this.parsed.testText, {});
|
p: this.p,
|
||||||
|
w: this.w,
|
||||||
|
font: this.font,
|
||||||
|
fontSize: this.fontSize,
|
||||||
|
wordWrap: this.wordWrap,
|
||||||
|
cutEmptyParagraphs: this.cutEmptyParagraphs,
|
||||||
|
addEmptyParagraphs: this.addEmptyParagraphs,
|
||||||
|
maxWordLength: wideLine.length - 1,
|
||||||
|
lineHeight: this.lineHeight,
|
||||||
|
showImages: this.showImages,
|
||||||
|
showInlineImagesInCenter: this.showInlineImagesInCenter,
|
||||||
|
imageHeightLines: this.imageHeightLines,
|
||||||
|
imageFitWidth: this.imageFitWidth,
|
||||||
|
compactTextPerc: this.compactTextPerc,
|
||||||
|
testWidth: 0,
|
||||||
|
measureText: this.drawHelper.measureText.bind(this.drawHelper),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//scrolling page
|
//scrolling page
|
||||||
const pageSpace = this.scrollHeight - this.pageLineCount*this.lineHeight;
|
const pageSpace = this.scrollHeight - this.pageRowsCount*this.lineHeight;
|
||||||
let top = pageSpace/2;
|
let top = pageSpace/2;
|
||||||
if (this.showStatusBar)
|
if (this.showStatusBar)
|
||||||
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
top += this.statusBarHeight*(this.statusBarTop ? 1 : 0);
|
||||||
@@ -262,14 +287,14 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
page1.perspective = page2.perspective = '3072px';
|
page1.perspective = page2.perspective = '3072px';
|
||||||
|
|
||||||
page1.width = page2.width = this.w + this.indentLR + 'px';
|
page1.width = page2.width = this.boxW + this.indentLR + 'px';
|
||||||
page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
page1.height = page2.height = this.scrollHeight - (pageSpace > 0 ? pageSpace : 0) + 'px';
|
||||||
page1.top = page2.top = top + 'px';
|
page1.top = page2.top = top + 'px';
|
||||||
page1.left = page2.left = this.indentLR + 'px';
|
page1.left = page2.left = this.indentLR + 'px';
|
||||||
|
|
||||||
page1 = this.$refs.scrollingPage1.style;
|
page1 = this.$refs.scrollingPage1.style;
|
||||||
page2 = this.$refs.scrollingPage2.style;
|
page2 = this.$refs.scrollingPage2.style;
|
||||||
page1.width = page2.width = this.w + this.indentLR + 'px';
|
page1.width = page2.width = this.boxW + this.indentLR + 'px';
|
||||||
page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
|
page1.height = page2.height = this.scrollHeight + this.lineHeight + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +310,7 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
let close = null;
|
let close = null;
|
||||||
(async() => {
|
(async() => {
|
||||||
await sleep(500);
|
await utils.sleep(500);
|
||||||
if (this.fontsLoading)
|
if (this.fontsLoading)
|
||||||
close = this.$root.notify.info('Загрузка шрифта <i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
|
close = this.$root.notify.info('Загрузка шрифта <i class="la la-snowflake icon-rotate" style="font-size: 150%"></i>');
|
||||||
})();
|
})();
|
||||||
@@ -333,20 +358,36 @@ class TextPage extends Vue {
|
|||||||
if (!omitLoadFonts)
|
if (!omitLoadFonts)
|
||||||
await this.loadFonts();
|
await this.loadFonts();
|
||||||
|
|
||||||
this.draw();
|
if (omitLoadFonts) {
|
||||||
|
this.draw();
|
||||||
|
} else {
|
||||||
|
// ширина шрифта некоторое время выдается неверно,
|
||||||
|
// не удалось событийно отловить этот момент, поэтому костыль
|
||||||
|
while (this.checkingFont) {
|
||||||
|
this.stopCheckingFont = true;
|
||||||
|
await utils.sleep(100);
|
||||||
|
}
|
||||||
|
|
||||||
// ширина шрифта некоторое время выдается неверно, поэтому
|
this.checkingFont = true;
|
||||||
if (!omitLoadFonts) {
|
this.stopCheckingFont = false;
|
||||||
const parsed = this.parsed;
|
try {
|
||||||
|
const parsed = this.parsed;
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const t = this.parsed.testText;
|
const t = 'Это тестовый текст. Его ширина выдается системой неправильно некоторое время.';
|
||||||
while (i++ < 50 && this.parsed === parsed && this.drawHelper.measureText(t, {}) === this.parsed.testWidth)
|
let twprev = 0;
|
||||||
await sleep(100);
|
//5 секунд проверяем изменения шрифта
|
||||||
|
while (!this.stopCheckingFont && i++ < 50 && this.parsed === parsed) {
|
||||||
if (this.parsed === parsed) {
|
const tw = this.drawHelper.measureText(t, {});
|
||||||
this.parsed.testWidth = this.drawHelper.measureText(t, {});
|
if (tw !== twprev) {
|
||||||
this.draw();
|
this.parsed.setSettings({testWidth: tw});
|
||||||
|
this.draw();
|
||||||
|
twprev = tw;
|
||||||
|
}
|
||||||
|
await utils.sleep(100);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.checkingFont = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,7 +407,6 @@ class TextPage extends Vue {
|
|||||||
this.updateLayout();
|
this.updateLayout();
|
||||||
this.book = null;
|
this.book = null;
|
||||||
this.meta = null;
|
this.meta = null;
|
||||||
this.fb2 = null;
|
|
||||||
this.parsed = null;
|
this.parsed = null;
|
||||||
|
|
||||||
this.linesUp = null;
|
this.linesUp = null;
|
||||||
@@ -383,7 +423,7 @@ class TextPage extends Vue {
|
|||||||
try {
|
try {
|
||||||
//подождем ленивый парсинг
|
//подождем ленивый парсинг
|
||||||
this.stopLazyParse = true;
|
this.stopLazyParse = true;
|
||||||
while (this.doingLazyParse) await sleep(10);
|
while (this.doingLazyParse) await utils.sleep(10);
|
||||||
|
|
||||||
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
const isParsed = await bookManager.hasBookParsed(this.lastBook);
|
||||||
if (!isParsed) {
|
if (!isParsed) {
|
||||||
@@ -392,21 +432,9 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
this.book = await bookManager.getBook(this.lastBook);
|
this.book = await bookManager.getBook(this.lastBook);
|
||||||
this.meta = bookManager.metaOnly(this.book);
|
this.meta = bookManager.metaOnly(this.book);
|
||||||
this.fb2 = this.meta.fb2;
|
const bt = utils.getBookTitle(this.meta.fb2);
|
||||||
|
|
||||||
let authorNames = [];
|
this.title = bt.fullTitle;
|
||||||
if (this.fb2.author) {
|
|
||||||
authorNames = this.fb2.author.map(a => _.compact([
|
|
||||||
a.lastName,
|
|
||||||
a.firstName,
|
|
||||||
a.middleName
|
|
||||||
]).join(' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.title = _.compact([
|
|
||||||
authorNames.join(', '),
|
|
||||||
this.fb2.bookTitle
|
|
||||||
]).join(' - ');
|
|
||||||
|
|
||||||
this.$root.$emit('set-app-title', this.title);
|
this.$root.$emit('set-app-title', this.title);
|
||||||
|
|
||||||
@@ -443,8 +471,18 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setBackground() {
|
setBackground() {
|
||||||
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
|
if (this.wallpaperIgnoreStatusBar) {
|
||||||
` background-color: ${this.backgroundColor}"></div>`;
|
this.background = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
|
||||||
|
` background-color: ${this.backgroundColor}">` +
|
||||||
|
`<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
|
||||||
|
`top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
|
||||||
|
`</div>` +
|
||||||
|
`</div>`;
|
||||||
|
} else {
|
||||||
|
this.background = `<div class="layout ${this.wallpaper}" style="width: ${this.realWidth}px; height: ${this.realHeight}px;` +
|
||||||
|
` background-color: ${this.backgroundColor}"></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onResize() {
|
async onResize() {
|
||||||
@@ -462,7 +500,7 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get font() {
|
get font() {
|
||||||
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px ${this.fontName}`;
|
return `${this.fontStyle} ${this.fontWeight} ${this.fontSize}px '${this.fontName}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPage1TransitionEnd() {
|
onPage1TransitionEnd() {
|
||||||
@@ -493,7 +531,7 @@ class TextPage extends Vue {
|
|||||||
let wait = (timeout + 201)/100;
|
let wait = (timeout + 201)/100;
|
||||||
while (wait > 0 && !this[stopPropertyName]) {
|
while (wait > 0 && !this[stopPropertyName]) {
|
||||||
wait--;
|
wait--;
|
||||||
await sleep(100);
|
await utils.sleep(100);
|
||||||
}
|
}
|
||||||
resolve();
|
resolve();
|
||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
@@ -503,13 +541,13 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
async startTextScrolling() {
|
async startTextScrolling() {
|
||||||
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
|
if (this.doingScrolling || !this.book || !this.parsed.textLength || !this.linesDown || this.pageLineCount < 1 ||
|
||||||
this.linesDown.length <= this.pageLineCount) {
|
this.linesDown.length <= this.pageLineCount || this.dualPageMode) {
|
||||||
this.doStopScrolling();
|
this.doStopScrolling();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
//ждем анимацию
|
//ждем анимацию
|
||||||
while (this.inAnimation) await sleep(10);
|
while (this.inAnimation) await utils.sleep(10);
|
||||||
|
|
||||||
this.stopScrolling = false;
|
this.stopScrolling = false;
|
||||||
this.doingScrolling = true;
|
this.doingScrolling = true;
|
||||||
@@ -520,7 +558,7 @@ class TextPage extends Vue {
|
|||||||
this.page1 = this.page2;
|
this.page1 = this.page2;
|
||||||
this.toggleLayout = true;
|
this.toggleLayout = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
await sleep(50);
|
await utils.sleep(50);
|
||||||
|
|
||||||
this.cachedPos = -1;
|
this.cachedPos = -1;
|
||||||
this.draw();
|
this.draw();
|
||||||
@@ -557,7 +595,7 @@ class TextPage extends Vue {
|
|||||||
page.style.transform = 'none';
|
page.style.transform = 'none';
|
||||||
page.offsetHeight;
|
page.offsetHeight;
|
||||||
|
|
||||||
while (this.doingScrolling) await sleep(10);
|
while (this.doingScrolling) await utils.sleep(10);
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
@@ -621,6 +659,7 @@ class TextPage extends Vue {
|
|||||||
if (!this.pageChangeAnimation)
|
if (!this.pageChangeAnimation)
|
||||||
this.debouncedPrepareNextPage();
|
this.debouncedPrepareNextPage();
|
||||||
this.debouncedDrawStatusBar();
|
this.debouncedDrawStatusBar();
|
||||||
|
this.debouncedDrawPageDividerAndOrnament();
|
||||||
|
|
||||||
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
|
||||||
this.doEnd(true);
|
this.doEnd(true);
|
||||||
@@ -735,8 +774,24 @@ class TextPage extends Vue {
|
|||||||
message = this.statusBarMessage;
|
message = this.statusBarMessage;
|
||||||
if (!message)
|
if (!message)
|
||||||
message = this.title;
|
message = this.title;
|
||||||
|
|
||||||
|
//check image num
|
||||||
|
let imageNum = 0;
|
||||||
|
const len = (lines.length > 2 ? 2 : lines.length);
|
||||||
|
loop:
|
||||||
|
for (let j = 0; j < len; j++) {
|
||||||
|
const line = lines[j];
|
||||||
|
for (const part of line.parts) {
|
||||||
|
if (part.image) {
|
||||||
|
imageNum = part.image.num;
|
||||||
|
break loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//drawing
|
||||||
this.statusBar = this.drawHelper.drawStatusBar(this.statusBarTop, this.statusBarHeight,
|
this.statusBar = this.drawHelper.drawStatusBar(this.statusBarTop, this.statusBarHeight,
|
||||||
lines[i].end, this.parsed.textLength, message);
|
lines[i].end, this.parsed.textLength, message, imageNum, this.parsed.images.length);
|
||||||
|
|
||||||
this.bookPosSeen = lines[i].end;
|
this.bookPosSeen = lines[i].end;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -744,6 +799,25 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawPageDividerAndOrnament() {
|
||||||
|
if (this.dualPageMode) {
|
||||||
|
this.pageDivider = `<div class="layout" style="width: ${this.realWidth}px; height: ${this.scrollHeight}px; ` +
|
||||||
|
`top: ${(this.showStatusBar && this.statusBarTop ? this.statusBarHeight + 1 : 0)}px; position: relative;">` +
|
||||||
|
`<div class="fit row justify-center items-center no-wrap">` +
|
||||||
|
`<div style="height: ${Math.round(this.scrollHeight*this.dualDivHeight/100)}px; width: ${this.dualDivWidth}px; ` +
|
||||||
|
`box-shadow: 0 0 ${this.dualDivShadowWidth}px ${this.dualDivRgbaColor}; ` +
|
||||||
|
`background-image: url("data:image/svg+xml;utf8,<svg width='100%' height='100%' xmlns='http://www.w3.org/2000/svg'>` +
|
||||||
|
`<line x1='${this.dualDivWidth/2}' y1='0' x2='${this.dualDivWidth/2}' y2='100%' stroke='${this.dualDivRgbaColor}' ` +
|
||||||
|
`stroke-width='${this.dualDivWidth}' stroke-dasharray='${this.dualDivStrokeFill} ${this.dualDivStrokeGap}'/>` +
|
||||||
|
`</svg>");">` +
|
||||||
|
`</div>` +
|
||||||
|
`</div>` +
|
||||||
|
`</div>`;
|
||||||
|
} else {
|
||||||
|
this.pageDivider = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
blinkCachedLoadMessage(state) {
|
blinkCachedLoadMessage(state) {
|
||||||
if (state === 'finish') {
|
if (state === 'finish') {
|
||||||
this.statusBarMessage = '';
|
this.statusBarMessage = '';
|
||||||
@@ -766,7 +840,7 @@ class TextPage extends Vue {
|
|||||||
for (let i = 0; i < this.parsed.para.length; i++) {
|
for (let i = 0; i < this.parsed.para.length; i++) {
|
||||||
j++;
|
j++;
|
||||||
if (j > 1) {
|
if (j > 1) {
|
||||||
await sleep(1);
|
await utils.sleep(1);
|
||||||
j = 0;
|
j = 0;
|
||||||
}
|
}
|
||||||
if (this.stopLazyParse)
|
if (this.stopLazyParse)
|
||||||
@@ -788,7 +862,7 @@ class TextPage extends Vue {
|
|||||||
async refreshTime() {
|
async refreshTime() {
|
||||||
if (!this.timeRefreshing) {
|
if (!this.timeRefreshing) {
|
||||||
this.timeRefreshing = true;
|
this.timeRefreshing = true;
|
||||||
await sleep(60*1000);
|
await utils.sleep(60*1000);
|
||||||
|
|
||||||
if (this.book && this.parsed.textLength) {
|
if (this.book && this.parsed.textLength) {
|
||||||
this.debouncedDrawStatusBar();
|
this.debouncedDrawStatusBar();
|
||||||
@@ -905,7 +979,7 @@ class TextPage extends Vue {
|
|||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
|
const newSize = (this.settings.fontSize + 1 < 200 ? this.settings.fontSize + 1 : 100);
|
||||||
this.commit('reader/setSettings', {fontSize: newSize});
|
this.commit('reader/setSettings', {fontSize: newSize});
|
||||||
await sleep(50);
|
await utils.sleep(50);
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -915,7 +989,7 @@ class TextPage extends Vue {
|
|||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
|
const newSize = (this.settings.fontSize - 1 > 5 ? this.settings.fontSize - 1 : 5);
|
||||||
this.commit('reader/setSettings', {fontSize: newSize});
|
this.commit('reader/setSettings', {fontSize: newSize});
|
||||||
await sleep(50);
|
await utils.sleep(50);
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -925,7 +999,7 @@ class TextPage extends Vue {
|
|||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
|
const newDelay = (this.settings.scrollingDelay - 50 > 1 ? this.settings.scrollingDelay - 50 : 1);
|
||||||
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||||
await sleep(50);
|
await utils.sleep(50);
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -935,7 +1009,7 @@ class TextPage extends Vue {
|
|||||||
this.settingsChanging = true;
|
this.settingsChanging = true;
|
||||||
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
|
const newDelay = (this.settings.scrollingDelay + 50 < 10000 ? this.settings.scrollingDelay + 50 : 10000);
|
||||||
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
this.commit('reader/setSettings', {scrollingDelay: newDelay});
|
||||||
await sleep(50);
|
await utils.sleep(50);
|
||||||
this.settingsChanging = false;
|
this.settingsChanging = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -949,7 +1023,7 @@ class TextPage extends Vue {
|
|||||||
let delay = 400;
|
let delay = 400;
|
||||||
while (this.repDoing) {
|
while (this.repDoing) {
|
||||||
this.handleClick(pointX, pointY);
|
this.handleClick(pointX, pointY);
|
||||||
await sleep(delay);
|
await utils.sleep(delay);
|
||||||
if (delay > 15)
|
if (delay > 15)
|
||||||
delay *= 0.8;
|
delay *= 0.8;
|
||||||
}
|
}
|
||||||
@@ -1066,7 +1140,7 @@ class TextPage extends Vue {
|
|||||||
|
|
||||||
onStatusBarClick() {
|
onStatusBarClick() {
|
||||||
const url = this.meta.url;
|
const url = this.meta.url;
|
||||||
if (url && url.indexOf('file://') != 0) {
|
if (url && url.indexOf('disk://') != 0) {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
} else {
|
} else {
|
||||||
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
this.$root.stdDialog.alert('Оригинал недоступен, т.к. файл книги был загружен с локального диска.', ' ', {color: 'info'});
|
||||||
@@ -1158,60 +1232,3 @@ class TextPage extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style>
|
|
||||||
.paper1 {
|
|
||||||
background: url("images/paper1.jpg") center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paper2 {
|
|
||||||
background: url("images/paper2.jpg") center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paper3 {
|
|
||||||
background: url("images/paper3.jpg") center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paper4 {
|
|
||||||
background: url("images/paper4.jpg") center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paper5 {
|
|
||||||
background: url("images/paper5.jpg") center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paper6 {
|
|
||||||
background: url("images/paper6.jpg") center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paper7 {
|
|
||||||
background: url("images/paper7.jpg") center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paper8 {
|
|
||||||
background: url("images/paper8.jpg") center;
|
|
||||||
background-size: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.paper9 {
|
|
||||||
background: url("images/paper9.jpg");
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes page1-animation-thaw {
|
|
||||||
0% { opacity: 0; }
|
|
||||||
100% { opacity: 1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes page2-animation-thaw {
|
|
||||||
0% { opacity: 1; }
|
|
||||||
100% { opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|||||||
BIN
client/components/Reader/TextPage/images/paper10.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
client/components/Reader/TextPage/images/paper11.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
client/components/Reader/TextPage/images/paper12.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
client/components/Reader/TextPage/images/paper13.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
client/components/Reader/TextPage/images/paper14.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
client/components/Reader/TextPage/images/paper15.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
client/components/Reader/TextPage/images/paper16.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
client/components/Reader/TextPage/images/paper17.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
@@ -1,24 +1,56 @@
|
|||||||
import he from 'he';
|
import he from 'he';
|
||||||
import sax from '../../../../server/core/sax';
|
import sax from '../../../../server/core/sax';
|
||||||
import {sleep} from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
|
|
||||||
const maxImageLineCount = 100;
|
const maxImageLineCount = 100;
|
||||||
|
|
||||||
|
// defaults
|
||||||
|
const defaultSettings = {
|
||||||
|
p: 30, //px, отступ параграфа
|
||||||
|
w: 500, //px, ширина страницы
|
||||||
|
|
||||||
|
font: '', //css описание шрифта
|
||||||
|
fontSize: 20, //px, размер шрифта
|
||||||
|
wordWrap: false, //перенос по слогам
|
||||||
|
cutEmptyParagraphs: false, //убирать пустые параграфы
|
||||||
|
addEmptyParagraphs: 0, //добавлять n пустых параграфов перед непустым
|
||||||
|
maxWordLength: 500, //px, максимальная длина слова без пробелов
|
||||||
|
lineHeight: 26, //px, высота строки
|
||||||
|
showImages: true, //показыввать изображения
|
||||||
|
showInlineImagesInCenter: true, //выносить изображения в центр, работает на этапе первичного парсинга (parse)
|
||||||
|
imageHeightLines: 100, //кол-во строк, максимальная высота изображения
|
||||||
|
imageFitWidth: true, //ширина изображения не более ширины страницы
|
||||||
|
dualPageMode: false, //двухстраничный режим
|
||||||
|
compactTextPerc: 0, //проценты, степень компактности текста
|
||||||
|
testWidth: 0, //ширина тестовой строки, пересчитывается извне при изменении шрифта браузером
|
||||||
|
isTesting: false, //тестовый режим
|
||||||
|
|
||||||
|
//заглушка, измеритель ширины текста
|
||||||
|
measureText: (text, style) => {// eslint-disable-line no-unused-vars
|
||||||
|
return text.length*20;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
//for splitToSlogi()
|
||||||
|
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
|
||||||
|
const soglas = new Set([
|
||||||
|
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
|
||||||
|
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
|
||||||
|
]);
|
||||||
|
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
|
||||||
|
const alpha = new Set([...glas, ...soglas, ...znak]);
|
||||||
|
|
||||||
export default class BookParser {
|
export default class BookParser {
|
||||||
constructor(settings) {
|
constructor(settings = {}) {
|
||||||
if (settings) {
|
this.sets = {};
|
||||||
this.showInlineImagesInCenter = settings.showInlineImagesInCenter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaults
|
this.setSettings(defaultSettings);
|
||||||
this.p = 30;// px, отступ параграфа
|
this.setSettings(settings);
|
||||||
this.w = 300;// px, ширина страницы
|
}
|
||||||
this.wordWrap = false;// перенос по слогам
|
|
||||||
|
|
||||||
//заглушка
|
setSettings(settings = {}) {
|
||||||
this.measureText = (text, style) => {// eslint-disable-line no-unused-vars
|
this.sets = Object.assign({}, this.sets, settings);
|
||||||
return text.length*20;
|
this.measureText = this.sets.measureText;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async parse(data, callback) {
|
async parse(data, callback) {
|
||||||
@@ -54,12 +86,14 @@ export default class BookParser {
|
|||||||
|
|
||||||
//оглавление
|
//оглавление
|
||||||
this.contents = [];
|
this.contents = [];
|
||||||
|
this.images = [];
|
||||||
let curTitle = {paraIndex: -1, title: '', subtitles: []};
|
let curTitle = {paraIndex: -1, title: '', subtitles: []};
|
||||||
let curSubtitle = {paraIndex: -1, title: ''};
|
let curSubtitle = {paraIndex: -1, title: ''};
|
||||||
let inTitle = false;
|
let inTitle = false;
|
||||||
let inSubtitle = false;
|
let inSubtitle = false;
|
||||||
let sectionLevel = 0;
|
let sectionLevel = 0;
|
||||||
let bodyIndex = 0;
|
let bodyIndex = 0;
|
||||||
|
let imageNum = 0;
|
||||||
|
|
||||||
let paraIndex = -1;
|
let paraIndex = -1;
|
||||||
let paraOffset = 0;
|
let paraOffset = 0;
|
||||||
@@ -74,6 +108,7 @@ export default class BookParser {
|
|||||||
*/
|
*/
|
||||||
const getImageDimensions = (binaryId, binaryType, data) => {
|
const getImageDimensions = (binaryId, binaryType, data) => {
|
||||||
return new Promise ((resolve, reject) => { (async() => {
|
return new Promise ((resolve, reject) => { (async() => {
|
||||||
|
data = data.replace(/[\n\r\s]/g, '');
|
||||||
const i = new Image();
|
const i = new Image();
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
i.onload = () => {
|
i.onload = () => {
|
||||||
@@ -90,7 +125,7 @@ export default class BookParser {
|
|||||||
i.onerror = reject;
|
i.onerror = reject;
|
||||||
|
|
||||||
i.src = `data:${binaryType};base64,${data}`;
|
i.src = `data:${binaryType};base64,${data}`;
|
||||||
await sleep(30*1000);
|
await utils.sleep(30*1000);
|
||||||
if (!resolved)
|
if (!resolved)
|
||||||
reject('Не удалось получить размер изображения');
|
reject('Не удалось получить размер изображения');
|
||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
@@ -112,20 +147,65 @@ export default class BookParser {
|
|||||||
i.onerror = reject;
|
i.onerror = reject;
|
||||||
|
|
||||||
i.src = src;
|
i.src = src;
|
||||||
await sleep(30*1000);
|
await utils.sleep(30*1000);
|
||||||
if (!resolved)
|
if (!resolved)
|
||||||
reject('Не удалось получить размер изображения');
|
reject('Не удалось получить размер изображения');
|
||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
};
|
};
|
||||||
|
|
||||||
const newParagraph = (text, len, addIndex) => {
|
const correctCurrentPara = () => {
|
||||||
|
//коррекция текущего параграфа
|
||||||
|
if (paraIndex >= 0) {
|
||||||
|
const prevParaIndex = paraIndex;
|
||||||
|
let p = para[paraIndex];
|
||||||
|
paraOffset -= p.length;
|
||||||
|
//добавление пустых (addEmptyParagraphs) параграфов перед текущим непустым
|
||||||
|
if (p.text.trim() != '') {
|
||||||
|
for (let i = 0; i < 2; i++) {
|
||||||
|
para[paraIndex] = {
|
||||||
|
index: paraIndex,
|
||||||
|
offset: paraOffset,
|
||||||
|
length: 1,
|
||||||
|
text: ' ',
|
||||||
|
addIndex: i + 1,
|
||||||
|
};
|
||||||
|
paraIndex++;
|
||||||
|
paraOffset++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curTitle.paraIndex == prevParaIndex)
|
||||||
|
curTitle.paraIndex = paraIndex;
|
||||||
|
if (curSubtitle.paraIndex == prevParaIndex)
|
||||||
|
curSubtitle.paraIndex = paraIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
//уберем пробелы с концов параграфа, минимум 1 пробел должен быть у пустого параграфа
|
||||||
|
let newParaText = p.text.trim();
|
||||||
|
newParaText = (newParaText.length ? newParaText : ' ');
|
||||||
|
const ldiff = p.text.length - newParaText.length;
|
||||||
|
if (ldiff != 0) {
|
||||||
|
p.text = newParaText;
|
||||||
|
p.length -= ldiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.index = paraIndex;
|
||||||
|
p.offset = paraOffset;
|
||||||
|
para[paraIndex] = p;
|
||||||
|
paraOffset += p.length;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newParagraph = (text = '', len = 0) => {
|
||||||
|
correctCurrentPara();
|
||||||
|
|
||||||
|
//новый параграф
|
||||||
paraIndex++;
|
paraIndex++;
|
||||||
let p = {
|
let p = {
|
||||||
index: paraIndex,
|
index: paraIndex,
|
||||||
offset: paraOffset,
|
offset: paraOffset,
|
||||||
length: len,
|
length: len,
|
||||||
text: text,
|
text: text,
|
||||||
addIndex: (addIndex ? addIndex : 0),
|
addIndex: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (inSubtitle) {
|
if (inSubtitle) {
|
||||||
@@ -135,53 +215,26 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
para[paraIndex] = p;
|
para[paraIndex] = p;
|
||||||
paraOffset += p.length;
|
paraOffset += len;
|
||||||
};
|
};
|
||||||
|
|
||||||
const growParagraph = (text, len) => {
|
const growParagraph = (text, len) => {
|
||||||
if (paraIndex < 0) {
|
if (paraIndex < 0) {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
growParagraph(text, len);
|
growParagraph(text, len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const prevParaIndex = paraIndex;
|
|
||||||
let p = para[paraIndex];
|
|
||||||
paraOffset -= p.length;
|
|
||||||
//добавление пустых (addEmptyParagraphs) параграфов перед текущим
|
|
||||||
if (p.length == 1 && p.text[0] == ' ' && len > 0) {
|
|
||||||
paraIndex--;
|
|
||||||
for (let i = 0; i < 2; i++) {
|
|
||||||
newParagraph(' ', 1, i + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
paraIndex++;
|
|
||||||
p.index = paraIndex;
|
|
||||||
p.offset = paraOffset;
|
|
||||||
para[paraIndex] = p;
|
|
||||||
|
|
||||||
if (curTitle.paraIndex == prevParaIndex)
|
|
||||||
curTitle.paraIndex = paraIndex;
|
|
||||||
if (curSubtitle.paraIndex == prevParaIndex)
|
|
||||||
curSubtitle.paraIndex = paraIndex;
|
|
||||||
|
|
||||||
//уберем начальный пробел
|
|
||||||
p.length = 0;
|
|
||||||
p.text = p.text.substr(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.length += len;
|
|
||||||
p.text += text;
|
|
||||||
|
|
||||||
|
|
||||||
if (inSubtitle) {
|
if (inSubtitle) {
|
||||||
curSubtitle.title += text;
|
curSubtitle.title += text;
|
||||||
} else if (inTitle) {
|
} else if (inTitle) {
|
||||||
curTitle.title += text;
|
curTitle.title += text;
|
||||||
}
|
}
|
||||||
|
|
||||||
para[paraIndex] = p;
|
const p = para[paraIndex];
|
||||||
paraOffset += p.length;
|
p.length += len;
|
||||||
|
p.text += text;
|
||||||
|
paraOffset += len;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
|
const onStartNode = (elemName, tail) => {// eslint-disable-line no-unused-vars
|
||||||
@@ -194,7 +247,8 @@ export default class BookParser {
|
|||||||
if (tag == 'binary') {
|
if (tag == 'binary') {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
binaryType = (attrs['content-type'] && attrs['content-type'].value ? attrs['content-type'].value : '');
|
||||||
if (binaryType == 'image/jpeg' || binaryType == 'image/png' || binaryType == 'application/octet-stream')
|
binaryType = (binaryType == 'image/jpg' || binaryType == 'application/octet-stream' ? 'image/jpeg' : binaryType);
|
||||||
|
if (binaryType == 'image/jpeg' || binaryType == 'image/png')
|
||||||
binaryId = (attrs.id.value ? attrs.id.value : '');
|
binaryId = (attrs.id.value ? attrs.id.value : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,36 +256,88 @@ export default class BookParser {
|
|||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
const href = attrs.href.value;
|
const href = attrs.href.value;
|
||||||
|
const alt = (attrs.alt && attrs.alt.value ? attrs.alt.value : '');
|
||||||
|
const {id, local} = this.imageHrefToId(href);
|
||||||
if (href[0] == '#') {//local
|
if (href[0] == '#') {//local
|
||||||
if (inPara && !this.showInlineImagesInCenter && !center)
|
imageNum++;
|
||||||
growParagraph(`<image-inline href="${href}"></image-inline>`, 0);
|
|
||||||
|
if (inPara && !this.sets.showInlineImagesInCenter && !center)
|
||||||
|
growParagraph(`<image-inline href="${href}" num="${imageNum}"></image-inline>`, 0);
|
||||||
else
|
else
|
||||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||||
if (inPara && this.showInlineImagesInCenter)
|
|
||||||
newParagraph(' ', 1);
|
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||||
|
|
||||||
|
if (inPara && this.sets.showInlineImagesInCenter)
|
||||||
|
newParagraph();
|
||||||
} else {//external
|
} else {//external
|
||||||
dimPromises.push(getExternalImageDimensions(href));
|
imageNum++;
|
||||||
newParagraph(`<image href="${href}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
|
||||||
|
if (!this.sets.isTesting) {
|
||||||
|
dimPromises.push(getExternalImageDimensions(href));
|
||||||
|
} else {
|
||||||
|
dimPromises.push(this.sets.getExternalImageDimensions(this, href));
|
||||||
|
}
|
||||||
|
newParagraph(`<image href="${href}" num="${imageNum}">${' '.repeat(maxImageLineCount)}</image>`, maxImageLineCount);
|
||||||
|
|
||||||
|
this.images.push({paraIndex, num: imageNum, id, local, alt});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'author' && path.indexOf('/fictionbook/description/title-info/author') == 0) {
|
if (path == '/fictionbook/description/title-info/author') {
|
||||||
if (!fb2.author)
|
if (!fb2.author)
|
||||||
fb2.author = [];
|
fb2.author = [];
|
||||||
|
|
||||||
fb2.author.push({});
|
fb2.author.push({});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPublishSequence = (path == '/fictionbook/description/publish-info/sequence');
|
||||||
|
if (path == '/fictionbook/description/title-info/sequence' || isPublishSequence) {
|
||||||
|
if (!fb2.sequence)
|
||||||
|
fb2.sequence = [];
|
||||||
|
|
||||||
|
if (!isPublishSequence || !fb2.sequence.length) {
|
||||||
|
const attrs = sax.getAttrsSync(tail);
|
||||||
|
const seq = {};
|
||||||
|
if (attrs.name && attrs.name.value) {
|
||||||
|
seq.name = attrs.name.value;
|
||||||
|
}
|
||||||
|
if (attrs.number && attrs.number.value) {
|
||||||
|
seq.number = attrs.number.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fb2.sequence.push(seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (path.indexOf('/fictionbook/body') == 0) {
|
if (path.indexOf('/fictionbook/body') == 0) {
|
||||||
if (tag == 'body') {
|
if (tag == 'body') {
|
||||||
|
if (isFirstBody && fb2.annotation) {
|
||||||
|
const ann = fb2.annotation.split('<p>').filter(v => v).map(v => utils.removeHtmlTags(v));
|
||||||
|
ann.forEach(a => {
|
||||||
|
newParagraph(`<emphasis><space w="1">${a}</space></emphasis>`, a.length);
|
||||||
|
});
|
||||||
|
if (ann.length)
|
||||||
|
newParagraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFirstBody && fb2.sequence && fb2.sequence.length) {
|
||||||
|
const bt = utils.getBookTitle(fb2);
|
||||||
|
if (bt.sequence) {
|
||||||
|
newParagraph(bt.sequence, bt.sequence.length);
|
||||||
|
newParagraph();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFirstBody)
|
if (!isFirstBody)
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
isFirstBody = false;
|
isFirstBody = false;
|
||||||
bodyIndex++;
|
bodyIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'title') {
|
if (tag == 'title') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
isFirstTitlePara = true;
|
isFirstTitlePara = true;
|
||||||
bold = true;
|
bold = true;
|
||||||
center = true;
|
center = true;
|
||||||
@@ -243,18 +349,18 @@ export default class BookParser {
|
|||||||
|
|
||||||
if (tag == 'section') {
|
if (tag == 'section') {
|
||||||
if (!isFirstSection)
|
if (!isFirstSection)
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
isFirstSection = false;
|
isFirstSection = false;
|
||||||
sectionLevel++;
|
sectionLevel++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'emphasis' || tag == 'strong') {
|
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||||
growParagraph(`<${tag}>`, 0);
|
growParagraph(`<${tag}>`, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
|
if ((tag == 'p' || tag == 'empty-line' || tag == 'v')) {
|
||||||
if (!(tag == 'p' && isFirstTitlePara))
|
if (!(tag == 'p' && isFirstTitlePara))
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
if (tag == 'p') {
|
if (tag == 'p') {
|
||||||
inPara = true;
|
inPara = true;
|
||||||
isFirstTitlePara = false;
|
isFirstTitlePara = false;
|
||||||
@@ -262,11 +368,16 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'subtitle') {
|
if (tag == 'subtitle') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
isFirstTitlePara = true;
|
isFirstTitlePara = true;
|
||||||
bold = true;
|
bold = true;
|
||||||
center = true;
|
center = true;
|
||||||
|
|
||||||
|
if (curTitle.paraIndex < 0) {
|
||||||
|
curTitle = {paraIndex, title: 'Оглавление', inset: sectionLevel, bodyIndex, subtitles: []};
|
||||||
|
this.contents.push(curTitle);
|
||||||
|
}
|
||||||
|
|
||||||
inSubtitle = true;
|
inSubtitle = true;
|
||||||
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
|
curSubtitle = {paraIndex, inset: sectionLevel, title: ''};
|
||||||
curTitle.subtitles.push(curSubtitle);
|
curTitle.subtitles.push(curSubtitle);
|
||||||
@@ -278,11 +389,12 @@ export default class BookParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'poem') {
|
if (tag == 'poem') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'text-author') {
|
if (tag == 'text-author') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
|
bold = true;
|
||||||
space += 1;
|
space += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,7 +418,7 @@ export default class BookParser {
|
|||||||
sectionLevel--;
|
sectionLevel--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'emphasis' || tag == 'strong') {
|
if (tag == 'emphasis' || tag == 'strong' || tag == 'sup' || tag == 'sub') {
|
||||||
growParagraph(`</${tag}>`, 0);
|
growParagraph(`</${tag}>`, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,15 +436,15 @@ export default class BookParser {
|
|||||||
if (tag == 'epigraph' || tag == 'annotation') {
|
if (tag == 'epigraph' || tag == 'annotation') {
|
||||||
italic = false;
|
italic = false;
|
||||||
space -= 1;
|
space -= 1;
|
||||||
if (tag == 'annotation')
|
newParagraph();
|
||||||
newParagraph(' ', 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'stanza') {
|
if (tag == 'stanza') {
|
||||||
newParagraph(' ', 1);
|
newParagraph();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'text-author') {
|
if (tag == 'text-author') {
|
||||||
|
bold = false;
|
||||||
space -= 1;
|
space -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,17 +461,14 @@ export default class BookParser {
|
|||||||
|
|
||||||
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
|
const onTextNode = (text) => {// eslint-disable-line no-unused-vars
|
||||||
text = he.decode(text);
|
text = he.decode(text);
|
||||||
text = text.replace(/>/g, '>');
|
text = text.replace(/>/g, '>').replace(/</g, '<').replace(/[\t\n\r\xa0]/g, ' ');
|
||||||
text = text.replace(/</g, '<');
|
|
||||||
|
|
||||||
if (text && text.trim() == '')
|
if (text && text.trim() == '')
|
||||||
text = (text.indexOf(' ') >= 0 ? ' ' : '');
|
text = ' ';
|
||||||
|
|
||||||
if (!text)
|
if (!text)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
text = text.replace(/[\t\n\r\xa0]/g, ' ');
|
|
||||||
|
|
||||||
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
|
const authorLength = (fb2.author && fb2.author.length ? fb2.author.length : 0);
|
||||||
switch (path) {
|
switch (path) {
|
||||||
case '/fictionbook/description/title-info/author/first-name':
|
case '/fictionbook/description/title-info/author/first-name':
|
||||||
@@ -397,35 +506,43 @@ export default class BookParser {
|
|||||||
fb2.annotation += text;
|
fb2.annotation += text;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tOpen = (center ? '<center>' : '');
|
if (binaryId) {
|
||||||
tOpen += (bold ? '<strong>' : '');
|
if (!this.sets.isTesting) {
|
||||||
tOpen += (italic ? '<emphasis>' : '');
|
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
|
||||||
tOpen += (space ? `<space w="${space}">` : '');
|
} else {
|
||||||
let tClose = (space ? '</space>' : '');
|
dimPromises.push(this.sets.getImageDimensions(this, binaryId, binaryType, text));
|
||||||
tClose += (italic ? '</emphasis>' : '');
|
}
|
||||||
tClose += (bold ? '</strong>' : '');
|
}
|
||||||
tClose += (center ? '</center>' : '');
|
|
||||||
|
|
||||||
if (path.indexOf('/fictionbook/body/title') == 0 ||
|
if (path.indexOf('/fictionbook/body/title') == 0 ||
|
||||||
path.indexOf('/fictionbook/body/section') == 0 ||
|
path.indexOf('/fictionbook/body/section') == 0 ||
|
||||||
path.indexOf('/fictionbook/body/epigraph') == 0
|
path.indexOf('/fictionbook/body/epigraph') == 0
|
||||||
) {
|
) {
|
||||||
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
let tOpen = (center ? '<center>' : '');
|
||||||
}
|
tOpen += (bold ? '<strong>' : '');
|
||||||
|
tOpen += (italic ? '<emphasis>' : '');
|
||||||
|
tOpen += (space ? `<space w="${space}">` : '');
|
||||||
|
let tClose = (space ? '</space>' : '');
|
||||||
|
tClose += (italic ? '</emphasis>' : '');
|
||||||
|
tClose += (bold ? '</strong>' : '');
|
||||||
|
tClose += (center ? '</center>' : '');
|
||||||
|
|
||||||
if (binaryId) {
|
if (text != ' ')
|
||||||
dimPromises.push(getImageDimensions(binaryId, binaryType, text));
|
growParagraph(`${tOpen}${text}${tClose}`, text.length);
|
||||||
|
else
|
||||||
|
growParagraph(' ', 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onProgress = async(prog) => {
|
const onProgress = async(prog) => {
|
||||||
await sleep(1);
|
await utils.sleep(1);
|
||||||
callback(prog);
|
callback(prog);
|
||||||
};
|
};
|
||||||
|
|
||||||
await sax.parse(data, {
|
await sax.parse(data, {
|
||||||
onStartNode, onEndNode, onTextNode, onProgress
|
onStartNode, onEndNode, onTextNode, onProgress
|
||||||
});
|
});
|
||||||
|
correctCurrentPara();
|
||||||
|
|
||||||
if (dimPromises.length) {
|
if (dimPromises.length) {
|
||||||
try {
|
try {
|
||||||
@@ -441,11 +558,20 @@ export default class BookParser {
|
|||||||
this.textLength = paraOffset;
|
this.textLength = paraOffset;
|
||||||
|
|
||||||
callback(100);
|
callback(100);
|
||||||
await sleep(10);
|
await utils.sleep(10);
|
||||||
|
|
||||||
return {fb2};
|
return {fb2};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
imageHrefToId(id) {
|
||||||
|
let local = false;
|
||||||
|
if (id[0] == '#') {
|
||||||
|
id = id.substr(1);
|
||||||
|
local = true;
|
||||||
|
}
|
||||||
|
return {id, local};
|
||||||
|
}
|
||||||
|
|
||||||
findParaIndex(bookPos) {
|
findParaIndex(bookPos) {
|
||||||
let result = undefined;
|
let result = undefined;
|
||||||
//дихотомия
|
//дихотомия
|
||||||
@@ -470,16 +596,26 @@ export default class BookParser {
|
|||||||
|
|
||||||
splitToStyle(s) {
|
splitToStyle(s) {
|
||||||
let result = [];/*array of {
|
let result = [];/*array of {
|
||||||
style: {bold: Boolean, italic: Boolean, center: Boolean, space: Number},
|
style: {bold: Boolean, italic: Boolean, sup: Boolean, sub: Boolean, center: Boolean, space: Number},
|
||||||
image: {local: Boolean, inline: Boolean, id: String},
|
image: {local: Boolean, inline: Boolean, id: String},
|
||||||
text: String,
|
text: String,
|
||||||
}*/
|
}*/
|
||||||
let style = {};
|
let style = {};
|
||||||
let image = {};
|
let image = {};
|
||||||
|
|
||||||
|
//оптимизация по памяти
|
||||||
|
const copyStyle = (s) => {
|
||||||
|
const r = {};
|
||||||
|
for (const prop in s) {
|
||||||
|
if (s[prop])
|
||||||
|
r[prop] = s[prop];
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
|
const onTextNode = async(text) => {// eslint-disable-line no-unused-vars
|
||||||
result.push({
|
result.push({
|
||||||
style: Object.assign({}, style),
|
style: copyStyle(style),
|
||||||
image,
|
image,
|
||||||
text
|
text
|
||||||
});
|
});
|
||||||
@@ -493,6 +629,12 @@ export default class BookParser {
|
|||||||
case 'emphasis':
|
case 'emphasis':
|
||||||
style.italic = true;
|
style.italic = true;
|
||||||
break;
|
break;
|
||||||
|
case 'sup':
|
||||||
|
style.sup = true;
|
||||||
|
break;
|
||||||
|
case 'sub':
|
||||||
|
style.sub = true;
|
||||||
|
break;
|
||||||
case 'center':
|
case 'center':
|
||||||
style.center = true;
|
style.center = true;
|
||||||
break;
|
break;
|
||||||
@@ -505,28 +647,21 @@ export default class BookParser {
|
|||||||
case 'image': {
|
case 'image': {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
let id = attrs.href.value;
|
image = this.imageHrefToId(attrs.href.value);
|
||||||
let local = false;
|
image.inline = false;
|
||||||
if (id[0] == '#') {
|
image.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||||
id = id.substr(1);
|
|
||||||
local = true;
|
|
||||||
}
|
|
||||||
image = {local, inline: false, id};
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'image-inline': {
|
case 'image-inline': {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
let attrs = sax.getAttrsSync(tail);
|
||||||
if (attrs.href && attrs.href.value) {
|
if (attrs.href && attrs.href.value) {
|
||||||
let id = attrs.href.value;
|
const img = this.imageHrefToId(attrs.href.value);
|
||||||
let local = false;
|
img.inline = true;
|
||||||
if (id[0] == '#') {
|
img.num = (attrs.num && attrs.num.value ? attrs.num.value : 0);
|
||||||
id = id.substr(1);
|
|
||||||
local = true;
|
|
||||||
}
|
|
||||||
result.push({
|
result.push({
|
||||||
style: Object.assign({}, style),
|
style: copyStyle(style),
|
||||||
image: {local, inline: true, id},
|
image: img,
|
||||||
text: ''
|
text: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -543,6 +678,12 @@ export default class BookParser {
|
|||||||
case 'emphasis':
|
case 'emphasis':
|
||||||
style.italic = false;
|
style.italic = false;
|
||||||
break;
|
break;
|
||||||
|
case 'sup':
|
||||||
|
style.sup = false;
|
||||||
|
break;
|
||||||
|
case 'sub':
|
||||||
|
style.sub = false;
|
||||||
|
break;
|
||||||
case 'center':
|
case 'center':
|
||||||
style.center = false;
|
style.center = false;
|
||||||
break;
|
break;
|
||||||
@@ -562,7 +703,7 @@ export default class BookParser {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//длинные слова (или белиберду без пробелов) тоже разобьем
|
//длинные слова (или белиберду без пробелов) тоже разобьем
|
||||||
const maxWordLength = this.maxWordLength;
|
const maxWordLength = this.sets.maxWordLength;
|
||||||
const parts = result;
|
const parts = result;
|
||||||
result = [];
|
result = [];
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
@@ -575,7 +716,7 @@ export default class BookParser {
|
|||||||
spaceIndex = i;
|
spaceIndex = i;
|
||||||
|
|
||||||
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
|
if (i - spaceIndex >= maxWordLength && i < p.text.length - 1 &&
|
||||||
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.w - this.p) {
|
this.measureText(p.text.substr(spaceIndex + 1, i - spaceIndex), p.style) >= this.sets.w - this.sets.p) {
|
||||||
result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)});
|
result.push({style: p.style, image: p.image, text: p.text.substr(0, i + 1)});
|
||||||
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
|
p = {style: p.style, image: p.image, text: p.text.substr(i + 1)};
|
||||||
spaceIndex = -1;
|
spaceIndex = -1;
|
||||||
@@ -593,86 +734,87 @@ export default class BookParser {
|
|||||||
splitToSlogi(word) {
|
splitToSlogi(word) {
|
||||||
let result = [];
|
let result = [];
|
||||||
|
|
||||||
const glas = new Set(['а', 'А', 'о', 'О', 'и', 'И', 'е', 'Е', 'ё', 'Ё', 'э', 'Э', 'ы', 'Ы', 'у', 'У', 'ю', 'Ю', 'я', 'Я']);
|
|
||||||
const soglas = new Set([
|
|
||||||
'б', 'в', 'г', 'д', 'ж', 'з', 'й', 'к', 'л', 'м', 'н', 'п', 'р', 'с', 'т', 'ф', 'х', 'ц', 'ч', 'ш', 'щ',
|
|
||||||
'Б', 'В', 'Г', 'Д', 'Ж', 'З', 'Й', 'К', 'Л', 'М', 'Н', 'П', 'Р', 'С', 'Т', 'Ф', 'Х', 'Ч', 'Ц', 'Ш', 'Щ'
|
|
||||||
]);
|
|
||||||
const znak = new Set(['ь', 'Ь', 'ъ', 'Ъ', 'й', 'Й']);
|
|
||||||
const alpha = new Set([...glas, ...soglas, ...znak]);
|
|
||||||
|
|
||||||
let slog = '';
|
|
||||||
let slogLen = 0;
|
|
||||||
const len = word.length;
|
const len = word.length;
|
||||||
word += ' ';
|
if (len > 3) {
|
||||||
for (let i = 0; i < len; i++) {
|
let slog = '';
|
||||||
slog += word[i];
|
let slogLen = 0;
|
||||||
if (alpha.has(word[i]))
|
word += ' ';
|
||||||
slogLen++;
|
for (let i = 0; i < len; i++) {
|
||||||
|
slog += word[i];
|
||||||
|
if (alpha.has(word[i]))
|
||||||
|
slogLen++;
|
||||||
|
|
||||||
if (slogLen > 1 && i < len - 2 && (
|
if (slogLen > 1 && i < len - 2 && (
|
||||||
//гласная, а следом не 2 согласные буквы
|
//гласная, а следом не 2 согласные буквы
|
||||||
(glas.has(word[i]) && !(soglas.has(word[i + 1]) &&
|
(glas.has(word[i]) && !( soglas.has(word[i + 1]) && soglas.has(word[i + 2]) ) &&
|
||||||
soglas.has(word[i + 2])) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||||
) ||
|
) ||
|
||||||
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
|
//предыдущая не согласная буква, текущая согласная, а следом согласная и согласная|гласная буквы
|
||||||
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) &&
|
(alpha.has(word[i - 1]) && !soglas.has(word[i - 1]) && soglas.has(word[i]) && soglas.has(word[i + 1]) &&
|
||||||
soglas.has(word[i]) && soglas.has(word[i + 1]) &&
|
( glas.has(word[i + 2]) || soglas.has(word[i + 2]) ) &&
|
||||||
(glas.has(word[i + 2]) || soglas.has(word[i + 2])) &&
|
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
||||||
alpha.has(word[i + 1]) && alpha.has(word[i + 2])
|
) ||
|
||||||
) ||
|
//мягкий или твердый знак или Й
|
||||||
//мягкий или твердый знак или Й
|
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
|
||||||
(znak.has(word[i]) && alpha.has(word[i + 1]) && alpha.has(word[i + 2])) ||
|
(word[i] == '-')
|
||||||
(word[i] == '-')
|
) &&
|
||||||
) &&
|
//нельзя оставлять окончания на ь, ъ, й
|
||||||
//нельзя оставлять окончания на ь, ъ, й
|
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
|
||||||
!(znak.has(word[i + 2]) && !alpha.has(word[i + 3]))
|
|
||||||
|
|
||||||
) {
|
) {
|
||||||
result.push(slog);
|
result.push(slog);
|
||||||
slog = '';
|
slog = '';
|
||||||
slogLen = 0;
|
slogLen = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if (slog)
|
||||||
|
result.push(slog);
|
||||||
|
} else {
|
||||||
|
result.push(word);
|
||||||
}
|
}
|
||||||
if (slog)
|
|
||||||
result.push(slog);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
parsePara(paraIndex) {
|
parsePara(paraIndex) {
|
||||||
const para = this.para[paraIndex];
|
const para = this.para[paraIndex];
|
||||||
|
const s = this.sets;
|
||||||
|
|
||||||
|
//перераспарсиваем только при изменении одного из параметров
|
||||||
if (!this.force &&
|
if (!this.force &&
|
||||||
para.parsed &&
|
para.parsed &&
|
||||||
para.parsed.testWidth === this.testWidth &&
|
para.parsed.p === s.p &&
|
||||||
para.parsed.w === this.w &&
|
para.parsed.w === s.w &&
|
||||||
para.parsed.p === this.p &&
|
para.parsed.font === s.font &&
|
||||||
para.parsed.wordWrap === this.wordWrap &&
|
para.parsed.fontSize === s.fontSize &&
|
||||||
para.parsed.maxWordLength === this.maxWordLength &&
|
para.parsed.wordWrap === s.wordWrap &&
|
||||||
para.parsed.font === this.font &&
|
para.parsed.cutEmptyParagraphs === s.cutEmptyParagraphs &&
|
||||||
para.parsed.cutEmptyParagraphs === this.cutEmptyParagraphs &&
|
para.parsed.addEmptyParagraphs === s.addEmptyParagraphs &&
|
||||||
para.parsed.addEmptyParagraphs === this.addEmptyParagraphs &&
|
para.parsed.maxWordLength === s.maxWordLength &&
|
||||||
para.parsed.showImages === this.showImages &&
|
para.parsed.lineHeight === s.lineHeight &&
|
||||||
para.parsed.imageHeightLines === this.imageHeightLines &&
|
para.parsed.showImages === s.showImages &&
|
||||||
para.parsed.imageFitWidth === this.imageFitWidth &&
|
para.parsed.imageHeightLines === s.imageHeightLines &&
|
||||||
para.parsed.compactTextPerc === this.compactTextPerc
|
para.parsed.imageFitWidth === (s.imageFitWidth || s.dualPageMode) &&
|
||||||
|
para.parsed.compactTextPerc === s.compactTextPerc &&
|
||||||
|
para.parsed.testWidth === s.testWidth
|
||||||
)
|
)
|
||||||
return para.parsed;
|
return para.parsed;
|
||||||
|
|
||||||
const parsed = {
|
const parsed = {
|
||||||
testWidth: this.testWidth,
|
p: s.p,
|
||||||
w: this.w,
|
w: s.w,
|
||||||
p: this.p,
|
font: s.font,
|
||||||
wordWrap: this.wordWrap,
|
fontSize: s.fontSize,
|
||||||
maxWordLength: this.maxWordLength,
|
wordWrap: s.wordWrap,
|
||||||
font: this.font,
|
cutEmptyParagraphs: s.cutEmptyParagraphs,
|
||||||
cutEmptyParagraphs: this.cutEmptyParagraphs,
|
addEmptyParagraphs: s.addEmptyParagraphs,
|
||||||
addEmptyParagraphs: this.addEmptyParagraphs,
|
maxWordLength: s.maxWordLength,
|
||||||
showImages: this.showImages,
|
lineHeight: s.lineHeight,
|
||||||
imageHeightLines: this.imageHeightLines,
|
showImages: s.showImages,
|
||||||
imageFitWidth: this.imageFitWidth,
|
imageHeightLines: s.imageHeightLines,
|
||||||
compactTextPerc: this.compactTextPerc,
|
imageFitWidth: (s.imageFitWidth || s.dualPageMode),
|
||||||
|
compactTextPerc: s.compactTextPerc,
|
||||||
|
testWidth: s.testWidth,
|
||||||
visible: true, //вычисляется позже
|
visible: true, //вычисляется позже
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -704,7 +846,7 @@ export default class BookParser {
|
|||||||
let ofs = 0;//смещение от начала параграфа para.offset
|
let ofs = 0;//смещение от начала параграфа para.offset
|
||||||
let imgW = 0;
|
let imgW = 0;
|
||||||
let imageInPara = false;
|
let imageInPara = false;
|
||||||
const compactWidth = this.measureText('W', {})*this.compactTextPerc/100;
|
const compactWidth = this.measureText('W', {})*parsed.compactTextPerc/100;
|
||||||
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
// тут начинается самый замес, перенос по слогам и стилизация, а также изображения
|
||||||
for (const part of parts) {
|
for (const part of parts) {
|
||||||
style = part.style;
|
style = part.style;
|
||||||
@@ -717,14 +859,14 @@ export default class BookParser {
|
|||||||
if (!bin)
|
if (!bin)
|
||||||
bin = {h: 1, w: 1};
|
bin = {h: 1, w: 1};
|
||||||
|
|
||||||
let lineCount = this.imageHeightLines;
|
let lineCount = parsed.imageHeightLines;
|
||||||
let c = Math.ceil(bin.h/this.lineHeight);
|
let c = Math.ceil(bin.h/parsed.lineHeight);
|
||||||
|
|
||||||
const maxH = lineCount*this.lineHeight;
|
const maxH = lineCount*parsed.lineHeight;
|
||||||
let maxH2 = maxH;
|
let maxH2 = maxH;
|
||||||
if (this.imageFitWidth && bin.w > this.w) {
|
if (parsed.imageFitWidth && bin.w > parsed.w) {
|
||||||
maxH2 = bin.h*this.w/bin.w;
|
maxH2 = bin.h*parsed.w/bin.w;
|
||||||
c = Math.ceil(maxH2/this.lineHeight);
|
c = Math.ceil(maxH2/parsed.lineHeight);
|
||||||
}
|
}
|
||||||
lineCount = (c < lineCount ? c : lineCount);
|
lineCount = (c < lineCount ? c : lineCount);
|
||||||
|
|
||||||
@@ -747,6 +889,7 @@ export default class BookParser {
|
|||||||
paraIndex,
|
paraIndex,
|
||||||
w: imageWidth,
|
w: imageWidth,
|
||||||
h: imageHeight,
|
h: imageHeight,
|
||||||
|
num: part.image.num
|
||||||
}});
|
}});
|
||||||
lines.push(line);
|
lines.push(line);
|
||||||
line = {begin: line.end + 1, parts: []};
|
line = {begin: line.end + 1, parts: []};
|
||||||
@@ -757,19 +900,19 @@ export default class BookParser {
|
|||||||
line.last = true;
|
line.last = true;
|
||||||
line.parts.push({style, text: ' ',
|
line.parts.push({style, text: ' ',
|
||||||
image: {local: part.image.local, inline: false, id: part.image.id,
|
image: {local: part.image.local, inline: false, id: part.image.id,
|
||||||
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight}
|
imageLine: i, lineCount, paraIndex, w: imageWidth, h: imageHeight, num: part.image.num}
|
||||||
});
|
});
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (part.image.id && part.image.inline && this.showImages) {
|
if (part.image.id && part.image.inline && parsed.showImages) {
|
||||||
const bin = this.binary[part.image.id];
|
const bin = this.binary[part.image.id];
|
||||||
if (bin) {
|
if (bin) {
|
||||||
let imgH = (bin.h > this.fontSize ? this.fontSize : bin.h);
|
let imgH = (bin.h > parsed.fontSize ? parsed.fontSize : bin.h);
|
||||||
imgW += bin.w*imgH/bin.h;
|
imgW += bin.w*imgH/bin.h;
|
||||||
line.parts.push({style, text: '',
|
line.parts.push({style, text: '',
|
||||||
image: {local: part.image.local, inline: true, id: part.image.id}});
|
image: {local: part.image.local, inline: true, id: part.image.id, num: part.image.num}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,11 +1024,11 @@ export default class BookParser {
|
|||||||
|
|
||||||
//parsed.visible
|
//parsed.visible
|
||||||
if (imageInPara) {
|
if (imageInPara) {
|
||||||
parsed.visible = this.showImages;
|
parsed.visible = parsed.showImages;
|
||||||
} else {
|
} else {
|
||||||
parsed.visible = !(
|
parsed.visible = !(
|
||||||
(para.addIndex > this.addEmptyParagraphs) ||
|
(para.addIndex > parsed.addEmptyParagraphs) ||
|
||||||
(para.addIndex == 0 && this.cutEmptyParagraphs && paragraphText.trim() == '')
|
(para.addIndex == 0 && parsed.cutEmptyParagraphs && paragraphText.trim() == '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,25 @@ import _ from 'lodash';
|
|||||||
import * as utils from '../../../share/utils';
|
import * as utils from '../../../share/utils';
|
||||||
import BookParser from './BookParser';
|
import BookParser from './BookParser';
|
||||||
|
|
||||||
const maxDataSize = 300*1024*1024;//compressed bytes
|
const maxDataSize = 500*1024*1024;//compressed bytes
|
||||||
|
|
||||||
|
//локальный кэш метаданных книг, ограничение maxDataSize
|
||||||
const bmMetaStore = localForage.createInstance({
|
const bmMetaStore = localForage.createInstance({
|
||||||
name: 'bmMetaStore'
|
name: 'bmMetaStore'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//локальный кэш самих книг, ограничение maxDataSize
|
||||||
const bmDataStore = localForage.createInstance({
|
const bmDataStore = localForage.createInstance({
|
||||||
name: 'bmDataStore'
|
name: 'bmDataStore'
|
||||||
});
|
});
|
||||||
|
|
||||||
const bmRecentStore = localForage.createInstance({
|
//список недавно открытых книг
|
||||||
|
const bmRecentStoreOld = localForage.createInstance({
|
||||||
name: 'bmRecentStore'
|
name: 'bmRecentStore'
|
||||||
});
|
});
|
||||||
|
const bmRecentStoreNew = localForage.createInstance({
|
||||||
|
name: 'bmRecentStoreNew'
|
||||||
|
});
|
||||||
|
|
||||||
class BookManager {
|
class BookManager {
|
||||||
async init(settings) {
|
async init(settings) {
|
||||||
@@ -25,15 +31,80 @@ class BookManager {
|
|||||||
|
|
||||||
this.eventListeners = [];
|
this.eventListeners = [];
|
||||||
this.books = {};
|
this.books = {};
|
||||||
this.recent = {};
|
|
||||||
|
|
||||||
this.recentLast = await bmRecentStore.getItem('recent-last');
|
this.recent = {};
|
||||||
if (this.recentLast) {
|
this.saveRecent = _.debounce(() => {
|
||||||
this.recent[this.recentLast.key] = this.recentLast;
|
bmRecentStoreNew.setItem('recent', this.recent);
|
||||||
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
}, 300, {maxWait: 800});
|
||||||
if (_.isObject(meta)) {
|
|
||||||
this.books[meta.key] = meta;
|
this.saveRecentItem = _.debounce(() => {
|
||||||
|
bmRecentStoreNew.setItem('recent-item', this.recentItem);
|
||||||
|
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
|
||||||
|
bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||||
|
}, 200, {maxWait: 300});
|
||||||
|
|
||||||
|
//загрузка bmRecentStore
|
||||||
|
this.recentRev = await bmRecentStoreNew.getItem('rev') || 0;
|
||||||
|
if (this.recentRev) {
|
||||||
|
this.recent = await bmRecentStoreNew.getItem('recent');
|
||||||
|
if (!this.recent)
|
||||||
|
this.recent = {};
|
||||||
|
|
||||||
|
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||||
|
if (this.recentItem)
|
||||||
|
this.recent[this.recentItem.key] = this.recentItem;
|
||||||
|
|
||||||
|
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
|
||||||
|
if (this.recentLastKey) {
|
||||||
|
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
|
||||||
|
if (_.isObject(meta)) {
|
||||||
|
this.books[meta.key] = meta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.cleanRecentBooks();
|
||||||
|
|
||||||
|
//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||||
|
{
|
||||||
|
await this.convertFileToDiskPrefix();
|
||||||
|
if (this.recentRev > 10)
|
||||||
|
await bmRecentStoreOld.clear();
|
||||||
|
}
|
||||||
|
} else {//TODO: убрать после 06.2021, когда bmRecentStoreOld устареет
|
||||||
|
this.recentLast = await bmRecentStoreOld.getItem('recent-last');
|
||||||
|
if (this.recentLast) {
|
||||||
|
this.recent[this.recentLast.key] = this.recentLast;
|
||||||
|
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLast.key}`);
|
||||||
|
if (_.isObject(meta)) {
|
||||||
|
this.books[meta.key] = meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = null;
|
||||||
|
const len = await bmRecentStoreOld.length();
|
||||||
|
for (let i = len - 1; i >= 0; i--) {
|
||||||
|
key = await bmRecentStoreOld.key(i);
|
||||||
|
if (key) {
|
||||||
|
let r = await bmRecentStoreOld.getItem(key);
|
||||||
|
if (_.isObject(r) && r.key) {
|
||||||
|
this.recent[r.key] = r;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await bmRecentStoreOld.removeItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//размножение для дебага
|
||||||
|
/*if (key) {
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
const k = this.keyFromUrl(i.toString());
|
||||||
|
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
await bmRecentStoreNew.setItem('recent', this.recent);
|
||||||
|
this.recentRev = 1;
|
||||||
|
await bmRecentStoreNew.setItem('rev', this.recentRev);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recentChanged = true;
|
this.recentChanged = true;
|
||||||
@@ -41,9 +112,7 @@ class BookManager {
|
|||||||
this.loadStored();//no await
|
this.loadStored();//no await
|
||||||
}
|
}
|
||||||
|
|
||||||
//Долгая асинхронная загрузка из хранилища.
|
//Ленивая асинхронная загрузка bmMetaStore
|
||||||
//Хранение в отдельных записях дает относительно
|
|
||||||
//нормальное поведение при нескольких вкладках с читалкой в браузере.
|
|
||||||
async loadStored() {
|
async loadStored() {
|
||||||
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
|
||||||
await utils.sleep(2000);
|
await utils.sleep(2000);
|
||||||
@@ -70,32 +139,7 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = null;
|
|
||||||
len = await bmRecentStore.length();
|
|
||||||
for (let i = len - 1; i >= 0; i--) {
|
|
||||||
key = await bmRecentStore.key(i);
|
|
||||||
if (key) {
|
|
||||||
let r = await bmRecentStore.getItem(key);
|
|
||||||
if (_.isObject(r) && r.key) {
|
|
||||||
this.recent[r.key] = r;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await bmRecentStore.removeItem(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//размножение для дебага
|
|
||||||
/*if (key) {
|
|
||||||
for (let i = 0; i < 1000; i++) {
|
|
||||||
const k = this.keyFromUrl(i.toString());
|
|
||||||
this.recent[k] = Object.assign({}, _.cloneDeep(this.recent[key]), {key: k, touchTime: Date.now() - 1000000, url: utils.randomHexString(300)});
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
await this.cleanBooks();
|
await this.cleanBooks();
|
||||||
await this.cleanRecentBooks();
|
|
||||||
|
|
||||||
this.recentChanged = true;
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
this.emit('load-stored-finish');
|
this.emit('load-stored-finish');
|
||||||
}
|
}
|
||||||
@@ -125,7 +169,7 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deflateWithProgress(data, callback) {
|
async deflateWithProgress(data, callback) {
|
||||||
const chunkSize = 128*1024;
|
const chunkSize = 512*1024;
|
||||||
const deflator = new utils.pako.Deflate({level: 5});
|
const deflator = new utils.pako.Deflate({level: 5});
|
||||||
|
|
||||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||||
@@ -159,7 +203,7 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async inflateWithProgress(data, callback) {
|
async inflateWithProgress(data, callback) {
|
||||||
const chunkSize = 64*1024;
|
const chunkSize = 512*1024;
|
||||||
const inflator = new utils.pako.Inflate({to: 'string'});
|
const inflator = new utils.pako.Inflate({to: 'string'});
|
||||||
|
|
||||||
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
let chunkTotal = 1 + Math.floor(data.length/chunkSize);
|
||||||
@@ -238,7 +282,7 @@ class BookManager {
|
|||||||
let book = this.books[meta.key];
|
let book = this.books[meta.key];
|
||||||
|
|
||||||
if (!book && !this.loaded) {
|
if (!book && !this.loaded) {
|
||||||
book = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
book = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||||
if (book)
|
if (book)
|
||||||
this.books[meta.key] = book;
|
this.books[meta.key] = book;
|
||||||
}
|
}
|
||||||
@@ -254,7 +298,7 @@ class BookManager {
|
|||||||
result = this.books[meta.key];
|
result = this.books[meta.key];
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
result = await bmDataStore.getItem(`bmMeta-${meta.key}`);
|
result = await bmMetaStore.getItem(`bmMeta-${meta.key}`);
|
||||||
if (result)
|
if (result)
|
||||||
this.books[meta.key] = result;
|
this.books[meta.key] = result;
|
||||||
}
|
}
|
||||||
@@ -328,51 +372,71 @@ class BookManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//-- recent --------------------------------------------------------------
|
//-- recent --------------------------------------------------------------
|
||||||
|
async recentSetItem(item = null, skipCheck = false) {
|
||||||
|
const rev = await bmRecentStoreNew.getItem('rev');
|
||||||
|
if (rev != this.recentRev && !skipCheck) {
|
||||||
|
const newRecent = await bmRecentStoreNew.getItem('recent');
|
||||||
|
Object.assign(this.recent, newRecent);
|
||||||
|
this.recentItem = await bmRecentStoreNew.getItem('recent-item');
|
||||||
|
this.recentRev = rev;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prevKey = (this.recentItem ? this.recentItem.key : '');
|
||||||
|
if (item) {
|
||||||
|
this.recent[item.key] = item;
|
||||||
|
this.recentItem = item;
|
||||||
|
} else {
|
||||||
|
this.recentItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.saveRecentItem();
|
||||||
|
|
||||||
|
if (!item || prevKey != item.key) {
|
||||||
|
this.saveRecent();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.recentChanged = true;
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
this.emit('recent-changed', item.key);
|
||||||
|
} else {
|
||||||
|
this.emit('recent-changed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async recentSetLastKey(key) {
|
||||||
|
this.recentLastKey = key;
|
||||||
|
await bmRecentStoreNew.setItem('recent-last-key', this.recentLastKey);
|
||||||
|
}
|
||||||
|
|
||||||
async setRecentBook(value) {
|
async setRecentBook(value) {
|
||||||
const result = this.metaOnly(value);
|
let result = this.metaOnly(value);
|
||||||
result.touchTime = Date.now();
|
result.touchTime = Date.now();
|
||||||
result.deleted = 0;
|
result.deleted = 0;
|
||||||
|
|
||||||
if (this.recent[result.key] && this.recent[result.key].deleted) {
|
if (this.recent[result.key]) {
|
||||||
//восстановим из небытия пользовательские данные
|
result = Object.assign({}, this.recent[result.key], result);
|
||||||
if (!result.bookPos)
|
|
||||||
result.bookPos = this.recent[result.key].bookPos;
|
|
||||||
if (!result.bookPosSeen)
|
|
||||||
result.bookPosSeen = this.recent[result.key].bookPosSeen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.recent[result.key] = result;
|
await this.recentSetLastKey(result.key);
|
||||||
|
await this.recentSetItem(result);
|
||||||
await bmRecentStore.setItem(result.key, result);
|
|
||||||
|
|
||||||
this.recentLast = result;
|
|
||||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
|
||||||
|
|
||||||
this.recentChanged = true;
|
|
||||||
this.emit('recent-changed', result.key);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRecentBook(value) {
|
async getRecentBook(value) {
|
||||||
let result = this.recent[value.key];
|
return this.recent[value.key];
|
||||||
if (!result) {
|
|
||||||
result = await bmRecentStore.getItem(value.key);
|
|
||||||
if (result)
|
|
||||||
this.recent[value.key] = result;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async delRecentBook(value) {
|
async delRecentBook(value) {
|
||||||
this.recent[value.key].deleted = 1;
|
const item = this.recent[value.key];
|
||||||
await bmRecentStore.setItem(value.key, this.recent[value.key]);
|
item.deleted = 1;
|
||||||
|
|
||||||
if (this.recentLast.key == value.key) {
|
if (this.recentLastKey == value.key) {
|
||||||
this.recentLast = null;
|
await this.recentSetLastKey(null);
|
||||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.recentSetItem(item);
|
||||||
this.emit('recent-deleted', value.key);
|
this.emit('recent-deleted', value.key);
|
||||||
this.emit('recent-changed', value.key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanRecentBooks() {
|
async cleanRecentBooks() {
|
||||||
@@ -380,24 +444,49 @@ class BookManager {
|
|||||||
|
|
||||||
let isDel = false;
|
let isDel = false;
|
||||||
for (let i = 1000; i < sorted.length; i++) {
|
for (let i = 1000; i < sorted.length; i++) {
|
||||||
await bmRecentStore.removeItem(sorted[i].key);
|
|
||||||
delete this.recent[sorted[i].key];
|
delete this.recent[sorted[i].key];
|
||||||
await bmRecentStore.removeItem(sorted[i].key);
|
|
||||||
isDel = true;
|
isDel = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sortedRecentCached = null;
|
this.sortedRecentCached = null;
|
||||||
|
|
||||||
if (isDel)
|
if (isDel)
|
||||||
this.emit('recent-changed');
|
await this.recentSetItem();
|
||||||
return isDel;
|
return isDel;
|
||||||
}
|
}
|
||||||
|
|
||||||
mostRecentBook() {
|
async convertFileToDiskPrefix() {
|
||||||
if (this.recentLast) {
|
let isConverted = false;
|
||||||
return this.recentLast;
|
|
||||||
|
const newRecent = {};
|
||||||
|
for (let key of Object.keys(this.recent)) {
|
||||||
|
let newKey = key;
|
||||||
|
let newUrl = this.recent[key].url;
|
||||||
|
|
||||||
|
if (newKey.indexOf('66696c65') == 0) {
|
||||||
|
newKey = newKey.replace(/^66696c65/, '6469736b');
|
||||||
|
if (newUrl)
|
||||||
|
newUrl = newUrl.replace(/^file/, 'disk');
|
||||||
|
isConverted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
newRecent[newKey] = this.recent[key];
|
||||||
|
newRecent[newKey].key = newKey;
|
||||||
|
if (newUrl)
|
||||||
|
newRecent[newKey].url = newUrl;
|
||||||
}
|
}
|
||||||
const oldRecentLast = this.recentLast;
|
if (isConverted) {
|
||||||
|
this.recent = newRecent;
|
||||||
|
await this.recentSetItem(null, true);
|
||||||
|
}
|
||||||
|
return isConverted;
|
||||||
|
}
|
||||||
|
|
||||||
|
mostRecentBook() {
|
||||||
|
if (this.recentLastKey) {
|
||||||
|
return this.recent[this.recentLastKey];
|
||||||
|
}
|
||||||
|
const oldKey = this.recentLastKey;
|
||||||
|
|
||||||
let max = 0;
|
let max = 0;
|
||||||
let result = null;
|
let result = null;
|
||||||
@@ -408,10 +497,11 @@ class BookManager {
|
|||||||
result = book;
|
result = book;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.recentLast = result;
|
|
||||||
bmRecentStore.setItem('recent-last', this.recentLast);//no await
|
|
||||||
|
|
||||||
if (this.recentLast !== oldRecentLast)
|
const newRecentLastKey = (result ? result.key : null);
|
||||||
|
this.recentSetLastKey(newRecentLastKey);//no await
|
||||||
|
|
||||||
|
if (newRecentLastKey !== oldKey)
|
||||||
this.emit('recent-changed');
|
this.emit('recent-changed');
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -442,24 +532,12 @@ class BookManager {
|
|||||||
delete mergedRecent[i];
|
delete mergedRecent[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
//"ленивое" обновление хранилища
|
|
||||||
(async() => {
|
|
||||||
for (const rec of Object.values(mergedRecent)) {
|
|
||||||
if (rec.key) {
|
|
||||||
await bmRecentStore.setItem(rec.key, rec);
|
|
||||||
await utils.sleep(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
this.recent = mergedRecent;
|
this.recent = mergedRecent;
|
||||||
|
|
||||||
this.recentLast = null;
|
await this.recentSetLastKey(null);
|
||||||
await bmRecentStore.setItem('recent-last', this.recentLast);
|
await this.recentSetItem(null, true);
|
||||||
|
|
||||||
this.recentChanged = true;
|
|
||||||
this.emit('set-recent');
|
this.emit('set-recent');
|
||||||
this.emit('recent-changed');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener(listener) {
|
addEventListener(listener) {
|
||||||
|
|||||||
40
client/components/Reader/share/wallpaperStorage.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import localForage from 'localforage';
|
||||||
|
//import _ from 'lodash';
|
||||||
|
|
||||||
|
const wpStore = localForage.createInstance({
|
||||||
|
name: 'wallpaperStorage'
|
||||||
|
});
|
||||||
|
|
||||||
|
class WallpaperStorage {
|
||||||
|
constructor() {
|
||||||
|
this.cachedKeys = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
this.cachedKeys = await wpStore.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLength() {
|
||||||
|
return await wpStore.length();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setData(key, data) {
|
||||||
|
await wpStore.setItem(key, data);
|
||||||
|
this.cachedKeys = await wpStore.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getData(key) {
|
||||||
|
return await wpStore.getItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeData(key) {
|
||||||
|
await wpStore.removeItem(key);
|
||||||
|
this.cachedKeys = await wpStore.keys();
|
||||||
|
}
|
||||||
|
|
||||||
|
keyExists(key) {//не асинхронная
|
||||||
|
return this.cachedKeys.includes(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new WallpaperStorage();
|
||||||
@@ -1,4 +1,67 @@
|
|||||||
export const versionHistory = [
|
export const versionHistory = [
|
||||||
|
{
|
||||||
|
showUntil: '2021-02-16',
|
||||||
|
header: '0.10.0 (2021-02-09)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлен двухстраничный режим</li>
|
||||||
|
<li>в настройки добавлены все кириллические веб-шрифты от google</li>
|
||||||
|
<li>в настройки добавлена возможность загрузки пользовательских обоев (пока без синхронизации)</li>
|
||||||
|
<li>немного улучшен парсинг fb2</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-12-17',
|
||||||
|
header: '0.9.12 (2020-12-18)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлена вкладка "Изображения" в окно оглавления</li>
|
||||||
|
<li>настройки конвертирования вынесены в отдельную вкладку</li>
|
||||||
|
<li>добавлена кнопка для быстрого доступа к настройкам конвертирования</li>
|
||||||
|
<li>улучшения работы конвертеров</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-12-08',
|
||||||
|
header: '0.9.11 (2020-12-09)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>оптимизации, улучшения работы конвертеров</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-12-10',
|
||||||
|
header: '0.9.10 (2020-12-03)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>добавлена частичная поддержка формата Djvu</li>
|
||||||
|
<li>добавлена поддержка Rar-архивов</li>
|
||||||
|
<li>исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
showUntil: '2020-11-20',
|
||||||
|
header: '0.9.9 (2020-11-21)',
|
||||||
|
content:
|
||||||
|
`
|
||||||
|
<ul>
|
||||||
|
<li>оптимизации, исправления багов</li>
|
||||||
|
</ul>
|
||||||
|
`
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
showUntil: '2020-11-12',
|
showUntil: '2020-11-12',
|
||||||
header: '0.9.8 (2020-11-13)',
|
header: '0.9.8 (2020-11-13)',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog v-model="active">
|
<q-dialog v-model="active" no-route-dismiss>
|
||||||
<div class="column bg-white no-wrap">
|
<div class="column bg-white no-wrap">
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<div class="caption col row items-center q-ml-md">
|
<div class="caption col row items-center q-ml-md">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide">
|
<q-dialog ref="dialog" v-model="active" @show="onShow" @hide="onHide" no-route-dismiss>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|
||||||
<!--------------------------------------------------->
|
<!--------------------------------------------------->
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import {QSlider} from 'quasar/src/components/slider';
|
|||||||
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
import {QTabs, QTab} from 'quasar/src/components/tabs';
|
||||||
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
//import {QTabPanels, QTabPanel} from 'quasar/src/components/tab-panels';
|
||||||
import {QSeparator} from 'quasar/src/components/separator';
|
import {QSeparator} from 'quasar/src/components/separator';
|
||||||
//import {QList, QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
//import {QList} from 'quasar/src/components/item';
|
||||||
|
import {QItem, QItemSection, QItemLabel} from 'quasar/src/components/item';
|
||||||
import {QTooltip} from 'quasar/src/components/tooltip';
|
import {QTooltip} from 'quasar/src/components/tooltip';
|
||||||
import {QSpinner} from 'quasar/src/components/spinner';
|
import {QSpinner} from 'quasar/src/components/spinner';
|
||||||
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
import {QTable, QTh, QTr, QTd} from 'quasar/src/components/table';
|
||||||
@@ -49,7 +50,8 @@ const components = {
|
|||||||
QTabs, QTab,
|
QTabs, QTab,
|
||||||
//QTabPanels, QTabPanel,
|
//QTabPanels, QTabPanel,
|
||||||
QSeparator,
|
QSeparator,
|
||||||
//QList, QItem, QItemSection, QItemLabel,
|
//QList,
|
||||||
|
QItem, QItemSection, QItemLabel,
|
||||||
QTooltip,
|
QTooltip,
|
||||||
QSpinner,
|
QSpinner,
|
||||||
QTable, QTh, QTr, QTd,
|
QTable, QTh, QTr, QTd,
|
||||||
|
|||||||
22
client/share/dynamicCss.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
class DynamicCss {
|
||||||
|
constructor() {
|
||||||
|
this.cssNodes = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
replace(name, cssText) {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.type = 'text/css';
|
||||||
|
style.innerHTML = cssText;
|
||||||
|
|
||||||
|
const parent = document.getElementsByTagName('head')[0];
|
||||||
|
|
||||||
|
if (this.cssNodes[name]) {
|
||||||
|
parent.removeChild(this.cssNodes[name]);
|
||||||
|
delete this.cssNodes[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cssNodes[name] = parent.appendChild(style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DynamicCss();
|
||||||
@@ -13,6 +13,10 @@ export function sleep(ms) {
|
|||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toHex(buf) {
|
||||||
|
return Buffer.from(buf).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
export function stringToHex(str) {
|
export function stringToHex(str) {
|
||||||
return Buffer.from(str).toString('hex');
|
return Buffer.from(str).toString('hex');
|
||||||
}
|
}
|
||||||
@@ -304,3 +308,55 @@ export function userHotKeysObjectSwap(userHotKeys) {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeHtmlTags(s) {
|
||||||
|
return s.replace(/(<([^>]+)>)/ig, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeValidFilename(filename, repl = '_') {
|
||||||
|
let f = filename.replace(/[\x00\\/:*"<>|]/g, repl); // eslint-disable-line no-control-regex
|
||||||
|
f = f.trim();
|
||||||
|
while (f.length && (f[f.length - 1] == '.' || f[f.length - 1] == '_')) {
|
||||||
|
f = f.substring(0, f.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f)
|
||||||
|
return f;
|
||||||
|
else
|
||||||
|
throw new Error('Invalid filename');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBookTitle(fb2) {
|
||||||
|
fb2 = (fb2 ? fb2 : {});
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
if (fb2.author) {
|
||||||
|
const authorNames = fb2.author.map(a => _.compact([
|
||||||
|
a.lastName,
|
||||||
|
a.firstName,
|
||||||
|
a.middleName
|
||||||
|
]).join(' '));
|
||||||
|
|
||||||
|
result.author = authorNames.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fb2.sequence) {
|
||||||
|
const seqs = fb2.sequence.map(s => _.compact([
|
||||||
|
s.name,
|
||||||
|
(s.number ? `#${s.number}` : null),
|
||||||
|
]).join(' '));
|
||||||
|
|
||||||
|
result.sequence = seqs.join(', ');
|
||||||
|
if (result.sequence)
|
||||||
|
result.sequenceTitle = `(${result.sequence})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.bookTitle = _.compact([result.sequenceTitle, fb2.bookTitle]).join(' ');
|
||||||
|
|
||||||
|
result.fullTitle = _.compact([
|
||||||
|
result.author,
|
||||||
|
result.bookTitle
|
||||||
|
]).join(' - ');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -10,18 +10,7 @@ const state = {
|
|||||||
const getters = {};
|
const getters = {};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
const actions = {
|
const actions = {};
|
||||||
async loadConfig({ commit, state }) {
|
|
||||||
commit('setApiError', null, { root: true });
|
|
||||||
commit('setConfig', {});
|
|
||||||
try {
|
|
||||||
const config = await miscApi.loadConfig();
|
|
||||||
commit('setConfig', config);
|
|
||||||
} catch (e) {
|
|
||||||
commit('setApiError', e, { root: true });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// mutations
|
// mutations
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
|||||||
1
client/store/modules/fonts/fonts.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
["Alegreya","Alegreya SC","Alegreya Sans","Alegreya Sans SC","Alice","Amatic SC","Andika","Anonymous Pro","Arimo","Arsenal","Bad Script","Balsamiq Sans","Bellota","Bellota Text","Bitter","Caveat","Comfortaa","Commissioner","Cormorant","Cormorant Garamond","Cormorant Infant","Cormorant SC","Cormorant Unicase","Cousine","Cuprum","Didact Gothic","EB Garamond","El Messiri","Exo 2","Fira Code","Fira Mono","Fira Sans","Fira Sans Condensed","Fira Sans Extra Condensed","Forum","Gabriela","Hachi Maru Pop","IBM Plex Mono","IBM Plex Sans","IBM Plex Serif","Inter","Istok Web","JetBrains Mono","Jost","Jura","Kelly Slab","Kosugi","Kosugi Maru","Kurale","Ledger","Literata","Lobster","Lora","M PLUS 1p","M PLUS Rounded 1c","Manrope","Marck Script","Marmelad","Merriweather","Montserrat","Montserrat Alternates","Neucha","Noto Sans","Noto Serif","Nunito","Old Standard TT","Open Sans","Open Sans Condensed","Oranienbaum","Oswald","PT Mono","PT Sans","PT Sans Caption","PT Sans Narrow","PT Serif","PT Serif Caption","Pacifico","Pangolin","Pattaya","Philosopher","Piazzolla","Play","Playfair Display","Playfair Display SC","Podkova","Poiret One","Prata","Press Start 2P","Prosto One","Raleway","Roboto","Roboto Condensed","Roboto Mono","Roboto Slab","Rubik","Rubik Mono One","Ruda","Ruslan Display","Russo One","Sawarabi Gothic","Scada","Seymour One","Source Code Pro","Source Sans Pro","Source Serif Pro","Spectral","Spectral SC","Stalinist One","Tenor Sans","Tinos","Ubuntu","Ubuntu Condensed","Ubuntu Mono","Underdog","Viaoda Libre","Vollkorn","Vollkorn SC","Yanone Kaffeesatz","Yeseva One"]
|
||||||
13
client/store/modules/fonts/fonts2list.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const webfonts = await fs.readFile('webfonts.json');
|
||||||
|
let fonts = JSON.parse(webfonts);
|
||||||
|
|
||||||
|
fonts = fonts.items.filter(item => item.subsets.includes('cyrillic'));
|
||||||
|
fonts = fonts.map(item => item.family);
|
||||||
|
fonts.sort();
|
||||||
|
|
||||||
|
await fs.writeFile('fonts.json', JSON.stringify(fonts));
|
||||||
|
}
|
||||||
|
main();
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as utils from '../../share/utils';
|
import * as utils from '../../share/utils';
|
||||||
|
import googleFonts from './fonts/fonts.json';
|
||||||
|
|
||||||
const readerActions = {
|
const readerActions = {
|
||||||
'help': 'Вызвать cправку',
|
'help': 'Вызвать cправку',
|
||||||
@@ -12,11 +13,11 @@ const readerActions = {
|
|||||||
'setPosition': 'Установить позицию',
|
'setPosition': 'Установить позицию',
|
||||||
'search': 'Найти в тексте',
|
'search': 'Найти в тексте',
|
||||||
'copyText': 'Скопировать текст со страницы',
|
'copyText': 'Скопировать текст со страницы',
|
||||||
'splitToPara': 'Обновить с разбиением на параграфы',
|
'convOptions': 'Настроить конвертирование',
|
||||||
'refresh': 'Принудительно обновить книгу',
|
'refresh': 'Принудительно обновить книгу',
|
||||||
'offlineMode': 'Автономный режим (без интернета)',
|
'offlineMode': 'Автономный режим (без интернета)',
|
||||||
'contents': 'Оглавление/закладки',
|
'contents': 'Оглавление/закладки',
|
||||||
'libs': 'Библиотека',
|
'libs': 'Сетевая библиотека',
|
||||||
'recentBooks': 'Открыть недавние',
|
'recentBooks': 'Открыть недавние',
|
||||||
'switchToolbar': 'Показать/скрыть панель управления',
|
'switchToolbar': 'Показать/скрыть панель управления',
|
||||||
'donate': '',
|
'donate': '',
|
||||||
@@ -41,7 +42,7 @@ const toolButtons = [
|
|||||||
{name: 'setPosition', show: true},
|
{name: 'setPosition', show: true},
|
||||||
{name: 'search', show: true},
|
{name: 'search', show: true},
|
||||||
{name: 'copyText', show: false},
|
{name: 'copyText', show: false},
|
||||||
{name: 'splitToPara', show: false},
|
{name: 'convOptions', show: true},
|
||||||
{name: 'refresh', show: true},
|
{name: 'refresh', show: true},
|
||||||
{name: 'contents', show: true},
|
{name: 'contents', show: true},
|
||||||
{name: 'libs', show: true},
|
{name: 'libs', show: true},
|
||||||
@@ -61,7 +62,7 @@ const hotKeys = [
|
|||||||
{name: 'setPosition', codes: ['P']},
|
{name: 'setPosition', codes: ['P']},
|
||||||
{name: 'search', codes: ['Ctrl+F']},
|
{name: 'search', codes: ['Ctrl+F']},
|
||||||
{name: 'copyText', codes: ['Ctrl+C']},
|
{name: 'copyText', codes: ['Ctrl+C']},
|
||||||
{name: 'splitToPara', codes: ['Shift+R']},
|
{name: 'convOptions', codes: ['Ctrl+M']},
|
||||||
{name: 'refresh', codes: ['R']},
|
{name: 'refresh', codes: ['R']},
|
||||||
{name: 'contents', codes: ['C']},
|
{name: 'contents', codes: ['C']},
|
||||||
{name: 'libs', codes: ['L']},
|
{name: 'libs', codes: ['L']},
|
||||||
@@ -91,125 +92,22 @@ const fonts = [
|
|||||||
{name: 'Rubik', fontVertShift: 0},
|
{name: 'Rubik', fontVertShift: 0},
|
||||||
];
|
];
|
||||||
|
|
||||||
const webFonts = [
|
//webFonts: [{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: 0}, ...],
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya', name: 'Alegreya', fontVertShift: -5},
|
const webFonts = [];
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya+Sans', name: 'Alegreya Sans', fontVertShift: 5},
|
for (const family of googleFonts) {
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Alegreya+SC', name: 'Alegreya SC', fontVertShift: -5},
|
webFonts.push({
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Alice', name: 'Alice', fontVertShift: 5},
|
css: `https://fonts.googleapis.com/css?family=${family.replace(/\s/g, '+')}`,
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Amatic+SC', name: 'Amatic SC', fontVertShift: 0},
|
name: family,
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Andika', name: 'Andika', fontVertShift: -35},
|
fontVertShift: 0,
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Anonymous+Pro', name: 'Anonymous Pro', fontVertShift: 5},
|
});
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Arsenal', name: 'Arsenal', fontVertShift: 0},
|
}
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Bad+Script', name: 'Bad Script', fontVertShift: -30},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Caveat', name: 'Caveat', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Comfortaa', name: 'Comfortaa', fontVertShift: 10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant', name: 'Cormorant', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Garamond', name: 'Cormorant Garamond', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Infant', name: 'Cormorant Infant', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Cormorant+Unicase', name: 'Cormorant Unicase', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Cousine', name: 'Cousine', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Cuprum', name: 'Cuprum', fontVertShift: 5},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Didact+Gothic', name: 'Didact Gothic', fontVertShift: -10},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=EB+Garamond', name: 'EB Garamond', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=El+Messiri', name: 'El Messiri', fontVertShift: -5},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Mono', name: 'Fira Mono', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans', name: 'Fira Sans', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Condensed', name: 'Fira Sans Condensed', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Fira+Sans+Extra+Condensed', name: 'Fira Sans Extra Condensed', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Forum', name: 'Forum', fontVertShift: 5},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Gabriela', name: 'Gabriela', fontVertShift: 5},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Mono', name: 'IBM Plex Mono', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Sans', name: 'IBM Plex Sans', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=IBM+Plex+Serif', name: 'IBM Plex Serif', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Istok+Web', name: 'Istok Web', fontVertShift: -5},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Jura', name: 'Jura', fontVertShift: 0},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Kelly+Slab', name: 'Kelly Slab', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Kosugi', name: 'Kosugi', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Kosugi+Maru', name: 'Kosugi Maru', fontVertShift: 10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Kurale', name: 'Kurale', fontVertShift: -15},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Ledger', name: 'Ledger', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Lobster', name: 'Lobster', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Lora', name: 'Lora', fontVertShift: 0},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Marck+Script', name: 'Marck Script', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Marmelad', name: 'Marmelad', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Merriweather', name: 'Merriweather', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Montserrat', name: 'Montserrat', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Montserrat+Alternates', name: 'Montserrat Alternates', fontVertShift: 0},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Neucha', name: 'Neucha', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans', name: 'Noto Sans', fontVertShift: -10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Sans+SC', name: 'Noto Sans SC', fontVertShift: -15},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif', name: 'Noto Serif', fontVertShift: -10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Noto+Serif+TC', name: 'Noto Serif TC', fontVertShift: -15},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Old+Standard+TT', name: 'Old Standard TT', fontVertShift: 15},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300', name: 'Open Sans Condensed', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Oranienbaum', name: 'Oranienbaum', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Oswald', name: 'Oswald', fontVertShift: -20},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Pacifico', name: 'Pacifico', fontVertShift: -35},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Pangolin', name: 'Pangolin', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Pattaya', name: 'Pattaya', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Philosopher', name: 'Philosopher', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Play', name: 'Play', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display', name: 'Playfair Display', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Playfair+Display+SC', name: 'Playfair Display SC', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Podkova', name: 'Podkova', fontVertShift: 10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Poiret+One', name: 'Poiret One', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Prata', name: 'Prata', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Prosto+One', name: 'Prosto One', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=PT+Mono', name: 'PT Mono', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans', name: 'PT Sans', fontVertShift: -10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Caption', name: 'PT Sans Caption', fontVertShift: -10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=PT+Sans+Narrow', name: 'PT Sans Narrow', fontVertShift: -10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=PT+Serif', name: 'PT Serif', fontVertShift: -10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=PT+Serif+Caption', name: 'PT Serif Caption', fontVertShift: -10},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Condensed', name: 'Roboto Condensed', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Mono', name: 'Roboto Mono', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Roboto+Slab', name: 'Roboto Slab', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Ruslan+Display', name: 'Ruslan Display', fontVertShift: 20},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Russo+One', name: 'Russo One', fontVertShift: 5},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Sawarabi+Gothic', name: 'Sawarabi Gothic', fontVertShift: -15},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Scada', name: 'Scada', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Seymour+One', name: 'Seymour One', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Source+Sans+Pro', name: 'Source Sans Pro', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Spectral', name: 'Spectral', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Stalinist+One', name: 'Stalinist One', fontVertShift: 0},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Tinos', name: 'Tinos', fontVertShift: 5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Tenor+Sans', name: 'Tenor Sans', fontVertShift: 5},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Underdog', name: 'Underdog', fontVertShift: 10},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Mono', name: 'Ubuntu Mono', fontVertShift: 0},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Ubuntu+Condensed', name: 'Ubuntu Condensed', fontVertShift: -5},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Vollkorn', name: 'Vollkorn', fontVertShift: -5},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Vollkorn+SC', name: 'Vollkorn SC', fontVertShift: 0},
|
|
||||||
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Yanone+Kaffeesatz', name: 'Yanone Kaffeesatz', fontVertShift: 20},
|
|
||||||
{css: 'https://fonts.googleapis.com/css?family=Yeseva+One', name: 'Yeseva One', fontVertShift: 10},
|
|
||||||
|
|
||||||
|
|
||||||
];
|
|
||||||
|
|
||||||
//----------------------------------------------------------------------------------------------------------
|
//----------------------------------------------------------------------------------------------------------
|
||||||
const settingDefaults = {
|
const settingDefaults = {
|
||||||
textColor: '#000000',
|
textColor: '#000000',
|
||||||
backgroundColor: '#EBE2C9',
|
backgroundColor: '#ebe2c9',
|
||||||
wallpaper: '',
|
wallpaper: '',
|
||||||
|
wallpaperIgnoreStatusBar: false,
|
||||||
fontStyle: '',// 'italic'
|
fontStyle: '',// 'italic'
|
||||||
fontWeight: '',// 'bold'
|
fontWeight: '',// 'bold'
|
||||||
fontSize: 20,// px
|
fontSize: 20,// px
|
||||||
@@ -226,9 +124,22 @@ const settingDefaults = {
|
|||||||
wordWrap: true,//перенос по слогам
|
wordWrap: true,//перенос по слогам
|
||||||
keepLastToFirst: false,// перенос последней строки в первую при листании
|
keepLastToFirst: false,// перенос последней строки в первую при листании
|
||||||
|
|
||||||
|
dualPageMode: false,
|
||||||
|
dualIndentLR: 10,// px, отступ слева и справа внутри страницы в двухстраничном режиме
|
||||||
|
dualDivWidth: 2,// px, ширина разделителя
|
||||||
|
dualDivHeight: 100,// процент, высота разделителя
|
||||||
|
dualDivColorAsText: true,//цвет как у текста
|
||||||
|
dualDivColor: '#000000',
|
||||||
|
dualDivColorAlpha: 0.7,// прозрачность разделителя
|
||||||
|
dualDivStrokeFill: 1,// px, заполнение пунктира
|
||||||
|
dualDivStrokeGap: 1,// px, промежуток пунктира
|
||||||
|
dualDivShadowWidth: 0,// px, ширина тени
|
||||||
|
|
||||||
showStatusBar: true,
|
showStatusBar: true,
|
||||||
statusBarTop: false,// top, bottom
|
statusBarTop: false,// top, bottom
|
||||||
statusBarHeight: 19,// px
|
statusBarHeight: 19,// px
|
||||||
|
statusBarColorAsText: true,//цвет как у текста
|
||||||
|
statusBarColor: '#000000',
|
||||||
statusBarColorAlpha: 0.4,
|
statusBarColorAlpha: 0.4,
|
||||||
statusBarClickOpen: true,
|
statusBarClickOpen: true,
|
||||||
|
|
||||||
@@ -251,15 +162,21 @@ const settingDefaults = {
|
|||||||
compactTextPerc: 0,
|
compactTextPerc: 0,
|
||||||
imageHeightLines: 100,
|
imageHeightLines: 100,
|
||||||
imageFitWidth: true,
|
imageFitWidth: true,
|
||||||
|
enableSitesFilter: true,
|
||||||
|
splitToPara: false,
|
||||||
|
djvuQuality: 20,
|
||||||
|
pdfAsText: true,
|
||||||
|
pdfQuality: 20,
|
||||||
|
|
||||||
showServerStorageMessages: true,
|
showServerStorageMessages: true,
|
||||||
showWhatsNewDialog: true,
|
showWhatsNewDialog: true,
|
||||||
showDonationDialog2020: true,
|
showDonationDialog2020: true,
|
||||||
showLiberamaTopDialog2020: true,
|
showNeedUpdateNotify: true,
|
||||||
enableSitesFilter: true,
|
|
||||||
|
|
||||||
fontShifts: {},
|
fontShifts: {},
|
||||||
showToolButton: {},
|
showToolButton: {},
|
||||||
userHotKeys: {},
|
userHotKeys: {},
|
||||||
|
userWallpapers: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const font of fonts)
|
for (const font of fonts)
|
||||||
@@ -271,12 +188,13 @@ for (const button of toolButtons)
|
|||||||
for (const hotKey of hotKeys)
|
for (const hotKey of hotKeys)
|
||||||
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
|
settingDefaults.userHotKeys[hotKey.name] = hotKey.codes;
|
||||||
|
|
||||||
const excludeDiffHotKeys = [];
|
const diffExclude = [];
|
||||||
for (const hotKey of hotKeys)
|
for (const hotKey of hotKeys)
|
||||||
excludeDiffHotKeys.push(`userHotKeys/${hotKey.name}`);
|
diffExclude.push(`userHotKeys/${hotKey.name}`);
|
||||||
|
diffExclude.push('userWallpapers');
|
||||||
|
|
||||||
function addDefaultsToSettings(settings) {
|
function addDefaultsToSettings(settings) {
|
||||||
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: excludeDiffHotKeys});
|
const diff = utils.getObjDiff(settings, settingDefaults, {exclude: diffExclude});
|
||||||
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
|
if (!utils.isEmptyObjDiffDeep(diff, {isApplyChange: false})) {
|
||||||
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
|
return utils.applyObjDiff(settings, diff, {isApplyChange: false});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
sudo -H -u www-data /home/beta.liberama/liberama
|
sudo -H -u www-data bash -c "cd /var/www; /home/beta.liberama/liberama"
|
||||||
|
|||||||
@@ -27,15 +27,19 @@ sudo chown www-data.www-data /home/liberama
|
|||||||
|
|
||||||
### external converter `calibre`, download from https://download.calibre-ebook.com/
|
### external converter `calibre`, download from https://download.calibre-ebook.com/
|
||||||
```
|
```
|
||||||
wget "https://download.calibre-ebook.com/3.39.1/calibre-3.39.1-x86_64.txz"
|
wget "https://download.calibre-ebook.com/5.5.0/calibre-5.5.0-x86_64.txz"
|
||||||
sudo -u www-data mkdir -p /home/liberama/data/calibre
|
sudo -u www-data mkdir -p /home/liberama/data/calibre
|
||||||
sudo -u www-data tar xvf calibre-3.39.1-x86_64.txz -C /home/liberama/data/calibre
|
sudo -u www-data tar xvf calibre-5.5.0-x86_64.txz -C /home/liberama/data/calibre
|
||||||
```
|
```
|
||||||
|
|
||||||
### external converters
|
### external converters
|
||||||
```
|
```
|
||||||
|
sudo apt install rar
|
||||||
sudo apt install libreoffice
|
sudo apt install libreoffice
|
||||||
sudo apt install poppler-utils
|
sudo apt install poppler-utils
|
||||||
|
sudo apt install djvulibre-bin
|
||||||
|
sudo apt install libtiff-tools
|
||||||
|
sudo apt install graphicsmagick-imagemagick-compat
|
||||||
```
|
```
|
||||||
|
|
||||||
### nginx, server config
|
### nginx, server config
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
if ! pgrep -x "liberama" > /dev/null ; then
|
if ! pgrep -x "liberama" > /dev/null ; then
|
||||||
sudo -H -u www-data /home/liberama/liberama
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama"
|
||||||
else
|
else
|
||||||
echo "Process 'liberama' already running"
|
echo "Process 'liberama' already running"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
sudo -H -u www-data /home/liberama/liberama &
|
sudo -H -u www-data bash -c "cd /var/www; /home/liberama/liberama" & disown
|
||||||
sudo service cron start
|
sudo service cron start
|
||||||
|
|||||||
17
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.9.8",
|
"version": "0.10.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -10124,6 +10124,21 @@
|
|||||||
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
|
"integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"pidusage": {
|
||||||
|
"version": "2.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/pidusage/-/pidusage-2.0.21.tgz",
|
||||||
|
"integrity": "sha512-cv3xAQos+pugVX+BfXpHsbyz/dLzX+lr44zNMsYiGxUw+kV5sgQCIcLd1z+0vq+KyC7dJ+/ts2PsfgWfSC3WXA==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "^5.2.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"pify": {
|
"pify": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Liberama",
|
"name": "Liberama",
|
||||||
"version": "0.9.8",
|
"version": "0.10.1",
|
||||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||||
"license": "CC0-1.0",
|
"license": "CC0-1.0",
|
||||||
"repository": "bookpauk/liberama",
|
"repository": "bookpauk/liberama",
|
||||||
@@ -8,10 +8,10 @@
|
|||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nodemon --inspect --exec 'node server'",
|
"dev": "nodemon --inspect --ignore server/public --ignore server/data --ignore client --exec 'node server'",
|
||||||
"build:client": "webpack --config build/webpack.prod.config.js",
|
"build:client": "webpack --config build/webpack.prod.config.js",
|
||||||
"build:linux": "npm run build:client && node build/linux && pkg -t latest-linux-x64 -o dist/linux/liberama .",
|
"build:linux": "npm run build:client && node build/linux && pkg -t node12-linux-x64 -o dist/linux/liberama .",
|
||||||
"build:win": "npm run build:client && node build/win && pkg -t latest-win-x64 -o dist/win/liberama .",
|
"build:win": "npm run build:client && node build/win && pkg -t node12-win-x64 -o dist/win/liberama .",
|
||||||
"lint": "eslint --ext=.js,.vue client server",
|
"lint": "eslint --ext=.js,.vue client server",
|
||||||
"build:client-dev": "webpack --config build/webpack.dev.config.js",
|
"build:client-dev": "webpack --config build/webpack.dev.config.js",
|
||||||
"postinstall": "npm run build:client-dev && node build/linux"
|
"postinstall": "npm run build:client-dev && node build/linux"
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
"multer": "^1.4.2",
|
"multer": "^1.4.2",
|
||||||
"pako": "^1.0.11",
|
"pako": "^1.0.11",
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
|
"pidusage": "^2.0.21",
|
||||||
"quasar": "^1.14.3",
|
"quasar": "^1.14.3",
|
||||||
"safe-buffer": "^5.2.0",
|
"safe-buffer": "^5.2.0",
|
||||||
"sjcl": "^1.0.8",
|
"sjcl": "^1.0.8",
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ class ReaderController extends BaseController {
|
|||||||
const workerId = this.readerWorker.loadBookUrl({
|
const workerId = this.readerWorker.loadBookUrl({
|
||||||
url: request.url,
|
url: request.url,
|
||||||
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
|
enableSitesFilter: (request.hasOwnProperty('enableSitesFilter') ? request.enableSitesFilter : true),
|
||||||
skipCheck: (request.hasOwnProperty('skipCheck') ? request.skipCheck : false),
|
skipHtmlCheck: (request.hasOwnProperty('skipHtmlCheck') ? request.skipHtmlCheck : false),
|
||||||
isText: (request.hasOwnProperty('isText') ? request.isText : false),
|
isText: (request.hasOwnProperty('isText') ? request.isText : false),
|
||||||
|
uploadFileName: (request.hasOwnProperty('uploadFileName') ? request.uploadFileName : false),
|
||||||
|
djvuQuality: (request.hasOwnProperty('djvuQuality') ? request.djvuQuality : false),
|
||||||
|
pdfAsText: (request.hasOwnProperty('pdfAsText') ? request.pdfAsText : false),
|
||||||
|
pdfQuality: (request.hasOwnProperty('pdfQuality') ? request.pdfQuality : false),
|
||||||
});
|
});
|
||||||
const state = this.workerState.getState(workerId);
|
const state = this.workerState.getState(workerId);
|
||||||
return (state ? state : {});
|
return (state ? state : {});
|
||||||
|
|||||||
@@ -50,8 +50,14 @@ class WebSocketController {
|
|||||||
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
log(`WebSocket-IN: ${message.substr(0, 4000)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.lastActivity = Date.now();
|
|
||||||
req = JSON.parse(message);
|
req = JSON.parse(message);
|
||||||
|
|
||||||
|
ws.lastActivity = Date.now();
|
||||||
|
|
||||||
|
//pong for WebSocketConnection
|
||||||
|
if (req._rpo === 1)
|
||||||
|
this.send({_rok: 1}, req, ws);
|
||||||
|
|
||||||
switch (req.action) {
|
switch (req.action) {
|
||||||
case 'test':
|
case 'test':
|
||||||
await this.test(req, ws); break;
|
await this.test(req, ws); break;
|
||||||
@@ -136,8 +142,9 @@ class WebSocketController {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
i++;
|
i++;
|
||||||
if (i > 2*60*1000/refreshPause) {//2 мин ждем телодвижений воркера
|
if (i > 3*60*1000/refreshPause) {//3 мин ждем телодвижений воркера
|
||||||
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
this.send({state: 'error', error: 'Время ожидания процесса истекло'}, req, ws);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
i = (prevProgress != state.progress || prevState != state.state ? 1 : i);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ class FileDecompressor {
|
|||||||
constructor(limitFileSize = 0) {
|
constructor(limitFileSize = 0) {
|
||||||
this.detector = new FileDetector();
|
this.detector = new FileDetector();
|
||||||
this.limitFileSize = limitFileSize;
|
this.limitFileSize = limitFileSize;
|
||||||
|
|
||||||
|
this.rarPath = '/usr/bin/rar';
|
||||||
|
this.rarExists = false;
|
||||||
|
(async() => {
|
||||||
|
if (await fs.pathExists(this.rarPath))
|
||||||
|
this.rarExists = true;
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
async decompressNested(filename, outputDir) {
|
async decompressNested(filename, outputDir) {
|
||||||
@@ -30,7 +37,11 @@ class FileDecompressor {
|
|||||||
files: []
|
files: []
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!fileType || !(fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz' || fileType.ext == 'tar')) {
|
if (!fileType || !(
|
||||||
|
fileType.ext == 'zip' || fileType.ext == 'bz2' || fileType.ext == 'gz'
|
||||||
|
|| fileType.ext == 'tar' || (this.rarExists && fileType.ext == 'rar')
|
||||||
|
)
|
||||||
|
) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +105,11 @@ class FileDecompressor {
|
|||||||
async decompress(fileExt, filename, outputDir) {
|
async decompress(fileExt, filename, outputDir) {
|
||||||
let files = [];
|
let files = [];
|
||||||
|
|
||||||
|
if (fileExt == 'rar' && this.rarExists) {
|
||||||
|
files = await this.unRar(filename, outputDir);
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
switch (fileExt) {
|
switch (fileExt) {
|
||||||
case 'zip':
|
case 'zip':
|
||||||
files = await this.unZip(filename, outputDir);
|
files = await this.unZip(filename, outputDir);
|
||||||
@@ -119,17 +135,16 @@ class FileDecompressor {
|
|||||||
try {
|
try {
|
||||||
return await zip.unpack(filename, outputDir, {
|
return await zip.unpack(filename, outputDir, {
|
||||||
limitFileSize: this.limitFileSize,
|
limitFileSize: this.limitFileSize,
|
||||||
limitFileCount: 1000,
|
limitFileCount: 10000,
|
||||||
decodeEntryNameCallback: (nameRaw) => {
|
decodeEntryNameCallback: (nameRaw) => {
|
||||||
return utils.bufferRemoveZeroes(nameRaw);
|
return utils.bufferRemoveZeroes(nameRaw);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
fs.emptyDir(outputDir);
|
fs.emptyDir(outputDir);
|
||||||
return await zip.unpack(filename, outputDir, {
|
return await zip.unpack(filename, outputDir, {
|
||||||
limitFileSize: this.limitFileSize,
|
limitFileSize: this.limitFileSize,
|
||||||
limitFileCount: 1000,
|
limitFileCount: 10000,
|
||||||
decodeEntryNameCallback: (nameRaw) => {
|
decodeEntryNameCallback: (nameRaw) => {
|
||||||
nameRaw = utils.bufferRemoveZeroes(nameRaw);
|
nameRaw = utils.bufferRemoveZeroes(nameRaw);
|
||||||
const enc = textUtils.getEncodingLite(nameRaw);
|
const enc = textUtils.getEncodingLite(nameRaw);
|
||||||
@@ -156,7 +171,7 @@ class FileDecompressor {
|
|||||||
|
|
||||||
if (this.limitFileSize) {
|
if (this.limitFileSize) {
|
||||||
if ((await fs.stat(filename)).size > this.limitFileSize) {
|
if ((await fs.stat(filename)).size > this.limitFileSize) {
|
||||||
reject('Файл слишком большой');
|
reject(new Error('Файл слишком большой'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -222,7 +237,39 @@ class FileDecompressor {
|
|||||||
|
|
||||||
inputStream.pipe(stream).pipe(outputStream);
|
inputStream.pipe(stream).pipe(outputStream);
|
||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async unRar(filename, outputDir) {
|
||||||
|
try {
|
||||||
|
const args = ['x', '-p-', '-y', filename, `${outputDir}`];
|
||||||
|
const result = await utils.spawnProcess(this.rarPath, {
|
||||||
|
killAfter: 60,
|
||||||
|
args
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code == 0) {
|
||||||
|
const files = [];
|
||||||
|
await utils.findFiles(async(file) => {
|
||||||
|
const stat = await fs.stat(file);
|
||||||
|
files.push({path: path.relative(outputDir, file), size: stat.size});
|
||||||
|
}, outputDir);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const error = `${result.code}|FORLOG|, exec: ${this.rarPath}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||||
|
throw new Error(`Архиватор Rar завершился с ошибкой: ${error}`);
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
if (e.status == 'killed') {
|
||||||
|
throw new Error('Слишком долгое ожидание архиватора Rar');
|
||||||
|
} else if (e.status == 'error') {
|
||||||
|
throw new Error(e.error);
|
||||||
|
} else {
|
||||||
|
throw new Error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async gzipBuffer(buf) {
|
async gzipBuffer(buf) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class FileDetector {
|
|||||||
|
|
||||||
fromBuffer(buffer, callback) {
|
fromBuffer(buffer, callback) {
|
||||||
let result = null;
|
let result = null;
|
||||||
|
//console.log(buffer);
|
||||||
const invalidSignaturesList = this.validateSigantures();
|
const invalidSignaturesList = this.validateSigantures();
|
||||||
if (invalidSignaturesList.length) {
|
if (invalidSignaturesList.length) {
|
||||||
return callback(invalidSignaturesList);
|
return callback(invalidSignaturesList);
|
||||||
|
|||||||
@@ -676,7 +676,9 @@
|
|||||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
|
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d22312e3022" },
|
||||||
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
|
{ "type": "equal", "end": 19, "bytes": "3c3f786d6c2076657273696f6e3d27312e3027" },
|
||||||
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" }
|
{ "type": "equal", "end": 22, "bytes": "efbbbf3c3f786d6c2076657273696f6e3d27312e3027" },
|
||||||
|
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00220031002e0030002200" },
|
||||||
|
{ "type": "equal", "end": 40, "bytes": "fffe3c003f0078006d006c002000760065007200730069006f006e003d00270031002e0030002700" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class LimitedQueue {
|
|||||||
get(onPlaceChange) {
|
get(onPlaceChange) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.destroyed)
|
if (this.destroyed)
|
||||||
reject('destroyed');
|
reject(new Error('destroyed'));
|
||||||
|
|
||||||
const take = () => {
|
const take = () => {
|
||||||
if (this.freed <= 0)
|
if (this.freed <= 0)
|
||||||
@@ -73,7 +73,7 @@ class LimitedQueue {
|
|||||||
if (onPlaceChange)
|
if (onPlaceChange)
|
||||||
onPlaceChange(this.listeners.length);
|
onPlaceChange(this.listeners.length);
|
||||||
} else {
|
} else {
|
||||||
reject('Превышен размер очереди ожидания');
|
reject(new Error('Превышен размер очереди ожидания'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
1
server/core/Reader/BookConverter/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
test
|
|
||||||
@@ -5,8 +5,9 @@ const he = require('he');
|
|||||||
const LimitedQueue = require('../../LimitedQueue');
|
const LimitedQueue = require('../../LimitedQueue');
|
||||||
const textUtils = require('./textUtils');
|
const textUtils = require('./textUtils');
|
||||||
const utils = require('../../utils');
|
const utils = require('../../utils');
|
||||||
|
const xmlParser = require('../../xmlParser');
|
||||||
|
|
||||||
const queue = new LimitedQueue(2, 20, 3*60*1000);//3 минуты ожидание подвижек
|
const queue = new LimitedQueue(3, 20, 2*60*1000);//2 минуты ожидание подвижек
|
||||||
|
|
||||||
class ConvertBase {
|
class ConvertBase {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
@@ -14,7 +15,6 @@ class ConvertBase {
|
|||||||
|
|
||||||
this.calibrePath = `${config.dataDir}/calibre/ebook-convert`;
|
this.calibrePath = `${config.dataDir}/calibre/ebook-convert`;
|
||||||
this.sofficePath = '/usr/bin/soffice';
|
this.sofficePath = '/usr/bin/soffice';
|
||||||
this.pdfToHtmlPath = '/usr/bin/pdftohtml';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(data, opts) {// eslint-disable-line no-unused-vars
|
async run(data, opts) {// eslint-disable-line no-unused-vars
|
||||||
@@ -27,9 +27,6 @@ class ConvertBase {
|
|||||||
|
|
||||||
if (!await fs.pathExists(this.sofficePath))
|
if (!await fs.pathExists(this.sofficePath))
|
||||||
throw new Error('Внешний конвертер LibreOffice не найден');
|
throw new Error('Внешний конвертер LibreOffice не найден');
|
||||||
|
|
||||||
if (!await fs.pathExists(this.pdfToHtmlPath))
|
|
||||||
throw new Error('Внешний конвертер pdftohtml не найден');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execConverter(path, args, onData, abort) {
|
async execConverter(path, args, onData, abort) {
|
||||||
@@ -42,22 +39,38 @@ class ConvertBase {
|
|||||||
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
throw new Error('Слишком большая очередь конвертирования. Пожалуйста, попробуйте позже.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abort = (abort ? abort : () => false);
|
||||||
|
const myAbort = () => {
|
||||||
|
return q.abort() || abort();
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (myAbort())
|
||||||
|
throw new Error('abort');
|
||||||
|
|
||||||
const result = await utils.spawnProcess(path, {
|
const result = await utils.spawnProcess(path, {
|
||||||
killAfter: 600,
|
killAfter: 3600,//1 час
|
||||||
args,
|
args,
|
||||||
onData: (data) => {
|
onData: (data) => {
|
||||||
q.resetTimeout();
|
if (queue.freed > 0)
|
||||||
|
q.resetTimeout();
|
||||||
onData(data);
|
onData(data);
|
||||||
},
|
},
|
||||||
abort
|
//будем периодически проверять работу конвертера и если очереди нет, то разрешаем работу пинком onData
|
||||||
|
onUsage: (stats) => {
|
||||||
|
if (queue.freed > 0 && stats.cpu >= 10) {
|
||||||
|
q.resetTimeout();
|
||||||
|
onData('.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onUsageInterval: 10,
|
||||||
|
abort: myAbort
|
||||||
});
|
});
|
||||||
if (result.code != 0) {
|
if (result.code != 0) {
|
||||||
let error = result.code;
|
const error = `${result.code}|FORLOG|, exec: ${path}, args: ${args.join(' ')}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
||||||
if (this.config.branch == 'development')
|
|
||||||
error = `exec: ${path}, stdout: ${result.stdout}, stderr: ${result.stderr}`;
|
|
||||||
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
|
throw new Error(`Внешний конвертер завершился с ошибкой: ${error}`);
|
||||||
}
|
}
|
||||||
|
return result;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (e.status == 'killed') {
|
if (e.status == 'killed') {
|
||||||
throw new Error('Слишком долгое ожидание конвертера');
|
throw new Error('Слишком долгое ожидание конвертера');
|
||||||
@@ -90,62 +103,20 @@ class ConvertBase {
|
|||||||
return he.escape(he.decode(text.replace(/ /g, ' ')));
|
return he.escape(he.decode(text.replace(/ /g, ' ')));
|
||||||
}
|
}
|
||||||
|
|
||||||
formatFb2(fb2) {
|
isDataXml(data) {
|
||||||
let out = '<?xml version="1.0" encoding="utf-8"?>';
|
const str = data.slice(0, 100).toString().trim();
|
||||||
out += '<FictionBook xmlns="http://www.gribuser.ru/xml/fictionbook/2.0" xmlns:l="http://www.w3.org/1999/xlink">';
|
return (str.indexOf('<?xml version="1.0"') == 0 || str.indexOf('<?xml version=\'1.0\'') == 0 );
|
||||||
out += this.formatFb2Node(fb2);
|
|
||||||
out += '</FictionBook>';
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formatFb2Node(node, name) {
|
formatFb2(fb2) {
|
||||||
let out = '';
|
const out = xmlParser.formatXml({
|
||||||
|
FictionBook: {
|
||||||
if (Array.isArray(node)) {
|
_attrs: {xmlns: 'http://www.gribuser.ru/xml/fictionbook/2.0', 'xmlns:l': 'http://www.w3.org/1999/xlink'},
|
||||||
for (const n of node) {
|
_a: [fb2],
|
||||||
out += this.formatFb2Node(n);
|
|
||||||
}
|
}
|
||||||
} else if (typeof node == 'string') {
|
}, 'utf-8', this.repSpaces);
|
||||||
if (name)
|
|
||||||
out += `<${name}>${this.repSpaces(node)}</${name}>`;
|
|
||||||
else
|
|
||||||
out += this.repSpaces(node);
|
|
||||||
} else {
|
|
||||||
if (node._n)
|
|
||||||
name = node._n;
|
|
||||||
|
|
||||||
let attrs = '';
|
return out.replace(/<p>\s*?<\/p>/g, '<empty-line/>');
|
||||||
if (node._attrs) {
|
|
||||||
for (let attrName in node._attrs) {
|
|
||||||
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tOpen = '';
|
|
||||||
let tBody = '';
|
|
||||||
let tClose = '';
|
|
||||||
if (name)
|
|
||||||
tOpen += `<${name}${attrs}>`;
|
|
||||||
if (node.hasOwnProperty('_t'))
|
|
||||||
tBody += this.repSpaces(node._t);
|
|
||||||
|
|
||||||
for (let nodeName in node) {
|
|
||||||
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
|
|
||||||
continue;
|
|
||||||
|
|
||||||
const n = node[nodeName];
|
|
||||||
tBody += this.formatFb2Node(n, nodeName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name)
|
|
||||||
tClose += `</${name}>`;
|
|
||||||
|
|
||||||
if (attrs == '' && name == 'p' && tBody.trim() == '')
|
|
||||||
out += '<empty-line/>'
|
|
||||||
else
|
|
||||||
out += `${tOpen}${tBody}${tClose}`;
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
119
server/core/Reader/BookConverter/ConvertDjvu.js
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const utils = require('../../utils');
|
||||||
|
|
||||||
|
const ConvertJpegPng = require('./ConvertJpegPng');
|
||||||
|
|
||||||
|
class ConvertDjvu extends ConvertJpegPng {
|
||||||
|
check(data, opts) {
|
||||||
|
const {inputFiles} = opts;
|
||||||
|
|
||||||
|
return this.config.useExternalBookConverter &&
|
||||||
|
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'djvu';
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(data, opts) {
|
||||||
|
if (!this.check(data, opts))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let {inputFiles, callback, abort, djvuQuality} = opts;
|
||||||
|
|
||||||
|
djvuQuality = (djvuQuality && djvuQuality <= 100 && djvuQuality >= 10 ? djvuQuality : 20);
|
||||||
|
let jpegQuality = djvuQuality;
|
||||||
|
let tiffQuality = djvuQuality + 30;
|
||||||
|
tiffQuality = (tiffQuality < 85 ? tiffQuality : 85);
|
||||||
|
|
||||||
|
const ddjvuPath = '/usr/bin/ddjvu';
|
||||||
|
if (!await fs.pathExists(ddjvuPath))
|
||||||
|
throw new Error('Внешний конвертер ddjvu не найден');
|
||||||
|
|
||||||
|
const djvusedPath = '/usr/bin/djvused';
|
||||||
|
if (!await fs.pathExists(djvusedPath))
|
||||||
|
throw new Error('Внешний конвертер djvused не найден');
|
||||||
|
|
||||||
|
const tiffsplitPath = '/usr/bin/tiffsplit';
|
||||||
|
if (!await fs.pathExists(tiffsplitPath))
|
||||||
|
throw new Error('Внешний конвертер tiffsplitPath не найден');
|
||||||
|
|
||||||
|
const mogrifyPath = '/usr/bin/mogrify';
|
||||||
|
if (!await fs.pathExists(mogrifyPath))
|
||||||
|
throw new Error('Внешний конвертер mogrifyPath не найден');
|
||||||
|
|
||||||
|
const dir = `${inputFiles.filesDir}/`;
|
||||||
|
const baseFile = `${dir}${path.basename(inputFiles.sourceFile)}`;
|
||||||
|
const tifFile = `${baseFile}.tif`;
|
||||||
|
|
||||||
|
//конвертируем в tiff
|
||||||
|
let perc = 0;
|
||||||
|
await this.execConverter(ddjvuPath, ['-format=tiff', `-quality=${tiffQuality}`, '-verbose', inputFiles.sourceFile, tifFile], () => {
|
||||||
|
perc = (perc < 100 ? perc + 1 : 40);
|
||||||
|
callback(perc);
|
||||||
|
}, abort);
|
||||||
|
|
||||||
|
const tifFileSize = (await fs.stat(tifFile)).size;
|
||||||
|
let limitSize = 4*this.config.maxUploadFileSize;
|
||||||
|
if (tifFileSize > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| tifFileSize: ${tifFileSize} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//разбиваем на файлы
|
||||||
|
await this.execConverter(tiffsplitPath, [tifFile, dir], null, abort);
|
||||||
|
|
||||||
|
await fs.remove(tifFile);
|
||||||
|
|
||||||
|
//конвертируем в jpg
|
||||||
|
await this.execConverter(mogrifyPath, ['-quality', jpegQuality, '-scale', '2048>', '-verbose', '-format', 'jpg', `${dir}*.tif`], () => {
|
||||||
|
perc = (perc < 100 ? perc + 1 : 40);
|
||||||
|
callback(perc);
|
||||||
|
}, abort);
|
||||||
|
|
||||||
|
limitSize = 2*this.config.maxUploadFileSize;
|
||||||
|
let jpgFilesSize = 0;
|
||||||
|
//ищем изображения
|
||||||
|
let files = [];
|
||||||
|
await utils.findFiles(async(file) => {
|
||||||
|
if (path.extname(file) == '.jpg') {
|
||||||
|
jpgFilesSize += (await fs.stat(file)).size;
|
||||||
|
if (jpgFilesSize > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| jpgFilesSize: ${jpgFilesSize} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({name: file, base: path.basename(file)});
|
||||||
|
}
|
||||||
|
}, dir);
|
||||||
|
|
||||||
|
files.sort((a, b) => a.base.localeCompare(b.base));
|
||||||
|
|
||||||
|
//схема документа (outline)
|
||||||
|
const djvusedResult = await this.execConverter(djvusedPath, ['-u', '-e', 'print-outline', inputFiles.sourceFile], null, abort);
|
||||||
|
|
||||||
|
const outline = [];
|
||||||
|
const lines = djvusedResult.stdout.match(/\(\s*".*"\s*?"#\d+"/g);
|
||||||
|
if (lines) {
|
||||||
|
lines.forEach(l => {
|
||||||
|
const m = l.match(/"(.*)"\s*?"#(\d+)"/);
|
||||||
|
if (m) {
|
||||||
|
const pageNum = m[2];
|
||||||
|
let s = outline[pageNum];
|
||||||
|
if (!s)
|
||||||
|
s = m[1].trim();
|
||||||
|
else
|
||||||
|
s += `${(s[s.length - 1] != '.' ? '.' : '')} ${m[1].trim()}`;
|
||||||
|
|
||||||
|
outline[pageNum] = s;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(100);
|
||||||
|
let i = 0;
|
||||||
|
const imageFiles = files.map(f => {
|
||||||
|
i++;
|
||||||
|
let alt = (outline[i] ? outline[i] : '');
|
||||||
|
return {src: f.name, alt};
|
||||||
|
});
|
||||||
|
return await super.run(data, Object.assign({}, opts, {imageFiles}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConvertDjvu;
|
||||||
@@ -1,34 +1,71 @@
|
|||||||
const ConvertBase = require('./ConvertBase');
|
const ConvertBase = require('./ConvertBase');
|
||||||
const iconv = require('iconv-lite');
|
const iconv = require('iconv-lite');
|
||||||
|
const textUtils = require('./textUtils');
|
||||||
|
|
||||||
class ConvertFb2 extends ConvertBase {
|
class ConvertFb2 extends ConvertBase {
|
||||||
check(data, opts) {
|
check(data, opts) {
|
||||||
const {dataType} = opts;
|
const {dataType} = opts;
|
||||||
|
|
||||||
return (dataType && dataType.ext == 'xml' && data.toString().indexOf('<FictionBook') >= 0);
|
return (
|
||||||
|
( (dataType && dataType.ext == 'xml') || this.isDataXml(data) ) &&
|
||||||
|
data.toString().indexOf('<FictionBook') >= 0
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(data, opts) {
|
async run(data, opts) {
|
||||||
if (!this.check(data, opts))
|
let newData = data.slice(0, 1024);
|
||||||
|
|
||||||
|
//Корректируем кодировку для проверки, 16-битные кодировки должны стать utf-8
|
||||||
|
const encoding = textUtils.getEncoding(newData);
|
||||||
|
if (encoding.indexOf('UTF-16') == 0) {
|
||||||
|
newData = Buffer.from(iconv.decode(newData, encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Проверяем
|
||||||
|
if (!this.check(newData, opts))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return this.checkEncoding(data);
|
//Корректируем кодировку всего объема
|
||||||
|
newData = data;
|
||||||
|
if (encoding.indexOf('UTF-16') == 0) {
|
||||||
|
newData = Buffer.from(iconv.decode(newData, encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
//Корректируем пробелы, всякие файлы попадаются :(
|
||||||
|
if (newData[0] == 32) {
|
||||||
|
newData = Buffer.from(newData.toString().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
//Окончательно корректируем кодировку
|
||||||
|
return this.checkEncoding(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEncoding(data) {
|
checkEncoding(data) {
|
||||||
let result = data;
|
let result = data;
|
||||||
|
|
||||||
const left = data.indexOf('<?xml version="1.0"');
|
let q = '"';
|
||||||
|
let left = data.indexOf('<?xml version="1.0"');
|
||||||
|
if (left < 0) {
|
||||||
|
left = data.indexOf('<?xml version=\'1.0\'');
|
||||||
|
q = '\'';
|
||||||
|
}
|
||||||
|
|
||||||
if (left >= 0) {
|
if (left >= 0) {
|
||||||
const right = data.indexOf('?>', left);
|
const right = data.indexOf('?>', left);
|
||||||
if (right >= 0) {
|
if (right >= 0) {
|
||||||
const head = data.slice(left, right + 2).toString();
|
const head = data.slice(left, right + 2).toString();
|
||||||
const m = head.match(/encoding="(.*?)"/);
|
const m = head.match(/encoding=['"](.*?)['"]/);
|
||||||
if (m) {
|
if (m) {
|
||||||
let encoding = m[1].toLowerCase();
|
let encoding = m[1].toLowerCase();
|
||||||
if (encoding != 'utf-8') {
|
if (encoding != 'utf-8') {
|
||||||
result = iconv.decode(data, encoding);
|
//encoding может не соответсвовать реальной кодировке файла, поэтому:
|
||||||
result = Buffer.from(result.toString().replace(m[0], 'encoding="utf-8"'));
|
let calcEncoding = textUtils.getEncoding(data);
|
||||||
|
if (calcEncoding.indexOf('ISO-8859') >= 0) {
|
||||||
|
calcEncoding = encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = iconv.decode(data, calcEncoding);
|
||||||
|
result = Buffer.from(result.toString().replace(m[0], `encoding=${q}utf-8${q}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const fs = require('fs-extra');
|
|||||||
|
|
||||||
const ConvertHtml = require('./ConvertHtml');
|
const ConvertHtml = require('./ConvertHtml');
|
||||||
|
|
||||||
class ConvertDocX extends ConvertHtml {
|
class ConvertFb3 extends ConvertHtml {
|
||||||
async check(data, opts) {
|
async check(data, opts) {
|
||||||
const {inputFiles} = opts;
|
const {inputFiles} = opts;
|
||||||
if (this.config.useExternalBookConverter &&
|
if (this.config.useExternalBookConverter &&
|
||||||
@@ -39,13 +39,14 @@ class ConvertDocX extends ConvertHtml {
|
|||||||
const title = this.getTitle(text)
|
const title = this.getTitle(text)
|
||||||
.replace(/<\/?p>/g, '')
|
.replace(/<\/?p>/g, '')
|
||||||
;
|
;
|
||||||
text = `<title>${title}</title>` + text
|
text = `<fb2-title>${title}</fb2-title>` + text
|
||||||
.replace(/<title>/g, '<br><b>')
|
.replace(/<title>/g, '<br><b>')
|
||||||
.replace(/<\/title>/g, '</b><br>')
|
.replace(/<\/title>/g, '</b><br>')
|
||||||
.replace(/<subtitle>/g, '<br><br><subtitle>')
|
.replace(/<subtitle>/g, '<br><br><fb2-subtitle>')
|
||||||
|
.replace(/<\/subtitle>/g, '</fb2-subtitle>')
|
||||||
;
|
;
|
||||||
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true});
|
return await super.run(Buffer.from(text), {skipHtmlCheck: true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = ConvertDocX;
|
module.exports = ConvertFb3;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
const {dataType} = opts;
|
const {dataType} = opts;
|
||||||
|
|
||||||
//html?
|
//html?
|
||||||
if (dataType && (dataType.ext == 'html' || dataType.ext == 'xml'))
|
if ( ( (dataType && (dataType.ext == 'html' || dataType.ext == 'xml')) ) || this.isDataXml(data) )
|
||||||
return {isText: false};
|
return {isText: false};
|
||||||
|
|
||||||
//может это чистый текст?
|
//может это чистый текст?
|
||||||
@@ -16,7 +16,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//из буфера обмена?
|
//из буфера обмена?
|
||||||
if (data.toString().indexOf('<buffer>') == 0) {
|
if (data.slice(0, 50).toString().indexOf('<buffer>') == 0) {
|
||||||
return {isText: false};
|
return {isText: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,17 +24,14 @@ class ConvertHtml extends ConvertBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(data, opts) {
|
async run(data, opts) {
|
||||||
let isText = false;
|
let {isText = false, uploadFileName = ''} = opts;
|
||||||
if (!opts.skipCheck) {
|
if (!opts.skipHtmlCheck) {
|
||||||
const checkResult = this.check(data, opts);
|
const checkResult = this.check(data, opts);
|
||||||
if (!checkResult)
|
if (!checkResult)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
isText = checkResult.isText;
|
isText = checkResult.isText;
|
||||||
} else {
|
|
||||||
isText = opts.isText;
|
|
||||||
}
|
}
|
||||||
let {cutTitle} = opts;
|
|
||||||
|
|
||||||
let titleInfo = {};
|
let titleInfo = {};
|
||||||
let desc = {_n: 'description', 'title-info': titleInfo};
|
let desc = {_n: 'description', 'title-info': titleInfo};
|
||||||
@@ -44,12 +41,17 @@ class ConvertHtml extends ConvertBase {
|
|||||||
let fb2 = [desc, body, binary];
|
let fb2 = [desc, body, binary];
|
||||||
|
|
||||||
let title = '';
|
let title = '';
|
||||||
|
let author = '';
|
||||||
let inTitle = false;
|
let inTitle = false;
|
||||||
|
let inSectionTitle = false;
|
||||||
|
let inAuthor = false;
|
||||||
let inSubTitle = false;
|
let inSubTitle = false;
|
||||||
let inImage = false;
|
let inImage = false;
|
||||||
let image = {};
|
let image = {};
|
||||||
let bold = false;
|
let bold = false;
|
||||||
let italic = false;
|
let italic = false;
|
||||||
|
let superscript = false;
|
||||||
|
let subscript = false;
|
||||||
let begining = true;
|
let begining = true;
|
||||||
|
|
||||||
let spaceCounter = [];
|
let spaceCounter = [];
|
||||||
@@ -62,7 +64,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const growParagraph = (text) => {
|
const growParagraph = (text) => {
|
||||||
if (!pars.length)
|
if (!pars.length || pars[pars.length - 1]._n != 'p')
|
||||||
newParagraph();
|
newParagraph();
|
||||||
|
|
||||||
const l = pars.length;
|
const l = pars.length;
|
||||||
@@ -94,12 +96,16 @@ class ConvertHtml extends ConvertBase {
|
|||||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
text = this.escapeEntities(text);
|
text = this.escapeEntities(text);
|
||||||
|
|
||||||
if (!cutCounter && !(cutTitle && inTitle)) {
|
if (!(cutCounter || inTitle || inSectionTitle || inSubTitle)) {
|
||||||
let tOpen = '';
|
let tOpen = '';
|
||||||
tOpen += (inSubTitle ? '<subtitle>' : '');
|
tOpen += (inSubTitle ? '<subtitle>' : '');
|
||||||
tOpen += (bold ? '<strong>' : '');
|
tOpen += (bold ? '<strong>' : '');
|
||||||
tOpen += (italic ? '<emphasis>' : '');
|
tOpen += (italic ? '<emphasis>' : '');
|
||||||
|
tOpen += (superscript ? '<sup>' : '');
|
||||||
|
tOpen += (subscript ? '<sub>' : '');
|
||||||
let tClose = ''
|
let tClose = ''
|
||||||
|
tClose += (subscript ? '</sub>' : '');
|
||||||
|
tClose += (superscript ? '</sup>' : '');
|
||||||
tClose += (italic ? '</emphasis>' : '');
|
tClose += (italic ? '</emphasis>' : '');
|
||||||
tClose += (bold ? '</strong>' : '');
|
tClose += (bold ? '</strong>' : '');
|
||||||
tClose += (inSubTitle ? '</subtitle>' : '');
|
tClose += (inSubTitle ? '</subtitle>' : '');
|
||||||
@@ -110,12 +116,22 @@ class ConvertHtml extends ConvertBase {
|
|||||||
if (inTitle && !title)
|
if (inTitle && !title)
|
||||||
title = text;
|
title = text;
|
||||||
|
|
||||||
|
if (inAuthor && !author)
|
||||||
|
author = text;
|
||||||
|
|
||||||
|
if (inSectionTitle) {
|
||||||
|
pars.unshift({_n: 'title', _t: text});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inSubTitle) {
|
||||||
|
pars.push({_n: 'subtitle', _t: text});
|
||||||
|
}
|
||||||
|
|
||||||
if (inImage) {
|
if (inImage) {
|
||||||
image._t = text;
|
image._t = text;
|
||||||
binary.push(image);
|
binary.push(image);
|
||||||
|
|
||||||
pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''});
|
pars.push({_n: 'image', _attrs: {'l:href': '#' + image._attrs.id}, _t: ''});
|
||||||
newParagraph();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
@@ -140,15 +156,27 @@ class ConvertHtml extends ConvertBase {
|
|||||||
bold = true;
|
bold = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tag == 'sup')
|
||||||
|
superscript = true;
|
||||||
|
|
||||||
|
if (tag == 'sub')
|
||||||
|
subscript = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'title' || tag == 'cut-title') {
|
if (tag == 'title' || tag == 'fb2-title') {
|
||||||
inTitle = true;
|
inTitle = true;
|
||||||
if (tag == 'cut-title')
|
|
||||||
cutTitle = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'subtitle') {
|
if (tag == 'fb2-author') {
|
||||||
|
inAuthor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'fb2-section-title') {
|
||||||
|
inSectionTitle = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'fb2-subtitle') {
|
||||||
inSubTitle = true;
|
inSubTitle = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,12 +207,26 @@ class ConvertHtml extends ConvertBase {
|
|||||||
bold = false;
|
bold = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tag == 'sup')
|
||||||
|
superscript = false;
|
||||||
|
|
||||||
|
if (tag == 'sub')
|
||||||
|
subscript = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == 'title' || tag == 'cut-title')
|
if (tag == 'title' || tag == 'fb2-title')
|
||||||
inTitle = false;
|
inTitle = false;
|
||||||
|
|
||||||
if (tag == 'subtitle')
|
if (tag == 'fb2-author') {
|
||||||
|
inAuthor = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'fb2-section-title') {
|
||||||
|
inSectionTitle = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'fb2-subtitle')
|
||||||
inSubTitle = false;
|
inSubTitle = false;
|
||||||
|
|
||||||
if (tag == 'fb2-image')
|
if (tag == 'fb2-image')
|
||||||
@@ -195,10 +237,20 @@ class ConvertHtml extends ConvertBase {
|
|||||||
|
|
||||||
sax.parseSync(buf, {
|
sax.parseSync(buf, {
|
||||||
onStartNode, onEndNode, onTextNode,
|
onStartNode, onEndNode, onTextNode,
|
||||||
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image'])
|
innerCut: new Set(['head', 'script', 'style', 'binary', 'fb2-image', 'fb2-title', 'fb2-author'])
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!title)
|
||||||
|
title = uploadFileName;
|
||||||
|
|
||||||
titleInfo['book-title'] = title;
|
titleInfo['book-title'] = title;
|
||||||
|
if (author)
|
||||||
|
titleInfo.author = {'last-name': author};
|
||||||
|
|
||||||
|
body.section._a[0] = pars;
|
||||||
|
|
||||||
|
//console.log(JSON.stringify(fb2, null, 2));
|
||||||
|
|
||||||
//подозрение на чистый текст, надо разбить на параграфы
|
//подозрение на чистый текст, надо разбить на параграфы
|
||||||
if (isText || (buf.length > 30*1024 && pars.length < buf.length/2000)) {
|
if (isText || (buf.length > 30*1024 && pars.length < buf.length/2000)) {
|
||||||
let total = 0;
|
let total = 0;
|
||||||
@@ -218,7 +270,7 @@ class ConvertHtml extends ConvertBase {
|
|||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
//если разброс не слишком большой, выделяем параграфы
|
//если разброс не слишком большой, выделяем параграфы
|
||||||
if (d < 10 && spaceCounter.length) {
|
if (d < 20 && spaceCounter.length) {
|
||||||
total /= 20;
|
total /= 20;
|
||||||
i = spaceCounter.length - 1;
|
i = spaceCounter.length - 1;
|
||||||
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
|
while (i > 0 && (!spaceCounter[i] || spaceCounter[i] < total)) i--;
|
||||||
@@ -228,56 +280,49 @@ class ConvertHtml extends ConvertBase {
|
|||||||
if (parIndent > 2) parIndent--;
|
if (parIndent > 2) parIndent--;
|
||||||
|
|
||||||
let newPars = [];
|
let newPars = [];
|
||||||
|
let curPar = {};
|
||||||
const newPar = () => {
|
const newPar = () => {
|
||||||
newPars.push({_n: 'p', _t: ''});
|
curPar = {_n: 'p', _t: ''};
|
||||||
|
newPars.push(curPar);
|
||||||
};
|
};
|
||||||
|
|
||||||
const growPar = (text) => {
|
|
||||||
if (!newPars.length)
|
|
||||||
newPar();
|
|
||||||
|
|
||||||
const l = newPars.length;
|
|
||||||
newPars[l - 1]._t += text;
|
|
||||||
}
|
|
||||||
|
|
||||||
i = 0;
|
|
||||||
for (const par of pars) {
|
for (const par of pars) {
|
||||||
if (par._n != 'p') {
|
if (par._n != 'p') {
|
||||||
newPars.push(par);
|
newPars.push(par);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i > 0)
|
newPar();
|
||||||
newPar();
|
|
||||||
i++;
|
|
||||||
|
|
||||||
let j = 0;
|
|
||||||
const lines = par._t.split('\n');
|
const lines = par._t.split('\n');
|
||||||
for (let line of lines) {
|
for (let j = 0; j < lines.length; j++) {
|
||||||
line = repCrLfTab(line);
|
const line = repCrLfTab(lines[j]);
|
||||||
|
|
||||||
let l = 0;
|
let l = 0;
|
||||||
while (l < line.length && line[l] == ' ') {
|
while (l < line.length && line[l] == ' ') {
|
||||||
l++;
|
l++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (l >= parIndent || line == '') {
|
if (j > 0 &&
|
||||||
if (j > 0)
|
(l >= parIndent ||
|
||||||
newPar();
|
(j < lines.length - 1 && line == '')
|
||||||
j++;
|
)
|
||||||
|
) {
|
||||||
|
newPar();
|
||||||
}
|
}
|
||||||
growPar(line.trim() + ' ');
|
|
||||||
|
curPar._t += line.trim() + ' ';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.section._a[0] = newPars;
|
body.section._a[0] = newPars;
|
||||||
} else {
|
|
||||||
body.section._a[0] = pars;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги
|
//убираем лишнее, делаем валидный fb2, т.к. в рез-те разбиения на параграфы бьются теги
|
||||||
bold = false;
|
bold = false;
|
||||||
italic = false;
|
italic = false;
|
||||||
|
superscript = false;
|
||||||
|
subscript = false;
|
||||||
inSubTitle = false;
|
inSubTitle = false;
|
||||||
pars = body.section._a[0];
|
pars = body.section._a[0];
|
||||||
for (let i = 0; i < pars.length; i++) {
|
for (let i = 0; i < pars.length; i++) {
|
||||||
@@ -297,7 +342,11 @@ class ConvertHtml extends ConvertBase {
|
|||||||
tOpen += (inSubTitle ? '<subtitle>' : '');
|
tOpen += (inSubTitle ? '<subtitle>' : '');
|
||||||
tOpen += (bold ? '<strong>' : '');
|
tOpen += (bold ? '<strong>' : '');
|
||||||
tOpen += (italic ? '<emphasis>' : '');
|
tOpen += (italic ? '<emphasis>' : '');
|
||||||
|
tOpen += (superscript ? '<sup>' : '');
|
||||||
|
tOpen += (subscript ? '<sub>' : '');
|
||||||
let tClose = ''
|
let tClose = ''
|
||||||
|
tClose += (subscript ? '</sub>' : '');
|
||||||
|
tClose += (superscript ? '</sup>' : '');
|
||||||
tClose += (italic ? '</emphasis>' : '');
|
tClose += (italic ? '</emphasis>' : '');
|
||||||
tClose += (bold ? '</strong>' : '');
|
tClose += (bold ? '</strong>' : '');
|
||||||
tClose += (inSubTitle ? '</subtitle>' : '');
|
tClose += (inSubTitle ? '</subtitle>' : '');
|
||||||
@@ -313,6 +362,10 @@ class ConvertHtml extends ConvertBase {
|
|||||||
bold = true;
|
bold = true;
|
||||||
if (tag == 'emphasis')
|
if (tag == 'emphasis')
|
||||||
italic = true;
|
italic = true;
|
||||||
|
if (tag == 'sup')
|
||||||
|
superscript = true;
|
||||||
|
if (tag == 'sub')
|
||||||
|
subscript = true;
|
||||||
if (tag == 'subtitle')
|
if (tag == 'subtitle')
|
||||||
inSubTitle = true;
|
inSubTitle = true;
|
||||||
}
|
}
|
||||||
@@ -322,6 +375,10 @@ class ConvertHtml extends ConvertBase {
|
|||||||
bold = false;
|
bold = false;
|
||||||
if (tag == 'emphasis')
|
if (tag == 'emphasis')
|
||||||
italic = false;
|
italic = false;
|
||||||
|
if (tag == 'sup')
|
||||||
|
superscript = false;
|
||||||
|
if (tag == 'sub')
|
||||||
|
subscript = false;
|
||||||
if (tag == 'subtitle')
|
if (tag == 'subtitle')
|
||||||
inSubTitle = false;
|
inSubTitle = false;
|
||||||
}
|
}
|
||||||
|
|||||||
100
server/core/Reader/BookConverter/ConvertJpegPng.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
//const utils = require('../../utils');
|
||||||
|
|
||||||
|
const ConvertBase = require('./ConvertBase');
|
||||||
|
|
||||||
|
class ConvertJpegPng extends ConvertBase {
|
||||||
|
check(data, opts) {
|
||||||
|
const {inputFiles} = opts;
|
||||||
|
|
||||||
|
return this.config.useExternalBookConverter &&
|
||||||
|
inputFiles.sourceFileType &&
|
||||||
|
(inputFiles.sourceFileType.ext == 'jpg' || inputFiles.sourceFileType.ext == 'png' );
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(data, opts) {
|
||||||
|
const {inputFiles, uploadFileName, imageFiles} = opts;
|
||||||
|
|
||||||
|
if (!imageFiles) {
|
||||||
|
if (!this.check(data, opts))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let files = [];
|
||||||
|
if (imageFiles) {
|
||||||
|
files = imageFiles;
|
||||||
|
} else {
|
||||||
|
const imageFile = `${inputFiles.filesDir}/${path.basename(inputFiles.sourceFile)}.${inputFiles.sourceFileType.ext}`;
|
||||||
|
await fs.copy(inputFiles.sourceFile, imageFile);
|
||||||
|
files.push({src: imageFile});
|
||||||
|
}
|
||||||
|
|
||||||
|
//читаем изображения
|
||||||
|
const limitSize = 2*this.config.maxUploadFileSize;
|
||||||
|
let imagesSize = 0;
|
||||||
|
|
||||||
|
const loadImage = async(image) => {
|
||||||
|
const src = path.parse(image.src);
|
||||||
|
let type = 'unknown';
|
||||||
|
switch (src.ext) {
|
||||||
|
case '.jpg': type = 'image/jpeg'; break;
|
||||||
|
case '.png': type = 'image/png'; break;
|
||||||
|
}
|
||||||
|
if (type != 'unknown') {
|
||||||
|
image.data = (await fs.readFile(image.src)).toString('base64');
|
||||||
|
image.type = type;
|
||||||
|
image.name = src.base;
|
||||||
|
|
||||||
|
imagesSize += image.data.length;
|
||||||
|
if (imagesSize > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| imagesSize: ${imagesSize} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let images = [];
|
||||||
|
let loading = [];
|
||||||
|
files.forEach(img => {
|
||||||
|
images.push(img);
|
||||||
|
loading.push(loadImage(img));
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(loading);
|
||||||
|
|
||||||
|
//формируем fb2
|
||||||
|
let titleInfo = {};
|
||||||
|
let desc = {_n: 'description', 'title-info': titleInfo};
|
||||||
|
let pars = [];
|
||||||
|
let body = {_n: 'body', section: {_a: [pars]}};
|
||||||
|
let binary = [];
|
||||||
|
let fb2 = [desc, body, binary];
|
||||||
|
|
||||||
|
let title = '';
|
||||||
|
if (uploadFileName)
|
||||||
|
title = uploadFileName;
|
||||||
|
|
||||||
|
titleInfo['book-title'] = title;
|
||||||
|
|
||||||
|
for (const image of images) {
|
||||||
|
if (image.type) {
|
||||||
|
const img = {_n: 'binary', _attrs: {id: image.name, 'content-type': image.type}, _t: image.data};
|
||||||
|
binary.push(img);
|
||||||
|
|
||||||
|
const attrs = {'l:href': `#${image.name}`};
|
||||||
|
if (image.alt) {
|
||||||
|
image.alt = (image.alt.length > 256 ? image.alt.substring(0, 256) : image.alt);
|
||||||
|
attrs.alt = image.alt;
|
||||||
|
}
|
||||||
|
|
||||||
|
pars.push({_n: 'p', _t: ''});
|
||||||
|
pars.push({_n: 'image', _attrs: attrs});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pars.push({_n: 'p', _t: ''});
|
||||||
|
|
||||||
|
return this.formatFb2(fb2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConvertJpegPng;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//const _ = require('lodash');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
@@ -14,17 +15,24 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run(notUsed, opts) {
|
async run(notUsed, opts) {
|
||||||
if (!this.check(notUsed, opts))
|
if (!opts.pdfAsText || !this.check(notUsed, opts))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
await this.checkExternalConverterPresent();
|
await this.checkExternalConverterPresent();
|
||||||
|
|
||||||
const {inputFiles, callback, abort} = opts;
|
const {inputFiles, callback, abort, uploadFileName} = opts;
|
||||||
|
|
||||||
const outFile = `${inputFiles.filesDir}/${utils.randomHexString(10)}.xml`;
|
const inpFile = inputFiles.sourceFile;
|
||||||
|
const outBasename = `${inputFiles.filesDir}/${utils.randomHexString(10)}`;
|
||||||
|
const outFile = `${outBasename}.xml`;
|
||||||
|
|
||||||
|
const pdftohtmlPath = '/usr/bin/pdftohtml';
|
||||||
|
if (!await fs.pathExists(pdftohtmlPath))
|
||||||
|
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||||
|
|
||||||
//конвертируем в xml
|
//конвертируем в xml
|
||||||
let perc = 0;
|
let perc = 0;
|
||||||
await this.execConverter(this.pdfToHtmlPath, ['-c', '-s', '-xml', inputFiles.sourceFile, outFile], () => {
|
await this.execConverter(pdftohtmlPath, ['-nodrm', '-c', '-s', '-xml', inpFile, outFile], () => {
|
||||||
perc = (perc < 80 ? perc + 10 : 40);
|
perc = (perc < 80 ? perc + 10 : 40);
|
||||||
callback(perc);
|
callback(perc);
|
||||||
}, abort);
|
}, abort);
|
||||||
@@ -33,17 +41,24 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
const data = await fs.readFile(outFile);
|
const data = await fs.readFile(outFile);
|
||||||
callback(90);
|
callback(90);
|
||||||
|
|
||||||
|
await utils.sleep(100);
|
||||||
|
|
||||||
//парсим xml
|
//парсим xml
|
||||||
let lines = [];
|
let lines = [];
|
||||||
|
let pagelines = [];
|
||||||
|
let line = {text: ''};
|
||||||
|
let page = {};
|
||||||
|
let fonts = {};
|
||||||
|
let sectionTitleFound = false;
|
||||||
|
|
||||||
let images = [];
|
let images = [];
|
||||||
let loading = [];
|
let loading = [];
|
||||||
|
|
||||||
let inText = false;
|
let inText = false;
|
||||||
let bold = false;
|
let bold = false;
|
||||||
let italic = false;
|
let italic = false;
|
||||||
let title = '';
|
|
||||||
let prevTop = 0;
|
|
||||||
let i = -1;
|
let i = -1;
|
||||||
let titleCount = 0;
|
|
||||||
|
|
||||||
const loadImage = async(image) => {
|
const loadImage = async(image) => {
|
||||||
const src = path.parse(image.src);
|
const src = path.parse(image.src);
|
||||||
@@ -57,7 +72,7 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
image.type = type;
|
image.type = type;
|
||||||
image.name = src.base;
|
image.name = src.base;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const putImage = (curTop) => {
|
const putImage = (curTop) => {
|
||||||
if (!isNaN(curTop) && images.length) {
|
if (!isNaN(curTop) && images.length) {
|
||||||
@@ -67,7 +82,72 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
images.shift();
|
images.shift();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const isTextBold = (text) => {
|
||||||
|
const m = text.trim().match(/^<b>(.*)<\/b>$/);
|
||||||
|
return m && !m[1].match(/<b>|<\/b>|<i>|<\/i>/g);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTextEmpty = (text) => {
|
||||||
|
return text.replace(/<b>|<\/b>|<i>|<\/i>/g, '').trim() == '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const putPageLines = () => {
|
||||||
|
pagelines.sort((a, b) => (Math.abs(a.top - b.top) > 3 ? a.top - b.top : 0)*10000 + (a.left - b.left))
|
||||||
|
|
||||||
|
//объединяем в одну строку равные по высоте
|
||||||
|
const pl = [];
|
||||||
|
let pt = 0;
|
||||||
|
let j = -1;
|
||||||
|
pagelines.forEach(line => {
|
||||||
|
if (isTextEmpty(line.text))
|
||||||
|
return;
|
||||||
|
|
||||||
|
//проверим, возможно это заголовок
|
||||||
|
if (line.fontId && line.pageWidth) {
|
||||||
|
const centerLeft = (line.pageWidth - line.width)/2;
|
||||||
|
if (isTextBold(line.text) && Math.abs(centerLeft - line.left) < 10) {
|
||||||
|
if (!sectionTitleFound) {
|
||||||
|
line.isSectionTitle = true;
|
||||||
|
sectionTitleFound = true;
|
||||||
|
} else {
|
||||||
|
line.isSubtitle = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//объединяем
|
||||||
|
if (pt == 0 || Math.abs(pt - line.top) > 3) {
|
||||||
|
j++;
|
||||||
|
pl[j] = line;
|
||||||
|
} else {
|
||||||
|
pl[j].text += ` ${line.text}`;
|
||||||
|
}
|
||||||
|
pt = line.top;
|
||||||
|
});
|
||||||
|
|
||||||
|
//заполняем lines
|
||||||
|
const lastIndex = i;
|
||||||
|
pl.forEach(line => {
|
||||||
|
putImage(line.top);
|
||||||
|
|
||||||
|
//добавим пустую строку, если надо
|
||||||
|
const prevLine = (i > lastIndex ? lines[i] : {fonts: [], top: 0});
|
||||||
|
if (prevLine && !prevLine.isImage) {
|
||||||
|
const f = (prevLine.fontId ? fonts[prevLine.fontId] : (line.fontId ? fonts[line.fontId] : null));
|
||||||
|
if (f && f.fontSize && !line.isImage && line.top - prevLine.top > f.fontSize * 1.8) {
|
||||||
|
i++;
|
||||||
|
lines[i] = {text: '<br>'};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
lines[i] = line;
|
||||||
|
});
|
||||||
|
pagelines = [];
|
||||||
|
putImage(100000);
|
||||||
|
};
|
||||||
|
|
||||||
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
if (!cutCounter && inText) {
|
if (!cutCounter && inText) {
|
||||||
@@ -76,67 +156,80 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
let tClose = (italic ? '</i>' : '');
|
let tClose = (italic ? '</i>' : '');
|
||||||
tClose += (bold ? '</b>' : '');
|
tClose += (bold ? '</b>' : '');
|
||||||
|
|
||||||
lines[i].text += `${tOpen}${text}${tClose} `;
|
line.text += ` ${tOpen}${text}${tClose}`;
|
||||||
if (titleCount < 2 && text.trim() != '') {
|
|
||||||
title += text + (titleCount ? '' : ' - ');
|
|
||||||
titleCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
if (!cutCounter) {
|
if (inText) {
|
||||||
if (inText) {
|
switch (tag) {
|
||||||
switch (tag) {
|
case 'i':
|
||||||
case 'i':
|
italic = true;
|
||||||
italic = true;
|
break;
|
||||||
break;
|
case 'b':
|
||||||
case 'b':
|
bold = true;
|
||||||
bold = true;
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tag == 'text' && !inText) {
|
if (tag == 'page') {
|
||||||
let attrs = sax.getAttrsSync(tail);
|
const attrs = sax.getAttrsSync(tail);
|
||||||
const line = {
|
page = {
|
||||||
text: '',
|
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
|
||||||
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10),
|
};
|
||||||
left: parseInt((attrs.left && attrs.left.value ? attrs.left.value : null), 10),
|
|
||||||
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
|
putPageLines();
|
||||||
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
|
}
|
||||||
|
|
||||||
|
if (tag == 'fontspec') {
|
||||||
|
const attrs = sax.getAttrsSync(tail);
|
||||||
|
const fontId = (attrs.id && attrs.id.value ? attrs.id.value : '');
|
||||||
|
const fontSize = (attrs.size && attrs.size.value ? attrs.size.value : '');
|
||||||
|
|
||||||
|
if (fontId) {
|
||||||
|
fonts[fontId] = {fontSize};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'text' && !inText) {
|
||||||
|
const attrs = sax.getAttrsSync(tail);
|
||||||
|
line = {
|
||||||
|
text: '',
|
||||||
|
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10),
|
||||||
|
left: parseInt((attrs.left && attrs.left.value ? attrs.left.value : null), 10),
|
||||||
|
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10),
|
||||||
|
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10),
|
||||||
|
isSectionTitle: false,
|
||||||
|
isSubtitle: false,
|
||||||
|
pageWidth: page.width,
|
||||||
|
fontId: (attrs.font && attrs.font.value ? attrs.font.value : ''),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (line.width != 0 || line.height != 0) {
|
||||||
|
inText = true;
|
||||||
|
pagelines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == 'image') {
|
||||||
|
const attrs = sax.getAttrsSync(tail);
|
||||||
|
let src = (attrs.src && attrs.src.value ? attrs.src.value : '');
|
||||||
|
if (src) {
|
||||||
|
const image = {
|
||||||
|
isImage: true,
|
||||||
|
src,
|
||||||
|
data: '',
|
||||||
|
type: '',
|
||||||
|
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0,
|
||||||
|
left: parseInt((attrs.left && attrs.left.value ? attrs.left.value : null), 10) || 0,
|
||||||
|
width: parseInt((attrs.width && attrs.width.value ? attrs.width.value : null), 10) || 0,
|
||||||
|
height: parseInt((attrs.height && attrs.height.value ? attrs.height.value : null), 10) || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (line.width != 0 || line.height != 0) {
|
loading.push(loadImage(image));
|
||||||
inText = true;
|
images.push(image);
|
||||||
if (isNaN(line.top) || isNaN(prevTop) || (Math.abs(prevTop - line.top) > 3)) {
|
images.sort((a, b) => (a.top - b.top)*10000 + (a.left - b.left));
|
||||||
putImage(line.top);
|
|
||||||
i++;
|
|
||||||
lines[i] = line;
|
|
||||||
}
|
|
||||||
prevTop = line.top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag == 'image') {
|
|
||||||
const attrs = sax.getAttrsSync(tail);
|
|
||||||
const src = (attrs.src && attrs.src.value ? attrs.src.value : '');
|
|
||||||
if (src) {
|
|
||||||
const image = {
|
|
||||||
isImage: true,
|
|
||||||
src,
|
|
||||||
data: '',
|
|
||||||
type: '',
|
|
||||||
top: parseInt((attrs.top && attrs.top.value ? attrs.top.value : null), 10) || 0,
|
|
||||||
};
|
|
||||||
loading.push(loadImage(image));
|
|
||||||
images.push(image);
|
|
||||||
images.sort((a, b) => a.top - b.top)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tag == 'page') {
|
|
||||||
putImage(100000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -162,9 +255,10 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
onStartNode, onEndNode, onTextNode
|
onStartNode, onEndNode, onTextNode
|
||||||
});
|
});
|
||||||
|
|
||||||
putImage(100000);
|
putPageLines();
|
||||||
|
|
||||||
await Promise.all(loading);
|
await Promise.all(loading);
|
||||||
|
await utils.sleep(100);
|
||||||
|
|
||||||
//найдем параграфы и отступы
|
//найдем параграфы и отступы
|
||||||
const indents = [];
|
const indents = [];
|
||||||
@@ -185,16 +279,49 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
}
|
}
|
||||||
indents[0] = 0;
|
indents[0] = 0;
|
||||||
|
|
||||||
|
//author & title
|
||||||
|
let {author, title} = await this.getPdfTitleAndAuthor(inpFile);
|
||||||
|
|
||||||
|
if (!title && uploadFileName)
|
||||||
|
title = uploadFileName;
|
||||||
|
|
||||||
|
//console.log(JSON.stringify(lines, null, 2));
|
||||||
//формируем текст
|
//формируем текст
|
||||||
let text = `<title>${title}</title>`;
|
const limitSize = 2*this.config.maxUploadFileSize;
|
||||||
|
let text = '';
|
||||||
|
if (title)
|
||||||
|
text += `<fb2-title>${title}</fb2-title>`;
|
||||||
|
if (author)
|
||||||
|
text += `<fb2-author>${author}</fb2-author>`;
|
||||||
|
|
||||||
let concat = '';
|
let concat = '';
|
||||||
let sp = '';
|
let sp = '';
|
||||||
|
let firstLine = true;
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
if (text.length > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| text.length: ${text.length} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
if (line.isImage) {
|
if (line.isImage) {
|
||||||
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
|
text += `<fb2-image type="${line.type}" name="${line.name}">${line.data}</fb2-image>`;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (line.isSectionTitle) {
|
||||||
|
if (firstLine)
|
||||||
|
text += `<fb2-section-title>${line.text.trim()}</fb2-section-title>`;
|
||||||
|
else
|
||||||
|
text += `<fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
firstLine = false;
|
||||||
|
|
||||||
|
if (line.isSubtitle) {
|
||||||
|
text += `<br><fb2-subtitle>${line.text.trim()}</fb2-subtitle>`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (concat == '') {
|
if (concat == '') {
|
||||||
const left = line.left || 0;
|
const left = line.left || 0;
|
||||||
sp = ' '.repeat(indents[left]);
|
sp = ' '.repeat(indents[left]);
|
||||||
@@ -212,8 +339,36 @@ class ConvertPdf extends ConvertHtml {
|
|||||||
if (concat)
|
if (concat)
|
||||||
text += sp + concat + "\n";
|
text += sp + concat + "\n";
|
||||||
|
|
||||||
return await super.run(Buffer.from(text), {skipCheck: true, isText: true, cutTitle: true});
|
//console.log(text);
|
||||||
|
await utils.sleep(100);
|
||||||
|
return await super.run(Buffer.from(text), {skipHtmlCheck: true, isText: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPdfTitleAndAuthor(pdfFile) {
|
||||||
|
const result = {author: '', title: ''};
|
||||||
|
|
||||||
|
const pdfinfoPath = '/usr/bin/pdfinfo';
|
||||||
|
|
||||||
|
if (!await fs.pathExists(pdfinfoPath))
|
||||||
|
throw new Error('Внешний конвертер pdfinfo не найден');
|
||||||
|
|
||||||
|
const execResult = await this.execConverter(pdfinfoPath, [pdfFile]);
|
||||||
|
|
||||||
|
const titlePrefix = 'Title:';
|
||||||
|
const authorPrefix = 'Author:';
|
||||||
|
|
||||||
|
const stdout = execResult.stdout.split("\n");
|
||||||
|
stdout.forEach(line => {
|
||||||
|
if (line.indexOf(titlePrefix) == 0)
|
||||||
|
result.title = line.substring(titlePrefix.length).trim();
|
||||||
|
|
||||||
|
if (line.indexOf(authorPrefix) == 0)
|
||||||
|
result.author = line.substring(authorPrefix.length).trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
module.exports = ConvertPdf;
|
module.exports = ConvertPdf;
|
||||||
|
|||||||
115
server/core/Reader/BookConverter/ConvertPdfImages.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
|
const utils = require('../../utils');
|
||||||
|
|
||||||
|
const sax = require('../../sax');
|
||||||
|
|
||||||
|
const ConvertJpegPng = require('./ConvertJpegPng');
|
||||||
|
|
||||||
|
class ConvertPdfImages extends ConvertJpegPng {
|
||||||
|
check(data, opts) {
|
||||||
|
const {inputFiles} = opts;
|
||||||
|
|
||||||
|
return this.config.useExternalBookConverter &&
|
||||||
|
inputFiles.sourceFileType && inputFiles.sourceFileType.ext == 'pdf';
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(data, opts) {
|
||||||
|
if (!this.check(data, opts))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
let {inputFiles, callback, abort, pdfQuality} = opts;
|
||||||
|
|
||||||
|
pdfQuality = (pdfQuality && pdfQuality <= 100 && pdfQuality >= 10 ? pdfQuality : 20);
|
||||||
|
|
||||||
|
const pdftoppmPath = '/usr/bin/pdftoppm';
|
||||||
|
if (!await fs.pathExists(pdftoppmPath))
|
||||||
|
throw new Error('Внешний конвертер pdftoppm не найден');
|
||||||
|
|
||||||
|
const pdftohtmlPath = '/usr/bin/pdftohtml';
|
||||||
|
if (!await fs.pathExists(pdftohtmlPath))
|
||||||
|
throw new Error('Внешний конвертер pdftohtml не найден');
|
||||||
|
|
||||||
|
const inpFile = inputFiles.sourceFile;
|
||||||
|
const dir = `${inputFiles.filesDir}/`;
|
||||||
|
const outBasename = `${dir}${utils.randomHexString(10)}`;
|
||||||
|
const outFile = `${outBasename}.tmp`;
|
||||||
|
|
||||||
|
//конвертируем в jpeg
|
||||||
|
let perc = 0;
|
||||||
|
await this.execConverter(pdftoppmPath, ['-jpeg', '-jpegopt', `quality=${pdfQuality},progressive=y`, inpFile, outFile], () => {
|
||||||
|
perc = (perc < 100 ? perc + 1 : 40);
|
||||||
|
callback(perc);
|
||||||
|
}, abort);
|
||||||
|
|
||||||
|
const limitSize = 2*this.config.maxUploadFileSize;
|
||||||
|
let jpgFilesSize = 0;
|
||||||
|
|
||||||
|
//ищем изображения
|
||||||
|
let files = [];
|
||||||
|
await utils.findFiles(async(file) => {
|
||||||
|
if (path.extname(file) == '.jpg') {
|
||||||
|
jpgFilesSize += (await fs.stat(file)).size;
|
||||||
|
if (jpgFilesSize > limitSize) {
|
||||||
|
throw new Error(`Файл для конвертирования слишком большой|FORLOG| jpgFilesSize: ${jpgFilesSize} > ${limitSize}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
files.push({name: file, base: path.basename(file)});
|
||||||
|
}
|
||||||
|
}, dir);
|
||||||
|
|
||||||
|
files.sort((a, b) => a.base.localeCompare(b.base));
|
||||||
|
|
||||||
|
//схема документа (outline)
|
||||||
|
const outXml = `${outBasename}.xml`;
|
||||||
|
await this.execConverter(pdftohtmlPath, ['-nodrm', '-i', '-c', '-s', '-xml', inpFile, outXml], null, abort);
|
||||||
|
const outline = [];
|
||||||
|
|
||||||
|
let inOutline = 0;
|
||||||
|
let inItem = false;
|
||||||
|
let pageNum = 0;
|
||||||
|
|
||||||
|
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
|
if (inOutline > 0 && inItem && pageNum) {
|
||||||
|
outline[pageNum] = text;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
|
if (tag == 'outline')
|
||||||
|
inOutline++;
|
||||||
|
|
||||||
|
if (inOutline > 0 && tag == 'item') {
|
||||||
|
const attrs = sax.getAttrsSync(tail);
|
||||||
|
pageNum = (attrs.page && attrs.page.value ? attrs.page.value : 0);
|
||||||
|
inItem = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
|
if (tag == 'outline')
|
||||||
|
inOutline--;
|
||||||
|
if (tag == 'item')
|
||||||
|
inItem = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataXml = await fs.readFile(outXml);
|
||||||
|
const buf = this.decode(dataXml).toString();
|
||||||
|
sax.parseSync(buf, {
|
||||||
|
onStartNode, onEndNode, onTextNode
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
await utils.sleep(100);
|
||||||
|
//формируем список файлов
|
||||||
|
let i = 0;
|
||||||
|
const imageFiles = files.map(f => {
|
||||||
|
i++;
|
||||||
|
let alt = (outline[i] ? outline[i] : '');
|
||||||
|
return {src: f.name, alt};
|
||||||
|
});
|
||||||
|
return await super.run(data, Object.assign({}, opts, {imageFiles}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ConvertPdfImages;
|
||||||
@@ -48,7 +48,7 @@ class ConvertSites extends ConvertHtml {
|
|||||||
if (text === false)
|
if (text === false)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return await super.run(Buffer.from(text), {skipCheck: true, cutTitle: true});
|
return await super.run(Buffer.from(text), {skipHtmlCheck: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTitle(text) {
|
getTitle(text) {
|
||||||
@@ -79,7 +79,7 @@ class ConvertSites extends ConvertHtml {
|
|||||||
let book = this.getTitle(text);
|
let book = this.getTitle(text);
|
||||||
book = book.replace(' (fb2) | Флибуста', '');
|
book = book.replace(' (fb2) | Флибуста', '');
|
||||||
|
|
||||||
const title = `<title>${author}${(author ? ' - ' : '')}${book}</title>`;
|
const title = `<fb2-title>${author}${(author ? ' - ' : '')}${book}</fb2-title>`;
|
||||||
|
|
||||||
let begin = '<h3 class="book">';
|
let begin = '<h3 class="book">';
|
||||||
if (text.indexOf(begin) <= 0)
|
if (text.indexOf(begin) <= 0)
|
||||||
@@ -95,12 +95,12 @@ class ConvertSites extends ConvertHtml {
|
|||||||
return text.substring(l, r)
|
return text.substring(l, r)
|
||||||
.replace(/blockquote class="?book"?/g, 'p')
|
.replace(/blockquote class="?book"?/g, 'p')
|
||||||
.replace(/<br\/?>\s*<\/h3>/g, '</h3>')
|
.replace(/<br\/?>\s*<\/h3>/g, '</h3>')
|
||||||
.replace(/<h3 class="?book"?>/g, '<br><br><subtitle>')
|
.replace(/<h3 class="?book"?>/g, '<br><br><fb2-subtitle>')
|
||||||
.replace(/<h5 class="?book"?>/g, '<br><br><subtitle>')
|
.replace(/<h5 class="?book"?>/g, '<br><br><fb2-subtitle>')
|
||||||
.replace(/<h3>/g, '<br><br><subtitle>')
|
.replace(/<h3>/g, '<br><br><fb2-subtitle>')
|
||||||
.replace(/<h5>/g, '<br><br><subtitle>')
|
.replace(/<h5>/g, '<br><br><fb2-subtitle>')
|
||||||
.replace(/<\/h3>/g, '</subtitle><br>')
|
.replace(/<\/h3>/g, '</fb2-subtitle><br>')
|
||||||
.replace(/<\/h5>/g, '</subtitle><br>')
|
.replace(/<\/h5>/g, '</fb2-subtitle><br>')
|
||||||
.replace(/<div class="?stanza"?>/g, '<br>')
|
.replace(/<div class="?stanza"?>/g, '<br>')
|
||||||
.replace(/<div>/g, '<br>')
|
.replace(/<div>/g, '<br>')
|
||||||
+ title;
|
+ title;
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ const FileDetector = require('../../FileDetector');
|
|||||||
|
|
||||||
//порядок важен
|
//порядок важен
|
||||||
const convertClassFactory = [
|
const convertClassFactory = [
|
||||||
|
require('./ConvertJpegPng'),
|
||||||
require('./ConvertEpub'),
|
require('./ConvertEpub'),
|
||||||
|
require('./ConvertDjvu'),
|
||||||
require('./ConvertPdf'),
|
require('./ConvertPdf'),
|
||||||
|
require('./ConvertPdfImages'),
|
||||||
require('./ConvertRtf'),
|
require('./ConvertRtf'),
|
||||||
require('./ConvertDocX'),
|
require('./ConvertDocX'),
|
||||||
require('./ConvertFb3'),
|
require('./ConvertFb3'),
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const utils = require('../utils');
|
|||||||
const log = new (require('../AppLogger'))().log;//singleton
|
const log = new (require('../AppLogger'))().log;//singleton
|
||||||
|
|
||||||
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
const cleanDirPeriod = 60*60*1000;//1 раз в час
|
||||||
const queue = new LimitedQueue(5, 100, 5*60*1000);//5 минут ожидание подвижек
|
const queue = new LimitedQueue(5, 100, 2*60*1000 + 15000);//2 минуты ожидание подвижек
|
||||||
|
|
||||||
let instance = null;
|
let instance = null;
|
||||||
|
|
||||||
@@ -30,13 +30,13 @@ class ReaderWorker {
|
|||||||
|
|
||||||
this.workerState = new WorkerState();
|
this.workerState = new WorkerState();
|
||||||
this.down = new FileDownloader(config.maxUploadFileSize);
|
this.down = new FileDownloader(config.maxUploadFileSize);
|
||||||
this.decomp = new FileDecompressor(2*config.maxUploadFileSize);
|
this.decomp = new FileDecompressor(3*config.maxUploadFileSize);
|
||||||
this.bookConverter = new BookConverter(this.config);
|
this.bookConverter = new BookConverter(this.config);
|
||||||
|
|
||||||
this.remoteWebDavStorage = false;
|
this.remoteWebDavStorage = false;
|
||||||
if (config.remoteWebDavStorage) {
|
if (config.remoteWebDavStorage) {
|
||||||
this.remoteWebDavStorage = new RemoteWebDavStorage(
|
this.remoteWebDavStorage = new RemoteWebDavStorage(
|
||||||
Object.assign({maxContentLength: config.maxUploadFileSize}, config.remoteWebDavStorage)
|
Object.assign({maxContentLength: 3*config.maxUploadFileSize}, config.remoteWebDavStorage)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ class ReaderWorker {
|
|||||||
const decompDirname = utils.randomHexString(30);
|
const decompDirname = utils.randomHexString(30);
|
||||||
|
|
||||||
//download or use uploaded
|
//download or use uploaded
|
||||||
if (url.indexOf('file://') != 0) {//download
|
if (url.indexOf('disk://') != 0) {//download
|
||||||
const downdata = await this.down.load(url, (progress) => {
|
const downdata = await this.down.load(url, (progress) => {
|
||||||
wState.set({progress});
|
wState.set({progress});
|
||||||
}, q.abort);
|
}, q.abort);
|
||||||
@@ -130,6 +130,8 @@ class ReaderWorker {
|
|||||||
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
convertFilename = `${this.config.tempDownloadDir}/${tempFilename2}`;
|
||||||
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
await this.bookConverter.convertToFb2(decompFiles, convertFilename, opts, progress => {
|
||||||
wState.set({progress});
|
wState.set({progress});
|
||||||
|
if (queue.freed > 0)
|
||||||
|
q.resetTimeout();
|
||||||
}, q.abort);
|
}, q.abort);
|
||||||
|
|
||||||
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
//сжимаем файл в tmp, если там уже нет с тем же именем-sha256
|
||||||
@@ -171,9 +173,15 @@ class ReaderWorker {
|
|||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(LM_ERR, e.stack);
|
log(LM_ERR, e.stack);
|
||||||
if (e.message == 'abort')
|
let mes = e.message.split('|FORLOG|');
|
||||||
e.message = overLoadMes;
|
if (mes[1])
|
||||||
wState.set({state: 'error', error: e.message});
|
log(LM_ERR, mes[0] + mes[1]);
|
||||||
|
log(LM_ERR, `downloadedFilename: ${downloadedFilename}`);
|
||||||
|
|
||||||
|
mes = mes[0];
|
||||||
|
if (mes == 'abort')
|
||||||
|
mes = overLoadMes;
|
||||||
|
wState.set({state: 'error', error: mes});
|
||||||
} finally {
|
} finally {
|
||||||
//clean
|
//clean
|
||||||
if (q)
|
if (q)
|
||||||
@@ -208,7 +216,7 @@ class ReaderWorker {
|
|||||||
await fs.remove(file.path);
|
await fs.remove(file.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `file://${hash}`;
|
return `disk://${hash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async restoreRemoteFile(filename) {
|
async restoreRemoteFile(filename) {
|
||||||
|
|||||||
237
server/core/WebSocketConnection.js
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
const isBrowser = (typeof window !== 'undefined');
|
||||||
|
|
||||||
|
const utils = {
|
||||||
|
sleep: (ms) => { return new Promise(resolve => setTimeout(resolve, ms)); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanPeriod = 5*1000;//5 секунд
|
||||||
|
|
||||||
|
class WebSocketConnection {
|
||||||
|
//messageLifeTime в секундах (проверка каждый cleanPeriod интервал)
|
||||||
|
constructor(url, openTimeoutSecs = 10, messageLifeTimeSecs = 30) {
|
||||||
|
this.WebSocket = (isBrowser ? WebSocket : require('ws'));
|
||||||
|
this.url = url;
|
||||||
|
this.ws = null;
|
||||||
|
this.listeners = [];
|
||||||
|
this.messageQueue = [];
|
||||||
|
this.messageLifeTime = messageLifeTimeSecs*1000;
|
||||||
|
this.openTimeout = openTimeoutSecs*1000;
|
||||||
|
this.requestId = 0;
|
||||||
|
|
||||||
|
this.wsErrored = false;
|
||||||
|
this.closed = false;
|
||||||
|
|
||||||
|
this.connecting = false;
|
||||||
|
this.periodicClean();//no await
|
||||||
|
}
|
||||||
|
|
||||||
|
//рассылаем сообщение и удаляем те обработчики, которые его получили
|
||||||
|
emit(mes, isError) {
|
||||||
|
const len = this.listeners.length;
|
||||||
|
if (len > 0) {
|
||||||
|
let newListeners = [];
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
let emitted = false;
|
||||||
|
if (isError) {
|
||||||
|
listener.onError(mes);
|
||||||
|
emitted = true;
|
||||||
|
} else {
|
||||||
|
if ( (listener.requestId && mes.requestId && listener.requestId === mes.requestId) ||
|
||||||
|
(!listener.requestId && !mes.requestId) ) {
|
||||||
|
listener.onMessage(mes);
|
||||||
|
emitted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emitted)
|
||||||
|
newListeners.push(listener);
|
||||||
|
}
|
||||||
|
this.listeners = newListeners;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.listeners.length != len;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isOpen() {
|
||||||
|
return (this.ws && this.ws.readyState == this.WebSocket.OPEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
processMessageQueue() {
|
||||||
|
let newMessageQueue = [];
|
||||||
|
for (const message of this.messageQueue) {
|
||||||
|
if (!this.emit(message.mes)) {
|
||||||
|
newMessageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messageQueue = newMessageQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_open() {
|
||||||
|
return new Promise((resolve, reject) => { (async() => {
|
||||||
|
if (this.closed)
|
||||||
|
reject(new Error('Этот экземпляр класса уничтожен. Пожалуйста, создайте новый.'));
|
||||||
|
|
||||||
|
if (this.connecting) {
|
||||||
|
let i = this.openTimeout/100;
|
||||||
|
while (i-- > 0 && this.connecting) {
|
||||||
|
await utils.sleep(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//проверим подключение, и если нет, то подключимся заново
|
||||||
|
if (this.isOpen) {
|
||||||
|
resolve(this.ws);
|
||||||
|
} else {
|
||||||
|
this.connecting = true;
|
||||||
|
this.terminate();
|
||||||
|
|
||||||
|
if (isBrowser) {
|
||||||
|
const protocol = (window.location.protocol == 'https:' ? 'wss:' : 'ws:');
|
||||||
|
const url = this.url || `${protocol}//${window.location.host}/ws`;
|
||||||
|
this.ws = new this.WebSocket(url);
|
||||||
|
} else {
|
||||||
|
this.ws = new this.WebSocket(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onopen = (e) => {
|
||||||
|
this.connecting = false;
|
||||||
|
resolve(this.ws);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onmessage = (data) => {
|
||||||
|
try {
|
||||||
|
if (isBrowser)
|
||||||
|
data = data.data;
|
||||||
|
const mes = JSON.parse(data);
|
||||||
|
this.messageQueue.push({regTime: Date.now(), mes});
|
||||||
|
|
||||||
|
this.processMessageQueue();
|
||||||
|
} catch (e) {
|
||||||
|
this.emit(e.message, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onerror = (e) => {
|
||||||
|
this.emit(e.message, true);
|
||||||
|
reject(new Error(e.message));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onclose = (e) => {
|
||||||
|
this.emit(e.message, true);
|
||||||
|
reject(new Error(e.message));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isBrowser) {
|
||||||
|
this.ws.onopen = onopen;
|
||||||
|
this.ws.onmessage = onmessage;
|
||||||
|
this.ws.onerror = onerror;
|
||||||
|
this.ws.onclose = onclose;
|
||||||
|
} else {
|
||||||
|
this.ws.on('open', onopen);
|
||||||
|
this.ws.on('message', onmessage);
|
||||||
|
this.ws.on('error', onerror);
|
||||||
|
this.ws.on('close', onclose);
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(this.openTimeout);
|
||||||
|
reject(new Error('Соединение не удалось'));
|
||||||
|
}
|
||||||
|
})() });
|
||||||
|
}
|
||||||
|
|
||||||
|
//timeout в секундах (проверка каждый cleanPeriod интервал)
|
||||||
|
message(requestId, timeoutSecs = 4) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.listeners.push({
|
||||||
|
regTime: Date.now(),
|
||||||
|
requestId,
|
||||||
|
timeout: timeoutSecs*1000,
|
||||||
|
onMessage: (mes) => {
|
||||||
|
resolve(mes);
|
||||||
|
},
|
||||||
|
onError: (mes) => {
|
||||||
|
reject(new Error(mes));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.processMessageQueue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async send(req, timeoutSecs = 4) {
|
||||||
|
await this._open();
|
||||||
|
if (this.isOpen) {
|
||||||
|
this.requestId = (this.requestId < 1000000 ? this.requestId + 1 : 1);
|
||||||
|
const requestId = this.requestId;//реентерабельность!!!
|
||||||
|
|
||||||
|
this.ws.send(JSON.stringify(Object.assign({requestId, _rpo: 1}, req)));//_rpo: 1 - ждем в ответ _rok: 1
|
||||||
|
|
||||||
|
let resp = {};
|
||||||
|
try {
|
||||||
|
resp = await this.message(requestId, timeoutSecs);
|
||||||
|
} catch(e) {
|
||||||
|
this.terminate();
|
||||||
|
throw new Error('WebSocket не отвечает');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp._rok) {
|
||||||
|
return requestId;
|
||||||
|
} else {
|
||||||
|
throw new Error('Запрос не принят сервером');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('WebSocket коннект закрыт');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
terminate() {
|
||||||
|
if (this.ws) {
|
||||||
|
if (isBrowser) {
|
||||||
|
this.ws.close();
|
||||||
|
} else {
|
||||||
|
this.ws.terminate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.terminate();
|
||||||
|
this.closed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async periodicClean() {
|
||||||
|
while (!this.closed) {
|
||||||
|
try {
|
||||||
|
const now = Date.now();
|
||||||
|
//чистка listeners
|
||||||
|
let newListeners = [];
|
||||||
|
for (const listener of this.listeners) {
|
||||||
|
if (now - listener.regTime < listener.timeout) {
|
||||||
|
newListeners.push(listener);
|
||||||
|
} else {
|
||||||
|
if (listener.onError)
|
||||||
|
listener.onError('Время ожидания ответа истекло');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.listeners = newListeners;
|
||||||
|
|
||||||
|
//чистка messageQueue
|
||||||
|
let newMessageQueue = [];
|
||||||
|
for (const message of this.messageQueue) {
|
||||||
|
if (now - message.regTime < this.messageLifeTime) {
|
||||||
|
newMessageQueue.push(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.messageQueue = newMessageQueue;
|
||||||
|
} catch(e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.sleep(cleanPeriod);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = WebSocketConnection;
|
||||||
@@ -76,13 +76,13 @@ class ZipStreamer {
|
|||||||
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
|
if (limitFileCount || limitFileSize || decodeEntryNameCallback) {
|
||||||
const entries = Object.values(unzip.entries());
|
const entries = Object.values(unzip.entries());
|
||||||
if (limitFileCount && entries.length > limitFileCount) {
|
if (limitFileCount && entries.length > limitFileCount) {
|
||||||
reject('Слишком много файлов');
|
reject(new Error('Слишком много файлов'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
|
if (limitFileSize && !entry.isDirectory && entry.size > limitFileSize) {
|
||||||
reject('Файл слишком большой');
|
reject(new Error('Файл слишком большой'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ function parseSync(xstr, options) {
|
|||||||
onCdata: _onCdata = dummy,
|
onCdata: _onCdata = dummy,
|
||||||
onComment: _onComment = dummy,
|
onComment: _onComment = dummy,
|
||||||
onProgress: _onProgress = dummy,
|
onProgress: _onProgress = dummy,
|
||||||
innerCut = new Set()
|
innerCut = new Set(),
|
||||||
|
lowerCase = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -91,7 +92,8 @@ function parseSync(xstr, options) {
|
|||||||
} else {
|
} else {
|
||||||
tag = tagData;
|
tag = tagData;
|
||||||
}
|
}
|
||||||
tag = tag.toLowerCase();
|
if (lowerCase)
|
||||||
|
tag = tag.toLowerCase();
|
||||||
|
|
||||||
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
||||||
if (!cutCounter)
|
if (!cutCounter)
|
||||||
@@ -146,7 +148,8 @@ async function parse(xstr, options) {
|
|||||||
onCdata: _onCdata = dummy,
|
onCdata: _onCdata = dummy,
|
||||||
onComment: _onComment = dummy,
|
onComment: _onComment = dummy,
|
||||||
onProgress: _onProgress = dummy,
|
onProgress: _onProgress = dummy,
|
||||||
innerCut = new Set()
|
innerCut = new Set(),
|
||||||
|
lowerCase = true,
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -231,7 +234,8 @@ async function parse(xstr, options) {
|
|||||||
} else {
|
} else {
|
||||||
tag = tagData;
|
tag = tagData;
|
||||||
}
|
}
|
||||||
tag = tag.toLowerCase();
|
if (lowerCase)
|
||||||
|
tag = tag.toLowerCase();
|
||||||
|
|
||||||
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
if (innerCut.has(tag) && (!cutCounter || cutTag === tag)) {
|
||||||
if (!cutCounter)
|
if (!cutCounter)
|
||||||
@@ -276,7 +280,7 @@ async function parse(xstr, options) {
|
|||||||
await _onProgress(100);
|
await _onProgress(100);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAttrsSync(tail) {
|
function getAttrsSync(tail, lowerCase = true) {
|
||||||
let result = {};
|
let result = {};
|
||||||
let name = '';
|
let name = '';
|
||||||
let value = '';
|
let value = '';
|
||||||
@@ -287,13 +291,16 @@ function getAttrsSync(tail) {
|
|||||||
let waitEq = false;
|
let waitEq = false;
|
||||||
|
|
||||||
const pushResult = () => {
|
const pushResult = () => {
|
||||||
|
if (lowerCase)
|
||||||
|
name = name.toLowerCase();
|
||||||
if (name != '') {
|
if (name != '') {
|
||||||
|
const fn = name;
|
||||||
let ns = '';
|
let ns = '';
|
||||||
if (name.indexOf(':') >= 0) {
|
if (fn.indexOf(':') >= 0) {
|
||||||
[ns, name] = name.split(':');
|
[ns, name] = fn.split(':');
|
||||||
}
|
}
|
||||||
|
|
||||||
result[name] = {value, ns};
|
result[name] = {value, ns, fn};
|
||||||
}
|
}
|
||||||
name = '';
|
name = '';
|
||||||
value = '';
|
value = '';
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const baseX = require('base-x');
|
const baseX = require('base-x');
|
||||||
|
const pidusage = require('pidusage');
|
||||||
|
|
||||||
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
|
const BASE36 = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||||
const bs36 = baseX(BASE36);
|
const bs36 = baseX(BASE36);
|
||||||
@@ -45,10 +47,11 @@ async function touchFile(filename) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function spawnProcess(cmd, opts) {
|
function spawnProcess(cmd, opts) {
|
||||||
let {args, killAfter, onData, abort} = opts;
|
let {args, killAfter, onData, onUsage, onUsageInterval, abort} = opts;
|
||||||
killAfter = (killAfter ? killAfter : 120);//seconds
|
killAfter = (killAfter ? killAfter : 120);//seconds
|
||||||
onData = (onData ? onData : () => {});
|
onData = (onData ? onData : () => {});
|
||||||
args = (args ? args : []);
|
args = (args ? args : []);
|
||||||
|
onUsageInterval = (onUsageInterval ? onUsageInterval : 30);//seconds
|
||||||
|
|
||||||
return new Promise((resolve, reject) => { (async() => {
|
return new Promise((resolve, reject) => { (async() => {
|
||||||
let resolved = false;
|
let resolved = false;
|
||||||
@@ -75,9 +78,19 @@ function spawnProcess(cmd, opts) {
|
|||||||
reject({status: 'error', error, stdout, stderr});
|
reject({status: 'error', error, stdout, stderr});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//ждем процесс, контролируем его работу раз в секунду
|
||||||
|
let onUsageCounter = onUsageInterval;
|
||||||
while (!resolved) {
|
while (!resolved) {
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
killAfter -= 1;
|
|
||||||
|
onUsageCounter--;
|
||||||
|
if (onUsage && onUsageCounter <= 0) {
|
||||||
|
const stats = await pidusage(proc.pid);
|
||||||
|
onUsage(stats);
|
||||||
|
onUsageCounter = onUsageInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
killAfter--;
|
||||||
if (killAfter <= 0 || (abort && abort())) {
|
if (killAfter <= 0 || (abort && abort())) {
|
||||||
process.kill(proc.pid);
|
process.kill(proc.pid);
|
||||||
if (killAfter <= 0) {
|
if (killAfter <= 0) {
|
||||||
@@ -91,6 +104,22 @@ function spawnProcess(cmd, opts) {
|
|||||||
})().catch(reject); });
|
})().catch(reject); });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findFiles(callback, dir) {
|
||||||
|
if (!(callback && dir))
|
||||||
|
return;
|
||||||
|
let result = true;
|
||||||
|
const files = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const found = path.resolve(dir, file.name);
|
||||||
|
if (file.isDirectory())
|
||||||
|
result = await findFiles(callback, found);
|
||||||
|
else
|
||||||
|
await callback(found);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
toBase36,
|
toBase36,
|
||||||
fromBase36,
|
fromBase36,
|
||||||
@@ -99,5 +128,6 @@ module.exports = {
|
|||||||
sleep,
|
sleep,
|
||||||
randomHexString,
|
randomHexString,
|
||||||
touchFile,
|
touchFile,
|
||||||
spawnProcess
|
spawnProcess,
|
||||||
|
findFiles
|
||||||
};
|
};
|
||||||
143
server/core/xmlParser.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
const sax = require('./sax');
|
||||||
|
|
||||||
|
function formatXml(xmlParsed, encoding = 'utf-8', textFilterFunc) {
|
||||||
|
let out = `<?xml version="1.0" encoding="${encoding}"?>`;
|
||||||
|
out += formatXmlNode(xmlParsed, textFilterFunc);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatXmlNode(node, textFilterFunc) {
|
||||||
|
textFilterFunc = (textFilterFunc ? textFilterFunc : text => text);
|
||||||
|
|
||||||
|
const formatNode = (node, name) => {
|
||||||
|
let out = '';
|
||||||
|
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
for (const n of node) {
|
||||||
|
out += formatNode(n);
|
||||||
|
}
|
||||||
|
} else if (typeof node == 'string') {
|
||||||
|
if (name)
|
||||||
|
out += `<${name}>${textFilterFunc(node)}</${name}>`;
|
||||||
|
else
|
||||||
|
out += textFilterFunc(node);
|
||||||
|
} else {
|
||||||
|
if (node._n)
|
||||||
|
name = node._n;
|
||||||
|
|
||||||
|
let attrs = '';
|
||||||
|
if (node._attrs) {
|
||||||
|
for (let attrName in node._attrs) {
|
||||||
|
attrs += ` ${attrName}="${node._attrs[attrName]}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tOpen = '';
|
||||||
|
let tBody = '';
|
||||||
|
let tClose = '';
|
||||||
|
if (name)
|
||||||
|
tOpen += `<${name}${attrs}>`;
|
||||||
|
if (node.hasOwnProperty('_t'))
|
||||||
|
tBody += textFilterFunc(node._t);
|
||||||
|
|
||||||
|
for (let nodeName in node) {
|
||||||
|
if (nodeName && nodeName[0] == '_' && nodeName != '_a')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const n = node[nodeName];
|
||||||
|
tBody += formatNode(n, nodeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name)
|
||||||
|
tClose += `</${name}>`;
|
||||||
|
|
||||||
|
out += `${tOpen}${tBody}${tClose}`;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatNode(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseXml(xmlString, lowerCase = true) {
|
||||||
|
let result = {};
|
||||||
|
let node = result;
|
||||||
|
|
||||||
|
const onTextNode = (text, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
|
node._t = text;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStartNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
|
if (tag == '?xml')
|
||||||
|
return;
|
||||||
|
|
||||||
|
const newNode = {_n: tag, _p: node};
|
||||||
|
|
||||||
|
if (tail) {
|
||||||
|
const parsedAttrs = sax.getAttrsSync(tail, lowerCase);
|
||||||
|
const atKeys = Object.keys(parsedAttrs);
|
||||||
|
if (atKeys.length) {
|
||||||
|
const attrs = {};
|
||||||
|
for (let i = 0; i < atKeys.length; i++) {
|
||||||
|
const attrName = atKeys[i];
|
||||||
|
attrs[parsedAttrs[attrName].fn] = parsedAttrs[attrName].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
newNode._attrs = attrs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!node._a)
|
||||||
|
node._a = [];
|
||||||
|
node._a.push(newNode);
|
||||||
|
node = newNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEndNode = (tag, tail, singleTag, cutCounter, cutTag) => {// eslint-disable-line no-unused-vars
|
||||||
|
if (node._p && node._n == tag)
|
||||||
|
node = node._p;
|
||||||
|
};
|
||||||
|
|
||||||
|
sax.parseSync(xmlString, {
|
||||||
|
onStartNode, onEndNode, onTextNode, lowerCase
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result._a)
|
||||||
|
result = result._a[0];
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function simplifyXmlParsed(node) {
|
||||||
|
|
||||||
|
const simplifyNodeArray = (a) => {
|
||||||
|
const result = {};
|
||||||
|
|
||||||
|
for (let i = 0; i < a.length; i++) {
|
||||||
|
const child = a[i];
|
||||||
|
if (child._n && !result[child._n]) {
|
||||||
|
result[child._n] = {};
|
||||||
|
if (child._a) {
|
||||||
|
result[child._n] = simplifyNodeArray(child._a);
|
||||||
|
}
|
||||||
|
if (child._t) {
|
||||||
|
result[child._n]._t = child._t;
|
||||||
|
}
|
||||||
|
if (child._attrs) {
|
||||||
|
result[child._n]._attrs = child._attrs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
return simplifyNodeArray([node]);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
formatXml,
|
||||||
|
formatXmlNode,
|
||||||
|
parseXml,
|
||||||
|
simplifyXmlParsed
|
||||||
|
}
|
||||||
@@ -32,11 +32,11 @@ class ConnManager {
|
|||||||
const dbFileName = this.config.dataDir + '/' + poolConfig.fileName;
|
const dbFileName = this.config.dataDir + '/' + poolConfig.fileName;
|
||||||
|
|
||||||
//бэкап
|
//бэкап
|
||||||
if (await fs.pathExists(dbFileName))
|
if (!poolConfig.noBak && await fs.pathExists(dbFileName))
|
||||||
await fs.copy(dbFileName, `${dbFileName}.bak`);
|
await fs.copy(dbFileName, `${dbFileName}.bak`);
|
||||||
|
|
||||||
const connPool = new SqliteConnectionPool();
|
const connPool = new SqliteConnectionPool();
|
||||||
await connPool.open(poolConfig.connCount, dbFileName);
|
await connPool.open(poolConfig, dbFileName);
|
||||||
|
|
||||||
log(`Opened database "${poolConfig.poolName}"`);
|
log(`Opened database "${poolConfig.poolName}"`);
|
||||||
//миграции
|
//миграции
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
const sqlite = require('sqlite');
|
const sqlite = require('sqlite');
|
||||||
const SQL = require('sql-template-strings');
|
const SQL = require('sql-template-strings');
|
||||||
|
|
||||||
const utils = require('../core/utils');
|
|
||||||
|
|
||||||
const waitingDelay = 100; //ms
|
|
||||||
|
|
||||||
class SqliteConnectionPool {
|
class SqliteConnectionPool {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.closed = true;
|
this.closed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async open(connCount, dbFileName) {
|
async open(poolConfig, dbFileName) {
|
||||||
if (!Number.isInteger(connCount) || connCount <= 0)
|
const connCount = poolConfig.connCount || 1;
|
||||||
return;
|
const busyTimeout = poolConfig.busyTimeout || 60*1000;
|
||||||
|
const cacheSize = poolConfig.cacheSize || 2000;
|
||||||
|
|
||||||
|
this.dbFileName = dbFileName;
|
||||||
this.connections = [];
|
this.connections = [];
|
||||||
this.freed = new Set();
|
this.freed = new Set();
|
||||||
|
this.waitingQueue = [];
|
||||||
|
|
||||||
for (let i = 0; i < connCount; i++) {
|
for (let i = 0; i < connCount; i++) {
|
||||||
let client = await sqlite.open(dbFileName);
|
let client = await sqlite.open(dbFileName);
|
||||||
client.configure('busyTimeout', 10000); //ms
|
|
||||||
|
client.configure('busyTimeout', busyTimeout); //ms
|
||||||
|
await client.exec(`PRAGMA cache_size = ${cacheSize}`);
|
||||||
|
|
||||||
client.ret = () => {
|
client.ret = () => {
|
||||||
this.freed.add(i);
|
this.freed.add(i);
|
||||||
|
if (this.waitingQueue.length) {
|
||||||
|
this.waitingQueue.shift().onFreed(i);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.freed.add(i);
|
this.freed.add(i);
|
||||||
@@ -30,30 +35,27 @@ class SqliteConnectionPool {
|
|||||||
this.closed = false;
|
this.closed = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_setImmediate() {
|
get() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setImmediate(() => {
|
if (this.closed)
|
||||||
return resolve();
|
throw new Error('Connection pool closed');
|
||||||
|
|
||||||
|
const freeConnIndex = this.freed.values().next().value;
|
||||||
|
if (freeConnIndex !== undefined) {
|
||||||
|
this.freed.delete(freeConnIndex);
|
||||||
|
resolve(this.connections[freeConnIndex]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.waitingQueue.push({
|
||||||
|
onFreed: (connIndex) => {
|
||||||
|
this.freed.delete(connIndex);
|
||||||
|
resolve(this.connections[connIndex]);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async get() {
|
|
||||||
if (this.closed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
let freeConnIndex = this.freed.values().next().value;
|
|
||||||
if (freeConnIndex == null) {
|
|
||||||
if (waitingDelay)
|
|
||||||
await utils.sleep(waitingDelay);
|
|
||||||
return await this._setImmediate().then(() => this.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.freed.delete(freeConnIndex);
|
|
||||||
|
|
||||||
return this.connections[freeConnIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(query) {
|
async run(query) {
|
||||||
const dbh = await this.get();
|
const dbh = await this.get();
|
||||||
try {
|
try {
|
||||||
|
|||||||