Compare commits

..

49 Commits

Author SHA1 Message Date
Book Pauk
873a08fee1 Merge branch 'release/0.11.7' 2022-07-12 19:08:27 +07:00
Book Pauk
7e89228803 Версия 0.11.7 2022-07-12 19:07:39 +07:00
Book Pauk
fc630923a4 Настройка методов сортировки 2022-07-12 18:50:35 +07:00
Book Pauk
928f911d03 Добавлены подсказки к кнопкам 2022-07-12 17:53:14 +07:00
Book Pauk
7ffcd3fe1b Поправки поведения при скроллинге 2022-07-12 17:33:03 +07:00
Book Pauk
0efbaf643a Поправил сообщение об ошибке 2022-07-12 17:32:19 +07:00
Book Pauk
f1bf8e54ae Добавлен метод scrollToActiveBook 2022-07-12 17:10:50 +07:00
Book Pauk
b4aa6ab6c8 Поправки поиска 2022-07-12 16:58:34 +07:00
Book Pauk
72431f0202 Работа над группировкой 2022-07-12 16:51:32 +07:00
Book Pauk
04a326c0e4 Работа над группировкой 2022-07-12 15:51:43 +07:00
Book Pauk
931966f4f3 Поправки разметки 2022-07-12 15:05:17 +07:00
Book Pauk
8808cc4779 Работа над группировкой по файлам 2022-07-12 14:46:34 +07:00
Book Pauk
988c959eba Работа над группировкой файлов 2022-07-12 04:05:51 +07:00
Book Pauk
c0b658d9e6 К предыдущему 2022-07-12 01:41:18 +07:00
Book Pauk
3190246f34 Улучшена реакция на onResize 2022-07-12 01:35:19 +07:00
Book Pauk
d957b4a5f9 Добавлена возможность автосокрытия панели при прокрутке 2022-07-12 01:03:44 +07:00
Book Pauk
bef9e5705c Поправки текстовых строк 2022-07-11 23:53:54 +07:00
Book Pauk
eb2affa518 Приведение input к единому стилю 2022-07-11 23:50:51 +07:00
Book Pauk
07b9a3c033 Мелкие правки 2022-07-11 22:28:48 +07:00
Book Pauk
3ca14ae06a Работа над группировкой 2022-07-11 22:26:34 +07:00
Book Pauk
7caa0c2112 Начало добавления группировки в RecentBooksPage 2022-07-11 20:11:38 +07:00
Book Pauk
9c69f5bc01 Поправил размер иконки 2022-07-11 20:10:51 +07:00
Book Pauk
125a2e0f17 Исправление багов 2022-07-11 17:12:17 +07:00
Book Pauk
1b4360b897 Дополнение в convertRecent 2022-07-11 16:26:03 +07:00
Book Pauk
4775d6e47b Поправлен баг 2022-07-10 20:07:33 +07:00
Book Pauk
33fc553c55 Добавлен запрос на объединение позиций при
обнаружении похожего файла в загруженных
2022-07-10 19:54:00 +07:00
Book Pauk
25cad81c50 Улучшение отображения загруженных 2022-07-10 19:53:30 +07:00
Book Pauk
02a2099c1f Поправлен z-index 2022-07-10 19:52:58 +07:00
Book Pauk
1cda186b1a Добавлен диалог askYesNo 2022-07-10 19:52:29 +07:00
Book Pauk
f10291b6c6 Поправка названия действия 2022-07-10 19:51:31 +07:00
Book Pauk
26ab5d6765 Рефакторинг 2022-07-10 18:27:05 +07:00
Book Pauk
5edeed0747 Изменение механизма хранения книг 2022-07-10 17:31:21 +07:00
Book Pauk
c878ce432f Небольшое исправление опознававния кодировки 2022-07-10 17:20:47 +07:00
Book Pauk
81798897c8 Изменения в механизме хранения книг:
теперь ориентируемся на "ключ-filepath", а не "ключ-url"
2022-07-10 16:38:54 +07:00
Book Pauk
63840fadbc К предыдущему 2022-07-10 14:59:39 +07:00
Book Pauk
36aa057035 Поправка цвета 2022-07-09 21:00:09 +07:00
Book Pauk
30afd2421c Рефакторинг 2022-07-09 20:50:31 +07:00
Book Pauk
53a1d90bd8 Улучшение поведения при очереди загрузки книг 2022-07-09 02:01:14 +07:00
Book Pauk
2ecf6beef2 Небольшой багфикс 2022-07-09 01:56:42 +07:00
Book Pauk
85910a20e9 Улучшение ContentsPage 2022-07-08 20:50:55 +07:00
Book Pauk
66cf7790b3 Улучшения ContentsPage 2022-07-08 19:09:57 +07:00
Book Pauk
4a9eb7e4bb Удалил устаревшее 2022-07-08 14:30:44 +07:00
Book Pauk
07446696c1 Поправлен цвет заголовка 2022-07-08 13:52:45 +07:00
Book Pauk
a29f9d9a4b Унификация размеров окон 2022-07-08 13:43:59 +07:00
Book Pauk
d49c9baec3 Унификация интерфейса 2022-07-08 13:34:53 +07:00
Book Pauk
8c9d4a12ee Настройка цветов 2022-07-08 13:24:13 +07:00
Book Pauk
fce69e4657 Настройка цветов 2022-07-08 13:21:42 +07:00
Book Pauk
b387509f88 Добавил блокировку при загрузке книг, теперь загружаются последовательно 2022-07-08 12:26:47 +07:00
Book Pauk
8dc8bdc0d6 Merge tag '0.11.6-2' into develop
0.11.6-2
2022-07-07 19:43:47 +07:00
24 changed files with 1024 additions and 349 deletions

View File

@@ -11,7 +11,7 @@
Открыть выбранную закладку
</q-tooltip>
</q-btn>
<q-input ref="search" v-model="search" class="col" rounded outlined dense bg-color="white" placeholder="Найти">
<q-input ref="search" v-model="search" class="col" outlined dense bg-color="white" placeholder="Найти">
<template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click="resetSearch" />
</template>

View File

@@ -5,19 +5,19 @@
</template>
<template #buttons>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
<span class="header-button row justify-center items-center" @mousedown.stop @click="fullScreenToggle">
<q-icon :name="(fullScreenActive ? 'la la-compress-arrows-alt': 'la la-expand-arrows-alt')" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">На весь экран</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(0.1)">
<q-icon name="la la-plus" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Увеличить масштаб</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
<span class="header-button row justify-center items-center" @mousedown.stop @click="changeScale(-0.1)">
<q-icon name="la la-minus" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Уменьшить масштаб</q-tooltip>
</span>
<span class="full-screen-button row justify-center items-center" @mousedown.stop @click="showHelp">
<span class="header-button row justify-center items-center" @mousedown.stop @click="showHelp">
<q-icon name="la la-question-circle" size="16px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">Справка</q-tooltip>
</span>
@@ -32,7 +32,7 @@
:options="rootLinkOptions"
style="width: 230px"
dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options display-value-sanitize options-sanitize
outlined dense emit-value map-options display-value-sanitize options-sanitize
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
>
<template #prepend>
@@ -61,7 +61,7 @@
:options="selectedLinkOptions"
style="width: 50px"
dropdown-icon="la la-angle-down la-sm"
rounded outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
outlined dense emit-value map-options hide-selected display-value-sanitize options-sanitize
@popup-show="onSelectPopupShow" @popup-hide="onSelectPopupHide"
>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
@@ -73,7 +73,7 @@
ref="input"
v-model="bookUrl"
class="col q-mr-sm"
rounded outlined dense
outlined dense
bg-color="white"
placeholder="Скопируйте сюда ссылку на книгу и нажмите 'Открыть'"
@focus="selectAllOnFocus" @keydown="bookUrlKeyDown"
@@ -99,7 +99,7 @@
</template>
</q-input>
<q-btn :disabled="!bookUrl" rounded color="green-7" no-caps size="14px" @click="submitUrl">
<q-btn :disabled="!bookUrl" color="green-7" no-caps size="14px" @click="submitUrl">
Открыть
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Открыть в читалке
@@ -894,14 +894,15 @@ export default vueComponent(ExternalLibs);
background-color: #A0A0A0;
}
.full-screen-button {
.header-button {
width: 30px;
height: 30px;
cursor: pointer;
}
.full-screen-button:hover {
background-color: #69C05F;
.header-button:hover {
color: white;
background-color: #39902F;
}
.transparent-layout {

View File

@@ -23,15 +23,15 @@
<div class="q-mb-sm" />
<div v-show="selectedTab == 'contents'" class="tab-panel">
<div v-show="selectedTab == 'contents'" ref="tabPanelContents" class="tab-panel">
<div>
<div v-for="item in contents" :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 :ref="`mainitem${item.key}`" 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)">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="20px" />
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': item.expanded}" color="green-8" size="24px" />
</div>
<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" />
<q-icon name="la la-stop" class="icon" style="visibility: hidden" size="24px" />
</div>
<div class="col row clickable" @click="setBookPos(item.offset)">
<div :style="item.indentStyle"></div>
@@ -42,8 +42,12 @@
</div>
</div>
<div v-if="item.expanded" :ref="`subitem${item.key}`" class="subitems-transition">
<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 v-if="item.expanded" :ref="`subdiv${item.key}`" class="subitems-transition">
<div
v-for="subitem in item.list"
:ref="`subitem${subitem.key}`"
: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="no-expand-button"></div>
<div :style="subitem.indentStyle"></div>
@@ -61,10 +65,10 @@
</div>
</div>
<div v-show="selectedTab == 'images'" class="tab-panel">
<div v-show="selectedTab == 'images'" ref="tabPanelImages" class="tab-panel">
<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 :ref="`image${item.key}`" 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">
@@ -124,7 +128,10 @@ const componentOptions = {
watch: {
bookPos() {
this.updateBookPosSelection();
}
},
selectedTab() {
this.updateBookPosScrollTop();
},
},
};
class ContentsPage {
@@ -282,31 +289,30 @@ class ContentsPage {
if (!this.isVisible)
return;
await utils.sleep(50);
await this.$nextTick();
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);
if (bp >= item.offset && bp < nextOffset) {
item.isBookPos = true;
} else if (item.isBookPos) {
item.isBookPos = false;
}
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.contents[i] = Object.assign(item, {list: item.list});
this.updateBookPosScrollTop('contents', item, subitem, j);
} else if (subitem.isBookPos) {
subitem.isBookPos = false;
this.contents[i] = Object.assign(item, {list: item.list});
}
}
if (bp >= item.offset && bp < nextOffset) {
this.contents[i] = Object.assign(item, {isBookPos: true});
} else if (item.isBookPos) {
this.contents[i] = Object.assign(item, {isBookPos: false});
}
}
for (let i = 0; i < this.images.length; i++) {
@@ -314,11 +320,92 @@ class ContentsPage {
const nextOffset = (i < this.images.length - 1 ? this.images[i + 1].offset : this.parsed.textLength);
if (bp >= img.offset && bp < nextOffset) {
this.images[i] = Object.assign(img, {isBookPos: true});
this.images[i].isBookPos = true;
} else if (img.isBookPos) {
this.images[i] = Object.assign(img, {isBookPos: false});
this.images[i].isBookPos = false;
}
}
this.updateBookPosScrollTop();
}
/*getOffsetTop(key) {
let el = this.getFirstElem(this.$refs[`mainitem${key}`]);
return (el ? el.offsetTop : 0);
}*/
async updateBookPosScrollTop() {
try {
await this.$nextTick();
if (this.selectedTab == 'contents') {
let item;
let subitem;
let i;
//ищем выделенные item
for(const _item of this.contents) {
if (_item.isBookPos) {
item = _item;
for (let ii = 0; ii < item.list.length; ii++) {
const _subitem = item.list[ii];
if (_subitem.isBookPos) {
subitem = _subitem;
i = ii;
break;
}
}
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`mainitem${item.key}`]);
let elShift = 0;
if (subitem && item.expanded) {
const subEl = this.getFirstElem(this.$refs[`subitem${subitem.key}`]);
elShift = el.offsetHeight - subEl.offsetHeight*(i + 1);
} else {
elShift = el.offsetHeight;
}
const tabPanel = this.$refs.tabPanelContents;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - elShift;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
if (this.selectedTab == 'images') {
let item;
//ищем выделенные item
for(const _item of this.images) {
if (_item.isBookPos) {
item = _item;
break;
}
}
if (!item)
return;
//вычисляем и смещаем tabPanel.scrollTop
let el = this.getFirstElem(this.$refs[`image${item.key}`]);
const tabPanel = this.$refs.tabPanelImages;
const halfH = tabPanel.clientHeight/2;
const newScrollTop = el.offsetTop - halfH - el.offsetHeight/2;
if (newScrollTop < 20 + tabPanel.scrollTop - halfH || newScrollTop > -20 + tabPanel.scrollTop + halfH)
tabPanel.scrollTop = newScrollTop;
}
} catch (e) {
console.error(e);
}
}
getFirstElem(items) {
@@ -330,8 +417,8 @@ class ContentsPage {
const expanded = !item.expanded;
if (!expanded) {
let subitems = this.getFirstElem(this.$refs[`subitem${key}`]);
subitems.style.height = '0';
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subdiv.style.height = '0';
await utils.sleep(200);
}
@@ -339,8 +426,8 @@ class ContentsPage {
if (expanded) {
await this.$nextTick();
let subitems = this.getFirstElem(this.$refs[`subitem${key}`]);
subitems.style.height = subitems.scrollHeight + 'px';
let subdiv = this.getFirstElem(this.$refs[`subdiv${key}`]);
subdiv.style.height = subdiv.scrollHeight + 'px';
}
}

View File

@@ -5,13 +5,20 @@
</template>
<div class="col column" style="min-width: 600px">
<q-btn-toggle
v-model="selectedTab"
toggle-color="primary"
no-caps unelevated
:options="buttons"
/>
<div class="separator"></div>
<div class="bg-grey-3 row">
<q-tabs
v-model="selectedTab"
active-color="black"
active-bg-color="white"
indicator-color="white"
dense
no-caps
inline-label
class="bg-grey-4 text-grey-7"
>
<q-tab v-for="btn in buttons" :key="btn.value" :name="btn.value" :label="btn.label" />
</q-tabs>
</div>
<keep-alive>
<component :is="activePage" ref="page" class="col"></component>
@@ -93,8 +100,4 @@ export default vueComponent(HelpPage);
</script>
<style scoped>
.separator {
height: 1px;
background-color: #E0E0E0;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div v-show="visible" class="column justify-center items-center z-max" style="background-color: rgba(0, 0, 0, 0.8)">
<div v-show="visible" class="column justify-center items-center" style="background-color: rgba(0, 0, 0, 0.8); z-index: 100;">
<div class="column justify-start items-center" style="height: 250px">
<q-circular-progress
show-value

View File

@@ -141,6 +141,7 @@
@load-file="loadFile"
@book-pos-changed="bookPosChanged"
@do-action="doAction"
@hide-tool-bar="hideToolBar"
></component>
</keep-alive>
@@ -201,6 +202,7 @@ import miscApi from '../../api/misc';
import {versionHistory} from './versionHistory';
import * as utils from '../../share/utils';
import LockQueue from '../../share/LockQueue';
const componentOptions = {
components: {
@@ -313,6 +315,8 @@ class Reader {
this.reader = this.$store.state.reader;
this.config = this.$store.state.config;
this.lock = new LockQueue(100);
this.$root.addEventHook('key', this.keyHook);
this.lastActivePage = false;
@@ -345,6 +349,13 @@ class Reader {
this.debouncedSetRecentBook(newValue);
}, 15000, {maxWait: 20000});
this.debouncedHideToolBar = _.debounce((event) => {
if (this.toolBarHideOnScroll && this.toolBarActive !== !!event.show) {
this.commit('reader/setToolBarActive', !!event.show);
this.$root.eventHook('resize');
}
}, 200);
document.addEventListener('fullscreenchange', () => {
this.fullScreenActive = (document.fullscreenElement !== null);
});
@@ -402,6 +413,7 @@ class Reader {
this.clickControlActive = this.clickControl;
this.blinkCachedLoad = settings.blinkCachedLoad;
this.showToolButton = settings.showToolButton;
this.toolBarHideOnScroll = settings.toolBarHideOnScroll;
this.enableSitesFilter = settings.enableSitesFilter;
this.showNeedUpdateNotify = settings.showNeedUpdateNotify;
this.splitToPara = settings.splitToPara;
@@ -662,6 +674,10 @@ class Reader {
this.$root.eventHook('resize');
}
hideToolBar(event) {
this.debouncedHideToolBar(event);
}
fullScreenToggle() {
this.fullScreenActive = !this.fullScreenActive;
if (this.fullScreenActive) {
@@ -897,7 +913,7 @@ class Reader {
refreshBook() {
const mrb = this.mostRecentBook();
this.loadBook({url: mrb.url, uploadFileName: mrb.uploadFileName, force: true});
this.loadBook(Object.assign({}, mrb, {force: true}));
}
undoAction() {
@@ -1051,7 +1067,7 @@ class Reader {
return result;
}
async loadBook(opts) {
async _loadBook(opts) {
if (!opts || !opts.url) {
this.mostRecentBook();
return;
@@ -1061,10 +1077,6 @@ class Reader {
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) &&
(url.indexOf('disk://') != 0))
url = 'http://' + url;
@@ -1091,33 +1103,36 @@ class Reader {
progress.show();
progress.setState({state: 'parse'});
// есть ли среди недавних
const key = bookManager.keyFromUrl(url);
let wasOpened = await bookManager.getRecentBook({key});
wasOpened = (wasOpened ? wasOpened : {});
const bookPos = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos);
const bookPosSeen = (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen);
const uploadFileName = (opts.uploadFileName ? opts.uploadFileName : '');
// есть ли среди загруженных
let wasOpened = bookManager.findRecentByUrlAndPath(url, opts.path);
wasOpened = (wasOpened ? _.cloneDeep(wasOpened) : {});
wasOpened = Object.assign(wasOpened, {
path: (opts.path !== undefined ? opts.path : wasOpened.path),
bookPos: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPos),
bookPosSeen: (opts.bookPos !== undefined ? opts.bookPos : wasOpened.bookPosSeen),
uploadFileName: (opts.uploadFileName ? opts.uploadFileName : wasOpened.uploadFileName),
});
let book = null;
if (!opts.force) {
// пытаемся загрузить и распарсить книгу в менеджере из локального кэша
const bookParsed = await bookManager.getBook({url, path: opts.path}, (prog) => {
const bookParsed = await bookManager.getBook(wasOpened, (prog) => {
progress.setState({progress: prog});
});
// если есть в локальном кэше
if (bookParsed) {
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen}, bookParsed));
await bookManager.setRecentBook(Object.assign(wasOpened, bookParsed));
this.mostRecentBook();
this.addAction(bookPos);
this.addAction(wasOpened.bookPos);
this.loaderActive = false;
progress.hide(); this.progressActive = false;
this.blinkCachedLoadMessage();
this.checkBookPosPercent();
await this.activateClickMapPage();
this.activateClickMapPage();//no await
return;
}
@@ -1131,7 +1146,7 @@ class Reader {
});
book = Object.assign({}, wasOpened, {data: resp.data});
} catch (e) {
//молчим
this.$root.notify.error('Конвертированный файл не найден на сервере.<br>Пробуем загрузить оригинал.', 'Ошибка загрузки');
}
}
}
@@ -1142,7 +1157,7 @@ class Reader {
if (!book) {
book = await readerApi.loadBook({
url,
uploadFileName,
uploadFileName: wasOpened.uploadFileName,
enableSitesFilter: this.enableSitesFilter,
skipHtmlCheck: (this.splitToPara ? true : false),
isText: (this.splitToPara ? true : false),
@@ -1159,14 +1174,44 @@ class Reader {
// добавляем в bookManager
progress.setState({state: 'parse', step: 5});
const addedBook = await bookManager.addBook(book, (prog) => {
progress.setState({progress: prog});
});
// sameBookKey
if (url.indexOf('disk://') == 0) {
//ищем такой файл в загруженных
let found = bookManager.findRecentBySameBookKey(wasOpened.uploadFileName);
found = (found ? _.cloneDeep(found) : found);
if (found) {
if (wasOpened.sameBookKey != found.sameBookKey) {
//спрашиваем, надо ли объединить файлы
const askResult = bookManager.keysEqual(found.path, addedBook.path) ||
await this.$root.stdDialog.askYesNo(`
Файл с именем "${wasOpened.uploadFileName}" уже есть в загруженных.
<br>Объединить позицию?`, 'Найдена похожая книга');
if (askResult) {
wasOpened.bookPos = found.bookPos;
wasOpened.bookPosSeen = found.bookPosSeen;
wasOpened.sameBookKey = found.sameBookKey;
}
}
} else {
wasOpened.sameBookKey = wasOpened.uploadFileName;
}
} else {
wasOpened.sameBookKey = addedBook.url;
}
if (!bookManager.keysEqual(wasOpened.path, addedBook.path))
delete wasOpened.loadTime;
// добавляем в историю
await bookManager.setRecentBook(Object.assign({bookPos, bookPosSeen, uploadFileName}, addedBook));
await bookManager.setRecentBook(Object.assign(wasOpened, addedBook));
this.mostRecentBook();
this.addAction(bookPos);
this.addAction(wasOpened.bookPos);
this.updateRoute(true);
this.loaderActive = false;
@@ -1177,11 +1222,11 @@ class Reader {
this.stopBlink = true;
this.checkBookPosPercent();
await this.activateClickMapPage();
this.activateClickMapPage();//no await
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
if (!this.showHelpOnErrorIfNeeded(e.message)) {
if (!this.showHelpOnErrorIfNeeded(url)) {
this.$root.stdDialog.alert(e.message, 'Ошибка', {color: 'negative'});
}
} finally {
@@ -1189,7 +1234,16 @@ class Reader {
}
}
async loadFile(opts) {
async loadBook(opts) {
await this.lock.get();
try {
await this._loadBook(opts);
} finally {
this.lock.ret();
}
}
async _loadFile(opts) {
this.progressActive = true;
await this.$nextTick();
@@ -1205,7 +1259,7 @@ class Reader {
progress.hide(); this.progressActive = false;
await this.loadBook({url, uploadFileName: opts.file.name, force: true});
await this._loadBook({url, uploadFileName: opts.file.name, force: true});
} catch (e) {
progress.hide(); this.progressActive = false;
this.loaderActive = true;
@@ -1213,6 +1267,15 @@ class Reader {
}
}
async loadFile(opts) {
await this.lock.get();
try {
await this._loadFile(opts);
} finally {
this.lock.ret();
}
}
blinkCachedLoadMessage() {
if (!this.blinkCachedLoad)
return;

View File

@@ -54,7 +54,7 @@
<br><br>
<div class="row justify-center">
<!--q-btn class="q-px-sm" color="primary" dense no-caps rounded @click="openDonate">
<!--q-btn class="q-px-sm" color="primary" dense no-caps @click="openDonate">
Помочь проекту
</q-btn-->
</div>

View File

@@ -9,88 +9,153 @@
<a ref="download" style="display: none;" target="_blank"></a>
<q-table
class="recent-books-table col"
:rows="tableData"
row-key="key"
:columns="columns"
:pagination="pagination"
separator="cell"
hide-bottom
virtual-scroll
dense
>
<template #header="props">
<q-tr :props="props">
<q-th key="num" class="td-mp" style="width: 25px" :props="props">
<span v-html="props.cols[0].label"></span>
</q-th>
<q-th key="date" class="td-mp break-word" style="width: 77px" :props="props">
<span v-html="props.cols[1].label"></span>
</q-th>
<q-th key="desc" class="td-mp" style="width: 332px" :props="props" colspan="4">
<q-input ref="input" v-model="search"
outlined dense rounded style="position: absolute; top: 6px; left: 90px; width: 380px" bg-color="white"
placeholder="Найти"
@click.stop
>
<template #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>
</q-th>
</q-tr>
</template>
<div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
<div ref="header" class="scroll-header row bg-blue-2">
<q-btn class="tool-button" round @click="showSameBookClick">
<q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': showSameBook}" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Показать/скрыть версии книг
</q-tooltip>
</q-btn>
<template #body="props">
<q-tr :props="props">
<q-td key="num" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 25px">
{{ props.row.num }}
<q-btn class="tool-button" round @click="scrollToBegin">
<q-icon name="la la-arrow-up" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В начало списка
</q-tooltip>
</q-btn>
<q-btn class="tool-button" round @click="scrollToEnd">
<q-icon name="la la-arrow-down" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
В конец списка
</q-tooltip>
</q-btn>
<q-btn class="tool-button" round @click="scrollToActiveBook">
<q-icon name="la la-location-arrow" color="green-8" size="24px" />
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
На текущую книгу
</q-tooltip>
</q-btn>
<q-select
ref="sortMethod"
v-model="sortMethod"
class="q-ml-md q-mt-xs"
:options="sortMethodOptions"
style="width: 180px"
bg-color="white"
dropdown-icon="la la-angle-down la-sm"
outlined dense emit-value map-options display-value-sanitize options-sanitize
options-html display-value-html
@update:model-value="sortMethodSelected"
>
<q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
Метод сортировки
</q-tooltip>
<template #selected-item="scope">
<div style="height: 28px; padding-top: 2px; overflow: hidden" v-html="scope.opt.label" />
</template>
</q-select>
<q-input
ref="input"
v-model="search"
class="q-ml-sm q-mt-xs"
outlined dense
style="width: 180px"
bg-color="white"
placeholder="Найти"
@click.stop
>
<template #append>
<q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
</template>
</q-input>
</div>
<q-virtual-scroll
ref="virtualScroll"
v-slot="{ item, index }"
:items="tableData"
scroll-target="#vs-container"
virtual-scroll-item-size="80"
@virtual-scroll="onScroll"
>
<div class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
<div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px; border-right: 1px solid #cccccc">
<q-icon name="la la-code-branch" size="24px" style="color: green" />
</div>
<div class="row-part column justify-center items-stretch" style="width: 80px">
<div class="col row justify-center items-center clickable" @click="loadBook(item)">
<q-icon name="la la-book" size="40px" style="color: #dddddd" />
</div>
</q-td>
<q-td key="date" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
<div class="break-word" style="width: 68px">
{{ props.row.touchDate }}<br>
{{ props.row.touchTime }}
<div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
{{ (item.group ? item.group.length + 1 : 0) }} верси{{ wordEnding((item.group ? item.group.length + 1 : 0), 1) }}
</div>
</q-td>
</div>
<q-td key="desc" auto-width :props="props" class="td-mp clickable" @click="loadBook(props.row.url)">
<div class="break-word" style="width: 332px; font-size: 90%">
<div style="color: green">
{{ props.row.desc.author }}
<div class="row-part column items-stretch clickable break-word" :style="{ 'width': (350 - 40*(+item.inGroup)) + 'px' }" style="font-size: 75%" @click="loadBook(item)">
<div class="row" style="font-size: 80%">
<div class="row justify-center row-info-top" style="width: 30px">
{{ item.num }}
</div>
<div class="row justify-center row-info-top" style="width: 130px">
Читался: {{ item.touchTime }}
</div>
<div class="row justify-center row-info-top" style="width: 138px">
Загружен: {{ item.loadTime }}
</div>
<div class="row justify-center row-info-top" style="width: 1px">
</div>
<div>{{ props.row.desc.title }}</div>
<div class="read-bar" :style="`width: ${332*props.row.readPart}px`"></div>
</div>
</q-td>
<q-td key="links" :props="props" class="td-mp" auto-width>
<div class="break-word" style="width: 75px; font-size: 90%">
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path, props.row.fullTitle)">Скачать FB2</a>
<div class="col q-mt-xs" :style="{ 'width': (340 - 40*(+item.inGroup)) + 'px' }">
<div class="text-green-10" style="font-size: 105%">
{{ item.desc.author }}
</div>
<div>{{ item.desc.title }}</div>
<!--div>{{ item.path }}</div-->
</div>
</q-td>
<q-td key="close" :props="props" class="td-mp" auto-width>
<div style="width: 38px">
<q-btn
dense
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(props.row.key)"
>
<q-icon class="la la-times" size="14px" />
</q-btn>
<div class="row q-mt-xs" style="font-size: 80%">
<div class="row justify-center row-info-bottom" style="width: 60px">
{{ item.desc.textLen }}
</div>
<div class="row justify-center row-info-bottom" style="width: 60px">
{{ item.desc.perc }}
</div>
<div class="row justify-center row-info-bottom" style="width: 1px">
</div>
</div>
</q-td>
<q-td key="last" :props="props" class="no-mp">
</q-td>
</q-tr>
</template>
</q-table>
<div class="read-bar" :style="`width: ${(340 - 40*(+item.inGroup))*item.readPart}px`"></div>
</div>
<div class="row-part column justify-center" style="width: 80px; font-size: 75%">
<div>
<a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
<a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
</div>
</div>
<div class="row-part column justify-center">
<q-btn
dense
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
@click="handleDel(item.key)"
>
<q-icon class="la la-times" size="14px" />
</q-btn>
</div>
</div>
</q-virtual-scroll>
</div>
</Window>
</template>
@@ -99,9 +164,10 @@
import vueComponent from '../../vueComponent.js';
import path from 'path-browserify';
//import _ from 'lodash';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import LockQueue from '../../../share/LockQueue';
import Window from '../../share/Window.vue';
import bookManager from '../share/bookManager';
import readerApi from '../../../api/reader';
@@ -111,9 +177,15 @@ const componentOptions = {
Window,
},
watch: {
search: function() {
search() {
this.updateTableData();
}
},
sortMethod() {
this.updateTableData();
},
settings() {
this.loadSettings();
},
},
};
class RecentBooksPage {
@@ -122,52 +194,18 @@ class RecentBooksPage {
loading = false;
search = '';
tableData = [];
columns = [];
pagination = {};
sortMethod = '';
showSameBook = false;
created() {
this.firstInit = true;
this.pagination = {rowsPerPage: 0};
this.commit = this.$store.commit;
this.columns = [
{
name: 'num',
label: '#',
align: 'center',
sortable: true,
field: 'num',
},
{
name: 'date',
label: 'Время<br>просм.',
align: 'left',
field: 'touchDateTime',
sortable: true,
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
},
{
name: 'desc',
label: 'Название',
align: 'left',
field: 'descString',
sortable: true,
},
{
name: 'links',
label: '',
align: 'left',
},
{
name: 'close',
label: '',
align: 'left',
},
{
name: 'last',
label: '',
align: 'left',
},
];
this.lastScrollTop1 = 0;
this.lastScrollTop2 = 0;
this.lock = new LockQueue(100);
this.loadSettings();
}
init() {
@@ -176,89 +214,186 @@ class RecentBooksPage {
this.$nextTick(() => {
//this.$refs.input.focus();//плохо на планшетах
});
(async() => {//подгрузка списка
if (this.initing)
return;
this.initing = true;
if (this.firstInit) {//для отзывчивости
await this.updateTableData(20);
this.firstInit = false;
}
await utils.sleep(50);
this.inited = true;
(async() => {
this.showBar();
await this.updateTableData();
this.initing = false;
await this.scrollToActiveBook();
})();
}
async updateTableData(limit) {
while (this.updating) await utils.sleep(100);
this.updating = true;
let result = [];
loadSettings() {
const settings = this.settings;
this.showSameBook = settings.recentShowSameBook;
this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
}
this.loading = !!limit;
const sorted = bookManager.getSortedRecent();
get settings() {
return this.$store.state.reader.settings;
}
let num = 0;
for (let i = 0; i < sorted.length; i++) {
const book = sorted[i];
if (book.deleted)
continue;
async updateTableData() {
if (!this.inited)
return;
num++;
if (limit && result.length >= limit)
break;
await this.lock.get();
try {
let result = [];
let d = new Date();
d.setTime(book.touchTime);
const t = utils.formatDate(d).split(' ');
const sorted = bookManager.getSortedRecent();
const activeBook = bookManager.mostRecentBook();
let readPart = 0;
let perc = '';
let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) {
readPart = p/book.textLength;
perc = ` [${(readPart*100).toFixed(2)}%]`;
textLen = ` ${Math.round(book.textLength/1000)}k`;
//подготовка полей
for (const book of sorted) {
if (book.deleted)
continue;
let d = new Date();
d.setTime(book.touchTime);
const touchTime = utils.formatDate(d);
const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
d.setTime(loadTimeRaw);
const loadTime = utils.formatDate(d);
let readPart = 0;
let perc = '';
let textLen = '';
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
if (book.textLength) {
readPart = p/book.textLength;
perc = `${(readPart*100).toFixed(2)}%`;
textLen = `${Math.floor(readPart*book.textLength/1000)}/${Math.floor(book.textLength/1000)}`;
}
const bt = utils.getBookTitle(book.fb2);
let title = bt.bookTitle;
title = (title ? `"${title}"`: '');
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
result.push({
touchTime,
loadTime,
desc: {
author,
title,
perc,
textLen,
},
readPart,
url: book.url,
path: book.path,
fullTitle: bt.fullTitle,
key: book.key,
sameBookKey: book.sameBookKey,
active: (activeBook.key == book.key),
activeParent: false,
inGroup: false,
//для сортировки
loadTimeRaw,
touchTimeRaw: book.touchTime,
});
}
const bt = utils.getBookTitle(book.fb2);
//нумерация
let num = 0;
let title = bt.bookTitle;
title = (title ? `"${title}"`: '');
const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : book.url));
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
for (const book of result) {
num++;
book.num = num;
}
result.push({
num,
touchDateTime: book.touchTime,
touchDate: t[0],
touchTime: t[1],
desc: {
author,
title: `${title}${perc}${textLen}`,
},
readPart,
descString: `${author}${title}${perc}${textLen}`,//для сортировки
url: book.url,
path: book.path,
fullTitle: bt.fullTitle,
key: book.key,
});
//фильтрация
const search = this.search;
if (search) {
result = result.filter(item => {
return !search ||
item.touchTime.includes(search) ||
item.loadTime.includes(search) ||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
item.desc.author.toLowerCase().includes(search.toLowerCase())
});
}
//сортировка
switch (this.sortMethod) {
case 'loadTimeDesc':
result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
break;
case 'loadTimeAsc':
result.sort((a, b) => a.loadTimeRaw - b.loadTimeRaw);
break;
case 'touchTimeDesc':
result.sort((a, b) => b.touchTimeRaw - a.touchTimeRaw);
break;
case 'touchTimeAsc':
result.sort((a, b) => a.touchTimeRaw - b.touchTimeRaw);
break;
case 'authorDesc':
result.sort((a, b) => b.desc.author.localeCompare(a.desc.author));
break;
case 'authorAsc':
result.sort((a, b) => a.desc.author.localeCompare(b.desc.author));
break;
case 'titleDesc':
result.sort((a, b) => b.desc.title.localeCompare(a.desc.title));
break;
case 'titleAsc':
result.sort((a, b) => a.desc.title.localeCompare(b.desc.title));
break;
}
//группировка
const groups = {};
const parents = {};
let newResult = [];
for (const book of result) {
if (book.sameBookKey !== undefined) {
if (!groups[book.sameBookKey]) {
groups[book.sameBookKey] = [];
parents[book.sameBookKey] = book;
book.group = groups[book.sameBookKey];
newResult.push(book);
} else {
book.inGroup = true;
if (book.active)
parents[book.sameBookKey].activeParent = true;
groups[book.sameBookKey].push(book);
}
} else {
newResult.push(book);
}
}
result = newResult;
//showSameBook
if (this.showSameBook) {
newResult = [];
for (const book of result) {
newResult.push(book);
if (book.group) {
for (const sameBook of book.group) {
newResult.push(sameBook);
}
}
}
result = newResult;
}
//другие стадии
//.....
this.tableData = result;
} finally {
this.lock.ret();
}
const search = this.search;
result = result.filter(item => {
return !search ||
item.touchTime.includes(search) ||
item.touchDate.includes(search) ||
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
item.desc.author.toLowerCase().includes(search.toLowerCase())
});
this.tableData = result;
this.updating = false;
}
resetSearch() {
@@ -266,19 +401,22 @@ class RecentBooksPage {
this.$refs.input.focus();
}
wordEnding(num) {
const endings = ['', 'а', 'и', 'и', 'и', '', '', '', '', ''];
wordEnding(num, type = 0) {
const endings = [
['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й']
];
const deci = num % 100;
if (deci > 10 && deci < 20) {
return '';
return endings[type][0];
} else {
return endings[num % 10];
return endings[type][num % 10];
}
}
get header() {
const len = (this.tableData ? this.tableData.length : 0);
return `${(this.search ? 'Найдено' : 'Всего')} ${len} книг${this.wordEnding(len)}`;
return `${(this.search ? 'Найдено' : 'Всего')} ${len} файл${this.wordEnding(len)}`;
}
async downloadBook(fb2path, fullTitle) {
@@ -311,8 +449,8 @@ class RecentBooksPage {
this.close();
}
loadBook(url) {
this.$emit('load-book', {url});
loadBook(row) {
this.$emit('load-book', {url: row.url, path: row.path});
this.close();
}
@@ -323,6 +461,111 @@ class RecentBooksPage {
return false;
}
showBar() {
this.lastScrollTop1 = this.$refs.vsContainer.scrollTop;
this.$refs.header.style.position = 'sticky';
this.$refs.header.style.top = 0;
}
onScroll() {
const curScrollTop = this.$refs.vsContainer.scrollTop;
if (this.lockScroll) {
this.lastScrollTop1 = curScrollTop;
return;
}
if (curScrollTop - this.lastScrollTop1 > 100) {
this.$refs.header.style.top = `-${this.$refs.header.offsetHeight}px`;
this.$refs.header.style.transition = 'top 0.2s ease 0s';
this.lastScrollTop1 = curScrollTop;
} else if (curScrollTop - this.lastScrollTop2 < 0) {
this.$refs.header.style.position = 'sticky';
this.$refs.header.style.top = 0;
this.lastScrollTop1 = curScrollTop;
}
this.lastScrollTop2 = curScrollTop;
}
showSameBookClick() {
this.showSameBook = !this.showSameBook;
const newSettings = _.cloneDeep(this.settings);
newSettings.recentShowSameBook = this.showSameBook;
this.commit('reader/setSettings', newSettings);
this.updateTableData();
}
sortMethodSelected() {
const newSettings = _.cloneDeep(this.settings);
newSettings.recentSortMethod = this.sortMethod;
this.commit('reader/setSettings', newSettings);
}
async scrollToActiveBook() {
this.lockScroll = true;
try {
let activeIndex = -1;
let activeParentIndex = -1;
for (let i = 0; i < this.tableData.length; i++) {
const book = this.tableData[i];
if (book.active)
activeIndex = i;
if (book.activeParent)
activeParentIndex = i;
if (activeIndex >= 0 && activeParentIndex >= 0)
break;
}
const index = (activeIndex >= 0 ? activeIndex : activeParentIndex);
if (index >= 0) {
this.$refs.virtualScroll.scrollTo(index, 'center');
}
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
async scrollToBegin() {
this.lockScroll = true;
try {
this.$refs.virtualScroll.scrollTo(0, 'center');
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
async scrollToEnd() {
this.lockScroll = true;
try {
this.$refs.virtualScroll.scrollTo(this.tableData.length, 'center');
} finally {
await utils.sleep(100);
this.lockScroll = false;
}
}
get sortMethodOptions() {
return [
{label: '<span style="font-size: 150%">&uarr;</span> Время загрузки', value: 'loadTimeDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Время загрузки', value: 'loadTimeAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Время чтения', value: 'touchTimeDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Время чтения', value: 'touchTimeAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Автор', value: 'authorDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Автор', value: 'authorAsc'},
{label: '<span style="font-size: 150%">&uarr;</span> Название', value: 'titleDesc'},
{label: '<span style="font-size: 150%">&darr;</span> Название', value: 'titleAsc'},
];
}
close() {
this.$emit('recent-books-close');
}
@@ -340,29 +583,34 @@ export default vueComponent(RecentBooksPage);
</script>
<style scoped>
.recent-books-table {
width: 600px;
.recent-books-scroll {
width: 573px;
overflow-y: auto;
overflow-x: hidden;
}
.scroll-header {
height: 50px;
position: sticky;
z-index: 1;
top: 0;
border-bottom: 2px solid #aaaaaa;
padding-left: 5px;
}
.table-row {
min-height: 80px;
border-bottom: 1px solid #cccccc;
}
.row-part {
padding: 4px 4px 4px 4px;
}
.clickable {
cursor: pointer;
}
.td-mp {
margin: 0 !important;
padding: 4px 4px 4px 4px !important;
border-bottom: 1px solid #ddd;
}
.no-mp {
margin: 0 !important;
padding: 0 !important;
border: 0;
border-left: 1px solid #ddd !important;
}
.break-word {
line-height: 180%;
overflow-wrap: break-word;
@@ -373,22 +621,47 @@ export default vueComponent(RecentBooksPage);
.read-bar {
height: 3px;
background-color: #aaaaaa;
}
</style>
<style>
.recent-books-table .q-table__middle {
height: 100%;
overflow-x: hidden;
margin-bottom: 2px;
}
.recent-books-table thead tr:first-child th {
position: sticky;
z-index: 1;
top: 0;
background-color: #c1f4cd;
.even {
background-color: #f2f2f2;
}
.recent-books-table tr:nth-child(even) {
background-color: #f8f8f8;
.active-book {
background-color: #b0f0b0 !important;
}
.active-parent-book {
background-color: #ffbbbb !important;
}
.icon {
transition: transform 0.2s;
}
.expanded-icon {
transform: rotate(90deg);
}
.row-info-top {
line-height: 110%;
border-left: 1px solid #cccccc;
border-bottom: 1px solid #cccccc;
}
.row-info-bottom {
line-height: 110%;
border: 1px solid #cccccc;
border-right: 0;
}
.tool-button {
min-width: 30px;
width: 30px;
min-height: 30px;
height: 30px;
margin: 10px 6px 0px 3px;
background-color: white;
}
</style>

View File

@@ -8,12 +8,10 @@
<span v-show="initStep">{{ initPercentage }}%</span>
<div v-show="!initStep" class="input">
<!--input ref="input"
placeholder="что ищем"
:value="needle" @input="needle = $event.target.value"/-->
<q-input ref="input" v-model="needle"
<q-input
ref="input" v-model="needle"
class="col" outlined dense
placeholder="что ищем"
placeholder="Найти"
@keydown="inputKeyDown"
/>
<div style="position: absolute; right: 10px; margin-top: 10px; font-size: 16px;">
@@ -108,7 +106,7 @@ class SearchPage {
this.parsed = parsed;
}
this.header = 'Найти';
this.header = 'Поиск в тексте';
await this.$nextTick();
this.$refs.input.focus();
this.$refs.input.select();

View File

@@ -1,9 +0,0 @@
<div class="part-header">Показывать кнопки панели</div>
<div class="item row" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
<div class="label-3"></div>
<div class="col row">
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<template>
<Window ref="window" height="95%" width="600px" @close="close">
<Window ref="window" width="600px" @close="close">
<template #header>
Настройки
</template>
@@ -24,7 +24,7 @@
<div v-show="tabsScrollable" class="q-pt-lg" />
<q-tab class="tab" name="profiles" icon="la la-users" label="Профили" />
<q-tab class="tab" name="view" icon="la la-eye" label="Вид" />
<q-tab class="tab" name="buttons" icon="la la-grip-horizontal" label="Кнопки" />
<q-tab class="tab" name="toolbar" icon="la la-grip-horizontal" 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="convert" icon="la la-magic" label="Конвертир." />
@@ -82,8 +82,8 @@
</div>
</div>
<!-- Кнопки ---------------------------------------------------------------------->
<div v-if="selectedTab == 'buttons'" class="fit tab-panel">
@@include('./ButtonsTab.inc');
<div v-if="selectedTab == 'toolbar'" class="fit tab-panel">
@@include('./ToolBarTab.inc');
</div>
<!-- Управление ------------------------------------------------------------------>
<div v-if="selectedTab == 'keys'" class="fit column">
@@ -702,11 +702,11 @@ export default vueComponent(SettingsPage);
margin-bottom: 5px;
}
.label-1, .label-7 {
.label-1, .label-3, .label-7 {
width: 75px;
}
.label-2, .label-3, .label-4, .label-5 {
.label-2, .label-4, .label-5 {
width: 110px;
}

View File

@@ -0,0 +1,18 @@
<div class="part-header">Отображение</div>
<div class="item row no-wrap">
<div class="label-3"></div>
<q-checkbox size="xs" v-model="toolBarHideOnScroll" label="Скрывать/показывать панель при прокрутке" >
<q-tooltip :delay="1000" anchor="top middle" self="bottom middle" content-style="font-size: 80%">
Скрывать/показывть панель при прокрутке текста вперед/назад
</q-tooltip>
</q-checkbox>
</div>
<div class="part-header">Показывать кнопки</div>
<div class="item row no-wrap" v-for="item in toolButtons" :key="item.name" v-show="item.name != 'libs' || mode == 'liberama.top'">
<div class="label-3"></div>
<q-checkbox size="xs" v-model="showToolButton[item.name]" :label="rstore.readerActions[item.name]"
/>
</div>

View File

@@ -13,7 +13,7 @@
ref="input"
v-model="search"
class="q-ml-sm col"
outlined dense rounded
outlined dense
bg-color="grey-4"
placeholder="Найти"
@click.stop

View File

@@ -66,7 +66,14 @@ const componentOptions = {
watch: {
bookPos: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
this.draw();
if (this.userBookPosChange) {
this.$emit('hide-tool-bar', {show: (this.bookPos == 0 || this.bookPos < this.prevBookPos)});
this.prevBookPos = this.bookPos;
this.userBookPosChange = false;
}
},
bookPosSeen: function() {
this.$emit('book-pos-changed', {bookPos: this.bookPos, bookPosSeen: this.bookPosSeen});
@@ -99,6 +106,8 @@ class TextPage {
lastBook = null;
bookPos = 0;
bookPosSeen = null;
prevBookPos = 0;
userBookPosChange = false;
fontStyle = null;
fontSize = null;
@@ -155,7 +164,7 @@ class TextPage {
this.$root.addEventHook('resize', async() => {
this.$nextTick(this.onResize);
await utils.sleep(500);
await utils.sleep(200);
this.$nextTick(this.onResize);
});
}
@@ -499,12 +508,25 @@ class TextPage {
}
async onResize() {
if (this.resizing)
return;
this.resizing = true;
try {
const scrolled = this.doingScrolling;
if (scrolled)
await this.stopTextScrolling();
this.calcDrawProps();
this.setBackground();
this.draw();
if (scrolled)
this.startTextScrolling();
} catch (e) {
//
} finally {
this.resizing = false;
}
}
@@ -652,7 +674,7 @@ class TextPage {
}
if (this.book && this.bookPos > 0 && this.bookPos >= this.parsed.textLength) {
this.doEnd(true);
this.doEnd(true, false);
return;
}
@@ -675,7 +697,7 @@ class TextPage {
this.debouncedDrawPageDividerAndOrnament();
if (this.book && this.linesDown && this.linesDown.length < this.pageLineCount) {
this.doEnd(true);
this.doEnd(true, false);
return;
}
}
@@ -911,12 +933,14 @@ class TextPage {
doDown() {
if (this.linesDown && this.linesDown.length > this.pageLineCount && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesDown[1].begin;
}
}
doUp() {
if (this.linesUp && this.linesUp.length > 1 && this.pageLineCount > 0) {
this.userBookPosChange = true;
this.bookPos = this.linesUp[1].begin;
}
}
@@ -929,6 +953,7 @@ class TextPage {
if (i >= 0 && this.linesDown.length >= 2*i + (this.keepLastToFirst ? 1 : 0)) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = true;
this.bookPos = this.linesDown[i].begin;
} else
this.doEnd();
@@ -944,6 +969,7 @@ class TextPage {
if (i >= 0 && this.linesUp.length > i) {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = this.linesUp[i].begin;
}
}
@@ -952,10 +978,11 @@ class TextPage {
doHome() {
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = false;
this.userBookPosChange = true;
this.bookPos = 0;
}
doEnd(noAni) {
doEnd(noAni, isUser = true) {
if (this.parsed.para.length && this.pageLineCount > 0) {
let i = this.parsed.para.length - 1;
let lastPos = this.parsed.para[i].offset + this.parsed.para[i].length - 1;
@@ -966,6 +993,7 @@ class TextPage {
if (!noAni)
this.currentAnimation = this.pageChangeAnimation;
this.pageChangeDirectionDown = true;
this.userBookPosChange = isUser;
this.bookPos = lines[i].begin;
}
}

View File

@@ -1,10 +1,12 @@
import localForage from 'localforage';
import path from 'path-browserify';
import _ from 'lodash';
import * as utils from '../../../share/utils';
import BookParser from './BookParser';
const maxDataSize = 500*1024*1024;//compressed bytes
const maxRecentLength = 5000;
//локальный кэш метаданных книг, ограничение maxDataSize
const bmMetaStore = localForage.createInstance({
@@ -17,9 +19,6 @@ const bmDataStore = localForage.createInstance({
});
//список недавно открытых книг
const bmRecentStoreOld = localForage.createInstance({
name: 'bmRecentStore'
});
const bmRecentStoreNew = localForage.createInstance({
name: 'bmRecentStoreNew'
});
@@ -39,7 +38,7 @@ class BookManager {
this.saveRecentItem = _.debounce(() => {
bmRecentStoreNew.setItem('recent-item', this.recentItem);
this.recentRev = (this.recentRev < 1000 ? this.recentRev + 1 : 1);
this.recentRev = (this.recentRev < maxRecentLength ? this.recentRev + 1 : 1);
bmRecentStoreNew.setItem('rev', this.recentRev);
}, 200, {maxWait: 300});
@@ -54,6 +53,9 @@ class BookManager {
if (this.recentItem)
this.recent[this.recentItem.key] = this.recentItem;
//конвертируем в новые ключи
await this.convertRecent();
this.recentLastKey = await bmRecentStoreNew.getItem('recent-last-key');
if (this.recentLastKey) {
const meta = await bmMetaStore.getItem(`bmMeta-${this.recentLastKey}`);
@@ -70,6 +72,40 @@ class BookManager {
this.loadStored();//no await
}
//TODO: убрать в 2025г
async convertRecent() {
const converted = await bmRecentStoreNew.getItem('recent-converted');
if (converted)
return;
const newRecent = {};
for (const book of Object.values(this.recent)) {
if (!book.path) {
continue;
}
const newKey = this.keyFromPath(book.path);
newRecent[newKey] = _.cloneDeep(book);
newRecent[newKey].key = newKey;
if (!newRecent[newKey].loadTime)
newRecent[newKey].loadTime = newRecent[newKey].addTime;
}
this.recent = newRecent;
//console.log(converted);
(async() => {
await utils.sleep(3000);
this.saveRecent();
this.emit('recent-changed');
this.emit('set-recent');
await bmRecentStoreNew.setItem('recent-converted', true);
})();
}
//Ленивая асинхронная загрузка bmMetaStore
async loadStored() {
//даем время для загрузки последней читаемой книги, чтобы не блокировать приложение
@@ -196,8 +232,8 @@ class BookManager {
async addBook(newBook, callback) {
let meta = {url: newBook.url, path: newBook.path};
meta.key = this.keyFromUrl(meta.url);
meta.addTime = Date.now();
meta.key = this.keyFromPath(meta.path);
meta.addTime = Date.now();//время добавления в кеш
const cb = (perc) => {
const p = Math.round(30*perc/100);
@@ -232,10 +268,10 @@ class BookManager {
async hasBookParsed(meta) {
if (!this.books)
return false;
if (!meta.url)
if (!meta.path)
return false;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
meta.key = this.keyFromPath(meta.path);
let book = this.books[meta.key];
@@ -250,8 +286,12 @@ class BookManager {
async getBook(meta, callback) {
let result = undefined;
if (!meta.path)
return;
if (!meta.key)
meta.key = this.keyFromUrl(meta.url);
meta.key = this.keyFromPath(meta.path);
result = this.books[meta.key];
@@ -261,11 +301,6 @@ class BookManager {
this.books[meta.key] = result;
}
//Если файл на сервере изменился, считаем, что в кеше его нету
if (meta.path && result && meta.path != result.path) {
return;
}
if (result && !result.parsed) {
let data = await bmDataStore.getItem(`bmData-${meta.key}`);
callback(5);
@@ -325,10 +360,20 @@ class BookManager {
return result;
}
keyFromUrl(url) {
/*keyFromUrl(url) {
return utils.stringToHex(url);
}*/
keyFromPath(bookPath) {
return path.basename(bookPath);
}
keysEqual(bookPath1, bookPath2) {
if (bookPath1 === undefined || bookPath2 === undefined)
return false;
return (this.keyFromPath(bookPath1) === this.keyFromPath(bookPath2));
}
//-- recent --------------------------------------------------------------
async recentSetItem(item = null, skipCheck = false) {
const rev = await bmRecentStoreNew.getItem('rev');
@@ -369,7 +414,10 @@ class BookManager {
async setRecentBook(value) {
let result = this.metaOnly(value);
result.touchTime = Date.now();
result.touchTime = Date.now();//время последнего чтения
if (!result.loadTime)
result.loadTime = Date.now();//время загрузки файла
result.deleted = 0;
if (this.recent[result.key]) {
@@ -401,7 +449,7 @@ class BookManager {
const sorted = this.getSortedRecent();
let isDel = false;
for (let i = 1000; i < sorted.length; i++) {
for (let i = maxRecentLength; i < sorted.length; i++) {
delete this.recent[sorted[i].key];
isDel = true;
}
@@ -421,7 +469,7 @@ class BookManager {
let max = 0;
let result = null;
for (let key in this.recent) {
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.touchTime > max) {
max = book.touchTime;
@@ -452,6 +500,43 @@ class BookManager {
return result;
}
findRecentByUrlAndPath(url, bookPath) {
if (bookPath) {
const key = this.keyFromPath(bookPath);
const book = this.recent[key];
if (book && !book.deleted)
return book;
}
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.url == url && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
findRecentBySameBookKey(sameKey) {
let max = 0;
let result = null;
for (const key in this.recent) {
const book = this.recent[key];
if (!book.deleted && book.sameBookKey == sameKey && book.loadTime > max) {
max = book.loadTime;
result = book;
}
}
return result;
}
async setRecent(value) {
const mergedRecent = _.cloneDeep(this.recent);

View File

@@ -1,4 +1,26 @@
export const versionHistory = [
{
version: '0.11.7',
releaseDate: '2022-07-12',
showUntil: '2022-07-19',
content:
`
<ul>
<li>добавлено автосокрытие панели управления при листании, отключается в настройках</li>
<li>изменения в окне загруженных книг:</li>
<ul>
<li>добавлена группировка по версиям файла одной и той же книги</li>
<li>группировка происходит по имени загружаемого файла, либо по URL книги</li>
<li>добавлены различные методы сортировки списка загруженных книг</li>
<li>нумерация всегда осуществляется по времени загрузки</li>
</ul>
<li>незначительные общие изменения интерфейса, приведение к единому стилю</li>
<li>исправления багов</li>
</ul>
`
},
{
version: '0.11.6',
releaseDate: '2022-07-02',

View File

@@ -55,6 +55,34 @@
</div>
</div>
<!--------------------------------------------------->
<div v-show="type == 'askYesNo'" class="bg-white no-wrap">
<div class="header row">
<div class="caption col row items-center q-ml-md">
<q-icon v-show="caption" class="q-mr-sm" :class="iconColor" :name="iconName" size="28px"></q-icon>
<div v-html="caption"></div>
</div>
<div class="close-icon column justify-center items-center">
<q-btn v-close-popup flat round dense>
<q-icon name="la la-times" size="18px"></q-icon>
</q-btn>
</div>
</div>
<div class="q-mx-md">
<div v-html="message"></div>
</div>
<div class="buttons row justify-end q-pa-md">
<q-btn v-close-popup class="q-px-md q-ml-sm" dense no-caps>
Нет
</q-btn>
<q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="okClick">
Да
</q-btn>
</div>
</div>
<!--------------------------------------------------->
<div v-show="type == 'prompt'" class="bg-white no-wrap">
<div class="header row">
@@ -262,6 +290,23 @@ class StdDialog {
});
}
askYesNo(message, caption, opts) {
return new Promise((resolve) => {
this.init(message, caption, opts);
this.hideTrigger = () => {
if (this.ok) {
resolve(true);
} else {
resolve(false);
}
};
this.type = 'askYesNo';
this.active = true;
});
}
prompt(message, caption, opts) {
return new Promise((resolve) => {
this.enableValidator = false;

View File

@@ -153,7 +153,7 @@ export default vueComponent(Window);
}
.header {
background: linear-gradient(to bottom right, green, #59B04F);
background: linear-gradient(to bottom right, #007000, #59B04F);
align-items: center;
height: 30px;
}
@@ -161,8 +161,8 @@ export default vueComponent(Window);
.header-text {
margin-left: 10px;
margin-right: 10px;
color: yellow;
text-shadow: 2px 1px 5px black, 2px 2px 5px black;
color: #FFFFA0;
text-shadow: 2px 2px 5px #005000, 2px 1px 5px #005000;
overflow: hidden;
white-space: nowrap;
}
@@ -174,7 +174,8 @@ export default vueComponent(Window);
}
.close-button:hover {
background-color: #69C05F;
color: white;
background-color: #FF3030;
}
</style>
</style>

View File

@@ -32,6 +32,8 @@ import {QPopupProxy} from 'quasar/src/components/popup-proxy';
import {QDialog} from 'quasar/src/components/dialog';
import {QChip} from 'quasar/src/components/chip';
import {QTree} from 'quasar/src/components/tree';
import {QVirtualScroll} from 'quasar/src/components/virtual-scroll';
//import {QExpansionItem} from 'quasar/src/components/expansion-item';
const components = {
@@ -62,6 +64,7 @@ const components = {
QChip,
QTree,
//QExpansionItem,
QVirtualScroll,
};
//directives

53
client/share/LockQueue.js Normal file
View File

@@ -0,0 +1,53 @@
class LockQueue {
constructor(queueSize) {
this.queueSize = queueSize;
this.freed = true;
this.waitingQueue = [];
}
//async
get(take = true) {
return new Promise((resolve, reject) => {
if (this.freed) {
if (take)
this.freed = false;
resolve();
return;
}
if (this.waitingQueue.length < this.queueSize) {
this.waitingQueue.push({resolve, reject});
} else {
reject(new Error('Lock queue is too long'));
}
});
}
ret() {
if (this.waitingQueue.length) {
this.waitingQueue.shift().resolve();
} else {
this.freed = true;
}
}
//async
wait() {
return this.get(false);
}
retAll() {
while (this.waitingQueue.length) {
this.waitingQueue.shift().resolve();
}
}
errAll(error = 'rejected') {
while (this.waitingQueue.length) {
this.waitingQueue.shift().reject(new Error(error));
}
}
}
export default LockQueue;

View File

@@ -21,7 +21,7 @@ const readerActions = {
'offlineMode': 'Автономный режим (без интернета)',
'contents': 'Оглавление/закладки',
'libs': 'Сетевая библиотека',
'recentBooks': 'Открыть недавние',
'recentBooks': 'Показать загруженные',
'switchToolbar': 'Показать/скрыть панель управления',
'donate': '',
'bookBegin': 'В начало книги',
@@ -185,8 +185,12 @@ const settingDefaults = {
fontShifts: {},
showToolButton: {},
toolBarHideOnScroll: true,
userHotKeys: {},
userWallpapers: [],
recentShowSameBook: false,
recentSortMethod: '',
};
for (const font of fonts)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "Liberama",
"version": "0.11.6",
"version": "0.11.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "Liberama",
"version": "0.11.6",
"version": "0.11.7",
"hasInstallScript": true,
"license": "CC0-1.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "Liberama",
"version": "0.11.6",
"version": "0.11.7",
"author": "Book Pauk <bookpauk@gmail.com>",
"license": "CC0-1.0",
"repository": "bookpauk/liberama",

View File

@@ -3,7 +3,7 @@ const chardet = require('chardet');
function getEncoding(buf) {
let selected = getEncodingLite(buf);
if (selected == 'ISO-8859-5') {
if (selected == 'ISO-8859-5' && buf.length > 10) {
const charsetAll = chardet.analyse(buf.slice(0, 20000));
for (const charset of charsetAll) {
if (charset.name.indexOf('ISO-8859') < 0) {