Search.vue 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487
  1. <template>
  2. <div class="root column fit" style="position: relative">
  3. <a ref="download" style="display: none;"></a>
  4. <div v-show="loadingMessage" class="fit row justify-center items-center" style="position: absolute; background-color: rgba(0, 0, 0, 0.2); z-index: 2">
  5. <div class="bg-white row justify-center items-center q-px-lg" style="min-width: 180px; height: 50px; border-radius: 10px; box-shadow: 2px 2px 10px #333333">
  6. <q-icon class="la la-spinner icon-rotate text-blue-8" size="28px" />
  7. <div class="q-ml-sm">
  8. {{ loadingMessage }}
  9. </div>
  10. </div>
  11. </div>
  12. <div v-show="loadingMessage2" class="fit row justify-center items-center" style="position: absolute; background-color: rgba(0, 0, 0, 0.2); z-index: 1">
  13. <div class="bg-white row justify-center items-center q-px-lg" style="min-width: 180px; height: 50px; border-radius: 10px; box-shadow: 2px 2px 10px #333333">
  14. <q-icon class="la la-spinner icon-rotate text-blue-8" size="28px" />
  15. <div class="q-ml-sm">
  16. {{ loadingMessage2 }}
  17. </div>
  18. </div>
  19. </div>
  20. <div ref="scroller" class="col fit column no-wrap" style="overflow: auto; position: relative" @scroll="onScroll">
  21. <div ref="toolPanel" class="tool-panel column bg-cyan-2" style="position: sticky; top: 0; z-index: 10;">
  22. <div class="header q-mx-md q-mb-xs q-mt-sm row items-center">
  23. <div style="height: 33px">
  24. <img src="./assets/logo.png" class="clickable2" @click="newSearch" />
  25. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  26. Новый поиск
  27. </q-tooltip>
  28. </div>
  29. <div class="row items-center q-ml-sm" style="font-size: 150%;">
  30. <div class="q-mr-xs">
  31. Коллекция
  32. </div>
  33. <div class="clickable" @click="showCollectionInfo">
  34. {{ collection }}
  35. </div>
  36. </div>
  37. <DivBtn class="q-ml-md text-white bg-secondary" :size="30" :icon-size="24" :imt="1" icon="la la-cog" round @click="settingsDialogVisible = true">
  38. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  39. Настройки
  40. </q-tooltip>
  41. </DivBtn>
  42. <DivBtn class="q-ml-sm text-white bg-secondary" :size="30" :icon-size="24" icon="la la-question" round @click="showSearchHelp">
  43. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  44. Памятка
  45. </q-tooltip>
  46. </DivBtn>
  47. <div class="col"></div>
  48. <div class="q-px-sm q-py-xs bg-green-12 clickable2" style="border: 1px solid #aaaaaa; border-radius: 6px" @click="openReleasePage">
  49. {{ projectName }}
  50. </div>
  51. </div>
  52. <div class="row q-mx-md q-mb-sm items-center">
  53. <q-input
  54. ref="authorInput" v-model="search.author" :maxlength="5000" :debounce="inputDebounce"
  55. class="bg-white q-mt-xs" style="width: 300px;" label="Автор" stack-label outlined dense clearable
  56. >
  57. <q-tooltip v-if="search.author" :delay="500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  58. {{ search.author }}
  59. </q-tooltip>
  60. </q-input>
  61. <div class="q-mx-xs" />
  62. <q-input
  63. v-model="search.series" :maxlength="inputMaxLength" :debounce="inputDebounce"
  64. class="bg-white q-mt-xs" style="width: 200px;" label="Серия" stack-label outlined dense clearable
  65. >
  66. <q-tooltip v-if="search.series" :delay="500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  67. {{ search.series }}
  68. </q-tooltip>
  69. </q-input>
  70. <div class="q-mx-xs" />
  71. <q-input
  72. v-model="search.title" :maxlength="inputMaxLength" :debounce="inputDebounce"
  73. class="bg-white q-mt-xs" style="width: 200px;" label="Название" stack-label outlined dense clearable
  74. >
  75. <q-tooltip v-if="search.title" :delay="500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  76. {{ search.title }}
  77. </q-tooltip>
  78. </q-input>
  79. <div class="q-mx-xs" />
  80. <q-input
  81. v-model="genreNames" :maxlength="inputMaxLength" :debounce="inputDebounce"
  82. class="bg-white q-mt-xs" input-style="cursor: pointer" style="width: 200px;" label="Жанр" stack-label outlined dense clearable readonly
  83. @click="selectGenre"
  84. >
  85. <template v-if="genreNames" #append>
  86. <q-icon name="la la-times-circle" class="q-field__focusable-action" @click.stop.prevent="search.genre = ''" />
  87. </template>
  88. <q-tooltip v-if="genreNames && showTooltips" :delay="500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  89. {{ genreNames }}
  90. </q-tooltip>
  91. </q-input>
  92. <div class="q-mx-xs" />
  93. <q-input
  94. v-model="search.lang" :maxlength="inputMaxLength" :debounce="inputDebounce"
  95. class="bg-white q-mt-xs" input-style="cursor: pointer" style="width: 80px;" label="Язык" stack-label outlined dense clearable readonly
  96. @click="selectLang"
  97. >
  98. <q-tooltip v-if="search.lang && showTooltips" :delay="500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  99. {{ search.lang }}
  100. </q-tooltip>
  101. </q-input>
  102. <!--div class="q-mx-xs" />
  103. <DivBtn class="text-white q-mt-xs bg-grey-13" :size="30" :icon-size="24" icon="la la-broom" round @click="setDefaults">
  104. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%" max-width="400px">
  105. Сбросить поиск
  106. </q-tooltip>
  107. </DivBtn-->
  108. <div class="q-mx-xs" />
  109. <div class="row items-center q-mt-xs">
  110. <div v-show="queryFound > 0">
  111. {{ foundAuthorsMessage }}
  112. </div>
  113. <div v-show="queryFound == 0">
  114. Ничего не найдено
  115. </div>
  116. </div>
  117. </div>
  118. </div>
  119. <div class="row justify-center" style="min-height: 48px">
  120. <PageScroller v-show="pageCount > 1" v-model="search.page" :page-count="pageCount" />
  121. </div>
  122. <!-- Формирование списка ------------------------------------------------------------------------>
  123. <div v-for="item in tableData" :key="item.key" class="column" :class="{'odd-author': item.num % 2}" style="font-size: 120%">
  124. <div class="row items-center q-ml-md q-mr-xs no-wrap">
  125. <div class="row items-center clickable2 q-py-xs no-wrap" @click="expandAuthor(item)">
  126. <div style="min-width: 30px">
  127. <div v-if="!isExpanded(item)">
  128. <q-icon name="la la-plus-square" size="28px" />
  129. </div>
  130. <div v-else>
  131. <q-icon name="la la-minus-square" size="28px" />
  132. </div>
  133. </div>
  134. </div>
  135. <div class="clickable2 q-ml-xs q-py-sm text-green-10 text-bold" @click="selectAuthor(item.author)">
  136. {{ item.name }}
  137. </div>
  138. <div class="q-ml-sm text-bold" style="color: #555">
  139. {{ getBookCount(item) }}
  140. </div>
  141. </div>
  142. <div v-if="item.bookLoading" class="book-row row items-center">
  143. <q-icon class="la la-spinner icon-rotate text-blue-8" size="28px" />
  144. <div class="q-ml-xs">
  145. Обработка...
  146. </div>
  147. </div>
  148. <div v-if="isExpanded(item) && item.books">
  149. <div v-for="book in item.books" :key="book.key" class="book-row column">
  150. <!-- серия книг -->
  151. <div v-if="book.type == 'series'" class="column">
  152. <div class="row items-center q-mr-xs no-wrap text-grey-9">
  153. <div class="row items-center clickable2 q-py-xs no-wrap" @click="expandSeries(book)">
  154. <div style="min-width: 30px">
  155. <div v-if="!isExpandedSeries(book)">
  156. <q-icon name="la la-plus-square" size="28px" />
  157. </div>
  158. <div v-else>
  159. <q-icon name="la la-minus-square" size="28px" />
  160. </div>
  161. </div>
  162. </div>
  163. <div class="clickable2 q-ml-xs q-py-sm text-bold" @click="selectSeries(book.series)">
  164. Серия: {{ book.series }}
  165. </div>
  166. </div>
  167. <div v-if="isExpandedSeries(book) && book.books">
  168. <div v-if="book.showAllBooks" class="book-row column">
  169. <BookView
  170. v-for="subbook in book.allBooks" :key="subbook.id"
  171. :book="subbook" :genre-tree="genreTree"
  172. show-author
  173. :show-read-link="showReadLink"
  174. :title-color="isFoundSeriesBook(book, subbook) ? 'text-blue-10' : 'text-red'"
  175. @book-event="bookEvent"
  176. />
  177. </div>
  178. <div v-else class="book-row column">
  179. <BookView v-for="subbook in book.books" :key="subbook.key" :book="subbook" :genre-tree="genreTree" :show-read-link="showReadLink" @book-event="bookEvent" />
  180. </div>
  181. <div
  182. v-if="book.allBooks && book.allBooks.length != book.books.length"
  183. class="q-my-sm clickable2"
  184. style="margin-left: 100px"
  185. @click="book.showAllBooks = !book.showAllBooks"
  186. >
  187. <div v-if="book.showAllBooks" class="row items-center text-blue-10">
  188. <q-icon class="la la-long-arrow-alt-up" size="28px" />
  189. Только найденные книги
  190. </div>
  191. <div v-else class="row items-center text-red">
  192. <q-icon class="la la-long-arrow-alt-down" size="28px" />
  193. Все книги серии
  194. </div>
  195. </div>
  196. </div>
  197. </div>
  198. <!-- книга без серии -->
  199. <BookView v-else :book="book" :genre-tree="genreTree" :show-read-link="showReadLink" @book-event="bookEvent" />
  200. </div>
  201. <div v-if="isExpanded(item) && item.books && !item.books.length" class="book-row row items-center">
  202. <q-icon class="la la-meh q-mr-xs" size="24px" />
  203. По каждому из заданных критериев у этого автора были найдены разные книги, но нет полного совпадения
  204. </div>
  205. </div>
  206. <div v-if="isExpanded(item) && item.showMore" class="row items-center book-row q-mb-sm">
  207. <i class="las la-ellipsis-h text-blue-10" style="font-size: 40px"></i>
  208. <q-btn class="q-ml-md" color="primary" style="width: 200px" dense rounded no-caps @click="showMore(item)">
  209. Показать еще {{ showMoreCount }}
  210. </q-btn>
  211. <q-btn class="q-ml-sm" color="primary" style="width: 200px" dense rounded no-caps @click="showMore(item, true)">
  212. Показать все
  213. </q-btn>
  214. </div>
  215. </div>
  216. <!-- Формирование списка конец ------------------------------------------------------------------>
  217. <div v-if="ready && !refreshing && !tableData.length" class="row items-center q-ml-md" style="font-size: 120%">
  218. <q-icon class="la la-meh q-mr-xs" size="28px" />
  219. Поиск не дал результатов
  220. </div>
  221. <div v-show="hiddenCount" class="row">
  222. <div class="q-ml-lg q-py-sm clickable2 text-red" style="font-size: 120%" @click="showHiddenHelp">
  223. {{ hiddenResultsMessage }}
  224. </div>
  225. </div>
  226. <div class="row justify-center">
  227. <PageScroller v-show="pageCount > 1" v-model="search.page" :page-count="pageCount" />
  228. </div>
  229. <div v-show="pageCount <= 1" class="q-mt-lg" />
  230. </div>
  231. <Dialog v-model="settingsDialogVisible">
  232. <template #header>
  233. <div class="row items-center" style="font-size: 130%">
  234. <q-icon class="q-mr-sm" name="la la-cog" size="28px"></q-icon>
  235. Настройки
  236. </div>
  237. </template>
  238. <div class="q-mx-md column" style="min-width: 300px; font-size: 120%;">
  239. <div class="row items-center q-ml-sm">
  240. <div class="q-mr-sm">
  241. Результатов на странице
  242. </div>
  243. <q-select
  244. v-model="limit" :options="limitOptions" class="bg-white"
  245. dropdown-icon="la la-angle-down la-sm"
  246. outlined dense emit-value map-options
  247. />
  248. </div>
  249. <q-checkbox v-model="showCounts" size="36px" label="Показывать количество" />
  250. <q-checkbox v-model="showRate" size="36px" label="Показывать оценки" />
  251. <q-checkbox v-model="showGenres" size="36px" label="Показывать жанры" />
  252. <q-checkbox v-model="showDeleted" size="36px" label="Показывать удаленные" />
  253. <q-checkbox v-model="abCacheEnabled" size="36px" label="Кешировать запросы" />
  254. </div>
  255. <template #footer>
  256. <q-btn class="q-px-md q-ml-sm" color="primary" dense no-caps @click="settingsDialogVisible = false">
  257. OK
  258. </q-btn>
  259. </template>
  260. </Dialog>
  261. <SelectGenreDialog v-model="selectGenreDialogVisible" v-model:genre="search.genre" :genre-tree="genreTree" />
  262. <SelectLangDialog v-model="selectLangDialogVisible" v-model:lang="search.lang" :lang-list="langList" :lang-default="langDefault" />
  263. </div>
  264. </template>
  265. <script>
  266. //-----------------------------------------------------------------------------
  267. import vueComponent from '../vueComponent.js';
  268. import { reactive } from 'vue';
  269. import PageScroller from './PageScroller/PageScroller.vue';
  270. import SelectGenreDialog from './SelectGenreDialog/SelectGenreDialog.vue';
  271. import SelectLangDialog from './SelectLangDialog/SelectLangDialog.vue';
  272. import BookView from './BookView/BookView.vue';
  273. import authorBooksStorage from './authorBooksStorage';
  274. import DivBtn from '../share/DivBtn.vue';
  275. import Dialog from '../share/Dialog.vue';
  276. import * as utils from '../../share/utils';
  277. import diffUtils from '../../share/diffUtils';
  278. import _ from 'lodash';
  279. const maxItemCount = 500;//выше этого значения показываем "Загрузка"
  280. const showMoreCount = 100;//значение для "Показать еще"
  281. const componentOptions = {
  282. components: {
  283. PageScroller,
  284. SelectGenreDialog,
  285. SelectLangDialog,
  286. BookView,
  287. Dialog,
  288. DivBtn
  289. },
  290. watch: {
  291. config() {
  292. this.makeProjectName();
  293. },
  294. settings() {
  295. this.loadSettings();
  296. },
  297. search: {
  298. handler(newValue) {
  299. this.limit = newValue.limit;
  300. if (this.pageCount > 1)
  301. this.prevPage = this.search.page;
  302. this.makeTitle();
  303. this.refresh();
  304. },
  305. deep: true,
  306. },
  307. limit(newValue) {
  308. this.setSetting('limit', newValue);
  309. this.updatePageCount();
  310. },
  311. showCounts(newValue) {
  312. this.setSetting('showCounts', newValue);
  313. },
  314. showRate(newValue) {
  315. this.setSetting('showRate', newValue);
  316. },
  317. showGenres(newValue) {
  318. this.setSetting('showGenres', newValue);
  319. },
  320. showDeleted(newValue) {
  321. this.setSetting('showDeleted', newValue);
  322. this.updateTableData();
  323. },
  324. abCacheEnabled(newValue) {
  325. this.setSetting('abCacheEnabled', newValue);
  326. },
  327. totalFound() {
  328. this.updatePageCount();
  329. },
  330. $route(to) {
  331. this.updateSearchFromRouteQuery(to);
  332. },
  333. langDefault() {
  334. this.updateSearchFromRouteQuery(this.$route);
  335. },
  336. },
  337. };
  338. class Search {
  339. _options = componentOptions;
  340. ready = false;
  341. collection = '';
  342. projectName = '';
  343. loadingMessage = '';
  344. loadingMessage2 = '';
  345. settingsDialogVisible = false;
  346. selectGenreDialogVisible = false;
  347. selectLangDialogVisible = false;
  348. pageCount = 1;
  349. //input field consts
  350. inputMaxLength = 1000;
  351. inputDebounce = 200;
  352. //search fields
  353. search = {
  354. author: '',
  355. series: '',
  356. title: '',
  357. genre: '',
  358. lang: '',
  359. page: 1,
  360. limit: 50,
  361. };
  362. //settings
  363. expanded = [];
  364. expandedSeries = [];
  365. showCounts = true;
  366. showRate = true;
  367. showGenres = true;
  368. showDeleted = false;
  369. abCacheEnabled = true;
  370. langDefault = '';
  371. limit = 20;
  372. //stuff
  373. refreshing = false;
  374. queryFound = -1;
  375. totalFound = 0;
  376. bookRowsOnPage = 100;
  377. inpxHash = '';
  378. genreTree = [];
  379. langList = [];
  380. genreTreeInpxHash = '';
  381. cachedAuthors = {};
  382. hiddenCount = 0;
  383. showTooltips = true;
  384. showMoreCount = showMoreCount;
  385. limitOptions = [
  386. {label: '10', value: 10},
  387. {label: '20', value: 20},
  388. {label: '50', value: 50},
  389. {label: '100', value: 100},
  390. {label: '200', value: 200},
  391. {label: '500', value: 500},
  392. {label: '1000', value: 1000},
  393. ];
  394. searchResult = {};
  395. tableData = [];
  396. liberamaReady = false;
  397. created() {
  398. this.commit = this.$store.commit;
  399. this.loadSettings();
  400. }
  401. mounted() {
  402. (async() => {
  403. //для встраивания в liberama
  404. window.addEventListener('message', (event) => {
  405. if (!_.isObject(event.data) || event.data.from != 'ExternalLibs')
  406. return;
  407. //console.log(event);
  408. this.recvMessage(event.data);
  409. });
  410. //локальный кеш
  411. await authorBooksStorage.init();
  412. this.api = this.$root.api;
  413. if (!this.$root.isMobileDevice)
  414. this.$refs.authorInput.focus();
  415. this.setDefaults();
  416. this.updateSearchFromRouteQuery(this.$route);
  417. //чтоб не вызывался лишний refresh
  418. await this.$nextTick();
  419. this.ready = true;
  420. this.refresh();//no await
  421. this.sendMessage({type: 'mes', data: 'hello-from-inpx-web'});
  422. })();
  423. }
  424. loadSettings() {
  425. const settings = this.settings;
  426. this.search.limit = settings.limit;
  427. this.expanded = _.cloneDeep(settings.expanded);
  428. this.expandedSeries = _.cloneDeep(settings.expandedSeries);
  429. this.showCounts = settings.showCounts;
  430. this.showRate = settings.showRate;
  431. this.showGenres = settings.showGenres;
  432. this.showDeleted = settings.showDeleted;
  433. this.abCacheEnabled = settings.abCacheEnabled;
  434. this.langDefault = settings.langDefault;
  435. }
  436. recvMessage(d) {
  437. if (d.type == 'mes') {
  438. switch(d.data) {
  439. case 'ready':
  440. this.liberamaReady = true;
  441. this.sendMessage({type: 'mes', data: 'ready'});
  442. this.sendCurrentUrl();
  443. break;
  444. }
  445. }
  446. }
  447. sendMessage(d) {
  448. window.parent.postMessage(Object.assign({}, {from: 'inpx-web'}, d), '*');
  449. }
  450. sendCurrentUrl() {
  451. this.sendMessage({type: 'urlChange', data: window.location.href});
  452. }
  453. get config() {
  454. return this.$store.state.config;
  455. }
  456. get settings() {
  457. return this.$store.state.settings;
  458. }
  459. get genreNames() {
  460. let result = [];
  461. const genre = new Set(this.search.genre.split(','));
  462. for (const section of this.genreTree) {
  463. for (const g of section.value)
  464. if (genre.has(g.value))
  465. result.push(g.name);
  466. }
  467. return result.join(', ');
  468. }
  469. get showReadLink() {
  470. return this.config.bookReadLink != '' || this.liberamaReady;
  471. }
  472. openReleasePage() {
  473. window.open('https://github.com/bookpauk/inpx-web/releases', '_blank');
  474. }
  475. makeProjectName() {
  476. const collection = this.config.dbConfig.inpxInfo.collection.split('\n');
  477. this.collection = collection[0].trim();
  478. this.projectName = `${this.config.name} v${this.config.webAppVersion}`;
  479. this.makeTitle();
  480. }
  481. makeTitle() {
  482. if (!this.collection)
  483. return;
  484. let result = `Коллекция ${this.collection}`;
  485. const search = this.search;
  486. const specSym = new Set(['*', '#']);
  487. const correctValue = (v) => {
  488. if (v) {
  489. if (v[0] === '=')
  490. v = v.substring(1);
  491. else if (!specSym.has(v[0]))
  492. v = '^' + v;
  493. }
  494. return v || '';
  495. };
  496. if (search.author || search.series || search.title) {
  497. const as = (search.author ? search.author.split(',') : []);
  498. const author = (as.length ? as[0] : '') + (as.length > 1 ? ' и др.' : '');
  499. const a = correctValue(author);
  500. const sc = correctValue(search.series);
  501. const s = (sc ? `(${sc})` : '');
  502. const t = correctValue(search.title);
  503. result = [s, t].filter(v => v).join(' ');
  504. result = [a, result].filter(v => v).join(' - ');
  505. }
  506. this.$root.setAppTitle(result);
  507. if (this.liberamaReady)
  508. this.sendMessage({type: 'titleChange', data: result});
  509. }
  510. showSearchHelp() {
  511. let info = '';
  512. info += `<div style="min-width: 250px" />`;
  513. info += `
  514. <p>
  515. Работу поискового движка можно описать простой фразой: найти авторов по указанным критериям.
  516. По тем же критериям среди найденных авторов фильтруются книги, сортируются и группируются по сериям.
  517. <br><br>
  518. По умолчанию поисковое значение трактуется как "начинается с". Например значение автора "Пушкин"
  519. трактуется как: найти авторов, имя которых начинается с "Пушкин". Поиск всегда ведется без
  520. учета регистра - значения "Ельцин" и "ельцин" равнозначны.
  521. <br><br>
  522. В поисковых полях "Автор", "Серия", "Название" также доступны следующие префиксы:
  523. <ul>
  524. <li>
  525. "=" поиск по точному совпадению. Например, если задать "=Пушкин Александр Сергеевич" в поле автора,
  526. то будет найден в точности этот автор
  527. </li>
  528. <br>
  529. <li>
  530. "*" поиск подстроки в строке. Например, для "*Александр" в поле автора, будут найдены
  531. все авторы, имя которых содержит "Александр"
  532. </li>
  533. <br>
  534. <li>
  535. "#" поиск подстроки в строке, но только для тех значений, которые не начинаются ни с одной буквы русского или латинского алфавита.
  536. Например, значение "#поворот" в поле автора означает: найти всех авторов, имя которых начинается не с русской или латинской буквы и содержит слово "поворот".
  537. Указание простого "#" в поиске по названию означает: найти всех авторов, названия книг которых начинаются не с русской или латинской буквы
  538. </li>
  539. <br>
  540. <li>
  541. "?" поиск пустых значений или тех, что начинаются с этого символа. Например, "?" в поле серии означает: найти всех авторов, у которых есть книги без серий
  542. или название серии начинается с "?".
  543. Значение "?" в поле названия означает: найти всех авторов, книги которых без названия или начинаются с "?"
  544. </li>
  545. </ul>
  546. <br>
  547. Специльное имя автора "?" служит для поиска и группировки книг без автора.
  548. </p>
  549. `;
  550. this.$root.stdDialog.alert(info, 'Памятка', {iconName: 'la la-info-circle'});
  551. }
  552. showHiddenHelp() {
  553. this.$root.stdDialog.alert(`
  554. Книги этих авторов помечены как удаленные. Для того, чтобы их увидеть, необходимо установить опцию "Показывать удаленные" в настройках.
  555. `, 'Пояснение', {iconName: 'la la-info-circle'});
  556. }
  557. showCollectionInfo() {
  558. /*
  559. "dbConfig": {
  560. "inpxInfo": {
  561. "collection": "Flibusta Offline 2 August 2022\r\nflibusta_all_local_2022-08-02\r\n65537\r\nFlibusta. A local collection. Total: 636591 books\r\nhttp://flibusta.is/",
  562. },
  563. "stats": {
  564. "recsLoaded": 687063,
  565. "authorCount": 153364,
  566. "authorCountAll": 177034,
  567. "bookCount": 576018,
  568. "bookCountAll": 687063,
  569. "bookDelCount": 111045,
  570. "noAuthorBookCount": 4347,
  571. "titleCount": 512671,
  572. "seriesCount": 54472,
  573. "genreCount": 238,
  574. "langCount": 102
  575. },
  576. */
  577. let info = '';
  578. const inpxInfo = this.config.dbConfig.inpxInfo;
  579. const stat = this.config.dbConfig.stats;
  580. const keyStyle = 'style="display: inline-block; text-align: right; margin-right: 5px; min-width: 200px"';
  581. info += `<div style="min-width: 250px" />`;
  582. info += `
  583. <div><div ${keyStyle}>Обработано ссылок на книги:</div><span>${stat.bookCountAll}</span></div>
  584. <div><div ${keyStyle}>Из них актуальных:</div><span>${stat.bookCount}</span></div>
  585. <div><div ${keyStyle}>Помеченных как удаленные:</div><span>${stat.bookDelCount}</span></div>
  586. <div><div ${keyStyle}>Актуальных без автора:</div><span>${stat.noAuthorBookCount}</span></div>
  587. <br>
  588. <div><div ${keyStyle}>Всего записей об авторах:</div><span>${stat.authorCountAll}</span></div>
  589. <div><div ${keyStyle}>Записей без соавторов:</div><span>${stat.authorCount}</span></div>
  590. <div><div ${keyStyle}>С соавторами:</div><span>${stat.authorCountAll- stat.authorCount}</span></div>
  591. <br>
  592. <div><div ${keyStyle}>Уникальных названий книг:</div><span>${stat.titleCount}</span></div>
  593. <div><div ${keyStyle}>Уникальных серий:</div><span>${stat.seriesCount}</span></div>
  594. <div><div ${keyStyle}>Найдено жанров:</div><span>${stat.genreCount}</span></div>
  595. <div><div ${keyStyle}>Найдено языков:</div><span>${stat.langCount}</span></div>
  596. `;
  597. info += `
  598. <div><hr/>
  599. <b>collection.info:</b>
  600. <pre>${inpxInfo.collection}</pre>
  601. </div>
  602. `;
  603. this.$root.stdDialog.alert(info, 'Статистика по коллекции', {iconName: 'la la-info-circle'});
  604. }
  605. newSearch() {
  606. window.location = window.location.origin;
  607. }
  608. async hideTooltip() {
  609. //Firefox bugfix: при всплывающем диалоге скрываем подсказку
  610. this.showTooltips = false;
  611. await utils.sleep(1000);
  612. this.showTooltips = true;
  613. }
  614. selectGenre() {
  615. this.hideTooltip();
  616. this.selectGenreDialogVisible = true;
  617. }
  618. selectLang() {
  619. this.hideTooltip();
  620. this.selectLangDialogVisible = true;
  621. }
  622. onScroll() {
  623. if (this.ignoreScrolling)
  624. return;
  625. const curScrollTop = this.$refs.scroller.scrollTop;
  626. if (!this.lastScrollTop)
  627. this.lastScrollTop = 0;
  628. if (!this.lastScrollTop2)
  629. this.lastScrollTop2 = 0;
  630. if (curScrollTop - this.lastScrollTop > 0) {
  631. this.$refs.toolPanel.style.position = 'relative';
  632. this.$refs.toolPanel.style.top = `${this.lastScrollTop2}px`;
  633. } else {
  634. this.$refs.toolPanel.style.position = 'sticky';
  635. this.$refs.toolPanel.style.top = 0;
  636. this.lastScrollTop2 = curScrollTop;
  637. }
  638. this.lastScrollTop = curScrollTop;
  639. }
  640. async ignoreScroll(ms = 50) {
  641. this.ignoreScrolling = true;
  642. await utils.sleep(ms);
  643. this.ignoreScrolling = false;
  644. }
  645. scrollToTop() {
  646. this.$refs.scroller.scrollTop = 0;
  647. this.lastScrollTop = 0;
  648. }
  649. get foundAuthorsMessage() {
  650. return `Найден${utils.wordEnding(this.totalFound, 2)} ${this.totalFound} автор${utils.wordEnding(this.totalFound)}`;
  651. }
  652. get hiddenResultsMessage() {
  653. return `+${this.hiddenCount} результат${utils.wordEnding(this.hiddenCount)} скрыт${utils.wordEnding(this.hiddenCount, 2)}`;
  654. }
  655. updatePageCount() {
  656. const prevPageCount = this.pageCount;
  657. this.pageCount = Math.ceil(this.totalFound/this.limit);
  658. this.pageCount = (this.pageCount < 1 ? 1 : this.pageCount);
  659. if (this.prevPage && prevPageCount == 1 && this.pageCount > 1 && this.prevPage <= this.pageCount) {
  660. this.search.page = this.prevPage;
  661. }
  662. if (this.search.page > this.pageCount)
  663. this.search.page = 1;
  664. }
  665. getBookCount(item) {
  666. let result = '';
  667. if (!this.showCounts || item.count === undefined)
  668. return result;
  669. if (item.loadedBooks) {
  670. let count = 0;
  671. for (const book of item.loadedBooks) {
  672. if (book.type == 'series')
  673. count += book.books.length;
  674. else
  675. count++;
  676. }
  677. result = `${count}/${item.count}`;
  678. } else
  679. result = `#/${item.count}`;
  680. return `(${result})`;
  681. }
  682. selectAuthor(author) {
  683. this.search.author = `=${author}`;
  684. this.scrollToTop();
  685. }
  686. selectSeries(series) {
  687. this.search.series = `=${series}`;
  688. }
  689. async download(book, action) {
  690. if (this.downloadFlag)
  691. return;
  692. this.downloadFlag = true;
  693. (async() => {
  694. await utils.sleep(200);
  695. if (this.downloadFlag)
  696. this.loadingMessage2 = 'Подготовка файла...';
  697. })();
  698. try {
  699. const makeValidFilenameOrEmpty = (s) => {
  700. try {
  701. return utils.makeValidFilename(s);
  702. } catch(e) {
  703. return '';
  704. }
  705. };
  706. //имя файла
  707. let downFileName = 'default-name';
  708. const author = book.author.split(',');
  709. const at = [author[0], book.title];
  710. downFileName = makeValidFilenameOrEmpty(at.filter(r => r).join(' - '))
  711. || makeValidFilenameOrEmpty(at[0])
  712. || makeValidFilenameOrEmpty(at[1])
  713. || downFileName;
  714. downFileName = downFileName.substring(0, 100);
  715. const ext = `.${book.ext}`;
  716. if (downFileName.substring(downFileName.length - ext.length) != ext)
  717. downFileName += ext;
  718. const bookPath = `${book.folder}/${book.file}${ext}`;
  719. //подготовка
  720. const response = await this.api.getBookLink({bookPath, downFileName});
  721. const link = response.link;
  722. const href = `${window.location.origin}${link}`;
  723. if (action == 'download') {
  724. //скачивание
  725. const d = this.$refs.download;
  726. d.href = href;
  727. d.download = downFileName;
  728. d.click();
  729. } else if (action == 'copyLink') {
  730. //копирование ссылки
  731. if (await utils.copyTextToClipboard(href))
  732. this.$root.notify.success('Ссылка успешно скопирована');
  733. else
  734. this.$root.stdDialog.alert(
  735. `Копирование ссылки не удалось. Пожалуйста, попробуйте еще раз.
  736. <br><br>
  737. <b>Пояснение</b>: вероятно, браузер запретил копирование, т.к. прошло<br>
  738. слишком много времени с момента нажатия на кнопку (инициация<br>
  739. пользовательского события). Сейчас ссылка уже закеширована,<br>
  740. поэтому повторная попытка должна быть успешной.`, 'Ошибка');
  741. } else if (action == 'readBook') {
  742. //читать
  743. if (this.liberamaReady) {
  744. this.sendMessage({type: 'submitUrl', data: href});
  745. } else {
  746. const url = this.config.bookReadLink.replace('${DOWNLOAD_LINK}', href);
  747. window.open(url, '_blank');
  748. }
  749. }
  750. } catch(e) {
  751. this.$root.stdDialog.alert(e.message, 'Ошибка');
  752. } finally {
  753. this.downloadFlag = false;
  754. this.loadingMessage2 = '';
  755. }
  756. }
  757. bookEvent(event) {
  758. switch (event.action) {
  759. case 'titleClick':
  760. this.search.title = `=${event.book.title}`;
  761. break;
  762. case 'download':
  763. case 'copyLink':
  764. case 'readBook':
  765. this.download(event.book, event.action);//no await
  766. break;
  767. }
  768. }
  769. isExpanded(item) {
  770. return this.expanded.indexOf(item.author) >= 0;
  771. }
  772. isExpandedSeries(seriesItem) {
  773. return this.expandedSeries.indexOf(seriesItem.key) >= 0;
  774. }
  775. isFoundSeriesBook(book, subbook) {
  776. if (!book.booksSet) {
  777. book.booksSet = new Set(book.books.map(b => b.id));
  778. }
  779. return book.booksSet.has(subbook.id);
  780. }
  781. setSetting(name, newValue) {
  782. this.commit('setSettings', {[name]: _.cloneDeep(newValue)});
  783. }
  784. setDefaults() {
  785. this.search = Object.assign({}, this.search, {
  786. author: '',
  787. series: '',
  788. title: '',
  789. genre: '',
  790. lang: this.langDefault,
  791. });
  792. }
  793. async updateSearchFromRouteQuery(to) {
  794. if (this.liberamaReady)
  795. this.sendCurrentUrl();
  796. if (this.routeUpdating)
  797. return;
  798. const query = to.query;
  799. this.search = Object.assign({}, this.search, {
  800. author: query.author || '',
  801. series: query.series || '',
  802. title: query.title || '',
  803. genre: query.genre || '',
  804. lang: (typeof(query.lang) == 'string' ? query.lang : this.langDefault),
  805. page: parseInt(query.page, 10) || 1,
  806. limit: parseInt(query.limit, 10) || this.search.limit,
  807. });
  808. if (this.search.limit > 1000)
  809. this.search.limit = 1000;
  810. }
  811. updateRouteQueryFromSearch() {
  812. this.routeUpdating = true;
  813. try {
  814. const oldQuery = this.$route.query;
  815. const query = _.pickBy(this.search);
  816. if (this.search.lang == this.langDefault) {
  817. delete query.lang;
  818. } else {
  819. query.lang = this.search.lang;
  820. }
  821. const diff = diffUtils.getObjDiff(oldQuery, query);
  822. if (!diffUtils.isEmptyObjDiff(diff)) {
  823. this.$router.replace({query});
  824. }
  825. } finally {
  826. (async() => {
  827. await utils.sleep(100);
  828. this.routeUpdating = false;
  829. })();
  830. }
  831. }
  832. async expandAuthor(item) {
  833. const expanded = _.cloneDeep(this.expanded);
  834. const key = item.author;
  835. if (!this.isExpanded(item)) {
  836. expanded.push(key);
  837. await this.getBooks(item);
  838. if (expanded.length > 10) {
  839. expanded.shift();
  840. }
  841. this.setSetting('expanded', expanded);
  842. this.ignoreScroll();
  843. } else {
  844. const i = expanded.indexOf(key);
  845. if (i >= 0) {
  846. expanded.splice(i, 1);
  847. this.setSetting('expanded', expanded);
  848. }
  849. }
  850. }
  851. expandSeries(seriesItem) {
  852. const expandedSeries = _.cloneDeep(this.expandedSeries);
  853. const key = seriesItem.key;
  854. if (!this.isExpandedSeries(seriesItem)) {
  855. expandedSeries.push(key);
  856. if (expandedSeries.length > 100) {
  857. expandedSeries.shift();
  858. }
  859. this.getSeriesBooks(seriesItem); //no await
  860. this.setSetting('expandedSeries', expandedSeries);
  861. this.ignoreScroll();
  862. } else {
  863. const i = expandedSeries.indexOf(key);
  864. if (i >= 0) {
  865. expandedSeries.splice(i, 1);
  866. this.setSetting('expandedSeries', expandedSeries);
  867. }
  868. }
  869. }
  870. async loadBooks(authorId) {
  871. try {
  872. let result;
  873. if (this.abCacheEnabled) {
  874. const key = `${authorId}-${this.inpxHash}`;
  875. const data = await authorBooksStorage.getData(key);
  876. if (data) {
  877. result = JSON.parse(data);
  878. } else {
  879. result = await this.api.getBookList(authorId);
  880. await authorBooksStorage.setData(key, JSON.stringify(result));
  881. }
  882. } else {
  883. result = await this.api.getBookList(authorId);
  884. }
  885. return (result.books ? JSON.parse(result.books) : []);
  886. } catch (e) {
  887. this.$root.stdDialog.alert(e.message, 'Ошибка');
  888. }
  889. }
  890. async loadSeriesBooks(series) {
  891. try {
  892. let result;
  893. if (this.abCacheEnabled) {
  894. const key = `series-${series}-${this.inpxHash}`;
  895. const data = await authorBooksStorage.getData(key);
  896. if (data) {
  897. result = JSON.parse(data);
  898. } else {
  899. result = await this.api.getSeriesBookList(series);
  900. await authorBooksStorage.setData(key, JSON.stringify(result));
  901. }
  902. } else {
  903. result = await this.api.getSeriesBookList(series);
  904. }
  905. return (result.books ? JSON.parse(result.books) : []);
  906. } catch (e) {
  907. this.$root.stdDialog.alert(e.message, 'Ошибка');
  908. }
  909. }
  910. async getSeriesBooks(seriesItem) {
  911. //асинхронно подгружаем все книги серии, блокируем повторный вызов
  912. if (seriesItem.allBooks === null) {
  913. seriesItem.allBooks = undefined;
  914. (async() => {
  915. seriesItem.allBooks = await this.loadSeriesBooks(seriesItem.series);
  916. if (seriesItem.allBooks) {
  917. seriesItem.allBooks = seriesItem.allBooks.filter(book => (this.showDeleted || !book.del));
  918. this.sortSeriesBooks(seriesItem.allBooks);
  919. } else {
  920. seriesItem.allBooks = null;
  921. }
  922. })();
  923. }
  924. }
  925. filterBooks(loadedBooks) {
  926. const s = this.search;
  927. const emptyFieldValue = '?';
  928. const ruAlphabet = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя';
  929. const enAlphabet = 'abcdefghijklmnopqrstuvwxyz';
  930. const enru = new Set((ruAlphabet + enAlphabet).split(''));
  931. const splitAuthor = (author) => {
  932. if (!author) {
  933. author = emptyFieldValue;
  934. }
  935. const result = author.split(',');
  936. if (result.length > 1)
  937. result.push(author);
  938. return result;
  939. };
  940. const filterBySearch = (bookValue, searchValue) => {
  941. if (!searchValue)
  942. return true;
  943. bookValue = bookValue.toLowerCase();
  944. searchValue = searchValue.toLowerCase();
  945. //особая обработка префиксов
  946. if (searchValue[0] == '=') {
  947. searchValue = searchValue.substring(1);
  948. return bookValue == searchValue;
  949. } else if (searchValue[0] == '*') {
  950. searchValue = searchValue.substring(1);
  951. return bookValue.indexOf(searchValue) >= 0;
  952. } else if (searchValue[0] == '#') {
  953. searchValue = searchValue.substring(1);
  954. return !bookValue || (!enru.has(bookValue[0]) && bookValue.indexOf(searchValue) >= 0);
  955. } else if (searchValue[0] == '?') {
  956. return bookValue == '' || bookValue.indexOf(searchValue) == 0;
  957. } else {
  958. return bookValue.indexOf(searchValue) == 0;
  959. }
  960. };
  961. return loadedBooks.filter((book) => {
  962. //author
  963. let authorFound = false;
  964. const authors = splitAuthor(book.author);
  965. for (const a of authors) {
  966. if (filterBySearch(a, s.author)) {
  967. authorFound = true;
  968. break;
  969. }
  970. }
  971. //genre
  972. let genreFound = !s.genre;
  973. if (!genreFound) {
  974. const searchGenres = new Set(s.genre.split(','));
  975. const bookGenres = book.genre.split(',');
  976. for (let g of bookGenres) {
  977. if (!g)
  978. g = emptyFieldValue;
  979. if (searchGenres.has(g)) {
  980. genreFound = true;
  981. break;
  982. }
  983. }
  984. }
  985. //lang
  986. let langFound = !s.lang;
  987. if (!langFound) {
  988. const searchLang = new Set(s.lang.split(','));
  989. langFound = searchLang.has(book.lang || emptyFieldValue);
  990. }
  991. return (this.showDeleted || !book.del)
  992. && authorFound
  993. && filterBySearch(book.series, s.series)
  994. && filterBySearch(book.title, s.title)
  995. && genreFound
  996. && langFound
  997. ;
  998. });
  999. }
  1000. showMore(item, all = false) {
  1001. if (item.loadedBooks) {
  1002. const currentLen = (item.books ? item.books.length : 0);
  1003. let books;
  1004. if (all) {
  1005. books = item.loadedBooks;
  1006. } else {
  1007. books = item.loadedBooks.slice(0, currentLen + showMoreCount);
  1008. }
  1009. item.showMore = (books.length < item.loadedBooks.length);
  1010. item.books = books;
  1011. }
  1012. }
  1013. sortSeriesBooks(books) {
  1014. books.sort((a, b) => {
  1015. const dserno = (a.serno || Number.MAX_VALUE) - (b.serno || Number.MAX_VALUE);
  1016. const dtitle = a.title.localeCompare(b.title);
  1017. const dext = a.ext.localeCompare(b.ext);
  1018. return (dserno ? dserno : (dtitle ? dtitle : dext));
  1019. });
  1020. }
  1021. async getBooks(item) {
  1022. if (item.books) {
  1023. if (item.count > maxItemCount) {
  1024. item.bookLoading = true;
  1025. await utils.sleep(1);//для перерисовки списка
  1026. item.bookLoading = false;
  1027. }
  1028. return;
  1029. }
  1030. if (!this.getBooksFlag)
  1031. this.getBooksFlag = 0;
  1032. this.getBooksFlag++;
  1033. if (item.count > maxItemCount)
  1034. item.bookLoading = true;
  1035. try {
  1036. if (this.getBooksFlag == 1) {
  1037. (async() => {
  1038. await utils.sleep(500);
  1039. if (this.getBooksFlag > 0)
  1040. this.loadingMessage2 = 'Загрузка списка книг...';
  1041. })();
  1042. }
  1043. const loadedBooks = await this.loadBooks(item.key);
  1044. const filtered = this.filterBooks(loadedBooks);
  1045. const prepareBook = (book) => {
  1046. return Object.assign(
  1047. {
  1048. key: book.id,
  1049. type: 'book',
  1050. },
  1051. book
  1052. );
  1053. };
  1054. //объединение по сериям
  1055. const books = [];
  1056. const seriesIndex = {};
  1057. for (const book of filtered) {
  1058. if (book.series) {
  1059. let index = seriesIndex[book.series];
  1060. if (index === undefined) {
  1061. index = books.length;
  1062. books.push(reactive({
  1063. key: `${item.author}-${book.series}`,
  1064. type: 'series',
  1065. series: book.series,
  1066. allBooks: null,
  1067. showAllBooks: false,
  1068. books: [],
  1069. }));
  1070. seriesIndex[book.series] = index;
  1071. }
  1072. books[index].books.push(prepareBook(book));
  1073. } else {
  1074. books.push(prepareBook(book));
  1075. }
  1076. }
  1077. //сортировка
  1078. books.sort((a, b) => {
  1079. if (a.type == 'series') {
  1080. return (b.type == 'series' ? a.key.localeCompare(b.key) : -1);
  1081. } else {
  1082. return (b.type == 'book' ? a.title.localeCompare(b.title) : 1);
  1083. }
  1084. });
  1085. //сортировка внутри серий
  1086. for (const book of books) {
  1087. if (book.type == 'series') {
  1088. this.sortSeriesBooks(book.books);
  1089. //асинхронно подгрузим все книги серии, если она раскрыта
  1090. if (this.isExpandedSeries(book)) {
  1091. this.getSeriesBooks(book);//no await
  1092. }
  1093. }
  1094. }
  1095. if (books.length == 1 && books[0].type == 'series' && !this.isExpandedSeries(books[0])) {
  1096. this.expandSeries(books[0]);
  1097. }
  1098. item.loadedBooks = books;
  1099. this.showMore(item);
  1100. await this.$nextTick();
  1101. } finally {
  1102. item.bookLoading = false;
  1103. this.getBooksFlag--;
  1104. if (this.getBooksFlag == 0)
  1105. this.loadingMessage2 = '';
  1106. }
  1107. }
  1108. async updateGenreTreeIfNeeded() {
  1109. try {
  1110. if (this.genreTreeInpxHash !== this.inpxHash) {
  1111. let result;
  1112. if (this.abCacheEnabled) {
  1113. const key = `genre-tree-${this.inpxHash}`;
  1114. const data = await authorBooksStorage.getData(key);
  1115. if (data) {
  1116. result = JSON.parse(data);
  1117. } else {
  1118. result = await this.api.getGenreTree();
  1119. await authorBooksStorage.setData(key, JSON.stringify(result));
  1120. }
  1121. } else {
  1122. result = await this.api.getGenreTree();
  1123. }
  1124. this.genreTree = result.genreTree;
  1125. this.langList = result.langList;
  1126. this.genreTreeInpxHash = result.inpxHash;
  1127. }
  1128. } catch (e) {
  1129. this.$root.stdDialog.alert(e.message, 'Ошибка');
  1130. }
  1131. }
  1132. async updateTableData() {
  1133. let result = [];
  1134. const expandedSet = new Set(this.expanded);
  1135. const authors = this.searchResult.author;
  1136. if (!authors)
  1137. return;
  1138. let num = 0;
  1139. this.hiddenCount = 0;
  1140. for (const rec of authors) {
  1141. this.cachedAuthors[rec.author] = rec;
  1142. const count = (this.showDeleted ? rec.bookCount + rec.bookDelCount : rec.bookCount);
  1143. if (!count) {
  1144. this.hiddenCount++;
  1145. continue;
  1146. }
  1147. const item = reactive({
  1148. key: rec.id,
  1149. num,
  1150. author: rec.author,
  1151. name: rec.author.replace(/,/g, ', '),
  1152. count,
  1153. loadedBooks: false,
  1154. books: false,
  1155. bookLoading: false,
  1156. showMore: false,
  1157. });
  1158. num++;
  1159. if (expandedSet.has(item.author)) {
  1160. if (authors.length > 1 || item.count > maxItemCount)
  1161. this.getBooks(item);//no await
  1162. else
  1163. await this.getBooks(item);
  1164. }
  1165. result.push(item);
  1166. }
  1167. if (result.length == 1 && !this.isExpanded(result[0])) {
  1168. this.expandAuthor(result[0]);
  1169. }
  1170. this.tableData = result;
  1171. }
  1172. async refresh() {
  1173. if (!this.ready)
  1174. return;
  1175. this.updateRouteQueryFromSearch();
  1176. //оптимизация
  1177. if (this.abCacheEnabled && this.search.author && this.search.author[0] == '=') {
  1178. const authorSearch = this.search.author.substring(1);
  1179. const author = this.cachedAuthors[authorSearch];
  1180. if (author) {
  1181. const key = `${author.id}-${this.inpxHash}`;
  1182. let data = await authorBooksStorage.getData(key);
  1183. if (data) {
  1184. this.queryFound = 1;
  1185. this.totalFound = 1;
  1186. this.searchResult = {author: [author]};
  1187. await this.updateTableData();
  1188. return;
  1189. }
  1190. }
  1191. }
  1192. //параметры запроса
  1193. const offset = (this.search.page - 1)*this.search.limit;
  1194. const newQuery = _.cloneDeep(this.search);
  1195. newQuery.offset = offset;
  1196. this.queryExecute = newQuery;
  1197. if (this.refreshing)
  1198. return;
  1199. this.refreshing = true;
  1200. try {
  1201. while (this.queryExecute) {
  1202. const query = this.queryExecute;
  1203. this.queryExecute = null;
  1204. let inSearch = true;
  1205. (async() => {
  1206. await utils.sleep(500);
  1207. if (inSearch)
  1208. this.loadingMessage = 'Поиск авторов...';
  1209. })();
  1210. try {
  1211. const result = await this.api.search(query);
  1212. this.queryFound = result.author.length;
  1213. this.totalFound = result.totalFound;
  1214. this.inpxHash = result.inpxHash;
  1215. this.searchResult = result;
  1216. await utils.sleep(1);
  1217. if (!this.queryExecute) {
  1218. await this.updateGenreTreeIfNeeded();
  1219. await this.updateTableData();
  1220. this.scrollToTop();
  1221. }
  1222. } catch (e) {
  1223. this.$root.stdDialog.alert(e.message, 'Ошибка');
  1224. } finally {
  1225. inSearch = false;
  1226. this.loadingMessage = '';
  1227. }
  1228. }
  1229. } finally {
  1230. this.refreshing = false;
  1231. }
  1232. }
  1233. }
  1234. export default vueComponent(Search);
  1235. //-----------------------------------------------------------------------------
  1236. </script>
  1237. <style scoped>
  1238. .root {
  1239. }
  1240. .tool-panel {
  1241. border-bottom: 1px solid black;
  1242. }
  1243. .header {
  1244. min-height: 30px;
  1245. }
  1246. .clickable {
  1247. color: blue;
  1248. cursor: pointer;
  1249. }
  1250. .clickable2 {
  1251. cursor: pointer;
  1252. }
  1253. .odd-author {
  1254. background-color: #e8e8e8;
  1255. }
  1256. .book-row {
  1257. margin-left: 50px;
  1258. }
  1259. </style>