RecentBooksPage.vue 22 KB

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