Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7c6b0e7ab | ||
|
|
94922f3926 | ||
|
|
a580b1eb6d | ||
|
|
cd7b8afb29 | ||
|
|
e634893ff3 | ||
|
|
fadc7ddc34 | ||
|
|
ed5dc25d94 | ||
|
|
dd11e8c5ad | ||
|
|
2db2b8cff4 | ||
|
|
4d3661b758 | ||
|
|
891b1e4fe8 | ||
|
|
d588b16885 | ||
|
|
a0e4651607 | ||
|
|
c21b8ffa0e | ||
|
|
f174617f33 | ||
|
|
2de9ad0edf | ||
|
|
a6592f2f8d | ||
|
|
b4da07e924 | ||
|
|
110d145b91 | ||
|
|
fc3d391aa0 | ||
|
|
4d01901463 | ||
|
|
2d380bd98f | ||
|
|
2dd67487dc | ||
|
|
a3190e4af3 | ||
|
|
3d28beddac |
@@ -231,12 +231,12 @@ class Api {
|
||||
return await this.request({action: 'get-genre-tree'});
|
||||
}
|
||||
|
||||
async getBookLink(bookId) {
|
||||
return await this.request({action: 'get-book-link', bookId}, 120);
|
||||
async getBookLink(bookUid) {
|
||||
return await this.request({action: 'get-book-link', bookUid}, 120);
|
||||
}
|
||||
|
||||
async getBookInfo(bookId) {
|
||||
return await this.request({action: 'get-book-info', bookId}, 120);
|
||||
async getBookInfo(bookUid) {
|
||||
return await this.request({action: 'get-book-info', bookUid}, 120);
|
||||
}
|
||||
|
||||
async getConfig() {
|
||||
|
||||
@@ -238,6 +238,13 @@ class AuthorList extends BaseList {
|
||||
const booksToFilter = await this.loadAuthorBooks(item.key);
|
||||
const filtered = this.filterBooks(booksToFilter);
|
||||
|
||||
if (!filtered.length && this.list.totalFound == 1) {
|
||||
this.list.queryFound = 0;
|
||||
this.list.totalFound = 0;
|
||||
this.searchResult.found = [];
|
||||
return false;
|
||||
}
|
||||
|
||||
const prepareBook = (book) => {
|
||||
return Object.assign(
|
||||
{
|
||||
@@ -345,7 +352,10 @@ class AuthorList extends BaseList {
|
||||
if (authors.length > 1 || item.count > this.maxItemCount)
|
||||
this.getAuthorBooks(item);//no await
|
||||
else
|
||||
await this.getAuthorBooks(item);
|
||||
if (await this.getAuthorBooks(item) === false) {
|
||||
this.tableData = [];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
result.push(item);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import moment from 'moment';
|
||||
import dayjs from 'dayjs';
|
||||
import _ from 'lodash';
|
||||
|
||||
import authorBooksStorage from './authorBooksStorage';
|
||||
@@ -130,7 +130,7 @@ export default class BaseList {
|
||||
|
||||
try {
|
||||
//подготовка
|
||||
const response = await this.api.getBookLink(book.id);
|
||||
const response = await this.api.getBookLink(book._uid);
|
||||
|
||||
const link = response.link;
|
||||
const href = `${window.location.origin}${link}`;
|
||||
@@ -164,7 +164,7 @@ export default class BaseList {
|
||||
}
|
||||
} else if (action == 'bookInfo') {
|
||||
//информация о книге
|
||||
const response = await this.api.getBookInfo(book.id);
|
||||
const response = await this.api.getBookInfo(book._uid);
|
||||
this.$emit('listEvent', {action: 'bookInfo', data: response.bookInfo});
|
||||
}
|
||||
} catch(e) {
|
||||
@@ -467,13 +467,13 @@ export default class BaseList {
|
||||
*/
|
||||
const sqlFormat = 'YYYY-MM-DD';
|
||||
switch (date) {
|
||||
case 'today': date = utils.dateFormat(moment(), sqlFormat); break;
|
||||
case '3days': date = utils.dateFormat(moment().subtract(3, 'days'), sqlFormat); break;
|
||||
case 'week': date = utils.dateFormat(moment().subtract(1, 'weeks'), sqlFormat); break;
|
||||
case '2weeks': date = utils.dateFormat(moment().subtract(2, 'weeks'), sqlFormat); break;
|
||||
case 'month': date = utils.dateFormat(moment().subtract(1, 'months'), sqlFormat); break;
|
||||
case '2months': date = utils.dateFormat(moment().subtract(2, 'months'), sqlFormat); break;
|
||||
case '3months': date = utils.dateFormat(moment().subtract(3, 'months'), sqlFormat); break;
|
||||
case 'today': date = utils.dateFormat(dayjs(), sqlFormat); break;
|
||||
case '3days': date = utils.dateFormat(dayjs().subtract(3, 'days'), sqlFormat); break;
|
||||
case 'week': date = utils.dateFormat(dayjs().subtract(1, 'weeks'), sqlFormat); break;
|
||||
case '2weeks': date = utils.dateFormat(dayjs().subtract(2, 'weeks'), sqlFormat); break;
|
||||
case 'month': date = utils.dateFormat(dayjs().subtract(1, 'months'), sqlFormat); break;
|
||||
case '2months': date = utils.dateFormat(dayjs().subtract(2, 'months'), sqlFormat); break;
|
||||
case '3months': date = utils.dateFormat(dayjs().subtract(3, 'months'), sqlFormat); break;
|
||||
default:
|
||||
date = '';
|
||||
}
|
||||
|
||||
@@ -17,10 +17,12 @@
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-sm no-wrap">
|
||||
<div class="column justify-center" style="height: 300px; width: 200px; min-width: 100px">
|
||||
<img v-if="coverSrc" :src="coverSrc" class="fit row justify-center items-center" style="object-fit: contain" @error="coverSrc = ''" />
|
||||
<div v-if="!coverSrc" class="fit row justify-center items-center text-grey-5" style="border: 1px solid #ccc; font-size: 300%">
|
||||
<i>{{ book.ext }}</i>
|
||||
<div class="poster-size">
|
||||
<div class="poster-size column justify-center items-center" :class="{poster: coverSrc}" @click.stop.prevent="posterClick">
|
||||
<img v-if="coverSrc" :src="coverSrc" class="fit row justify-center items-center" style="object-fit: contain" @error="coverSrc = ''" />
|
||||
<div v-if="!coverSrc" class="fit row justify-center items-center text-grey-5" style="border: 1px solid #ccc; font-size: 300%">
|
||||
<i>{{ book.ext }}</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,6 +73,19 @@
|
||||
OK
|
||||
</q-btn>
|
||||
</template>
|
||||
|
||||
|
||||
<Dialog v-model="posterDialogVisible">
|
||||
<template #header>
|
||||
<div class="row items-center">
|
||||
<div style="font-size: 110%">
|
||||
Обложка
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<img :src="coverSrc" class="fit q-pb-sm" style="height: 100%; max-height: calc(100vh - 140px); object-fit: contain" />
|
||||
</Dialog>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -106,6 +121,7 @@ class BookInfoDialog {
|
||||
};
|
||||
|
||||
dialogVisible = false;
|
||||
posterDialogVisible = false;
|
||||
selectedTab = 'fb2';
|
||||
|
||||
//info props
|
||||
@@ -185,7 +201,10 @@ class BookInfoDialog {
|
||||
return utils.sqlDateFormat(value);
|
||||
|
||||
if (nodePath == 'fileInfo/del')
|
||||
return (value ? 'Да' : 'Нет');
|
||||
return (value ? 'Да' : null);
|
||||
|
||||
if (nodePath == 'fileInfo/insno')
|
||||
return (value ? value : null);
|
||||
|
||||
if (nodePath == 'titleInfo/author')
|
||||
return value.split(',').join(', ');
|
||||
@@ -239,7 +258,6 @@ class BookInfoDialog {
|
||||
|
||||
parseBookInfo() {
|
||||
const bookInfo = this.bookInfo;
|
||||
const parser = new Fb2Parser();
|
||||
|
||||
//cover
|
||||
if (bookInfo.cover)
|
||||
@@ -247,16 +265,10 @@ class BookInfoDialog {
|
||||
|
||||
//fb2
|
||||
if (bookInfo.fb2) {
|
||||
this.fb2 = parser.bookInfoList(bookInfo.fb2, {
|
||||
valueToString(value, nodePath, origVTS) {//eslint-disable-line no-unused-vars
|
||||
if (nodePath == 'documentInfo/historyHtml' && value)
|
||||
return value.replace(/<p>/g, `<p class="p-history">`);
|
||||
const parser = new Fb2Parser(bookInfo.fb2);
|
||||
|
||||
const infoObj = parser.bookInfo();
|
||||
|
||||
return origVTS(value, nodePath);
|
||||
},
|
||||
});
|
||||
|
||||
const infoObj = parser.bookInfo(bookInfo.fb2);
|
||||
if (infoObj.titleInfo) {
|
||||
let ann = infoObj.titleInfo.annotationHtml;
|
||||
if (ann) {
|
||||
@@ -264,6 +276,15 @@ class BookInfoDialog {
|
||||
this.annotation = ann;
|
||||
}
|
||||
}
|
||||
|
||||
this.fb2 = parser.bookInfoList(infoObj, {
|
||||
valueToString(value, nodePath, origVTS) {//eslint-disable-line no-unused-vars
|
||||
if (nodePath == 'documentInfo/historyHtml' && value)
|
||||
return value.replace(/<p>/g, `<p class="p-history">`);
|
||||
|
||||
return origVTS(value, nodePath);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
//book
|
||||
@@ -271,6 +292,13 @@ class BookInfoDialog {
|
||||
this.book = bookInfo.book;
|
||||
}
|
||||
|
||||
posterClick() {
|
||||
if (!this.coverSrc)
|
||||
return;
|
||||
|
||||
this.posterDialogVisible = true;
|
||||
}
|
||||
|
||||
okClick() {
|
||||
this.dialogVisible = false;
|
||||
}
|
||||
@@ -281,6 +309,26 @@ export default vueComponent(BookInfoDialog);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.poster-size {
|
||||
height: 300px;
|
||||
width: 200px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.poster:hover {
|
||||
position: relative;
|
||||
top: -1%;
|
||||
left: -1%;
|
||||
width: 102%;
|
||||
height: 102%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<div class="q-ml-sm column">
|
||||
<div v-if="(mode == 'series' || mode == 'title') && bookAuthor" class="row">
|
||||
<div class="clickable2 text-green-10" @click="emit('authorClick')">
|
||||
<div class="clickable2 text-green-10" @click.stop.prevent="emit('authorClick')">
|
||||
{{ bookAuthor }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,10 +43,10 @@
|
||||
<div v-if="book.serno" class="q-mr-xs">
|
||||
{{ book.serno }}.
|
||||
</div>
|
||||
<div class="clickable2" :class="titleColor" @click="emit('titleClick')">
|
||||
<div class="clickable2" :class="titleColor" @click.stop.prevent="emit('titleClick')">
|
||||
{{ book.title }}
|
||||
</div>
|
||||
<div v-if="mode == 'title' && bookSeries" class="q-ml-xs clickable2" @click="emit('seriesClick')">
|
||||
<div v-if="mode == 'title' && bookSeries" class="q-ml-xs clickable2" @click.stop.prevent="emit('seriesClick')">
|
||||
{{ bookSeries }}
|
||||
</div>
|
||||
|
||||
@@ -55,19 +55,19 @@
|
||||
{{ bookSize }}, {{ book.ext }}
|
||||
</div>
|
||||
|
||||
<div v-if="showInfo" class="row items-center q-ml-sm clickable" @click="emit('bookInfo')">
|
||||
[ . . . ]
|
||||
<div v-if="showInfo" class="q-ml-sm clickable" @click.stop.prevent="emit('bookInfo')">
|
||||
(инфо)
|
||||
</div>
|
||||
|
||||
<div class="q-ml-sm clickable" @click="emit('download')">
|
||||
<div class="q-ml-sm clickable" @click.stop.prevent="emit('download')">
|
||||
(скачать)
|
||||
</div>
|
||||
|
||||
<div class="q-ml-sm clickable" @click="emit('copyLink')">
|
||||
<div class="q-ml-sm clickable" @click.stop.prevent="emit('copyLink')">
|
||||
<q-icon name="la la-copy" size="20px" />
|
||||
</div>
|
||||
|
||||
<div v-if="showReadLink" class="q-ml-sm clickable" @click="emit('readBook')">
|
||||
<div v-if="showReadLink" class="q-ml-sm clickable" @click.stop.prevent="emit('readBook')">
|
||||
(читать)
|
||||
</div>
|
||||
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
<div class="q-mr-xs">
|
||||
Коллекция
|
||||
</div>
|
||||
<div class="clickable" @click="showCollectionInfo">
|
||||
<div class="clickable" @click.stop.prevent="showCollectionInfo">
|
||||
{{ collection }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col"></div>
|
||||
|
||||
<DivBtn class="q-ml-md text-white bg-secondary" :size="30" :icon-size="24" icon="la la-question" round @click="showSearchHelp">
|
||||
<DivBtn class="q-ml-md text-white bg-secondary" :size="30" :icon-size="24" icon="la la-question" round @click.stop.prevent="showSearchHelp">
|
||||
<template #tooltip>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
|
||||
Памятка
|
||||
@@ -39,7 +39,7 @@
|
||||
</template>
|
||||
</DivBtn>
|
||||
|
||||
<DivBtn class="q-ml-sm text-white bg-secondary" :size="30" :icon-size="24" :imt="1" icon="la la-cog" round @click="settingsDialogVisible = true">
|
||||
<DivBtn class="q-ml-sm text-white bg-secondary" :size="30" :icon-size="24" :imt="1" icon="la la-cog" round @click.stop.prevent="settingsDialogVisible = true">
|
||||
<template #tooltip>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
|
||||
Настройки
|
||||
@@ -51,7 +51,7 @@
|
||||
<DivBtn
|
||||
class="text-grey-5 bg-yellow-1 q-mt-xs" :size="34" :icon-size="24" round
|
||||
:icon="(extendedParams ? 'la la-angle-double-up' : 'la la-angle-double-down')"
|
||||
@click="extendedParams = !extendedParams"
|
||||
@click.stop.prevent="extendedParams = !extendedParams"
|
||||
>
|
||||
<template #tooltip>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
|
||||
@@ -90,7 +90,7 @@
|
||||
<q-input
|
||||
v-model="search.lang" :maxlength="inputMaxLength" :debounce="inputDebounce"
|
||||
class="q-mt-xs" :bg-color="inputBgColor()" input-style="cursor: pointer" style="width: 90px;" label="Язык" stack-label outlined dense clearable readonly
|
||||
@click="selectLang"
|
||||
@click.stop.prevent="selectLang"
|
||||
>
|
||||
<template v-if="search.lang" #append>
|
||||
<q-icon name="la la-times-circle" class="q-field__focusable-action" @click.stop.prevent="search.lang = ''" />
|
||||
@@ -104,7 +104,7 @@
|
||||
<DivBtn
|
||||
class="text-grey-8 bg-yellow-1 q-mt-xs" :size="34" :icon-size="24" round
|
||||
icon="la la-level-up-alt"
|
||||
@click="cloneSearch"
|
||||
@click.stop.prevent="cloneSearch"
|
||||
>
|
||||
<template #tooltip>
|
||||
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
|
||||
@@ -119,7 +119,7 @@
|
||||
<q-input
|
||||
v-model="genreNames" :maxlength="inputMaxLength" :debounce="inputDebounce"
|
||||
class="q-mt-xs" :bg-color="inputBgColor()" input-style="cursor: pointer" style="width: 200px;" label="Жанр" stack-label outlined dense clearable readonly
|
||||
@click="selectGenre"
|
||||
@click.stop.prevent="selectGenre"
|
||||
>
|
||||
<template v-if="genreNames" #append>
|
||||
<q-icon name="la la-times-circle" class="q-field__focusable-action" @click.stop.prevent="search.genre = ''" />
|
||||
@@ -151,7 +151,7 @@
|
||||
</template>
|
||||
|
||||
<template #option="scope">
|
||||
<q-item v-bind="scope.itemProps" @click="dateSelectItemClick(scope.opt.value)">
|
||||
<q-item v-bind="scope.itemProps" @click.stop.prevent="dateSelectItemClick(scope.opt.value)">
|
||||
<q-item-section>
|
||||
<q-item-label>
|
||||
{{ scope.opt.label }}
|
||||
@@ -165,7 +165,7 @@
|
||||
<q-input
|
||||
v-model="librateNames" :maxlength="inputMaxLength" :debounce="inputDebounce"
|
||||
class="q-mt-xs" :bg-color="inputBgColor()" input-style="cursor: pointer" style="width: 90px;" label="Оценка" stack-label outlined dense clearable readonly
|
||||
@click="selectLibRate"
|
||||
@click.stop.prevent="selectLibRate"
|
||||
>
|
||||
<template v-if="librateNames" #append>
|
||||
<q-icon name="la la-times-circle" class="q-field__focusable-action" @click.stop.prevent="search.librate = ''" />
|
||||
@@ -176,7 +176,7 @@
|
||||
</q-tooltip>
|
||||
</q-input>
|
||||
</div>
|
||||
<div v-show="!extendedParams && extendedParamsMessage" class="row q-mx-md items-center clickable" @click="extendedParams = true">
|
||||
<div v-show="!extendedParams && extendedParamsMessage" class="row q-mx-md items-center clickable" @click.stop.prevent="extendedParams = true">
|
||||
+{{ extendedParamsMessage }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,48 +204,13 @@
|
||||
</div>
|
||||
|
||||
<div class="row justify-center">
|
||||
<div class="q-mb-sm q-px-sm q-py-xs bg-cyan-2 clickable2" style="border: 1px solid #aaaaaa; border-radius: 6px; white-space: nowrap;" @click="openReleasePage">
|
||||
<div class="q-mb-lg q-px-sm q-py-xs bg-cyan-2 clickable2" style="border: 1px solid #aaaaaa; border-radius: 6px; white-space: nowrap;" @click.stop.prevent="openReleasePage">
|
||||
{{ projectName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog v-model="settingsDialogVisible">
|
||||
<template #header>
|
||||
<div class="row items-center" style="font-size: 110%">
|
||||
<q-icon class="q-mr-sm text-green" name="la la-cog" size="28px"></q-icon>
|
||||
Настройки
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="q-mx-md column" style="min-width: 300px; font-size: 120%;">
|
||||
<div class="row items-center q-ml-sm">
|
||||
<div class="q-mr-sm">
|
||||
Результатов на странице
|
||||
</div>
|
||||
<q-select
|
||||
v-model="limit" :options="limitOptions" class="bg-white"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-checkbox v-model="showCounts" size="36px" label="Показывать количество" />
|
||||
<q-checkbox v-model="showRates" size="36px" label="Показывать оценки" />
|
||||
<q-checkbox v-model="showInfo" size="36px" label="Показывать кнопку 'инфо'" />
|
||||
<q-checkbox v-model="showGenres" size="36px" label="Показывать жанры" />
|
||||
<q-checkbox v-model="showDates" size="36px" label="Показывать даты поступления" />
|
||||
<q-checkbox v-model="showDeleted" size="36px" label="Показывать удаленные" />
|
||||
<q-checkbox v-model="abCacheEnabled" size="36px" label="Кешировать запросы" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="settingsDialogVisible = false">
|
||||
OK
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<SettingsDialog v-model="settingsDialogVisible" />
|
||||
<SelectGenreDialog v-model="selectGenreDialogVisible" v-model:genre="search.genre" :genre-tree="genreTree" />
|
||||
<SelectLangDialog v-model="selectLangDialogVisible" v-model:lang="search.lang" :lang-list="langList" :lang-default="langDefault" />
|
||||
<SelectLibRateDialog v-model="selectLibRateDialogVisible" v-model:librate="search.librate" />
|
||||
@@ -263,6 +228,7 @@ import SeriesList from './SeriesList/SeriesList.vue';
|
||||
import TitleList from './TitleList/TitleList.vue';
|
||||
|
||||
import PageScroller from './PageScroller/PageScroller.vue';
|
||||
import SettingsDialog from './SettingsDialog/SettingsDialog.vue';
|
||||
import SelectGenreDialog from './SelectGenreDialog/SelectGenreDialog.vue';
|
||||
import SelectLangDialog from './SelectLangDialog/SelectLangDialog.vue';
|
||||
import SelectLibRateDialog from './SelectLibRateDialog/SelectLibRateDialog.vue';
|
||||
@@ -290,6 +256,7 @@ const componentOptions = {
|
||||
SeriesList,
|
||||
TitleList,
|
||||
PageScroller,
|
||||
SettingsDialog,
|
||||
SelectGenreDialog,
|
||||
SelectLangDialog,
|
||||
SelectLibRateDialog,
|
||||
@@ -328,27 +295,6 @@ const componentOptions = {
|
||||
|
||||
this.updatePageCount();
|
||||
},
|
||||
showCounts(newValue) {
|
||||
this.setSetting('showCounts', newValue);
|
||||
},
|
||||
showRates(newValue) {
|
||||
this.setSetting('showRates', newValue);
|
||||
},
|
||||
showInfo(newValue) {
|
||||
this.setSetting('showInfo', newValue);
|
||||
},
|
||||
showGenres(newValue) {
|
||||
this.setSetting('showGenres', newValue);
|
||||
},
|
||||
showDates(newValue) {
|
||||
this.setSetting('showDates', newValue);
|
||||
},
|
||||
showDeleted(newValue) {
|
||||
this.setSetting('showDeleted', newValue);
|
||||
},
|
||||
abCacheEnabled(newValue) {
|
||||
this.setSetting('abCacheEnabled', newValue);
|
||||
},
|
||||
$route(to) {
|
||||
this.updateListFromRoute(to);
|
||||
this.updateSearchFromRouteQuery(to);
|
||||
@@ -436,12 +382,6 @@ class Search {
|
||||
prevManualDate = '';
|
||||
|
||||
//settings
|
||||
showCounts = true;
|
||||
showRates = true;
|
||||
showInfo = true;
|
||||
showGenres = true;
|
||||
showDates = true;
|
||||
showDeleted = false;
|
||||
abCacheEnabled = true;
|
||||
langDefault = '';
|
||||
limit = 20;
|
||||
@@ -464,16 +404,6 @@ class Search {
|
||||
|
||||
bookInfo = {};
|
||||
|
||||
limitOptions = [
|
||||
{label: '10', value: 10},
|
||||
{label: '20', value: 20},
|
||||
{label: '50', value: 50},
|
||||
{label: '100', value: 100},
|
||||
{label: '200', value: 200},
|
||||
{label: '500', value: 500},
|
||||
{label: '1000', value: 1000},
|
||||
];
|
||||
|
||||
searchDateOptions = [
|
||||
{label: 'сегодня', value: 'today'},
|
||||
{label: 'за 3 дня', value: '3days'},
|
||||
@@ -530,12 +460,6 @@ class Search {
|
||||
this.extendedParams = settings.extendedParams;
|
||||
this.expanded = _.cloneDeep(settings.expanded);
|
||||
this.expandedSeries = _.cloneDeep(settings.expandedSeries);
|
||||
this.showCounts = settings.showCounts;
|
||||
this.showRates = settings.showRates;
|
||||
this.showInfo = settings.showInfo;
|
||||
this.showGenres = settings.showGenres;
|
||||
this.showDates = settings.showDates;
|
||||
this.showDeleted = settings.showDeleted;
|
||||
this.abCacheEnabled = settings.abCacheEnabled;
|
||||
this.langDefault = settings.langDefault;
|
||||
}
|
||||
|
||||
151
client/components/Search/SettingsDialog/SettingsDialog.vue
Normal file
151
client/components/Search/SettingsDialog/SettingsDialog.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<Dialog ref="dialog" v-model="dialogVisible">
|
||||
<template #header>
|
||||
<div class="row items-center" style="font-size: 110%">
|
||||
<q-icon class="q-mr-sm text-green" name="la la-cog" size="28px"></q-icon>
|
||||
Настройки
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="q-mx-md column" style="min-width: 300px; font-size: 120%;">
|
||||
<div class="row items-center q-ml-sm">
|
||||
<div class="q-mr-sm">
|
||||
Результатов на странице
|
||||
</div>
|
||||
<q-select
|
||||
v-model="limit" :options="limitOptions" class="bg-white"
|
||||
dropdown-icon="la la-angle-down la-sm"
|
||||
outlined dense emit-value map-options
|
||||
/>
|
||||
</div>
|
||||
|
||||
<q-checkbox v-model="showCounts" size="36px" label="Показывать количество" />
|
||||
<q-checkbox v-model="showRates" size="36px" label="Показывать оценки" />
|
||||
<q-checkbox v-model="showInfo" size="36px" label="Показывать кнопку (инфо)" />
|
||||
<q-checkbox v-model="showGenres" size="36px" label="Показывать жанры" />
|
||||
<q-checkbox v-model="showDates" size="36px" label="Показывать даты поступления" />
|
||||
<q-checkbox v-model="showDeleted" size="36px" label="Показывать удаленные" />
|
||||
<q-checkbox v-model="abCacheEnabled" size="36px" label="Кешировать запросы" />
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
|
||||
OK
|
||||
</q-btn>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
//-----------------------------------------------------------------------------
|
||||
import vueComponent from '../../vueComponent.js';
|
||||
|
||||
import Dialog from '../../share/Dialog.vue';
|
||||
|
||||
const componentOptions = {
|
||||
components: {
|
||||
Dialog
|
||||
},
|
||||
watch: {
|
||||
modelValue(newValue) {
|
||||
this.dialogVisible = newValue;
|
||||
},
|
||||
dialogVisible(newValue) {
|
||||
this.$emit('update:modelValue', newValue);
|
||||
},
|
||||
|
||||
settings() {
|
||||
this.loadSettings();
|
||||
},
|
||||
limit(newValue) {
|
||||
this.commit('setSettings', {'limit': newValue});
|
||||
},
|
||||
showCounts(newValue) {
|
||||
this.commit('setSettings', {'showCounts': newValue});
|
||||
},
|
||||
showRates(newValue) {
|
||||
this.commit('setSettings', {'showRates': newValue});
|
||||
},
|
||||
showInfo(newValue) {
|
||||
this.commit('setSettings', {'showInfo': newValue});
|
||||
},
|
||||
showGenres(newValue) {
|
||||
this.commit('setSettings', {'showGenres': newValue});
|
||||
},
|
||||
showDates(newValue) {
|
||||
this.commit('setSettings', {'showDates': newValue});
|
||||
},
|
||||
showDeleted(newValue) {
|
||||
this.commit('setSettings', {'showDeleted': newValue});
|
||||
},
|
||||
abCacheEnabled(newValue) {
|
||||
this.commit('setSettings', {'abCacheEnabled': newValue});
|
||||
},
|
||||
}
|
||||
};
|
||||
class SettingsDialog {
|
||||
_options = componentOptions;
|
||||
_props = {
|
||||
modelValue: Boolean,
|
||||
};
|
||||
|
||||
dialogVisible = false;
|
||||
|
||||
//settings
|
||||
limit = 20;
|
||||
showCounts = true;
|
||||
showRates = true;
|
||||
showInfo = true;
|
||||
showGenres = true;
|
||||
showDates = true;
|
||||
showDeleted = false;
|
||||
abCacheEnabled = true;
|
||||
|
||||
limitOptions = [
|
||||
{label: '10', value: 10},
|
||||
{label: '20', value: 20},
|
||||
{label: '50', value: 50},
|
||||
{label: '100', value: 100},
|
||||
{label: '200', value: 200},
|
||||
{label: '500', value: 500},
|
||||
{label: '1000', value: 1000},
|
||||
];
|
||||
|
||||
created() {
|
||||
this.commit = this.$store.commit;
|
||||
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
mounted() {
|
||||
}
|
||||
|
||||
get settings() {
|
||||
return this.$store.state.settings;
|
||||
}
|
||||
|
||||
loadSettings() {
|
||||
const settings = this.settings;
|
||||
|
||||
this.limit = settings.limit;
|
||||
|
||||
this.showCounts = settings.showCounts;
|
||||
this.showRates = settings.showRates;
|
||||
this.showInfo = settings.showInfo;
|
||||
this.showGenres = settings.showGenres;
|
||||
this.showDates = settings.showDates;
|
||||
this.showDeleted = settings.showDeleted;
|
||||
this.abCacheEnabled = settings.abCacheEnabled;
|
||||
}
|
||||
|
||||
okClick() {
|
||||
this.dialogVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default vueComponent(SettingsDialog);
|
||||
//-----------------------------------------------------------------------------
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
import moment from 'moment';
|
||||
import dayjs from 'dayjs';
|
||||
import {Buffer} from 'safe-buffer';
|
||||
//import _ from 'lodash';
|
||||
|
||||
@@ -121,11 +121,11 @@ export function isDigit(c) {
|
||||
}
|
||||
|
||||
export function dateFormat(date, format = 'DD.MM.YYYY') {
|
||||
return moment(date).format(format);
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
export function sqlDateFormat(date, format = 'DD.MM.YYYY') {
|
||||
return moment(date, 'YYYY-MM-DD').format(format);
|
||||
return dayjs(date, 'YYYY-MM-DD').format(format);
|
||||
}
|
||||
|
||||
export function isManualDate(date) {
|
||||
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "inpx-web",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "inpx-web",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "CC0-1.0",
|
||||
"dependencies": {
|
||||
"@quasar/extras": "^1.15.0",
|
||||
"axios": "^0.27.2",
|
||||
"chardet": "^1.5.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"express": "^4.18.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
@@ -20,7 +21,6 @@
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.6",
|
||||
"moment": "^2.29.4",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"quasar": "^2.7.5",
|
||||
"safe-buffer": "^5.2.1",
|
||||
@@ -3433,6 +3433,11 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
|
||||
"integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz",
|
||||
"integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ=="
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -5519,14 +5524,6 @@
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -11324,6 +11321,11 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz",
|
||||
"integrity": "sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA=="
|
||||
},
|
||||
"dayjs": {
|
||||
"version": "1.11.6",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.6.tgz",
|
||||
"integrity": "sha512-zZbY5giJAinCG+7AGaw0wIhNZ6J8AhWuSXKvuc1KAyMiRsvGQWqh4L+MomvhdAYjN+lqvVCMq1I41e3YHvXkyQ=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
@@ -12879,11 +12881,6 @@
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"dev": true
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.29.4",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
|
||||
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "inpx-web",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.4",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/inpx-web",
|
||||
@@ -52,6 +52,7 @@
|
||||
"@quasar/extras": "^1.15.0",
|
||||
"axios": "^0.27.2",
|
||||
"chardet": "^1.5.0",
|
||||
"dayjs": "^1.11.6",
|
||||
"express": "^4.18.1",
|
||||
"fs-extra": "^10.1.0",
|
||||
"iconv-lite": "^0.6.3",
|
||||
@@ -59,7 +60,6 @@
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"minimist": "^1.2.6",
|
||||
"moment": "^2.29.4",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"quasar": "^2.7.5",
|
||||
"safe-buffer": "^5.2.1",
|
||||
|
||||
@@ -16,7 +16,7 @@ module.exports = {
|
||||
|
||||
//поправить в случае, если были критические изменения в DbCreator или InpxParser
|
||||
//иначе будет рассинхронизация между сервером и клиентом на уровне БД
|
||||
dbVersion: '6',
|
||||
dbVersion: '7',
|
||||
dbCacheSize: 5,
|
||||
|
||||
maxPayloadSize: 500,//in MB
|
||||
|
||||
@@ -165,19 +165,19 @@ class WebSocketController {
|
||||
}
|
||||
|
||||
async getBookLink(req, ws) {
|
||||
if (!utils.hasProp(req, 'bookId'))
|
||||
throw new Error(`bookId is empty`);
|
||||
if (!utils.hasProp(req, 'bookUid'))
|
||||
throw new Error(`bookUid is empty`);
|
||||
|
||||
const result = await this.webWorker.getBookLink(req.bookId);
|
||||
const result = await this.webWorker.getBookLink(req.bookUid);
|
||||
|
||||
this.send(result, req, ws);
|
||||
}
|
||||
|
||||
async getBookInfo(req, ws) {
|
||||
if (!utils.hasProp(req, 'bookId'))
|
||||
throw new Error(`bookId is empty`);
|
||||
if (!utils.hasProp(req, 'bookUid'))
|
||||
throw new Error(`bookUid is empty`);
|
||||
|
||||
const result = await this.webWorker.getBookInfo(req.bookId);
|
||||
const result = await this.webWorker.getBookInfo(req.bookUid);
|
||||
|
||||
this.send(result, req, ws);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ class DbCreator {
|
||||
let librateMap = new Map();//оценка
|
||||
let librateArr = [];
|
||||
|
||||
let uidSet = new Set();//уникальные идентификаторы
|
||||
|
||||
//stats
|
||||
let authorCount = 0;
|
||||
let bookCount = 0;
|
||||
@@ -221,13 +223,14 @@ class DbCreator {
|
||||
let filtered = false;
|
||||
for (const rec of chunk) {
|
||||
//сначала фильтр
|
||||
if (!filter(rec)) {
|
||||
if (!filter(rec) || uidSet.has(rec._uid)) {
|
||||
rec.id = 0;
|
||||
filtered = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
rec.id = ++id;
|
||||
uidSet.add(rec._uid);
|
||||
|
||||
if (!rec.del) {
|
||||
bookCount++;
|
||||
@@ -269,6 +272,7 @@ class DbCreator {
|
||||
delMap = null;
|
||||
dateMap = null;
|
||||
librateMap = null;
|
||||
uidSet = null;
|
||||
|
||||
await db.close({table: 'book'});
|
||||
await db.freeMemory();
|
||||
@@ -624,6 +628,12 @@ class DbCreator {
|
||||
stats.filesDelCount = res.filesDelCount;
|
||||
}
|
||||
|
||||
//заодно добавим нужный индекс
|
||||
await db.create({
|
||||
in: 'book',
|
||||
hash: {field: '_uid', type: 'string', depth: 100, unique: true},
|
||||
});
|
||||
|
||||
countDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const ZipReader = require('./ZipReader');
|
||||
|
||||
const collectionInfo = 'collection.info';
|
||||
@@ -98,9 +99,13 @@ class InpxParser {
|
||||
if (line[line.length - 1] == '\x0D')
|
||||
line = line.substring(0, line.length - 1);
|
||||
|
||||
const rec = {};
|
||||
//уникальный идентификатор записи
|
||||
const sha256 = crypto.createHash('sha256');
|
||||
rec._uid = sha256.update(line).digest('base64');
|
||||
|
||||
//парсим запись
|
||||
const parts = line.split('\x04');
|
||||
const rec = {};
|
||||
|
||||
const len = (parts.length > structLen ? structLen : parts.length);
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
||||
@@ -58,9 +58,9 @@ class RemoteLib {
|
||||
}
|
||||
}
|
||||
|
||||
async downloadBook(bookPath, downFileName) {
|
||||
async downloadBook(bookUid) {
|
||||
try {
|
||||
const response = await await this.wsRequest({action: 'get-book-link', bookPath, downFileName});
|
||||
const response = await await this.wsRequest({action: 'get-book-link', bookUid});
|
||||
const link = response.link;
|
||||
|
||||
const buf = await this.down.load(`${this.remoteHost}${link}`, {decompress: false});
|
||||
|
||||
@@ -354,7 +354,7 @@ class WebWorker {
|
||||
}
|
||||
}
|
||||
|
||||
async restoreBook(bookPath, downFileName) {
|
||||
async restoreBook(bookUid, bookPath, downFileName) {
|
||||
const db = this.db;
|
||||
|
||||
let extractedFile = '';
|
||||
@@ -364,12 +364,12 @@ class WebWorker {
|
||||
extractedFile = await this.extractBook(bookPath);
|
||||
hash = await utils.getFileHash(extractedFile, 'sha256', 'hex');
|
||||
} else {
|
||||
hash = await this.remoteLib.downloadBook(bookPath, downFileName);
|
||||
hash = await this.remoteLib.downloadBook(bookUid);
|
||||
}
|
||||
|
||||
const link = `${this.config.filesPathStatic}/${hash}`;
|
||||
const bookFile = `${this.config.filesDir}/${hash}`;
|
||||
const bookFileDesc = `${bookFile}.json`;
|
||||
const bookFileDesc = `${bookFile}.d.json`;
|
||||
|
||||
if (!await fs.pathExists(bookFile) || !await fs.pathExists(bookFileDesc)) {
|
||||
if (!await fs.pathExists(bookFile) && extractedFile) {
|
||||
@@ -402,7 +402,7 @@ class WebWorker {
|
||||
return link;
|
||||
}
|
||||
|
||||
async getBookLink(bookId) {
|
||||
async getBookLink(bookUid) {
|
||||
this.checkMyState();
|
||||
|
||||
try {
|
||||
@@ -410,11 +410,11 @@ class WebWorker {
|
||||
let link = '';
|
||||
|
||||
//найдем bookPath и downFileName
|
||||
let rows = await db.select({table: 'book', where: `@@id(${db.esc(bookId)})`});
|
||||
let rows = await db.select({table: 'book', where: `@@hash('_uid', ${db.esc(bookUid)})`});
|
||||
if (!rows.length)
|
||||
throw new Error('404 Файл не найден');
|
||||
|
||||
const book = rows[0];
|
||||
const book = rows[0];
|
||||
let downFileName = book.file;
|
||||
const author = book.author.split(',');
|
||||
const at = [author[0], book.title];
|
||||
@@ -435,7 +435,7 @@ class WebWorker {
|
||||
if (rows.length) {//хеш найден по bookPath
|
||||
const hash = rows[0].hash;
|
||||
const bookFile = `${this.config.filesDir}/${hash}`;
|
||||
const bookFileDesc = `${bookFile}.json`;
|
||||
const bookFileDesc = `${bookFile}.d.json`;
|
||||
|
||||
if (await fs.pathExists(bookFile) && await fs.pathExists(bookFileDesc)) {
|
||||
link = `${this.config.filesPathStatic}/${hash}`;
|
||||
@@ -443,7 +443,7 @@ class WebWorker {
|
||||
}
|
||||
|
||||
if (!link) {
|
||||
link = await this.restoreBook(bookPath, downFileName)
|
||||
link = await this.restoreBook(bookUid, bookPath, downFileName)
|
||||
}
|
||||
|
||||
if (!link)
|
||||
@@ -458,30 +458,34 @@ class WebWorker {
|
||||
}
|
||||
}
|
||||
|
||||
async getBookInfo(bookId) {
|
||||
async getBookInfo(bookUid) {
|
||||
this.checkMyState();
|
||||
|
||||
try {
|
||||
const db = this.db;
|
||||
|
||||
let bookInfo = await this.getBookLink(bookId);
|
||||
let bookInfo = await this.getBookLink(bookUid);
|
||||
const hash = path.basename(bookInfo.link);
|
||||
const bookFile = `${this.config.filesDir}/${hash}`;
|
||||
const bookFileInfo = `${bookFile}.info`;
|
||||
const bookFileInfo = `${bookFile}.i.json`;
|
||||
|
||||
const restoreBookInfo = async() => {
|
||||
const restoreBookInfo = async(info) => {
|
||||
const result = {};
|
||||
|
||||
const rows = await db.select({table: 'book', where: `@@id(${db.esc(bookId)})`});
|
||||
let rows = await db.select({table: 'book', where: `@@hash('_uid', ${db.esc(bookUid)})`});
|
||||
if (!rows.length)
|
||||
throw new Error('404 Файл не найден');
|
||||
const book = rows[0];
|
||||
|
||||
result.book = book;
|
||||
result.cover = '';
|
||||
result.fb2 = false;
|
||||
let parser = null;
|
||||
|
||||
if (book.ext == 'fb2') {
|
||||
const {fb2, cover, coverExt} = await this.fb2Helper.getDescAndCover(bookFile);
|
||||
result.fb2 = fb2;
|
||||
parser = fb2;
|
||||
result.fb2 = fb2.rawNodes;
|
||||
|
||||
if (cover) {
|
||||
result.cover = `${this.config.filesPathStatic}/${hash}${coverExt}`;
|
||||
@@ -489,12 +493,16 @@ class WebWorker {
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
Object.assign(info ,result);
|
||||
await fs.writeFile(bookFileInfo, JSON.stringify(info));
|
||||
|
||||
if (this.config.branch === 'development') {
|
||||
await fs.writeFile(`${bookFile}.dev`, `${JSON.stringify(info, null, 2)}\n\n${parser ? parser.toString({format: true}) : ''}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (!await fs.pathExists(bookFileInfo)) {
|
||||
Object.assign(bookInfo, await restoreBookInfo());
|
||||
await fs.writeFile(bookFileInfo, JSON.stringify(bookInfo, null, 2));
|
||||
await restoreBookInfo(bookInfo);
|
||||
} else {
|
||||
await utils.touchFile(bookFileInfo);
|
||||
const info = await fs.readFile(bookFileInfo, 'utf-8');
|
||||
@@ -506,8 +514,7 @@ class WebWorker {
|
||||
coverFile = `${this.config.publicFilesDir}${tmpInfo.cover}`;
|
||||
|
||||
if (coverFile && !await fs.pathExists(coverFile)) {
|
||||
Object.assign(bookInfo, await restoreBookInfo());
|
||||
await fs.writeFile(bookFileInfo, JSON.stringify(bookInfo, null, 2));
|
||||
await restoreBookInfo(bookInfo);
|
||||
} else {
|
||||
bookInfo = tmpInfo;
|
||||
}
|
||||
|
||||
@@ -63,12 +63,11 @@ class Fb2Helper {
|
||||
pickNode: route => route.indexOf('fictionbook/body') !== 0,
|
||||
});
|
||||
|
||||
const desc = parser.$$('description').toObject();
|
||||
const coverImage = parser.inspector(desc).$('description/title-info/coverpage/image');
|
||||
const coverImage = parser.$$('/description/title-info/coverpage/image');
|
||||
|
||||
let cover = null;
|
||||
let coverExt = '';
|
||||
if (coverImage) {
|
||||
if (coverImage.count) {
|
||||
const coverAttrs = coverImage.attrs();
|
||||
const href = coverAttrs[`${parser.xlinkNS}:href`];
|
||||
let coverType = coverAttrs['content-type'];
|
||||
@@ -79,24 +78,21 @@ class Fb2Helper {
|
||||
const binaryId = (href[0] == '#' ? href.substring(1) : href);
|
||||
|
||||
//найдем нужный image
|
||||
parser.$$('binary').eachSelf(node => {
|
||||
for (const node of parser.$$array('/binary')) {
|
||||
let attrs = node.attrs();
|
||||
if (!attrs)
|
||||
return;
|
||||
attrs = Object.fromEntries(attrs);
|
||||
|
||||
if (attrs.id === binaryId) {
|
||||
const textNode = new Fb2Parser(node.value);
|
||||
const base64 = textNode.$self('*TEXT').value;
|
||||
|
||||
const base64 = node.text();
|
||||
cover = (base64 ? Buffer.from(base64, 'base64') : null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parser.remove('binary');
|
||||
return {fb2: parser.toObject(), cover, coverExt};
|
||||
return {fb2: parser, cover, coverExt};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ const XmlParser = require('../xml/XmlParser');
|
||||
class Fb2Parser extends XmlParser {
|
||||
get xlinkNS() {
|
||||
if (!this._xlinkNS) {
|
||||
const rootAttrs = this.$self().attrs();
|
||||
const rootAttrs = this.selectFirstSelf().attrs();
|
||||
let ns = 'l';
|
||||
for (const [key, value] of rootAttrs) {
|
||||
if (value == 'http://www.w3.org/1999/xlink') {
|
||||
@@ -18,27 +18,24 @@ class Fb2Parser extends XmlParser {
|
||||
return this._xlinkNS;
|
||||
}
|
||||
|
||||
bookInfo(fb2Object) {
|
||||
bookInfo() {
|
||||
const result = {};
|
||||
|
||||
if (!fb2Object)
|
||||
fb2Object = this.toObject();
|
||||
|
||||
const desc = this.inspector(fb2Object).$('fictionbook/description');
|
||||
const desc = this.$$('/description/');
|
||||
|
||||
if (!desc)
|
||||
return result;
|
||||
|
||||
const parseAuthors = (node, tagName) => {
|
||||
const authors = [];
|
||||
for (const a of node.$$(tagName)) {
|
||||
for (const a of node.$$array(tagName)) {
|
||||
let names = [];
|
||||
names.push(a.text('last-name'));
|
||||
names.push(a.text('first-name'));
|
||||
names.push(a.text('middle-name'));
|
||||
names.push(a.text('/last-name'));
|
||||
names.push(a.text('/first-name'));
|
||||
names.push(a.text('/middle-name'));
|
||||
names = names.filter(n => n);
|
||||
if (!names.length)
|
||||
names.push(a.text('nickname'));
|
||||
names.push(a.text('/nickname'));
|
||||
|
||||
authors.push(names.join(' '));
|
||||
}
|
||||
@@ -48,7 +45,7 @@ class Fb2Parser extends XmlParser {
|
||||
|
||||
const parseSequence = (node, tagName) => {
|
||||
const sequence = [];
|
||||
for (const s of node.$$(tagName)) {
|
||||
for (const s of node.$$array(tagName)) {
|
||||
const seqAttrs = s.attrs() || {};
|
||||
const name = seqAttrs['name'] || null;
|
||||
const num = seqAttrs['number'] || null;
|
||||
@@ -64,7 +61,7 @@ class Fb2Parser extends XmlParser {
|
||||
const info = {};
|
||||
|
||||
info.genre = [];
|
||||
for (const g of titleInfo.$$('genre'))
|
||||
for (const g of titleInfo.$$array('genre'))
|
||||
info.genre.push(g.text());
|
||||
|
||||
info.author = parseAuthors(titleInfo, 'author');
|
||||
@@ -77,7 +74,7 @@ class Fb2Parser extends XmlParser {
|
||||
info.annotationHtml = null;
|
||||
if (info.annotation) {
|
||||
//annotation как кусок xml
|
||||
info.annotationXml = (new XmlParser()).fromObject(info.annotation).toString({noHeader: true});
|
||||
info.annotationXml = titleInfo.$$('annotation/').toString({noHeader: true});
|
||||
|
||||
//annotation как html
|
||||
info.annotationHtml = this.toHtml(info.annotationXml);
|
||||
@@ -97,19 +94,19 @@ class Fb2Parser extends XmlParser {
|
||||
}
|
||||
|
||||
//title-info
|
||||
const titleInfo = desc.$('title-info');
|
||||
const titleInfo = desc.$$('title-info/');
|
||||
if (titleInfo) {
|
||||
result.titleInfo = parseTitleInfo(titleInfo);
|
||||
}
|
||||
|
||||
//src-title-info
|
||||
const srcTitleInfo = desc.$('src-title-info');
|
||||
const srcTitleInfo = desc.$$('src-title-info/');
|
||||
if (srcTitleInfo) {
|
||||
result.srcTitleInfo = parseTitleInfo(srcTitleInfo);
|
||||
}
|
||||
|
||||
//document-info
|
||||
const documentInfo = desc.$('document-info');
|
||||
const documentInfo = desc.$$('document-info/');
|
||||
if (documentInfo) {
|
||||
const info = {};
|
||||
|
||||
@@ -118,7 +115,7 @@ class Fb2Parser extends XmlParser {
|
||||
info.date = documentInfo.text('date');
|
||||
|
||||
info.srcUrl = [];
|
||||
for (const url of documentInfo.$$('src-url'))
|
||||
for (const url of documentInfo.$$array('src-url'))
|
||||
info.srcUrl.push(url.text());
|
||||
|
||||
info.srcOcr = documentInfo.text('src-ocr');
|
||||
@@ -131,7 +128,7 @@ class Fb2Parser extends XmlParser {
|
||||
info.historyHtml = null;
|
||||
if (info.history) {
|
||||
//history как кусок xml
|
||||
info.historyXml = (new XmlParser()).fromObject(info.history).toString({noHeader: true});
|
||||
info.historyXml = documentInfo.$$('history/').toString({noHeader: true});
|
||||
|
||||
//history как html
|
||||
info.historyHtml = this.toHtml(info.historyXml);
|
||||
@@ -143,7 +140,7 @@ class Fb2Parser extends XmlParser {
|
||||
}
|
||||
|
||||
//publish-info
|
||||
const publishInfo = desc.$('publish-info');
|
||||
const publishInfo = desc.$$('publish-info/');
|
||||
if (publishInfo) {
|
||||
const info = {};
|
||||
|
||||
@@ -160,7 +157,7 @@ class Fb2Parser extends XmlParser {
|
||||
return result;
|
||||
}
|
||||
|
||||
bookInfoList(fb2Object, options = {}) {
|
||||
bookInfoList(bookInfo, options = {}) {
|
||||
let {
|
||||
correctMapping = false,
|
||||
valueToString = false,
|
||||
@@ -236,7 +233,7 @@ class Fb2Parser extends XmlParser {
|
||||
];
|
||||
|
||||
mapping = correctMapping(mapping);
|
||||
const bookInfo = this.bookInfo(fb2Object);
|
||||
bookInfo = (bookInfo ? bookInfo : this.bookInfo());
|
||||
|
||||
//заполняем mapping
|
||||
let result = [];
|
||||
|
||||
@@ -3,7 +3,7 @@ class ObjectInspector {
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
makeSelector(selector) {
|
||||
narrowSelector(selector) {
|
||||
const result = [];
|
||||
selector = selector.trim();
|
||||
|
||||
@@ -31,7 +31,7 @@ class ObjectInspector {
|
||||
}
|
||||
|
||||
select(selector = '') {
|
||||
selector = this.makeSelector(selector);
|
||||
selector = this.narrowSelector(selector);
|
||||
|
||||
let raw = this.raw;
|
||||
for (const s of selector) {
|
||||
@@ -50,14 +50,10 @@ class ObjectInspector {
|
||||
}
|
||||
|
||||
if (raw === undefined || raw === null) {
|
||||
raw = null;
|
||||
break;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
if (raw === null)
|
||||
return [];
|
||||
|
||||
raw = (Array.isArray(raw) ? raw : [raw]);
|
||||
|
||||
const result = [];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const sax = require('./sax');
|
||||
const ObjectInspector = require('./ObjectInspector');
|
||||
|
||||
//node types
|
||||
const NODE = 1;
|
||||
@@ -22,7 +21,7 @@ const type2name = {
|
||||
};
|
||||
|
||||
class NodeBase {
|
||||
makeSelectorObj(selectorString) {
|
||||
wideSelector(selectorString) {
|
||||
const result = {all: false, before: false, type: 0, name: ''};
|
||||
|
||||
if (selectorString === '') {
|
||||
@@ -153,7 +152,7 @@ class NodeObject extends NodeBase {
|
||||
if (this.type !== NODE)
|
||||
return;
|
||||
|
||||
const selectorObj = this.makeSelectorObj(after);
|
||||
const selectorObj = this.wideSelector(after);
|
||||
|
||||
if (!Array.isArray(this.raw[3]))
|
||||
this.raw[3] = [];
|
||||
@@ -172,7 +171,7 @@ class NodeObject extends NodeBase {
|
||||
if (this.type !== NODE || !this.raw[3])
|
||||
return;
|
||||
|
||||
const selectorObj = this.makeSelectorObj(selector);
|
||||
const selectorObj = this.wideSelector(selector);
|
||||
|
||||
this.rawRemove(this.raw[3], selectorObj);
|
||||
if (!this.raw[3].length)
|
||||
@@ -233,6 +232,14 @@ class XmlParser extends NodeBase {
|
||||
return this.rawNodes.length;
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
const result = [];
|
||||
for (const n of this.rawNodes)
|
||||
result.push(new NodeObject(n));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
nodeObject(node) {
|
||||
return new NodeObject(node);
|
||||
}
|
||||
@@ -279,7 +286,7 @@ class XmlParser extends NodeBase {
|
||||
}
|
||||
|
||||
add(node, after = '*') {
|
||||
const selectorObj = this.makeSelectorObj(after);
|
||||
const selectorObj = this.wideSelector(after);
|
||||
|
||||
for (const n of this.rawNodes) {
|
||||
if (n && n[0] === NODE) {
|
||||
@@ -299,7 +306,7 @@ class XmlParser extends NodeBase {
|
||||
}
|
||||
|
||||
addRoot(node, after = '*') {
|
||||
const selectorObj = this.makeSelectorObj(after);
|
||||
const selectorObj = this.wideSelector(after);
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const node_ of node)
|
||||
@@ -312,7 +319,7 @@ class XmlParser extends NodeBase {
|
||||
}
|
||||
|
||||
remove(selector = '') {
|
||||
const selectorObj = this.makeSelectorObj(selector);
|
||||
const selectorObj = this.wideSelector(selector);
|
||||
|
||||
for (const n of this.rawNodes) {
|
||||
if (n && n[0] === NODE && Array.isArray(n[3])) {
|
||||
@@ -326,7 +333,7 @@ class XmlParser extends NodeBase {
|
||||
}
|
||||
|
||||
removeRoot(selector = '') {
|
||||
const selectorObj = this.makeSelectorObj(selector);
|
||||
const selectorObj = this.wideSelector(selector);
|
||||
|
||||
this.rawRemove(this.rawNodes, selectorObj);
|
||||
|
||||
@@ -409,7 +416,7 @@ class XmlParser extends NodeBase {
|
||||
|
||||
newRawNodes = res.rawNodes;
|
||||
} else {
|
||||
const selectorObj = this.makeSelectorObj(selector);
|
||||
const selectorObj = this.wideSelector(selector);
|
||||
|
||||
if (self) {
|
||||
this.rawSelect(this.rawNodes, selectorObj, (node) => {
|
||||
@@ -429,11 +436,7 @@ class XmlParser extends NodeBase {
|
||||
return new XmlParser(newRawNodes);
|
||||
}
|
||||
|
||||
$$(selector, self) {
|
||||
return this.select(selector, self);
|
||||
}
|
||||
|
||||
$$self(selector) {
|
||||
selectSelf(selector) {
|
||||
return this.select(selector, true);
|
||||
}
|
||||
|
||||
@@ -443,11 +446,7 @@ class XmlParser extends NodeBase {
|
||||
return new NodeObject(node);
|
||||
}
|
||||
|
||||
$(selector, self) {
|
||||
return this.selectFirst(selector, self);
|
||||
}
|
||||
|
||||
$self(selector) {
|
||||
selectFirstSelf(selector) {
|
||||
return this.selectFirst(selector, true);
|
||||
}
|
||||
|
||||
@@ -760,12 +759,138 @@ class XmlParser extends NodeBase {
|
||||
return this;
|
||||
}
|
||||
|
||||
inspector(obj) {
|
||||
if (!obj)
|
||||
obj = this.toObject();
|
||||
// XML Inspector start
|
||||
narrowSelector(selector) {
|
||||
const result = [];
|
||||
selector = selector.trim();
|
||||
|
||||
//последний индекс не учитывется, только если не задан явно
|
||||
if (selector && selector[selector.length - 1] == ']')
|
||||
selector += '/';
|
||||
|
||||
return new ObjectInspector(obj);
|
||||
const levels = selector.split('/');
|
||||
|
||||
for (const level of levels) {
|
||||
const [name, indexPart] = level.split('[');
|
||||
let index = 0;
|
||||
if (indexPart) {
|
||||
const i = indexPart.indexOf(']');
|
||||
index = parseInt(indexPart.substring(0, i), 10) || 0;
|
||||
}
|
||||
|
||||
let type = NODE;
|
||||
if (name[0] === '*') {
|
||||
const typeName = name.substring(1);
|
||||
type = name2type[typeName];
|
||||
if (!type)
|
||||
throw new Error(`Unknown selector type: ${typeName}`);
|
||||
}
|
||||
|
||||
result.push({type, name, index});
|
||||
}
|
||||
|
||||
if (result.length);
|
||||
result[result.length - 1].last = true;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inspect(selector = '') {
|
||||
selector = this.narrowSelector(selector);
|
||||
|
||||
let raw = this.rawNodes;
|
||||
for (const s of selector) {
|
||||
if (s.name) {
|
||||
let found = [];
|
||||
for (const n of raw) {
|
||||
if (n[0] === s.type && (n[0] !== NODE || s.name === '*NODE' || n[1] === s.name)) {
|
||||
found.push(n);
|
||||
|
||||
if (found.length > s.index && !s.last)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
raw = found;
|
||||
}
|
||||
|
||||
if (raw.length && !s.last) {
|
||||
if (s.index < raw.length) {
|
||||
raw = raw[s.index];
|
||||
if (raw[0] === NODE && raw[3])
|
||||
raw = raw[3];
|
||||
else {
|
||||
raw = [];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
raw = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new XmlParser(raw);
|
||||
}
|
||||
|
||||
$$(selector) {
|
||||
return this.inspect(selector);
|
||||
}
|
||||
|
||||
$$array(selector) {
|
||||
const res = this.inspect(selector);
|
||||
const result = [];
|
||||
for (const n of res.rawNodes)
|
||||
if (n[0] === NODE)
|
||||
result.push(new XmlParser([n]));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
$(selector) {
|
||||
const res = this.inspect(selector);
|
||||
const node = (res.count ? res.rawNodes[0] : null);
|
||||
return new NodeObject(node);
|
||||
}
|
||||
|
||||
v(selector = '') {
|
||||
const res = this.$(selector);
|
||||
return (res.type ? res.value : null);
|
||||
}
|
||||
|
||||
text(selector = '') {
|
||||
const res = this.$(`${selector}/*TEXT`);
|
||||
return (res.type === TEXT ? res.value : null);
|
||||
}
|
||||
|
||||
comment(selector = '') {
|
||||
const res = this.$(`${selector}/*COMMENT`);
|
||||
return (res.type === COMMENT ? res.value : null);
|
||||
}
|
||||
|
||||
cdata(selector = '') {
|
||||
const res = this.$(`${selector}/*CDATA`);
|
||||
return (res.type === CDATA ? res.value : null);
|
||||
}
|
||||
|
||||
concat(selector = '') {
|
||||
const res = this.$$(selector);
|
||||
const out = [];
|
||||
for (const n of res.rawNodes) {
|
||||
const node = new NodeObject(n);
|
||||
if (node.type && node.type !== NODE)
|
||||
out.push(node.value);
|
||||
}
|
||||
|
||||
return (out.length ? out.join('') : null);
|
||||
}
|
||||
|
||||
attrs(selector = '') {
|
||||
const res = this.$(selector);
|
||||
const attrs = res.attrs();
|
||||
return (res.type === NODE && attrs ? Object.fromEntries(attrs) : null);
|
||||
}
|
||||
// XML Inspector finish
|
||||
}
|
||||
|
||||
module.exports = XmlParser;
|
||||
@@ -191,7 +191,7 @@ function initStatic(app, config) {
|
||||
|
||||
if (path.extname(req.path) == '') {
|
||||
const bookFile = `${config.publicFilesDir}${req.path}`;
|
||||
const bookFileDesc = `${bookFile}.json`;
|
||||
const bookFileDesc = `${bookFile}.d.json`;
|
||||
|
||||
let downFileName = '';
|
||||
//восстановим из json-файла описания
|
||||
|
||||
Reference in New Issue
Block a user