BaseList.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. import axios from 'axios';
  2. import dayjs from 'dayjs';
  3. import _ from 'lodash';
  4. import authorBooksStorage from './authorBooksStorage';
  5. import BookView from './BookView/BookView.vue';
  6. import LoadingMessage from './LoadingMessage/LoadingMessage.vue';
  7. import * as utils from '../../share/utils';
  8. const showMoreCount = 100;//значение для "Показать еще"
  9. const maxItemCount = 500;//выше этого значения показываем "Загрузка"
  10. const componentOptions = {
  11. components: {
  12. BookView,
  13. LoadingMessage,
  14. },
  15. watch: {
  16. settings() {
  17. this.loadSettings();
  18. },
  19. search: {
  20. handler(newValue) {
  21. this.limit = newValue.limit;
  22. if (this.pageCount > 1)
  23. this.prevPage = this.search.page;
  24. this.refresh();
  25. },
  26. deep: true,
  27. },
  28. showDeleted() {
  29. this.refresh();
  30. },
  31. },
  32. };
  33. export default class BaseList {
  34. _options = componentOptions;
  35. _props = {
  36. list: Object,
  37. search: Object,
  38. genreMap: Object,
  39. };
  40. loadingMessage = '';
  41. loadingMessage2 = '';
  42. //settings
  43. expandedAuthor = [];
  44. expandedSeries = [];
  45. downloadAsZip = false;
  46. showCounts = true;
  47. showRates = true;
  48. showGenres = true;
  49. showDeleted = false;
  50. abCacheEnabled = true;
  51. //stuff
  52. refreshing = false;
  53. showMoreCount = showMoreCount;
  54. maxItemCount = maxItemCount;
  55. searchResult = {};
  56. tableData = [];
  57. created() {
  58. this.commit = this.$store.commit;
  59. this.api = this.$root.api;
  60. this.loadSettings();
  61. }
  62. mounted() {
  63. this.refresh();//no await
  64. }
  65. loadSettings() {
  66. const settings = this.settings;
  67. this.expandedAuthor = _.cloneDeep(settings.expandedAuthor);
  68. this.expandedSeries = _.cloneDeep(settings.expandedSeries);
  69. this.downloadAsZip = settings.downloadAsZip;
  70. this.showCounts = settings.showCounts;
  71. this.showRates = settings.showRates;
  72. this.showGenres = settings.showGenres;
  73. this.showDeleted = settings.showDeleted;
  74. this.abCacheEnabled = settings.abCacheEnabled;
  75. }
  76. get config() {
  77. return this.$store.state.config;
  78. }
  79. get settings() {
  80. return this.$store.state.settings;
  81. }
  82. get showReadLink() {
  83. return this.config.bookReadLink != '' || this.list.liberamaReady;
  84. }
  85. scrollToTop() {
  86. this.$emit('listEvent', {action: 'scrollToTop'});
  87. }
  88. selectAuthor(author) {
  89. this.search.author = `=${author}`;
  90. this.scrollToTop();
  91. }
  92. selectSeries(series) {
  93. this.search.series = `=${series}`;
  94. }
  95. selectTitle(title) {
  96. this.search.title = `=${title}`;
  97. }
  98. async download(book, action) {
  99. if (this.downloadFlag)
  100. return;
  101. this.downloadFlag = true;
  102. (async() => {
  103. await utils.sleep(200);
  104. if (this.downloadFlag)
  105. this.loadingMessage2 = 'Подготовка файла...';
  106. })();
  107. try {
  108. //подготовка
  109. const response = await this.api.getBookLink(book._uid);
  110. const link = response.link;
  111. let href = `${window.location.origin}${link}`;
  112. //downloadAsZip
  113. if (this.downloadAsZip && (action == 'download' || action == 'copyLink')) {
  114. href += '/zip';
  115. //подожлем формирования zip-файла
  116. await axios.head(href);
  117. }
  118. //action
  119. if (action == 'download') {
  120. //скачивание
  121. const d = this.$refs.download;
  122. d.href = href;
  123. d.click();
  124. } else if (action == 'copyLink') {
  125. //копирование ссылки
  126. if (await utils.copyTextToClipboard(href))
  127. this.$root.notify.success('Ссылка успешно скопирована');
  128. else
  129. this.$root.stdDialog.alert(
  130. `Копирование ссылки не удалось. Пожалуйста, попробуйте еще раз.
  131. <br><br>
  132. <b>Пояснение</b>: вероятно, браузер запретил копирование, т.к. прошло<br>
  133. слишком много времени с момента нажатия на кнопку (инициация<br>
  134. пользовательского события). Сейчас ссылка уже закеширована,<br>
  135. поэтому повторная попытка должна быть успешной.`, 'Ошибка');
  136. } else if (action == 'readBook') {
  137. //читать
  138. if (this.list.liberamaReady) {
  139. this.$emit('listEvent', {action: 'submitUrl', data: href});
  140. } else {
  141. const url = this.config.bookReadLink.replace('${DOWNLOAD_LINK}', href);
  142. window.open(url, '_blank');
  143. }
  144. } else if (action == 'bookInfo') {
  145. //информация о книге
  146. const response = await this.api.getBookInfo(book._uid);
  147. this.$emit('listEvent', {action: 'bookInfo', data: response.bookInfo});
  148. }
  149. } catch(e) {
  150. this.$root.stdDialog.alert(e.message, 'Ошибка');
  151. } finally {
  152. this.downloadFlag = false;
  153. this.loadingMessage2 = '';
  154. }
  155. }
  156. bookEvent(event) {
  157. switch (event.action) {
  158. case 'authorClick':
  159. this.selectAuthor(event.book.author);
  160. break;
  161. case 'seriesClick':
  162. this.selectSeries(event.book.series);
  163. break;
  164. case 'titleClick':
  165. this.selectTitle(event.book.title);
  166. break;
  167. case 'download':
  168. case 'copyLink':
  169. case 'readBook':
  170. case 'bookInfo':
  171. this.download(event.book, event.action);//no await
  172. break;
  173. }
  174. }
  175. isExpandedAuthor(item) {
  176. return this.expandedAuthor.indexOf(item.author) >= 0;
  177. }
  178. isExpandedSeries(seriesItem) {
  179. return this.expandedSeries.indexOf(seriesItem.key) >= 0;
  180. }
  181. setSetting(name, newValue) {
  182. this.commit('setSettings', {[name]: _.cloneDeep(newValue)});
  183. }
  184. highlightPageScroller(query) {
  185. this.$emit('listEvent', {action: 'highlightPageScroller', query});
  186. }
  187. async expandSeries(seriesItem) {
  188. this.$emit('listEvent', {action: 'ignoreScroll'});
  189. const expandedSeries = _.cloneDeep(this.expandedSeries);
  190. const key = seriesItem.key;
  191. if (!this.isExpandedSeries(seriesItem)) {
  192. expandedSeries.push(key);
  193. if (expandedSeries.length > 100) {
  194. expandedSeries.shift();
  195. }
  196. this.getSeriesBooks(seriesItem); //no await
  197. this.setSetting('expandedSeries', expandedSeries);
  198. } else {
  199. const i = expandedSeries.indexOf(key);
  200. if (i >= 0) {
  201. expandedSeries.splice(i, 1);
  202. this.setSetting('expandedSeries', expandedSeries);
  203. }
  204. }
  205. }
  206. async loadAuthorBooks(authorId) {
  207. try {
  208. let result;
  209. if (this.abCacheEnabled) {
  210. const key = `author-${authorId}-${this.list.inpxHash}`;
  211. const data = await authorBooksStorage.getData(key);
  212. if (data) {
  213. result = JSON.parse(data);
  214. } else {
  215. result = await this.api.getAuthorBookList(authorId);
  216. await authorBooksStorage.setData(key, JSON.stringify(result));
  217. }
  218. } else {
  219. result = await this.api.getAuthorBookList(authorId);
  220. }
  221. return result.books;
  222. } catch (e) {
  223. this.$root.stdDialog.alert(e.message, 'Ошибка');
  224. }
  225. }
  226. async loadAuthorSeries(authorId) {
  227. try {
  228. let result;
  229. if (this.abCacheEnabled) {
  230. const key = `author-${authorId}-series-${this.list.inpxHash}`;
  231. const data = await authorBooksStorage.getData(key);
  232. if (data) {
  233. result = JSON.parse(data);
  234. } else {
  235. result = await this.api.getAuthorSeriesList(authorId);
  236. await authorBooksStorage.setData(key, JSON.stringify(result));
  237. }
  238. } else {
  239. result = await this.api.getAuthorSeriesList(authorId);
  240. }
  241. return result.series;
  242. } catch (e) {
  243. this.$root.stdDialog.alert(e.message, 'Ошибка');
  244. }
  245. }
  246. async loadSeriesBooks(series) {
  247. try {
  248. let result;
  249. if (this.abCacheEnabled) {
  250. const key = `series-${series}-${this.list.inpxHash}`;
  251. const data = await authorBooksStorage.getData(key);
  252. if (data) {
  253. result = JSON.parse(data);
  254. } else {
  255. result = await this.api.getSeriesBookList(series);
  256. await authorBooksStorage.setData(key, JSON.stringify(result));
  257. }
  258. } else {
  259. result = await this.api.getSeriesBookList(series);
  260. }
  261. return result.books;
  262. } catch (e) {
  263. this.$root.stdDialog.alert(e.message, 'Ошибка');
  264. }
  265. }
  266. async getSeriesBooks(seriesItem) {
  267. //блокируем повторный вызов
  268. if (seriesItem.seriesBookLoading)
  269. return;
  270. seriesItem.seriesBookLoading = true;
  271. try {
  272. seriesItem.allBooksLoaded = await this.loadSeriesBooks(seriesItem.series);
  273. if (seriesItem.allBooksLoaded) {
  274. seriesItem.allBooksLoaded = seriesItem.allBooksLoaded.filter(book => (this.showDeleted || !book.del));
  275. this.sortSeriesBooks(seriesItem.allBooksLoaded);
  276. this.showMoreAll(seriesItem);
  277. }
  278. } finally {
  279. seriesItem.seriesBookLoading = false;
  280. }
  281. }
  282. filterBooks(books) {
  283. const s = this.search;
  284. const emptyFieldValue = '?';
  285. const maxUtf8Char = String.fromCodePoint(0xFFFFF);
  286. const ruAlphabet = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя';
  287. const enAlphabet = 'abcdefghijklmnopqrstuvwxyz';
  288. const enru = new Set((ruAlphabet + enAlphabet).split(''));
  289. const splitAuthor = (author) => {
  290. if (!author) {
  291. author = emptyFieldValue;
  292. }
  293. const result = author.split(',');
  294. if (result.length > 1)
  295. result.push(author);
  296. return result;
  297. };
  298. const filterBySearch = (bookValue, searchValue) => {
  299. if (!searchValue)
  300. return true;
  301. if (!bookValue)
  302. bookValue = emptyFieldValue;
  303. bookValue = bookValue.toLowerCase();
  304. searchValue = searchValue.toLowerCase();
  305. //особая обработка префиксов
  306. if (searchValue[0] == '=') {
  307. searchValue = searchValue.substring(1);
  308. return bookValue.localeCompare(searchValue) == 0;
  309. } else if (searchValue[0] == '*') {
  310. searchValue = searchValue.substring(1);
  311. return bookValue !== emptyFieldValue && bookValue.indexOf(searchValue) >= 0;
  312. } else if (searchValue[0] == '#') {
  313. searchValue = searchValue.substring(1);
  314. return !bookValue || (bookValue !== emptyFieldValue && !enru.has(bookValue[0]) && bookValue.indexOf(searchValue) >= 0);
  315. } else {
  316. //where = `@dirtyIndexLR('value', ${db.esc(a)}, ${db.esc(a + maxUtf8Char)})`;
  317. return bookValue.localeCompare(searchValue) >= 0 && bookValue.localeCompare(searchValue + maxUtf8Char) <= 0;
  318. }
  319. };
  320. return books.filter((book) => {
  321. //author
  322. let authorFound = false;
  323. const authors = splitAuthor(book.author);
  324. for (const a of authors) {
  325. if (filterBySearch(a, s.author)) {
  326. authorFound = true;
  327. break;
  328. }
  329. }
  330. //genre
  331. let genreFound = !s.genre;
  332. if (!genreFound) {
  333. const searchGenres = new Set(s.genre.split(','));
  334. const bookGenres = book.genre.split(',');
  335. for (let g of bookGenres) {
  336. if (!g)
  337. g = emptyFieldValue;
  338. if (searchGenres.has(g)) {
  339. genreFound = true;
  340. break;
  341. }
  342. }
  343. }
  344. //lang
  345. let langFound = !s.lang;
  346. if (!langFound) {
  347. const searchLang = new Set(s.lang.split(','));
  348. langFound = searchLang.has(book.lang || emptyFieldValue);
  349. }
  350. //date
  351. let dateFound = !s.date;
  352. if (!dateFound) {
  353. const date = this.queryDate(s.date).split(',');
  354. let [from = '0000-00-00', to = '9999-99-99'] = date;
  355. dateFound = (book.date >= from && book.date <= to);
  356. }
  357. //librate
  358. let librateFound = !s.librate;
  359. if (!librateFound) {
  360. const searchLibrate = new Set(s.librate.split(',').map(n => parseInt(n, 10)).filter(n => !isNaN(n)));
  361. librateFound = searchLibrate.has(book.librate);
  362. }
  363. return (this.showDeleted || !book.del)
  364. && authorFound
  365. && filterBySearch(book.series, s.series)
  366. && filterBySearch(book.title, s.title)
  367. && genreFound
  368. && langFound
  369. && dateFound
  370. && librateFound
  371. ;
  372. });
  373. }
  374. showMore(item, all = false) {
  375. if (item.booksLoaded) {
  376. const currentLen = (item.books ? item.books.length : 0);
  377. let books;
  378. if (all || currentLen + this.showMoreCount*1.5 > item.booksLoaded.length) {
  379. books = item.booksLoaded;
  380. } else {
  381. books = item.booksLoaded.slice(0, currentLen + this.showMoreCount);
  382. }
  383. item.showMore = (books.length < item.booksLoaded.length);
  384. item.books = books;
  385. }
  386. }
  387. showMoreAll(seriesItem, all = false) {
  388. if (seriesItem.allBooksLoaded) {
  389. const currentLen = (seriesItem.allBooks ? seriesItem.allBooks.length : 0);
  390. let books;
  391. if (all || currentLen + this.showMoreCount*1.5 > seriesItem.allBooksLoaded.length) {
  392. books = seriesItem.allBooksLoaded;
  393. } else {
  394. books = seriesItem.allBooksLoaded.slice(0, currentLen + this.showMoreCount);
  395. }
  396. seriesItem.showMoreAll = (books.length < seriesItem.allBooksLoaded.length);
  397. seriesItem.allBooks = books;
  398. }
  399. }
  400. sortSeriesBooks(seriesBooks) {
  401. seriesBooks.sort((a, b) => {
  402. const dserno = (a.serno || Number.MAX_VALUE) - (b.serno || Number.MAX_VALUE);
  403. const dtitle = a.title.localeCompare(b.title);
  404. const dext = a.ext.localeCompare(b.ext);
  405. return (dserno ? dserno : (dtitle ? dtitle : dext));
  406. });
  407. }
  408. queryDate(date) {
  409. if (!utils.isManualDate(date)) {//!manual
  410. /*
  411. {label: 'сегодня', value: 'today'},
  412. {label: 'за 3 дня', value: '3days'},
  413. {label: 'за неделю', value: 'week'},
  414. {label: 'за 2 недели', value: '2weeks'},
  415. {label: 'за месяц', value: 'month'},
  416. {label: 'за 2 месяца', value: '2months'},
  417. {label: 'за 3 месяца', value: '3months'},
  418. {label: 'указать даты', value: 'manual'},
  419. */
  420. const sqlFormat = 'YYYY-MM-DD';
  421. switch (date) {
  422. case 'today': date = utils.dateFormat(dayjs(), sqlFormat); break;
  423. case '3days': date = utils.dateFormat(dayjs().subtract(3, 'days'), sqlFormat); break;
  424. case 'week': date = utils.dateFormat(dayjs().subtract(1, 'weeks'), sqlFormat); break;
  425. case '2weeks': date = utils.dateFormat(dayjs().subtract(2, 'weeks'), sqlFormat); break;
  426. case 'month': date = utils.dateFormat(dayjs().subtract(1, 'months'), sqlFormat); break;
  427. case '2months': date = utils.dateFormat(dayjs().subtract(2, 'months'), sqlFormat); break;
  428. case '3months': date = utils.dateFormat(dayjs().subtract(3, 'months'), sqlFormat); break;
  429. default:
  430. date = '';
  431. }
  432. }
  433. return date;
  434. }
  435. getQuery() {
  436. let newQuery = _.cloneDeep(this.search);
  437. newQuery = newQuery.setDefaults(newQuery);
  438. delete newQuery.setDefaults;
  439. //дата
  440. if (newQuery.date) {
  441. newQuery.date = this.queryDate(newQuery.date);
  442. }
  443. //offset
  444. newQuery.offset = (newQuery.page - 1)*newQuery.limit;
  445. //del
  446. if (!this.showDeleted)
  447. newQuery.del = 0;
  448. return newQuery;
  449. }
  450. }