From 7553b88b8997a5a7b59defca4d913b90463e6020 Mon Sep 17 00:00:00 2001 From: Book Pauk Date: Tue, 25 Oct 2022 15:40:08 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=B5=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE?= =?UTF-8?q?=D0=B7=D0=B8=D1=86=D0=B8=D1=8F,=20=D0=B2=D1=8B=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20BaseList,=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B8=D0=BC=D0=B5=D0=BD=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Search/AuthorList/AuthorList.vue | 583 +----------------- client/components/Search/BaseList.js | 577 +++++++++++++++++ .../Search/SeriesList/SeriesList.vue | 288 +++++++++ client/store/root.js | 2 +- 4 files changed, 873 insertions(+), 577 deletions(-) create mode 100644 client/components/Search/SeriesList/SeriesList.vue diff --git a/client/components/Search/AuthorList/AuthorList.vue b/client/components/Search/AuthorList/AuthorList.vue index 41364d9..b79b377 100644 --- a/client/components/Search/AuthorList/AuthorList.vue +++ b/client/components/Search/AuthorList/AuthorList.vue @@ -10,7 +10,7 @@
-
+
@@ -35,7 +35,7 @@
-
+
@@ -104,13 +104,13 @@
-
+
По каждому из заданных критериев у этого автора были найдены разные книги, но нет полного совпадения
-
+
Показать еще (~{{ showMoreCount }}) @@ -141,8 +141,6 @@ import vueComponent from '../../vueComponent.js'; import { reactive } from 'vue'; import BaseList from '../BaseList'; -import BookView from '../BookView/BookView.vue'; -import LoadingMessage from '../LoadingMessage/LoadingMessage.vue'; import authorBooksStorage from '../authorBooksStorage'; @@ -150,99 +148,9 @@ import * as utils from '../../../share/utils'; import _ from 'lodash'; -const maxItemCount = 500;//выше этого значения показываем "Загрузка" -const showMoreCount = 100;//значение для "Показать еще" - -const componentOptions = { - components: { - BookView, - LoadingMessage, - }, - watch: { - settings() { - this.loadSettings(); - }, - search: { - handler(newValue) { - this.limit = newValue.limit; - - if (this.pageCount > 1) - this.prevPage = this.search.page; - - this.refresh(); - }, - deep: true, - }, - showDeleted() { - this.updateTableData(); - }, - }, -}; class AuthorList extends BaseList { - _options = componentOptions; - _props = { - list: Object, - search: Object, - genreMap: Object, - }; - - loadingMessage = ''; - loadingMessage2 = ''; - - //settings - expanded = []; - expandedSeries = []; - - showCounts = true; - showRate = true; - showGenres = true; - showDeleted = false; - abCacheEnabled = true; - - //stuff - refreshing = false; - cachedAuthors = {}; hiddenCount = 0; - showMoreCount = showMoreCount; - - searchResult = {}; - tableData = []; - - created() { - this.commit = this.$store.commit; - this.api = this.$root.api; - - this.loadSettings(); - } - - mounted() { - this.refresh();//no await - } - - loadSettings() { - const settings = this.settings; - - this.expanded = _.cloneDeep(settings.expanded); - this.expandedSeries = _.cloneDeep(settings.expandedSeries); - this.showCounts = settings.showCounts; - this.showRate = settings.showRate; - this.showGenres = settings.showGenres; - this.showDeleted = settings.showDeleted; - this.abCacheEnabled = settings.abCacheEnabled; - } - - get config() { - return this.$store.state.config; - } - - get settings() { - return this.$store.state.settings; - } - - get showReadLink() { - return this.config.bookReadLink != '' || this.list.liberamaReady; - } showHiddenHelp() { this.$root.stdDialog.alert(` @@ -250,10 +158,6 @@ class AuthorList extends BaseList { `, 'Пояснение', {iconName: 'la la-info-circle'}); } - scrollToTop() { - this.$emit('listEvent', {action: 'scrollToTop'}); - } - get hiddenResultsMessage() { return `+${this.hiddenCount} результат${utils.wordEnding(this.hiddenCount)} скрыт${utils.wordEnding(this.hiddenCount, 2)}`; } @@ -279,120 +183,6 @@ class AuthorList extends BaseList { return `(${result})`; } - selectAuthor(author) { - this.search.author = `=${author}`; - this.scrollToTop(); - } - - selectSeries(series) { - this.search.series = `=${series}`; - } - - selectTitle(title) { - this.search.title = `=${title}`; - } - - async download(book, action) { - if (this.downloadFlag) - return; - - this.downloadFlag = true; - (async() => { - await utils.sleep(200); - if (this.downloadFlag) - this.loadingMessage2 = 'Подготовка файла...'; - })(); - - try { - const makeValidFilenameOrEmpty = (s) => { - try { - return utils.makeValidFilename(s); - } catch(e) { - return ''; - } - }; - - //имя файла - let downFileName = 'default-name'; - const author = book.author.split(','); - const at = [author[0], book.title]; - downFileName = makeValidFilenameOrEmpty(at.filter(r => r).join(' - ')) - || makeValidFilenameOrEmpty(at[0]) - || makeValidFilenameOrEmpty(at[1]) - || downFileName; - downFileName = downFileName.substring(0, 100); - - const ext = `.${book.ext}`; - if (downFileName.substring(downFileName.length - ext.length) != ext) - downFileName += ext; - - const bookPath = `${book.folder}/${book.file}${ext}`; - //подготовка - const response = await this.api.getBookLink({bookPath, downFileName}); - - const link = response.link; - const href = `${window.location.origin}${link}`; - - if (action == 'download') { - //скачивание - const d = this.$refs.download; - d.href = href; - d.download = downFileName; - - d.click(); - } else if (action == 'copyLink') { - //копирование ссылки - if (await utils.copyTextToClipboard(href)) - this.$root.notify.success('Ссылка успешно скопирована'); - else - this.$root.stdDialog.alert( -`Копирование ссылки не удалось. Пожалуйста, попробуйте еще раз. -

-Пояснение: вероятно, браузер запретил копирование, т.к. прошло
-слишком много времени с момента нажатия на кнопку (инициация
-пользовательского события). Сейчас ссылка уже закеширована,
-поэтому повторная попытка должна быть успешной.`, 'Ошибка'); - } else if (action == 'readBook') { - //читать - if (this.list.liberamaReady) { - this.sendMessage({type: 'submitUrl', data: href}); - } else { - const url = this.config.bookReadLink.replace('${DOWNLOAD_LINK}', href); - window.open(url, '_blank'); - } - } - } catch(e) { - this.$root.stdDialog.alert(e.message, 'Ошибка'); - } finally { - this.downloadFlag = false; - this.loadingMessage2 = ''; - } - } - - bookEvent(event) { - switch (event.action) { - case 'authorClick': - this.selectAuthor(event.book.author); - break; - case 'titleClick': - this.selectTitle(event.book.title); - break; - case 'download': - case 'copyLink': - case 'readBook': - this.download(event.book, event.action);//no await - break; - } - } - - isExpanded(item) { - return this.expanded.indexOf(item.author) >= 0; - } - - isExpandedSeries(seriesItem) { - return this.expandedSeries.indexOf(seriesItem.key) >= 0; - } - isFoundSeriesBook(seriesItem, seriesBook) { if (!seriesItem.booksSet) { seriesItem.booksSet = new Set(seriesItem.seriesBooks.map(b => b.id)); @@ -401,369 +191,10 @@ class AuthorList extends BaseList { return seriesItem.booksSet.has(seriesBook.id); } - setSetting(name, newValue) { - this.commit('setSettings', {[name]: _.cloneDeep(newValue)}); - } - - highlightPageScroller(query) { - this.$emit('listEvent', {action: 'highlightPageScroller', query}); - } - - async expandAuthor(item) { - const expanded = _.cloneDeep(this.expanded); - const key = item.author; - - if (!this.isExpanded(item)) { - expanded.push(key); - - await this.getBooks(item); - - if (expanded.length > 10) { - expanded.shift(); - } - - //this.$emit('listEvent', {action: 'ignoreScroll'}); - this.setSetting('expanded', expanded); - } else { - const i = expanded.indexOf(key); - if (i >= 0) { - expanded.splice(i, 1); - this.setSetting('expanded', expanded); - } - } - } - - async expandSeries(seriesItem) { - const expandedSeries = _.cloneDeep(this.expandedSeries); - const key = seriesItem.key; - - if (!this.isExpandedSeries(seriesItem)) { - expandedSeries.push(key); - - if (expandedSeries.length > 100) { - expandedSeries.shift(); - } - - this.getSeriesBooks(seriesItem); //no await - - //this.$emit('listEvent', {action: 'ignoreScroll'}); - this.setSetting('expandedSeries', expandedSeries); - } else { - const i = expandedSeries.indexOf(key); - if (i >= 0) { - expandedSeries.splice(i, 1); - this.setSetting('expandedSeries', expandedSeries); - } - } - } - - async loadBooks(authorId) { - try { - let result; - - if (this.abCacheEnabled) { - const key = `author-${authorId}-${this.list.inpxHash}`; - const data = await authorBooksStorage.getData(key); - if (data) { - result = JSON.parse(data); - } else { - result = await this.api.getBookList(authorId); - await authorBooksStorage.setData(key, JSON.stringify(result)); - } - } else { - result = await this.api.getBookList(authorId); - } - - return (result.books ? JSON.parse(result.books) : []); - } catch (e) { - this.$root.stdDialog.alert(e.message, 'Ошибка'); - } - } - - async loadSeriesBooks(series) { - try { - let result; - - if (this.abCacheEnabled) { - const key = `series-${series}-${this.list.inpxHash}`; - const data = await authorBooksStorage.getData(key); - if (data) { - result = JSON.parse(data); - } else { - result = await this.api.getSeriesBookList(series); - await authorBooksStorage.setData(key, JSON.stringify(result)); - } - } else { - result = await this.api.getSeriesBookList(series); - } - - return (result.books ? JSON.parse(result.books) : []); - } catch (e) { - this.$root.stdDialog.alert(e.message, 'Ошибка'); - } - } - - async getSeriesBooks(seriesItem) { - //асинхронно подгружаем все книги серии, блокируем повторный вызов - if (seriesItem.allBooksLoaded === null) { - seriesItem.allBooksLoaded = undefined; - (async() => { - seriesItem.allBooksLoaded = await this.loadSeriesBooks(seriesItem.series); - - if (seriesItem.allBooksLoaded) { - seriesItem.allBooksLoaded = seriesItem.allBooksLoaded.filter(book => (this.showDeleted || !book.del)); - this.sortSeriesBooks(seriesItem.allBooksLoaded); - this.showMoreSeries(seriesItem); - } else { - seriesItem.allBooksLoaded = null; - } - })(); - } - } - - filterBooks(books) { - const s = this.search; - - const emptyFieldValue = '?'; - const maxUtf8Char = String.fromCodePoint(0xFFFFF); - const ruAlphabet = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'; - const enAlphabet = 'abcdefghijklmnopqrstuvwxyz'; - const enru = new Set((ruAlphabet + enAlphabet).split('')); - - const splitAuthor = (author) => { - if (!author) { - author = emptyFieldValue; - } - - const result = author.split(','); - if (result.length > 1) - result.push(author); - - return result; - }; - - const filterBySearch = (bookValue, searchValue) => { - if (!searchValue) - return true; - - if (!bookValue) - bookValue = emptyFieldValue; - - bookValue = bookValue.toLowerCase(); - searchValue = searchValue.toLowerCase(); - - //особая обработка префиксов - if (searchValue[0] == '=') { - - searchValue = searchValue.substring(1); - return bookValue.localeCompare(searchValue) == 0; - } else if (searchValue[0] == '*') { - - searchValue = searchValue.substring(1); - return bookValue !== emptyFieldValue && bookValue.indexOf(searchValue) >= 0; - } else if (searchValue[0] == '#') { - - searchValue = searchValue.substring(1); - return !bookValue || (bookValue !== emptyFieldValue && !enru.has(bookValue[0]) && bookValue.indexOf(searchValue) >= 0); - } else { - //where = `@dirtyIndexLR('value', ${db.esc(a)}, ${db.esc(a + maxUtf8Char)})`; - return bookValue.localeCompare(searchValue) >= 0 && bookValue.localeCompare(searchValue + maxUtf8Char) <= 0; - } - }; - - return books.filter((book) => { - //author - let authorFound = false; - const authors = splitAuthor(book.author); - for (const a of authors) { - if (filterBySearch(a, s.author)) { - authorFound = true; - break; - } - } - - //genre - let genreFound = !s.genre; - if (!genreFound) { - const searchGenres = new Set(s.genre.split(',')); - const bookGenres = book.genre.split(','); - - for (let g of bookGenres) { - if (!g) - g = emptyFieldValue; - - if (searchGenres.has(g)) { - genreFound = true; - break; - } - } - } - - //lang - let langFound = !s.lang; - if (!langFound) { - const searchLang = new Set(s.lang.split(',')); - langFound = searchLang.has(book.lang || emptyFieldValue); - } - - return (this.showDeleted || !book.del) - && authorFound - && filterBySearch(book.series, s.series) - && filterBySearch(book.title, s.title) - && genreFound - && langFound - ; - }); - } - - showMore(item, all = false) { - if (item.booksLoaded) { - const currentLen = (item.books ? item.books.length : 0); - let books; - if (all || currentLen + showMoreCount*1.5 > item.booksLoaded.length) { - books = item.booksLoaded; - } else { - books = item.booksLoaded.slice(0, currentLen + showMoreCount); - } - - item.showMore = (books.length < item.booksLoaded.length); - item.books = books; - } - } - - showMoreSeries(seriesItem, all = false) { - if (seriesItem.allBooksLoaded) { - const currentLen = (seriesItem.allBooks ? seriesItem.allBooks.length : 0); - let books; - if (all || currentLen + showMoreCount*1.5 > seriesItem.allBooksLoaded.length) { - books = seriesItem.allBooksLoaded; - } else { - books = seriesItem.allBooksLoaded.slice(0, currentLen + showMoreCount); - } - - seriesItem.showMore = (books.length < seriesItem.allBooksLoaded.length); - seriesItem.allBooks = books; - } - } - - sortSeriesBooks(seriesBooks) { - seriesBooks.sort((a, b) => { - const dserno = (a.serno || Number.MAX_VALUE) - (b.serno || Number.MAX_VALUE); - const dtitle = a.title.localeCompare(b.title); - const dext = a.ext.localeCompare(b.ext); - return (dserno ? dserno : (dtitle ? dtitle : dext)); - }); - } - - async getBooks(item) { - if (item.books) { - if (item.count > maxItemCount) { - item.bookLoading = true; - await utils.sleep(1);//для перерисовки списка - item.bookLoading = false; - } - return; - } - - if (!this.getBooksFlag) - this.getBooksFlag = 0; - - this.getBooksFlag++; - if (item.count > maxItemCount) - item.bookLoading = true; - - try { - if (this.getBooksFlag == 1) { - (async() => { - await utils.sleep(500); - if (this.getBooksFlag > 0) - this.loadingMessage2 = 'Загрузка списка книг...'; - })(); - } - - const booksToFilter = await this.loadBooks(item.key); - const filtered = this.filterBooks(booksToFilter); - - const prepareBook = (book) => { - return Object.assign( - { - key: book.id, - type: 'book', - }, - book - ); - }; - - //объединение по сериям - const books = []; - const seriesIndex = {}; - for (const book of filtered) { - if (book.series) { - let index = seriesIndex[book.series]; - if (index === undefined) { - index = books.length; - books.push(reactive({ - key: `${item.author}-${book.series}`, - type: 'series', - series: book.series, - allBooksLoaded: null, - allBooks: null, - showAllBooks: false, - showMore: false, - - seriesBooks: [], - })); - - seriesIndex[book.series] = index; - } - - books[index].seriesBooks.push(prepareBook(book)); - } else { - books.push(prepareBook(book)); - } - } - - //сортировка - books.sort((a, b) => { - if (a.type == 'series') { - return (b.type == 'series' ? a.key.localeCompare(b.key) : -1); - } else { - return (b.type == 'book' ? a.title.localeCompare(b.title) : 1); - } - }); - - //сортировка внутри серий - for (const book of books) { - if (book.type == 'series') { - this.sortSeriesBooks(book.seriesBooks); - - //асинхронно подгрузим все книги серии, если она раскрыта - if (this.isExpandedSeries(book)) { - this.getSeriesBooks(book);//no await - } - } - } - - if (books.length == 1 && books[0].type == 'series' && !this.isExpandedSeries(books[0])) { - this.expandSeries(books[0]); - } - - item.booksLoaded = books; - this.showMore(item); - - await this.$nextTick(); - } finally { - item.bookLoading = false; - this.getBooksFlag--; - if (this.getBooksFlag == 0) - this.loadingMessage2 = ''; - } - } - async updateTableData() { let result = []; - const expandedSet = new Set(this.expanded); + const expandedSet = new Set(this.expandedAuthor); const authors = this.searchResult.author; if (!authors) return; @@ -793,7 +224,7 @@ class AuthorList extends BaseList { num++; if (expandedSet.has(item.author)) { - if (authors.length > 1 || item.count > maxItemCount) + if (authors.length > 1 || item.count > this.maxItemCount) this.getBooks(item);//no await else await this.getBooks(item); @@ -802,7 +233,7 @@ class AuthorList extends BaseList { result.push(item); } - if (result.length == 1 && !this.isExpanded(result[0])) { + if (result.length == 1 && !this.isExpandedAuthor(result[0])) { this.expandAuthor(result[0]); } diff --git a/client/components/Search/BaseList.js b/client/components/Search/BaseList.js index 4053afe..d85a513 100644 --- a/client/components/Search/BaseList.js +++ b/client/components/Search/BaseList.js @@ -1,2 +1,579 @@ +import { reactive } from 'vue'; +import _ from 'lodash'; + +import authorBooksStorage from './authorBooksStorage'; + +import BookView from './BookView/BookView.vue'; +import LoadingMessage from './LoadingMessage/LoadingMessage.vue'; +import * as utils from '../../share/utils'; + +const showMoreCount = 100;//значение для "Показать еще" +const maxItemCount = 500;//выше этого значения показываем "Загрузка" + +const componentOptions = { + components: { + BookView, + LoadingMessage, + }, + watch: { + settings() { + this.loadSettings(); + }, + search: { + handler(newValue) { + this.limit = newValue.limit; + + if (this.pageCount > 1) + this.prevPage = this.search.page; + + this.refresh(); + }, + deep: true, + }, + showDeleted() { + this.updateTableData(); + }, + }, +}; export default class BaseList { + _options = componentOptions; + _props = { + list: Object, + search: Object, + genreMap: Object, + }; + + loadingMessage = ''; + loadingMessage2 = ''; + + //settings + expandedAuthor = []; + expandedSeries = []; + + showCounts = true; + showRate = true; + showGenres = true; + showDeleted = false; + abCacheEnabled = true; + + //stuff + refreshing = false; + + showMoreCount = showMoreCount; + maxItemCount = maxItemCount; + + searchResult = {}; + tableData = []; + + created() { + this.commit = this.$store.commit; + this.api = this.$root.api; + + this.loadSettings(); + } + + mounted() { + this.refresh();//no await + } + + loadSettings() { + const settings = this.settings; + + this.expandedAuthor = _.cloneDeep(settings.expandedAuthor); + this.expandedSeries = _.cloneDeep(settings.expandedSeries); + this.showCounts = settings.showCounts; + this.showRate = settings.showRate; + this.showGenres = settings.showGenres; + this.showDeleted = settings.showDeleted; + this.abCacheEnabled = settings.abCacheEnabled; + } + + get config() { + return this.$store.state.config; + } + + get settings() { + return this.$store.state.settings; + } + + get showReadLink() { + return this.config.bookReadLink != '' || this.list.liberamaReady; + } + + scrollToTop() { + this.$emit('listEvent', {action: 'scrollToTop'}); + } + + selectAuthor(author) { + this.search.author = `=${author}`; + this.scrollToTop(); + } + + selectSeries(series) { + this.search.series = `=${series}`; + } + + selectTitle(title) { + this.search.title = `=${title}`; + } + + async download(book, action) { + if (this.downloadFlag) + return; + + this.downloadFlag = true; + (async() => { + await utils.sleep(200); + if (this.downloadFlag) + this.loadingMessage2 = 'Подготовка файла...'; + })(); + + try { + const makeValidFilenameOrEmpty = (s) => { + try { + return utils.makeValidFilename(s); + } catch(e) { + return ''; + } + }; + + //имя файла + let downFileName = 'default-name'; + const author = book.author.split(','); + const at = [author[0], book.title]; + downFileName = makeValidFilenameOrEmpty(at.filter(r => r).join(' - ')) + || makeValidFilenameOrEmpty(at[0]) + || makeValidFilenameOrEmpty(at[1]) + || downFileName; + downFileName = downFileName.substring(0, 100); + + const ext = `.${book.ext}`; + if (downFileName.substring(downFileName.length - ext.length) != ext) + downFileName += ext; + + const bookPath = `${book.folder}/${book.file}${ext}`; + //подготовка + const response = await this.api.getBookLink({bookPath, downFileName}); + + const link = response.link; + const href = `${window.location.origin}${link}`; + + if (action == 'download') { + //скачивание + const d = this.$refs.download; + d.href = href; + d.download = downFileName; + + d.click(); + } else if (action == 'copyLink') { + //копирование ссылки + if (await utils.copyTextToClipboard(href)) + this.$root.notify.success('Ссылка успешно скопирована'); + else + this.$root.stdDialog.alert( +`Копирование ссылки не удалось. Пожалуйста, попробуйте еще раз. +

+Пояснение: вероятно, браузер запретил копирование, т.к. прошло
+слишком много времени с момента нажатия на кнопку (инициация
+пользовательского события). Сейчас ссылка уже закеширована,
+поэтому повторная попытка должна быть успешной.`, 'Ошибка'); + } else if (action == 'readBook') { + //читать + if (this.list.liberamaReady) { + this.sendMessage({type: 'submitUrl', data: href}); + } else { + const url = this.config.bookReadLink.replace('${DOWNLOAD_LINK}', href); + window.open(url, '_blank'); + } + } + } catch(e) { + this.$root.stdDialog.alert(e.message, 'Ошибка'); + } finally { + this.downloadFlag = false; + this.loadingMessage2 = ''; + } + } + + bookEvent(event) { + switch (event.action) { + case 'authorClick': + this.selectAuthor(event.book.author); + break; + case 'titleClick': + this.selectTitle(event.book.title); + break; + case 'download': + case 'copyLink': + case 'readBook': + this.download(event.book, event.action);//no await + break; + } + } + + isExpandedAuthor(item) { + return this.expandedAuthor.indexOf(item.author) >= 0; + } + + isExpandedSeries(seriesItem) { + return this.expandedSeries.indexOf(seriesItem.key) >= 0; + } + + setSetting(name, newValue) { + this.commit('setSettings', {[name]: _.cloneDeep(newValue)}); + } + + highlightPageScroller(query) { + this.$emit('listEvent', {action: 'highlightPageScroller', query}); + } + + async expandAuthor(item) { + const expanded = _.cloneDeep(this.expandedAuthor); + const key = item.author; + + if (!this.isExpandedAuthor(item)) { + expanded.push(key); + + await this.getBooks(item); + + if (expanded.length > 10) { + expanded.shift(); + } + + //this.$emit('listEvent', {action: 'ignoreScroll'}); + this.setSetting('expandedAuthor', expanded); + } else { + const i = expanded.indexOf(key); + if (i >= 0) { + expanded.splice(i, 1); + this.setSetting('expandedAuthor', expanded); + } + } + } + + async expandSeries(seriesItem) { + const expandedSeries = _.cloneDeep(this.expandedSeries); + const key = seriesItem.key; + + if (!this.isExpandedSeries(seriesItem)) { + expandedSeries.push(key); + + if (expandedSeries.length > 100) { + expandedSeries.shift(); + } + + this.getSeriesBooks(seriesItem); //no await + + //this.$emit('listEvent', {action: 'ignoreScroll'}); + this.setSetting('expandedSeries', expandedSeries); + } else { + const i = expandedSeries.indexOf(key); + if (i >= 0) { + expandedSeries.splice(i, 1); + this.setSetting('expandedSeries', expandedSeries); + } + } + } + + async loadBooks(authorId) { + try { + let result; + + if (this.abCacheEnabled) { + const key = `author-${authorId}-${this.list.inpxHash}`; + const data = await authorBooksStorage.getData(key); + if (data) { + result = JSON.parse(data); + } else { + result = await this.api.getBookList(authorId); + await authorBooksStorage.setData(key, JSON.stringify(result)); + } + } else { + result = await this.api.getBookList(authorId); + } + + return (result.books ? JSON.parse(result.books) : []); + } catch (e) { + this.$root.stdDialog.alert(e.message, 'Ошибка'); + } + } + + async loadSeriesBooks(series) { + try { + let result; + + if (this.abCacheEnabled) { + const key = `series-${series}-${this.list.inpxHash}`; + const data = await authorBooksStorage.getData(key); + if (data) { + result = JSON.parse(data); + } else { + result = await this.api.getSeriesBookList(series); + await authorBooksStorage.setData(key, JSON.stringify(result)); + } + } else { + result = await this.api.getSeriesBookList(series); + } + + return (result.books ? JSON.parse(result.books) : []); + } catch (e) { + this.$root.stdDialog.alert(e.message, 'Ошибка'); + } + } + + async getSeriesBooks(seriesItem) { + //асинхронно подгружаем все книги серии, блокируем повторный вызов + if (seriesItem.allBooksLoaded === null) { + seriesItem.allBooksLoaded = undefined; + (async() => { + seriesItem.allBooksLoaded = await this.loadSeriesBooks(seriesItem.series); + + if (seriesItem.allBooksLoaded) { + seriesItem.allBooksLoaded = seriesItem.allBooksLoaded.filter(book => (this.showDeleted || !book.del)); + this.sortSeriesBooks(seriesItem.allBooksLoaded); + this.showMoreSeries(seriesItem); + } else { + seriesItem.allBooksLoaded = null; + } + })(); + } + } + + filterBooks(books) { + const s = this.search; + + const emptyFieldValue = '?'; + const maxUtf8Char = String.fromCodePoint(0xFFFFF); + const ruAlphabet = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'; + const enAlphabet = 'abcdefghijklmnopqrstuvwxyz'; + const enru = new Set((ruAlphabet + enAlphabet).split('')); + + const splitAuthor = (author) => { + if (!author) { + author = emptyFieldValue; + } + + const result = author.split(','); + if (result.length > 1) + result.push(author); + + return result; + }; + + const filterBySearch = (bookValue, searchValue) => { + if (!searchValue) + return true; + + if (!bookValue) + bookValue = emptyFieldValue; + + bookValue = bookValue.toLowerCase(); + searchValue = searchValue.toLowerCase(); + + //особая обработка префиксов + if (searchValue[0] == '=') { + + searchValue = searchValue.substring(1); + return bookValue.localeCompare(searchValue) == 0; + } else if (searchValue[0] == '*') { + + searchValue = searchValue.substring(1); + return bookValue !== emptyFieldValue && bookValue.indexOf(searchValue) >= 0; + } else if (searchValue[0] == '#') { + + searchValue = searchValue.substring(1); + return !bookValue || (bookValue !== emptyFieldValue && !enru.has(bookValue[0]) && bookValue.indexOf(searchValue) >= 0); + } else { + //where = `@dirtyIndexLR('value', ${db.esc(a)}, ${db.esc(a + maxUtf8Char)})`; + return bookValue.localeCompare(searchValue) >= 0 && bookValue.localeCompare(searchValue + maxUtf8Char) <= 0; + } + }; + + return books.filter((book) => { + //author + let authorFound = false; + const authors = splitAuthor(book.author); + for (const a of authors) { + if (filterBySearch(a, s.author)) { + authorFound = true; + break; + } + } + + //genre + let genreFound = !s.genre; + if (!genreFound) { + const searchGenres = new Set(s.genre.split(',')); + const bookGenres = book.genre.split(','); + + for (let g of bookGenres) { + if (!g) + g = emptyFieldValue; + + if (searchGenres.has(g)) { + genreFound = true; + break; + } + } + } + + //lang + let langFound = !s.lang; + if (!langFound) { + const searchLang = new Set(s.lang.split(',')); + langFound = searchLang.has(book.lang || emptyFieldValue); + } + + return (this.showDeleted || !book.del) + && authorFound + && filterBySearch(book.series, s.series) + && filterBySearch(book.title, s.title) + && genreFound + && langFound + ; + }); + } + + showMore(item, all = false) { + if (item.booksLoaded) { + const currentLen = (item.books ? item.books.length : 0); + let books; + if (all || currentLen + this.showMoreCount*1.5 > item.booksLoaded.length) { + books = item.booksLoaded; + } else { + books = item.booksLoaded.slice(0, currentLen + this.showMoreCount); + } + + item.showMore = (books.length < item.booksLoaded.length); + item.books = books; + } + } + + showMoreSeries(seriesItem, all = false) { + if (seriesItem.allBooksLoaded) { + const currentLen = (seriesItem.allBooks ? seriesItem.allBooks.length : 0); + let books; + if (all || currentLen + this.showMoreCount*1.5 > seriesItem.allBooksLoaded.length) { + books = seriesItem.allBooksLoaded; + } else { + books = seriesItem.allBooksLoaded.slice(0, currentLen + this.showMoreCount); + } + + seriesItem.showMore = (books.length < seriesItem.allBooksLoaded.length); + seriesItem.allBooks = books; + } + } + + sortSeriesBooks(seriesBooks) { + seriesBooks.sort((a, b) => { + const dserno = (a.serno || Number.MAX_VALUE) - (b.serno || Number.MAX_VALUE); + const dtitle = a.title.localeCompare(b.title); + const dext = a.ext.localeCompare(b.ext); + return (dserno ? dserno : (dtitle ? dtitle : dext)); + }); + } + + async getBooks(item) { + if (item.books) { + if (item.count > maxItemCount) { + item.bookLoading = true; + await utils.sleep(1);//для перерисовки списка + item.bookLoading = false; + } + return; + } + + if (!this.getBooksFlag) + this.getBooksFlag = 0; + + this.getBooksFlag++; + if (item.count > maxItemCount) + item.bookLoading = true; + + try { + if (this.getBooksFlag == 1) { + (async() => { + await utils.sleep(500); + if (this.getBooksFlag > 0) + this.loadingMessage2 = 'Загрузка списка книг...'; + })(); + } + + const booksToFilter = await this.loadBooks(item.key); + const filtered = this.filterBooks(booksToFilter); + + const prepareBook = (book) => { + return Object.assign( + { + key: book.id, + type: 'book', + }, + book + ); + }; + + //объединение по сериям + const books = []; + const seriesIndex = {}; + for (const book of filtered) { + if (book.series) { + let index = seriesIndex[book.series]; + if (index === undefined) { + index = books.length; + books.push(reactive({ + key: `${item.author}-${book.series}`, + type: 'series', + series: book.series, + allBooksLoaded: null, + allBooks: null, + showAllBooks: false, + showMore: false, + + seriesBooks: [], + })); + + seriesIndex[book.series] = index; + } + + books[index].seriesBooks.push(prepareBook(book)); + } else { + books.push(prepareBook(book)); + } + } + + //сортировка + books.sort((a, b) => { + if (a.type == 'series') { + return (b.type == 'series' ? a.key.localeCompare(b.key) : -1); + } else { + return (b.type == 'book' ? a.title.localeCompare(b.title) : 1); + } + }); + + //сортировка внутри серий + for (const book of books) { + if (book.type == 'series') { + this.sortSeriesBooks(book.seriesBooks); + + //асинхронно подгрузим все книги серии, если она раскрыта + if (this.isExpandedSeries(book)) { + this.getSeriesBooks(book);//no await + } + } + } + + if (books.length == 1 && books[0].type == 'series' && !this.isExpandedSeries(books[0])) { + this.expandSeries(books[0]); + } + + item.booksLoaded = books; + this.showMore(item); + + await this.$nextTick(); + } finally { + item.bookLoading = false; + this.getBooksFlag--; + if (this.getBooksFlag == 0) + this.loadingMessage2 = ''; + } + } } \ No newline at end of file diff --git a/client/components/Search/SeriesList/SeriesList.vue b/client/components/Search/SeriesList/SeriesList.vue new file mode 100644 index 0000000..11e53c8 --- /dev/null +++ b/client/components/Search/SeriesList/SeriesList.vue @@ -0,0 +1,288 @@ + + + + + diff --git a/client/store/root.js b/client/store/root.js index 521060e..5f054e6 100644 --- a/client/store/root.js +++ b/client/store/root.js @@ -4,7 +4,7 @@ const state = { settings: { accessToken: '', limit: 20, - expanded: [], + expandedAuthor: [], expandedSeries: [], showCounts: true, showRate: true,