526 lines
16 KiB
Vue
526 lines
16 KiB
Vue
<template>
|
|
<Window width="600px" ref="window" @close="close">
|
|
<template slot="header">
|
|
<span v-show="!loading">Последние {{tableData ? tableData.length : 0}} открытых книг</span>
|
|
<span v-if="loading"><q-spinner class="q-mr-sm" color="lime-12" size="20px" :thickness="7"/>Список загружается</span>
|
|
</template>
|
|
|
|
<a ref="download" style='display: none;'></a>
|
|
|
|
<!--q-table
|
|
title="Treats"
|
|
:data="data1"
|
|
:columns="columns1"
|
|
row-key="name"
|
|
/-->
|
|
<q-table
|
|
class="recent-books-table col"
|
|
:data="tableData"
|
|
:columns="columns"
|
|
row-key="key"
|
|
:pagination.sync="pagination"
|
|
separator="cell"
|
|
hide-bottom
|
|
virtual-scroll
|
|
dense
|
|
>
|
|
<template v-slot:header="props">
|
|
<q-tr :props="props">
|
|
<q-th
|
|
v-for="col in props.cols"
|
|
:key="col.name"
|
|
:props="props"
|
|
>
|
|
<span v-html="col.label"></span>
|
|
</q-th>
|
|
</q-tr>
|
|
</template>
|
|
|
|
<template v-slot:body="props">
|
|
<q-tr :props="props">
|
|
<q-td key="num" :props="props" class="td-mp" auto-width>
|
|
<div class="break-word" style="width: 25px">
|
|
{{ props.row.num }}
|
|
</div>
|
|
</q-td>
|
|
|
|
<q-td key="date" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
|
<div class="break-word" style="width: 68px">
|
|
{{ props.row.touchDate }}<br>
|
|
{{ props.row.touchTime }}
|
|
</div>
|
|
</q-td>
|
|
|
|
<q-td key="desc" :props="props" class="td-mp clickable" @click="loadBook(props.row.url)" auto-width>
|
|
<div class="break-word" style="width: 332px; font-size: 90%">
|
|
<div style="color: green">{{ props.row.desc.author }}</div>
|
|
<div>{{ props.row.desc.title }}</div>
|
|
</div>
|
|
</q-td>
|
|
|
|
<q-td key="links" :props="props" class="td-mp" auto-width>
|
|
<div class="break-word" style="width: 75px; font-size: 90%">
|
|
<a v-show="isUrl(props.row.url)" :href="props.row.url" target="_blank">Оригинал</a><br>
|
|
<a :href="props.row.path" @click.prevent="downloadBook(props.row.path)">Скачать FB2</a>
|
|
</div>
|
|
</q-td>
|
|
|
|
<q-td key="close" :props="props" class="td-mp" auto-width>
|
|
<div style="width: 38px">
|
|
<q-btn
|
|
dense
|
|
style="width: 30px; height: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
|
@click="handleDel(props.row.key)">
|
|
<q-icon class="la la-times" size="16px" style="top: -5px"/>
|
|
</q-btn>
|
|
</div>
|
|
</q-td>
|
|
<q-td key="last" :props="props" class="no-mp">
|
|
</q-td>
|
|
</q-tr>
|
|
</template>
|
|
</q-table>
|
|
|
|
</Window>
|
|
</template>
|
|
|
|
<script>
|
|
/*
|
|
<el-table
|
|
:data="tableData"
|
|
style="width: 570px"
|
|
size="mini"
|
|
height="1px"
|
|
stripe
|
|
border
|
|
:default-sort = "{prop: 'touchDateTime', order: 'descending'}"
|
|
:header-cell-style = "headerCellStyle"
|
|
:row-key = "rowKey"
|
|
>
|
|
|
|
<el-table-column
|
|
type="index"
|
|
width="35px"
|
|
>
|
|
</el-table-column>
|
|
<el-table-column
|
|
prop="touchDateTime"
|
|
min-width="85px"
|
|
sortable
|
|
>
|
|
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
<span style="font-size: 90%">Время<br>просм.</span>
|
|
</template>
|
|
<template slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
<div class="desc" @click="loadBook(scope.row.url)">
|
|
{{ scope.row.touchDate }}<br>
|
|
{{ scope.row.touchTime }}
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column
|
|
>
|
|
<template slot="header" slot-scope="scope"><!-- eslint-disable-line vue/no-unused-vars -->
|
|
<!--el-input ref="input"
|
|
:value="search" @input="search = $event"
|
|
size="mini"
|
|
style="margin: 0; padding: 0; vertical-align: bottom; margin-top: 10px"
|
|
placeholder="Найти"/-->
|
|
<div class="el-input el-input--mini">
|
|
<input class="el-input__inner"
|
|
ref="input"
|
|
placeholder="Найти"
|
|
style="margin: 0; vertical-align: bottom; margin-top: 20px; padding: 0 10px 0 10px"
|
|
:value="search" @input="search = $event.target.value"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<el-table-column
|
|
min-width="280px"
|
|
>
|
|
<template slot-scope="scope">
|
|
<div class="desc" @click="loadBook(scope.row.url)">
|
|
<span style="color: green">{{ scope.row.desc.author }}</span><br>
|
|
<span>{{ scope.row.desc.title }}</span>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column
|
|
min-width="90px"
|
|
>
|
|
<template slot-scope="scope">
|
|
<a v-show="isUrl(scope.row.url)" :href="scope.row.url" target="_blank">Оригинал</a><br>
|
|
<a :href="scope.row.path" @click.prevent="downloadBook(scope.row.path)">Скачать FB2</a>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
<el-table-column
|
|
width="60px"
|
|
>
|
|
<template slot-scope="scope">
|
|
<el-button
|
|
size="mini"
|
|
style="width: 30px; padding: 7px 0 7px 0; margin-left: 4px"
|
|
@click="handleDel(scope.row.key)"><i class="el-icon-close"></i>
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
|
|
</el-table-column>
|
|
|
|
</el-table>
|
|
*/
|
|
//-----------------------------------------------------------------------------
|
|
import Vue from 'vue';
|
|
import Component from 'vue-class-component';
|
|
import path from 'path';
|
|
import _ from 'lodash';
|
|
|
|
import * as utils from '../../../share/utils';
|
|
import Window from '../../share/Window.vue';
|
|
import bookManager from '../share/bookManager';
|
|
import readerApi from '../../../api/reader';
|
|
|
|
export default @Component({
|
|
components: {
|
|
Window,
|
|
},
|
|
watch: {
|
|
search: function() {
|
|
this.updateTableData();
|
|
}
|
|
},
|
|
})
|
|
class RecentBooksPage extends Vue {
|
|
loading = false;
|
|
search = null;
|
|
tableData = [];
|
|
columns = [];
|
|
pagination = {};
|
|
|
|
created() {
|
|
/* this.columns1 = [
|
|
{
|
|
name: 'name',
|
|
required: true,
|
|
label: 'Dessert (100g serving)',
|
|
align: 'left',
|
|
field: row => row.name,
|
|
format: val => `${val}`,
|
|
sortable: true
|
|
},
|
|
{ name: 'calories', align: 'center', label: 'Calories', field: 'calories', sortable: true },
|
|
{ name: 'fat', label: 'Fat (g)', field: 'fat', sortable: true },
|
|
{ name: 'carbs', label: 'Carbs (g)', field: 'carbs' },
|
|
{ name: 'protein', label: 'Protein (g)', field: 'protein' },
|
|
{ name: 'sodium', label: 'Sodium (mg)', field: 'sodium' },
|
|
{ name: 'calcium', label: 'Calcium (%)', field: 'calcium', sortable: true, sort: (a, b) => parseInt(a, 10) - parseInt(b, 10) },
|
|
{ name: 'iron', label: 'Iron (%)', field: 'iron', sortable: true, sort: (a, b) => parseInt(a, 10) - parseInt(b, 10) }
|
|
];
|
|
this.data1 = [
|
|
{
|
|
name: 'Frozen Yogurt',
|
|
calories: 159,
|
|
fat: 6.0,
|
|
carbs: 24,
|
|
protein: 4.0,
|
|
sodium: 87,
|
|
calcium: '14%',
|
|
iron: '1%'
|
|
},
|
|
{
|
|
name: 'Ice cream sandwich',
|
|
calories: 237,
|
|
fat: 9.0,
|
|
carbs: 37,
|
|
protein: 4.3,
|
|
sodium: 129,
|
|
calcium: '8%',
|
|
iron: '1%'
|
|
},
|
|
{
|
|
name: 'Eclair',
|
|
calories: 262,
|
|
fat: 16.0,
|
|
carbs: 23,
|
|
protein: 6.0,
|
|
sodium: 337,
|
|
calcium: '6%',
|
|
iron: '7%'
|
|
}
|
|
];
|
|
*/
|
|
this.pagination = {rowsPerPage: 0};
|
|
|
|
this.columns = [
|
|
{
|
|
name: 'num',
|
|
label: '#',
|
|
align: 'center',
|
|
field: 'num',
|
|
},
|
|
{
|
|
name: 'date',
|
|
label: 'Время<br>просм.',
|
|
align: 'left',
|
|
sortable: true,
|
|
sort: (a, b, rowA, rowB) => rowA.touchDateTime - rowB.touchDateTime,
|
|
},
|
|
{
|
|
name: 'desc',
|
|
label: 'Название',
|
|
align: 'left',
|
|
sortable: true,
|
|
},
|
|
{
|
|
name: 'links',
|
|
label: '',
|
|
align: 'left',
|
|
},
|
|
{
|
|
name: 'close',
|
|
label: '',
|
|
align: 'left',
|
|
},
|
|
{
|
|
name: 'last',
|
|
label: '',
|
|
align: 'left',
|
|
},
|
|
];
|
|
}
|
|
|
|
init() {
|
|
this.$refs.window.init();
|
|
|
|
this.$nextTick(() => {
|
|
//this.$refs.input.focus();
|
|
});
|
|
(async() => {//подгрузка списка
|
|
if (this.initing)
|
|
return;
|
|
this.initing = true;
|
|
|
|
|
|
if (!bookManager.loaded) {
|
|
await this.updateTableData(10);
|
|
//для отзывчивости
|
|
await utils.sleep(100);
|
|
let i = 0;
|
|
let j = 5;
|
|
while (i < 500 && !bookManager.loaded) {
|
|
if (i % j == 0) {
|
|
bookManager.sortedRecentCached = null;
|
|
await this.updateTableData(20);
|
|
j *= 2;
|
|
}
|
|
|
|
await utils.sleep(100);
|
|
i++;
|
|
}
|
|
} else {
|
|
//для отзывчивости
|
|
await utils.sleep(100);
|
|
}
|
|
await this.updateTableData();
|
|
this.initing = false;
|
|
})();
|
|
}
|
|
|
|
async updateTableData(limit) {
|
|
while (this.updating) await utils.sleep(100);
|
|
this.updating = true;
|
|
let result = [];
|
|
|
|
this.loading = !!limit;
|
|
const sorted = bookManager.getSortedRecent();
|
|
|
|
let num = 0;
|
|
for (let i = 0; i < sorted.length; i++) {
|
|
const book = sorted[i];
|
|
if (book.deleted)
|
|
continue;
|
|
|
|
num++;
|
|
if (limit && result.length >= limit)
|
|
break;
|
|
|
|
let d = new Date();
|
|
d.setTime(book.touchTime);
|
|
const t = utils.formatDate(d).split(' ');
|
|
|
|
let perc = '';
|
|
let textLen = '';
|
|
const p = (book.bookPosSeen ? book.bookPosSeen : (book.bookPos ? book.bookPos : 0));
|
|
if (book.textLength) {
|
|
perc = ` [${((p/book.textLength)*100).toFixed(2)}%]`;
|
|
textLen = ` ${Math.round(book.textLength/1000)}k`;
|
|
}
|
|
|
|
const fb2 = (book.fb2 ? book.fb2 : {});
|
|
|
|
let title = fb2.bookTitle;
|
|
if (title)
|
|
title = `"${title}"`;
|
|
else
|
|
title = '';
|
|
|
|
let author = '';
|
|
if (fb2.author) {
|
|
const authorNames = fb2.author.map(a => _.compact([
|
|
a.lastName,
|
|
a.firstName,
|
|
a.middleName
|
|
]).join(' '));
|
|
author = authorNames.join(', ');
|
|
} else {//TODO: убрать в будущем
|
|
author = _.compact([
|
|
fb2.lastName,
|
|
fb2.firstName,
|
|
fb2.middleName
|
|
]).join(' ');
|
|
}
|
|
author = (author ? author : (fb2.bookTitle ? fb2.bookTitle : book.url));
|
|
|
|
result.push({
|
|
num,
|
|
touchDateTime: book.touchTime,
|
|
touchDate: t[0],
|
|
touchTime: t[1],
|
|
desc: {
|
|
title: `${title}${perc}${textLen}`,
|
|
author,
|
|
},
|
|
url: book.url,
|
|
path: book.path,
|
|
key: book.key,
|
|
});
|
|
}
|
|
|
|
const search = this.search;
|
|
result = result.filter(item => {
|
|
return !search ||
|
|
item.touchTime.includes(search) ||
|
|
item.touchDate.includes(search) ||
|
|
item.desc.title.toLowerCase().includes(search.toLowerCase()) ||
|
|
item.desc.author.toLowerCase().includes(search.toLowerCase())
|
|
});
|
|
|
|
/*for (let i = 0; i < result.length; i++) {
|
|
if (!_.isEqual(this.tableData[i], result[i])) {
|
|
this.$set(this.tableData, i, result[i]);
|
|
await utils.sleep(10);
|
|
}
|
|
}
|
|
if (this.tableData.length > result.length)
|
|
this.tableData.splice(result.length);*/
|
|
|
|
this.tableData = result;
|
|
this.updating = false;
|
|
}
|
|
|
|
async downloadBook(fb2path) {
|
|
try {
|
|
await readerApi.checkCachedBook(fb2path);
|
|
|
|
const d = this.$refs.download;
|
|
d.href = fb2path;
|
|
d.download = path.basename(fb2path).substr(0, 10) + '.fb2';
|
|
d.click();
|
|
} catch (e) {
|
|
let errMes = e.message;
|
|
if (errMes.indexOf('404') >= 0)
|
|
errMes = 'Файл не найден на сервере (возможно был удален как устаревший)';
|
|
this.$alert(errMes, 'Ошибка', {type: 'error'});
|
|
}
|
|
}
|
|
|
|
openOriginal(url) {
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
openFb2(path) {
|
|
window.open(path, '_blank');
|
|
}
|
|
|
|
async handleDel(key) {
|
|
await bookManager.delRecentBook({key});
|
|
this.updateTableData();
|
|
|
|
if (!bookManager.mostRecentBook())
|
|
this.close();
|
|
}
|
|
|
|
loadBook(url) {
|
|
this.$emit('load-book', {url});
|
|
this.close();
|
|
}
|
|
|
|
isUrl(url) {
|
|
if (url)
|
|
return (url.indexOf('file://') != 0);
|
|
else
|
|
return false;
|
|
}
|
|
|
|
close() {
|
|
this.$emit('recent-books-close');
|
|
}
|
|
|
|
keyHook(event) {
|
|
if (event.type == 'keydown' && event.code == 'Escape') {
|
|
this.close();
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
//-----------------------------------------------------------------------------
|
|
</script>
|
|
|
|
<style scoped>
|
|
.recent-books-table {
|
|
width: 600px;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.clickable {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.td-mp {
|
|
margin: 0 !important;
|
|
padding: 4px 4px 4px 4px !important;
|
|
}
|
|
|
|
.no-mp {
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
}
|
|
|
|
.break-word {
|
|
line-height: 180%;
|
|
overflow-wrap: break-word;
|
|
word-wrap: break-word;
|
|
white-space: normal;
|
|
}
|
|
|
|
</style>
|
|
|
|
<style>
|
|
.recent-books-table .q-table__middle {
|
|
height: 100%;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.recent-books-table thead tr:first-child th {
|
|
position: sticky;
|
|
z-index: 1;
|
|
top: 0;
|
|
background-color: #c1f4cd;
|
|
}
|
|
</style>
|