BaseList.js 17 KB

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