RecentBooksPage.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. <template>
  2. <Window ref="window" width="600px" @close="close">
  3. <template #header>
  4. <span v-show="!loading">{{ header }}</span>
  5. <span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7" />
  6. Список загружается
  7. </span>
  8. </template>
  9. <a ref="download" style="display: none;" target="_blank"></a>
  10. <div id="vs-container" ref="vsContainer" class="recent-books-scroll col">
  11. <div ref="header" class="scroll-header row bg-blue-2">
  12. <q-btn class="tool-button" round @click="showSameBookClick">
  13. <q-icon name="la la-caret-right" class="icon" :class="{'expanded-icon': showSameBook}" color="green-8" size="24px" />
  14. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
  15. Показать/скрыть версии книг
  16. </q-tooltip>
  17. </q-btn>
  18. <q-btn class="tool-button" round @click="scrollToBegin">
  19. <q-icon name="la la-arrow-up" color="green-8" size="24px" />
  20. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
  21. В начало списка
  22. </q-tooltip>
  23. </q-btn>
  24. <q-btn class="tool-button" round @click="scrollToEnd">
  25. <q-icon name="la la-arrow-down" color="green-8" size="24px" />
  26. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
  27. В конец списка
  28. </q-tooltip>
  29. </q-btn>
  30. <q-btn class="tool-button" round @click="scrollToActiveBook">
  31. <q-icon name="la la-location-arrow" color="green-8" size="24px" />
  32. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
  33. На текущую книгу
  34. </q-tooltip>
  35. </q-btn>
  36. <q-input
  37. ref="input"
  38. v-model="search"
  39. class="q-ml-sm q-mt-xs"
  40. outlined dense
  41. style="width: 185px"
  42. bg-color="white"
  43. placeholder="Найти"
  44. @click.stop
  45. >
  46. <template #append>
  47. <q-icon v-if="search !== ''" name="la la-times" class="cursor-pointer" @click.stop="resetSearch" />
  48. </template>
  49. </q-input>
  50. <q-select
  51. ref="sortMethod"
  52. v-model="sortMethod"
  53. class="q-ml-sm q-mt-xs"
  54. :options="sortMethodOptions"
  55. style="width: 180px"
  56. bg-color="white"
  57. dropdown-icon="la la-angle-down la-sm"
  58. outlined dense emit-value map-options display-value-sanitize options-sanitize
  59. options-html display-value-html
  60. @update:model-value="sortMethodSelected"
  61. >
  62. <q-tooltip :delay="1500" anchor="bottom middle" content-style="font-size: 80%">
  63. Метод сортировки
  64. </q-tooltip>
  65. <template #selected-item="scope">
  66. <div style="height: 28px; padding-top: 2px; overflow: hidden" v-html="scope.opt.label" />
  67. </template>
  68. </q-select>
  69. </div>
  70. <q-virtual-scroll
  71. ref="virtualScroll"
  72. v-slot="{ item, index }"
  73. :items="tableData"
  74. scroll-target="#vs-container"
  75. virtual-scroll-item-size="80"
  76. @virtual-scroll="onScroll"
  77. >
  78. <div class="table-row row" :class="{even: index % 2 > 0, 'active-book': item.active, 'active-parent-book': item.activeParent}">
  79. <div v-show="item.inGroup" class="row-part column justify-center items-center" style="width: 40px">
  80. <q-icon name="la la-code-branch" size="24px" style="color: green" />
  81. </div>
  82. <div class="row-part column justify-center items-stretch" style="width: 80px">
  83. <div class="col row justify-center items-center clickable" @click="loadBook(item)">
  84. <q-icon name="la la-book" size="40px" style="color: #dddddd" />
  85. </div>
  86. <div v-show="!showSameBook && item.group && item.group.length > 0" class="row justify-center" style="font-size: 70%">
  87. {{ (item.group ? item.group.length + 1 : 0) }} верси{{ wordEnding((item.group ? item.group.length + 1 : 0), 1) }}
  88. </div>
  89. </div>
  90. <div class="row-part column items-stretch clickable break-word" @click="loadBook(item)">
  91. <div
  92. class="col" style="border: 1px solid #cccccc; border-bottom: 0; padding: 4px; line-height: 140%;"
  93. :style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }"
  94. >
  95. <div class="text-green-10" style="font-size: 80%">
  96. {{ item.desc.author }}
  97. </div>
  98. <div style="font-size: 75%">
  99. {{ item.desc.title }}
  100. </div>
  101. </div>
  102. <div class="row" style="font-size: 10px">
  103. <div class="row justify-center items-center row-info-top" style="width: 60px">
  104. {{ item.desc.textLen }}
  105. </div>
  106. <div class="row items-center row-info-top" :style="`width: ${(260 - 40*(+item.inGroup))}px; padding: 1px`">
  107. <div class="read-bar" :style="`width: ${100*item.readPart}%`"></div>
  108. </div>
  109. <div class="row justify-center items-center row-info-top" style="width: 59px">
  110. {{ item.desc.perc }}
  111. </div>
  112. <div class="row-info-top" style="width: 1px">
  113. </div>
  114. </div>
  115. <div class="row" style="font-size: 10px" :style="{ 'width': (380 - 40*(+item.inGroup)) + 'px' }">
  116. <div class="row justify-center items-center row-info-bottom" style="width: 30px">
  117. {{ item.num }}
  118. </div>
  119. <div class="col row">
  120. <div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
  121. Загружен: {{ item.loadTime }}
  122. </div>
  123. <div class="row justify-center items-center row-info-bottom time-info" style="width: 50%">
  124. Читался: {{ item.touchTime }}
  125. </div>
  126. </div>
  127. <div class="row-info-bottom" style="width: 1px">
  128. </div>
  129. </div>
  130. </div>
  131. <div
  132. class="row-part column"
  133. style="width: 90px;"
  134. >
  135. <div
  136. class="col column justify-center"
  137. style="font-size: 75%; padding-left: 6px; border: 1px solid #cccccc; border-left: 0;"
  138. >
  139. <div>
  140. <a v-show="isUrl(item.url)" :href="item.url" target="_blank">Оригинал</a><br><br>
  141. <a :href="item.path" @click.prevent="downloadBook(item.path, item.fullTitle)">Скачать FB2</a>
  142. </div>
  143. </div>
  144. <div
  145. class="del-button self-end row justify-center items-center clickable"
  146. style="position: absolute; border-left: 1px solid #cccccc; border-bottom: 1px solid #cccccc;"
  147. @click="handleDel(item.key)"
  148. >
  149. <q-icon class="la la-times" size="12px" />
  150. </div>
  151. </div>
  152. </div>
  153. </q-virtual-scroll>
  154. </div>
  155. </Window>
  156. </template>
  157. <script>
  158. //-----------------------------------------------------------------------------
  159. import vueComponent from '../../vueComponent.js';
  160. import path from 'path-browserify';
  161. import _ from 'lodash';
  162. import * as utils from '../../../share/utils';
  163. import LockQueue from '../../../share/LockQueue';
  164. import Window from '../../share/Window.vue';
  165. import bookManager from '../share/bookManager';
  166. import readerApi from '../../../api/reader';
  167. const componentOptions = {
  168. components: {
  169. Window,
  170. },
  171. watch: {
  172. search() {
  173. this.updateTableData();
  174. },
  175. sortMethod() {
  176. this.updateTableData();
  177. },
  178. settings() {
  179. this.loadSettings();
  180. },
  181. },
  182. };
  183. class RecentBooksPage {
  184. _options = componentOptions;
  185. loading = false;
  186. search = '';
  187. tableData = [];
  188. sortMethod = '';
  189. showSameBook = false;
  190. created() {
  191. this.commit = this.$store.commit;
  192. this.lastScrollTop1 = 0;
  193. this.lastScrollTop2 = 0;
  194. this.lock = new LockQueue(100);
  195. this.loadSettings();
  196. }
  197. init() {
  198. this.$refs.window.init();
  199. this.$nextTick(() => {
  200. //this.$refs.input.focus();//плохо на планшетах
  201. });
  202. this.inited = true;
  203. (async() => {
  204. this.showBar();
  205. await this.updateTableData();
  206. await this.scrollToActiveBook();
  207. })();
  208. }
  209. loadSettings() {
  210. const settings = this.settings;
  211. this.showSameBook = settings.recentShowSameBook;
  212. this.sortMethod = settings.recentSortMethod || 'loadTimeDesc';
  213. }
  214. get settings() {
  215. return this.$store.state.reader.settings;
  216. }
  217. async updateTableData() {
  218. if (!this.inited)
  219. return;
  220. await this.lock.get();
  221. try {
  222. let result = [];
  223. const sorted = bookManager.getSortedRecent();
  224. const activeBook = bookManager.mostRecentBook();
  225. //подготовка полей
  226. for (const book of sorted) {
  227. if (book.deleted)
  228. continue;
  229. let d = new Date();
  230. d.setTime(book.touchTime);
  231. const touchTime = utils.formatDate(d);
  232. const loadTimeRaw = (book.loadTime ? book.loadTime : 0);//book.addTime);
  233. d.setTime(loadTimeRaw);
  234. const loadTime = utils.formatDate(d);
  235. let readPart = 0;
  236. let perc = '';
  237. let textLen = '';
  238. const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
  239. if (book.textLength) {
  240. readPart = p/book.textLength;
  241. perc = `${(readPart*100).toFixed(2)}%`;
  242. textLen = `${Math.floor(readPart*book.textLength/1000)}/${Math.floor(book.textLength/1000)}`;
  243. }
  244. const bt = utils.getBookTitle(book.fb2);
  245. let title = bt.bookTitle;
  246. title = (title ? `"${title}"`: '');
  247. const author = (bt.author ? bt.author : (bt.bookTitle ? bt.bookTitle : (book.uploadFileName ? book.uploadFileName : book.url)));
  248. result.push({
  249. touchTime,
  250. loadTime,
  251. desc: {
  252. author,
  253. title,
  254. perc,
  255. textLen,
  256. },
  257. readPart,
  258. url: book.url,
  259. path: book.path,
  260. fullTitle: bt.fullTitle,
  261. key: book.key,
  262. sameBookKey: book.sameBookKey,
  263. active: (activeBook.key == book.key),
  264. activeParent: false,
  265. inGroup: false,
  266. //для сортировки
  267. loadTimeRaw,
  268. touchTimeRaw: book.touchTime,
  269. });
  270. }
  271. //нумерация
  272. result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
  273. let num = 0;
  274. for (let i = result.length - 1; i >= 0; i--) {
  275. num++;
  276. result[i].num = num;
  277. }
  278. //фильтрация
  279. const search = this.search;
  280. if (search) {
  281. result = result.filter(item => {
  282. return !search ||
  283. item.touchTime.includes(search) ||
  284. item.loadTime.includes(search) ||
  285. item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
  286. item.desc.author.toLowerCase().includes(search.toLowerCase())
  287. });
  288. }
  289. //сортировка
  290. switch (this.sortMethod) {
  291. case 'loadTimeDesc':
  292. result.sort((a, b) => b.loadTimeRaw - a.loadTimeRaw);
  293. break;
  294. case 'loadTimeAsc':
  295. result.sort((a, b) => a.loadTimeRaw - b.loadTimeRaw);
  296. break;
  297. case 'touchTimeDesc':
  298. result.sort((a, b) => b.touchTimeRaw - a.touchTimeRaw);
  299. break;
  300. case 'touchTimeAsc':
  301. result.sort((a, b) => a.touchTimeRaw - b.touchTimeRaw);
  302. break;
  303. case 'authorDesc':
  304. result.sort((a, b) => b.desc.author.localeCompare(a.desc.author));
  305. break;
  306. case 'authorAsc':
  307. result.sort((a, b) => a.desc.author.localeCompare(b.desc.author));
  308. break;
  309. case 'titleDesc':
  310. result.sort((a, b) => b.desc.title.localeCompare(a.desc.title));
  311. break;
  312. case 'titleAsc':
  313. result.sort((a, b) => a.desc.title.localeCompare(b.desc.title));
  314. break;
  315. }
  316. //группировка
  317. const groups = {};
  318. const parents = {};
  319. let newResult = [];
  320. for (const book of result) {
  321. if (book.sameBookKey !== undefined) {
  322. if (!groups[book.sameBookKey]) {
  323. groups[book.sameBookKey] = [];
  324. parents[book.sameBookKey] = book;
  325. book.group = groups[book.sameBookKey];
  326. newResult.push(book);
  327. } else {
  328. book.inGroup = true;
  329. if (book.active)
  330. parents[book.sameBookKey].activeParent = true;
  331. groups[book.sameBookKey].push(book);
  332. }
  333. } else {
  334. newResult.push(book);
  335. }
  336. }
  337. result = newResult;
  338. //showSameBook
  339. if (this.showSameBook) {
  340. newResult = [];
  341. for (const book of result) {
  342. newResult.push(book);
  343. if (book.group) {
  344. for (const sameBook of book.group) {
  345. newResult.push(sameBook);
  346. }
  347. }
  348. }
  349. result = newResult;
  350. }
  351. //другие стадии
  352. //.....
  353. this.tableData = result;
  354. } finally {
  355. this.lock.ret();
  356. }
  357. }
  358. resetSearch() {
  359. this.search = '';
  360. this.$refs.input.focus();
  361. }
  362. wordEnding(num, type = 0) {
  363. const endings = [
  364. ['ов', '', 'а', 'а', 'а', 'ов', 'ов', 'ов', 'ов', 'ов'],
  365. ['й', 'я', 'и', 'и', 'и', 'й', 'й', 'й', 'й', 'й']
  366. ];
  367. const deci = num % 100;
  368. if (deci > 10 && deci < 20) {
  369. return endings[type][0];
  370. } else {
  371. return endings[type][num % 10];
  372. }
  373. }
  374. get header() {
  375. const len = (this.tableData ? this.tableData.length : 0);
  376. return `${(this.search ? 'Найдено' : 'Всего')} ${len} файл${this.wordEnding(len)}`;
  377. }
  378. async downloadBook(fb2path, fullTitle) {
  379. try {
  380. await readerApi.checkCachedBook(fb2path);
  381. const d = this.$refs.download;
  382. d.href = fb2path;
  383. try {
  384. const fn = utils.makeValidFilename(fullTitle);
  385. d.download = fn.substring(0, 100) + '.fb2';
  386. } catch(e) {
  387. d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
  388. }
  389. d.click();
  390. } catch (e) {
  391. let errMes = e.message;
  392. if (errMes.indexOf('404') >= 0)
  393. errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
  394. this.$root.stdDialog.alert(errMes, 'Ошибка', {color: 'negative'});
  395. }
  396. }
  397. async handleDel(key) {
  398. await bookManager.delRecentBook({key});
  399. //this.updateTableData();//обновление уже происходит Reader.bookManagerEvent
  400. if (!bookManager.mostRecentBook())
  401. this.close();
  402. }
  403. loadBook(row) {
  404. this.$emit('load-book', {url: row.url, path: row.path});
  405. this.close();
  406. }
  407. isUrl(url) {
  408. if (url)
  409. return (url.indexOf('disk://') != 0);
  410. else
  411. return false;
  412. }
  413. showBar() {
  414. this.lastScrollTop1 = this.$refs.vsContainer.scrollTop;
  415. this.$refs.header.style.position = 'sticky';
  416. this.$refs.header.style.top = 0;
  417. }
  418. onScroll() {
  419. const curScrollTop = this.$refs.vsContainer.scrollTop;
  420. if (this.lockScroll) {
  421. this.lastScrollTop1 = curScrollTop;
  422. return;
  423. }
  424. if (curScrollTop - this.lastScrollTop1 > 100) {
  425. this.$refs.header.style.top = `-${this.$refs.header.offsetHeight}px`;
  426. this.$refs.header.style.transition = 'top 0.2s ease 0s';
  427. this.lastScrollTop1 = curScrollTop;
  428. } else if (curScrollTop - this.lastScrollTop2 < 0) {
  429. this.$refs.header.style.position = 'sticky';
  430. this.$refs.header.style.top = 0;
  431. this.lastScrollTop1 = curScrollTop;
  432. }
  433. this.lastScrollTop2 = curScrollTop;
  434. }
  435. showSameBookClick() {
  436. this.showSameBook = !this.showSameBook;
  437. const newSettings = _.cloneDeep(this.settings);
  438. newSettings.recentShowSameBook = this.showSameBook;
  439. this.commit('reader/setSettings', newSettings);
  440. this.updateTableData();
  441. }
  442. sortMethodSelected() {
  443. const newSettings = _.cloneDeep(this.settings);
  444. newSettings.recentSortMethod = this.sortMethod;
  445. this.commit('reader/setSettings', newSettings);
  446. }
  447. async scrollToActiveBook() {
  448. this.lockScroll = true;
  449. try {
  450. let activeIndex = -1;
  451. let activeParentIndex = -1;
  452. for (let i = 0; i < this.tableData.length; i++) {
  453. const book = this.tableData[i];
  454. if (book.active)
  455. activeIndex = i;
  456. if (book.activeParent)
  457. activeParentIndex = i;
  458. if (activeIndex >= 0 && activeParentIndex >= 0)
  459. break;
  460. }
  461. const index = (activeIndex >= 0 ? activeIndex : activeParentIndex);
  462. if (index >= 0) {
  463. this.$refs.virtualScroll.scrollTo(index, 'center');
  464. }
  465. } finally {
  466. await utils.sleep(100);
  467. this.lockScroll = false;
  468. }
  469. }
  470. async scrollToBegin() {
  471. this.lockScroll = true;
  472. try {
  473. this.$refs.virtualScroll.scrollTo(0, 'center');
  474. } finally {
  475. await utils.sleep(100);
  476. this.lockScroll = false;
  477. }
  478. }
  479. async scrollToEnd() {
  480. this.lockScroll = true;
  481. try {
  482. this.$refs.virtualScroll.scrollTo(this.tableData.length, 'center');
  483. } finally {
  484. await utils.sleep(100);
  485. this.lockScroll = false;
  486. }
  487. }
  488. get sortMethodOptions() {
  489. return [
  490. {label: '<span style="font-size: 150%">&uarr;</span> Время загрузки', value: 'loadTimeDesc'},
  491. {label: '<span style="font-size: 150%">&darr;</span> Время загрузки', value: 'loadTimeAsc'},
  492. {label: '<span style="font-size: 150%">&uarr;</span> Время чтения', value: 'touchTimeDesc'},
  493. {label: '<span style="font-size: 150%">&darr;</span> Время чтения', value: 'touchTimeAsc'},
  494. {label: '<span style="font-size: 150%">&uarr;</span> Автор', value: 'authorDesc'},
  495. {label: '<span style="font-size: 150%">&darr;</span> Автор', value: 'authorAsc'},
  496. {label: '<span style="font-size: 150%">&uarr;</span> Название', value: 'titleDesc'},
  497. {label: '<span style="font-size: 150%">&darr;</span> Название', value: 'titleAsc'},
  498. ];
  499. }
  500. close() {
  501. this.$emit('recent-books-close');
  502. }
  503. keyHook(event) {
  504. if (!this.$root.stdDialog.active && event.type == 'keydown' && event.key == 'Escape') {
  505. this.close();
  506. }
  507. return true;
  508. }
  509. }
  510. export default vueComponent(RecentBooksPage);
  511. //-----------------------------------------------------------------------------
  512. </script>
  513. <style scoped>
  514. .recent-books-scroll {
  515. width: 573px;
  516. overflow-y: auto;
  517. overflow-x: hidden;
  518. }
  519. .scroll-header {
  520. height: 50px;
  521. position: sticky;
  522. z-index: 1;
  523. top: 0;
  524. border-bottom: 2px solid #aaaaaa;
  525. padding-left: 5px;
  526. }
  527. .table-row {
  528. min-height: 80px;
  529. }
  530. .row-part {
  531. padding: 4px 0px 4px 0px;
  532. }
  533. .clickable {
  534. cursor: pointer;
  535. }
  536. .break-word {
  537. overflow-wrap: break-word;
  538. word-wrap: break-word;
  539. white-space: normal;
  540. }
  541. .even {
  542. background-color: #f2f2f2;
  543. }
  544. .active-book {
  545. background-color: #b0f0b0 !important;
  546. }
  547. .active-parent-book {
  548. background-color: #ffbbbb !important;
  549. }
  550. .icon {
  551. transition: transform 0.2s;
  552. }
  553. .expanded-icon {
  554. transform: rotate(90deg);
  555. }
  556. .tool-button {
  557. min-width: 30px;
  558. width: 30px;
  559. min-height: 30px;
  560. height: 30px;
  561. margin: 10px 6px 0px 3px;
  562. background-color: white;
  563. }
  564. .row-info-bottom {
  565. line-height: 110%;
  566. border-left: 1px solid #cccccc;
  567. border-bottom: 1px solid #cccccc;
  568. height: 12px;
  569. }
  570. .row-info-top {
  571. line-height: 110%;
  572. border: 1px solid #cccccc;
  573. border-right: 0;
  574. height: 12px;
  575. }
  576. .time-info, .row-info-top {
  577. color: #888888;
  578. }
  579. .read-bar {
  580. height: 4px;
  581. background-color: #bbbbbb;
  582. }
  583. .del-button {
  584. width: 20px;
  585. height: 20px
  586. }
  587. .del-button:hover {
  588. color: white;
  589. background-color: #FF3030;
  590. }
  591. </style>