mirror of
https://github.com/gotson/komga.git
synced 2025-12-06 08:32:25 +01:00
feat(webui): add pagination to readlist/collection browse view
Closes: #817
This commit is contained in:
parent
17ca7f74eb
commit
ff70fea71a
2 changed files with 235 additions and 39 deletions
|
|
@ -36,6 +36,8 @@
|
|||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<page-size-select v-model="pageSize"/>
|
||||
|
||||
<v-btn icon @click="drawer = !drawer">
|
||||
<v-icon :color="filterActive ? 'secondary' : ''">mdi-filter-variant</v-icon>
|
||||
</v-btn>
|
||||
|
|
@ -90,7 +92,7 @@
|
|||
|
||||
<v-container fluid>
|
||||
<empty-state
|
||||
v-if="series.length === 0"
|
||||
v-if="totalPages === 0"
|
||||
:title="$t('common.filter_no_matches')"
|
||||
:sub-title="$t('common.use_filter_panel_to_change_filter')"
|
||||
icon="mdi-book-multiple"
|
||||
|
|
@ -99,14 +101,29 @@
|
|||
<v-btn @click="resetFilters">{{ $t('common.reset_filters') }}</v-btn>
|
||||
</empty-state>
|
||||
|
||||
<item-browser
|
||||
v-else
|
||||
:items.sync="series"
|
||||
:selected.sync="selectedSeries"
|
||||
:edit-function="isAdmin ? editSingleSeries : undefined"
|
||||
:draggable="editElements && collection.ordered"
|
||||
:deletable="editElements"
|
||||
/>
|
||||
<template v-else>
|
||||
<v-pagination
|
||||
v-if="totalPages > 1"
|
||||
v-model="page"
|
||||
:total-visible="paginationVisible"
|
||||
:length="totalPages"
|
||||
/>
|
||||
|
||||
<item-browser
|
||||
:items.sync="series"
|
||||
:selected.sync="selectedSeries"
|
||||
:edit-function="isAdmin ? editSingleSeries : undefined"
|
||||
:draggable="editElements && collection.ordered"
|
||||
:deletable="editElements"
|
||||
/>
|
||||
|
||||
<v-pagination
|
||||
v-if="totalPages > 1"
|
||||
v-model="page"
|
||||
:total-visible="paginationVisible"
|
||||
:length="totalPages"
|
||||
/>
|
||||
</template>
|
||||
|
||||
</v-container>
|
||||
|
||||
|
|
@ -138,16 +155,18 @@ import {Location} from 'vue-router'
|
|||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import {SeriesDto} from '@/types/komga-series'
|
||||
import {authorRoles} from '@/types/author-roles'
|
||||
import {AuthorDto} from '@/types/komga-books'
|
||||
import {AuthorDto, BookDto} from '@/types/komga-books'
|
||||
import {CollectionSseDto, ReadProgressSeriesSseDto, SeriesSseDto} from '@/types/komga-sse'
|
||||
import {throttle} from 'lodash'
|
||||
import {LibraryDto} from '@/types/komga-libraries'
|
||||
import {parseBooleanFilter} from '@/functions/query-params'
|
||||
import {ContextOrigin} from '@/types/context'
|
||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseCollection',
|
||||
components: {
|
||||
PageSizeSelect,
|
||||
ToolbarSticky,
|
||||
ItemBrowser,
|
||||
CollectionActionsMenu,
|
||||
|
|
@ -163,9 +182,16 @@ export default Vue.extend({
|
|||
series: [] as SeriesDto[],
|
||||
seriesCopy: [] as SeriesDto[],
|
||||
selectedSeries: [] as SeriesDto[],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
unpaged: false,
|
||||
totalPages: 1,
|
||||
totalElements: null as number | null,
|
||||
editElements: false,
|
||||
filters: {} as FiltersActive,
|
||||
filterUnwatch: null as any,
|
||||
pageUnwatch: null as any,
|
||||
pageSizeUnwatch: null as any,
|
||||
drawer: false,
|
||||
filterOptions: {
|
||||
library: [] as NameValue[],
|
||||
|
|
@ -201,7 +227,12 @@ export default Vue.extend({
|
|||
this.$eventHub.$off(READPROGRESS_SERIES_DELETED, this.readProgressChanged)
|
||||
},
|
||||
async mounted() {
|
||||
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
|
||||
|
||||
// restore from query param
|
||||
await this.resetParams(this.$route, this.collectionId)
|
||||
if (this.$route.query.page) this.page = Number(this.$route.query.page)
|
||||
if (this.$route.query.pageSize) this.pageSize = Number(this.$route.query.pageSize)
|
||||
|
||||
this.loadCollection(this.collectionId)
|
||||
|
||||
|
|
@ -213,6 +244,9 @@ export default Vue.extend({
|
|||
|
||||
// reset
|
||||
await this.resetParams(this.$route, to.params.collectionId)
|
||||
this.page = 1
|
||||
this.totalPages = 1
|
||||
this.totalElements = null
|
||||
this.series = []
|
||||
this.editElements = false
|
||||
|
||||
|
|
@ -224,6 +258,19 @@ export default Vue.extend({
|
|||
next()
|
||||
},
|
||||
computed: {
|
||||
paginationVisible(): number {
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
case 'xs':
|
||||
return 5
|
||||
case 'sm':
|
||||
case 'md':
|
||||
return 10
|
||||
case 'lg':
|
||||
case 'xl':
|
||||
default:
|
||||
return 15
|
||||
}
|
||||
},
|
||||
filterOptionsList(): FiltersOptions {
|
||||
return {
|
||||
readStatus: {
|
||||
|
|
@ -352,9 +399,20 @@ export default Vue.extend({
|
|||
this.$store.commit('setCollectionFilter', {id: this.collectionId, filter: val})
|
||||
this.updateRouteAndReload()
|
||||
})
|
||||
this.pageSizeUnwatch = this.$watch('pageSize', (val) => {
|
||||
this.$store.commit('setBrowsingPageSize', val)
|
||||
this.updateRouteAndReload()
|
||||
})
|
||||
|
||||
this.pageUnwatch = this.$watch('page', (val) => {
|
||||
this.updateRoute()
|
||||
this.loadPage(this.collectionId, val)
|
||||
})
|
||||
},
|
||||
unsetWatches() {
|
||||
this.filterUnwatch()
|
||||
this.pageUnwatch()
|
||||
this.pageSizeUnwatch()
|
||||
},
|
||||
collectionChanged(event: CollectionSseDto) {
|
||||
if (event.collectionId === this.collectionId) {
|
||||
|
|
@ -369,15 +427,25 @@ export default Vue.extend({
|
|||
updateRouteAndReload() {
|
||||
this.unsetWatches()
|
||||
|
||||
this.page = 1
|
||||
|
||||
this.updateRoute()
|
||||
this.loadSeries(this.collectionId)
|
||||
this.loadPage(this.collectionId, this.page)
|
||||
|
||||
this.setWatches()
|
||||
},
|
||||
reloadSeries: throttle(function (this: any) {
|
||||
this.loadSeries(this.collectionId)
|
||||
}, 1000),
|
||||
async loadSeries(collectionId: string) {
|
||||
// reloadSeries: throttle(function (this: any) {
|
||||
// this.loadSeries(this.collectionId)
|
||||
// }, 1000),
|
||||
async loadPage(collectionId: string, page: number) {
|
||||
this.selectedSeries = []
|
||||
|
||||
const pageRequest = {
|
||||
page: page - 1,
|
||||
size: this.pageSize,
|
||||
unpaged: this.unpaged,
|
||||
} as PageRequest
|
||||
|
||||
let authorsFilter = [] as AuthorDto[]
|
||||
authorRoles.forEach((role: string) => {
|
||||
if (role in this.filters) this.filters[role].forEach((name: string) => authorsFilter.push({
|
||||
|
|
@ -387,22 +455,48 @@ export default Vue.extend({
|
|||
})
|
||||
|
||||
const complete = parseBooleanFilter(this.filters.complete)
|
||||
this.series = (await this.$komgaCollections.getSeries(collectionId, {unpaged: true} as PageRequest, this.filters.library, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, complete)).content
|
||||
const seriesPage = await this.$komgaCollections.getSeries(collectionId, pageRequest, this.filters.library, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, complete)
|
||||
|
||||
this.totalPages = seriesPage.totalPages
|
||||
this.totalElements = seriesPage.totalElements
|
||||
this.series = seriesPage.content
|
||||
|
||||
this.series.forEach((x: SeriesDto) => x.context = {origin: ContextOrigin.COLLECTION, id: collectionId})
|
||||
this.seriesCopy = [...this.series]
|
||||
this.selectedSeries = []
|
||||
},
|
||||
reloadPage: throttle(function (this: any) {
|
||||
this.loadPage(this.collectionId, this.page)
|
||||
}, 1000),
|
||||
// async loadSeries(collectionId: string) {
|
||||
// let authorsFilter = [] as AuthorDto[]
|
||||
// authorRoles.forEach((role: string) => {
|
||||
// if (role in this.filters) this.filters[role].forEach((name: string) => authorsFilter.push({
|
||||
// name: name,
|
||||
// role: role,
|
||||
// }))
|
||||
// })
|
||||
//
|
||||
// const complete = parseBooleanFilter(this.filters.complete)
|
||||
// this.series = (await this.$komgaCollections.getSeries(collectionId, {unpaged: true} as PageRequest, this.filters.library, this.filters.status, replaceCompositeReadStatus(this.filters.readStatus), this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, complete)).content
|
||||
// this.series.forEach((x: SeriesDto) => x.context = {origin: ContextOrigin.COLLECTION, id: collectionId})
|
||||
// this.seriesCopy = [...this.series]
|
||||
// this.selectedSeries = []
|
||||
// },
|
||||
async loadCollection(collectionId: string) {
|
||||
this.$komgaCollections.getOneCollection(collectionId)
|
||||
.then(v => this.collection = v)
|
||||
|
||||
await this.loadSeries(collectionId)
|
||||
await this.loadPage(collectionId, this.page)
|
||||
},
|
||||
updateRoute() {
|
||||
const loc = {
|
||||
name: this.$route.name,
|
||||
params: {collectionId: this.$route.params.collectionId},
|
||||
query: {},
|
||||
query: {
|
||||
page: `${this.page}`,
|
||||
pageSize: `${this.pageSize}`,
|
||||
},
|
||||
} as Location
|
||||
mergeFilterParams(this.filters, loc.query)
|
||||
this.$router.replace(loc).catch((_: any) => {
|
||||
|
|
@ -429,13 +523,17 @@ export default Vue.extend({
|
|||
addToCollection() {
|
||||
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries)
|
||||
},
|
||||
startEditElements() {
|
||||
async startEditElements() {
|
||||
this.filters = {}
|
||||
this.unpaged = true
|
||||
await this.reloadPage()
|
||||
this.editElements = true
|
||||
},
|
||||
cancelEditElements() {
|
||||
this.editElements = false
|
||||
this.series = [...this.seriesCopy]
|
||||
this.unpaged = false
|
||||
this.reloadPage()
|
||||
},
|
||||
doEditElements() {
|
||||
this.editElements = false
|
||||
|
|
@ -443,15 +541,17 @@ export default Vue.extend({
|
|||
seriesIds: this.series.map(x => x.id),
|
||||
} as CollectionUpdateDto
|
||||
this.$komgaCollections.patchCollection(this.collectionId, update)
|
||||
this.unpaged = false
|
||||
this.reloadPage()
|
||||
},
|
||||
editCollection() {
|
||||
this.$store.dispatch('dialogEditCollection', this.collection)
|
||||
},
|
||||
seriesChanged(event: SeriesSseDto) {
|
||||
if (this.series.some(s => s.id === event.seriesId)) this.reloadSeries()
|
||||
if (this.series.some(s => s.id === event.seriesId)) this.reloadPage()
|
||||
},
|
||||
readProgressChanged(event: ReadProgressSeriesSseDto) {
|
||||
if (this.series.some(b => b.id === event.seriesId)) this.reloadSeries()
|
||||
if (this.series.some(b => b.id === event.seriesId)) this.reloadPage()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
</v-tooltip>
|
||||
</v-btn>
|
||||
|
||||
<page-size-select v-model="pageSize"/>
|
||||
|
||||
<v-btn icon @click="drawer = !drawer">
|
||||
<v-icon :color="filterActive ? 'secondary' : ''">mdi-filter-variant</v-icon>
|
||||
</v-btn>
|
||||
|
|
@ -108,14 +110,40 @@
|
|||
|
||||
<v-divider class="my-3"/>
|
||||
|
||||
<item-browser
|
||||
:items.sync="books"
|
||||
:item-context="[ItemContext.SHOW_SERIES]"
|
||||
:selected.sync="selectedBooks"
|
||||
:edit-function="editSingleBook"
|
||||
:draggable="editElements"
|
||||
:deletable="editElements"
|
||||
/>
|
||||
<empty-state
|
||||
v-if="totalPages === 0"
|
||||
:title="$t('common.filter_no_matches')"
|
||||
:sub-title="$t('common.use_filter_panel_to_change_filter')"
|
||||
icon="mdi-book-multiple"
|
||||
icon-color="secondary"
|
||||
>
|
||||
<v-btn @click="resetFilters">{{ $t('common.reset_filters') }}</v-btn>
|
||||
</empty-state>
|
||||
|
||||
<template v-else>
|
||||
<v-pagination
|
||||
v-if="totalPages > 1"
|
||||
v-model="page"
|
||||
:total-visible="paginationVisible"
|
||||
:length="totalPages"
|
||||
/>
|
||||
|
||||
<item-browser
|
||||
:items.sync="books"
|
||||
:item-context="[ItemContext.SHOW_SERIES]"
|
||||
:selected.sync="selectedBooks"
|
||||
:edit-function="isAdmin ? editSingleBook : undefined"
|
||||
:draggable="editElements"
|
||||
:deletable="editElements"
|
||||
/>
|
||||
|
||||
<v-pagination
|
||||
v-if="totalPages > 1"
|
||||
v-model="page"
|
||||
:total-visible="paginationVisible"
|
||||
:length="totalPages"
|
||||
/>
|
||||
</template>
|
||||
|
||||
</v-container>
|
||||
|
||||
|
|
@ -151,10 +179,14 @@ import {mergeFilterParams, toNameValue} from '@/functions/filter'
|
|||
import {Location} from 'vue-router'
|
||||
import {readListFileUrl} from '@/functions/urls'
|
||||
import {ItemContext} from '@/types/items'
|
||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseReadList',
|
||||
components: {
|
||||
EmptyState,
|
||||
PageSizeSelect,
|
||||
ToolbarSticky,
|
||||
ItemBrowser,
|
||||
ReadListActionsMenu,
|
||||
|
|
@ -171,9 +203,16 @@ export default Vue.extend({
|
|||
books: [] as BookDto[],
|
||||
booksCopy: [] as BookDto[],
|
||||
selectedBooks: [] as BookDto[],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
unpaged: false,
|
||||
totalPages: 1,
|
||||
totalElements: null as number | null,
|
||||
editElements: false,
|
||||
filters: {} as FiltersActive,
|
||||
filterUnwatch: null as any,
|
||||
pageUnwatch: null as any,
|
||||
pageSizeUnwatch: null as any,
|
||||
drawer: false,
|
||||
filterOptions: {
|
||||
library: [] as NameValue[],
|
||||
|
|
@ -204,7 +243,12 @@ export default Vue.extend({
|
|||
this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged)
|
||||
},
|
||||
async mounted() {
|
||||
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
|
||||
|
||||
// restore from query param
|
||||
await this.resetParams(this.$route, this.readListId)
|
||||
if (this.$route.query.page) this.page = Number(this.$route.query.page)
|
||||
if (this.$route.query.pageSize) this.pageSize = Number(this.$route.query.pageSize)
|
||||
|
||||
this.loadReadList(this.readListId)
|
||||
|
||||
|
|
@ -216,6 +260,9 @@ export default Vue.extend({
|
|||
|
||||
// reset
|
||||
await this.resetParams(this.$route, this.readListId)
|
||||
this.page = 1
|
||||
this.totalPages = 1
|
||||
this.totalElements = null
|
||||
this.books = []
|
||||
this.editElements = false
|
||||
|
||||
|
|
@ -227,6 +274,19 @@ export default Vue.extend({
|
|||
next()
|
||||
},
|
||||
computed: {
|
||||
paginationVisible(): number {
|
||||
switch (this.$vuetify.breakpoint.name) {
|
||||
case 'xs':
|
||||
return 5
|
||||
case 'sm':
|
||||
case 'md':
|
||||
return 10
|
||||
case 'lg':
|
||||
case 'xl':
|
||||
default:
|
||||
return 15
|
||||
}
|
||||
},
|
||||
filterOptionsList(): FiltersOptions {
|
||||
return {
|
||||
readStatus: {
|
||||
|
|
@ -319,15 +379,28 @@ export default Vue.extend({
|
|||
this.$store.commit('setReadListFilter', {id: this.readListId, filter: val})
|
||||
this.updateRouteAndReload()
|
||||
})
|
||||
this.pageSizeUnwatch = this.$watch('pageSize', (val) => {
|
||||
this.$store.commit('setBrowsingPageSize', val)
|
||||
this.updateRouteAndReload()
|
||||
})
|
||||
|
||||
this.pageUnwatch = this.$watch('page', (val) => {
|
||||
this.updateRoute()
|
||||
this.loadPage(this.readListId, val)
|
||||
})
|
||||
},
|
||||
unsetWatches() {
|
||||
this.filterUnwatch()
|
||||
this.pageUnwatch()
|
||||
this.pageSizeUnwatch()
|
||||
},
|
||||
updateRouteAndReload() {
|
||||
this.unsetWatches()
|
||||
|
||||
this.page = 1
|
||||
|
||||
this.updateRoute()
|
||||
this.loadBooks(this.readListId)
|
||||
this.loadPage(this.readListId, this.page)
|
||||
|
||||
this.setWatches()
|
||||
},
|
||||
|
|
@ -335,7 +408,10 @@ export default Vue.extend({
|
|||
const loc = {
|
||||
name: this.$route.name,
|
||||
params: {readListId: this.$route.params.readListId},
|
||||
query: {},
|
||||
query: {
|
||||
page: `${this.page}`,
|
||||
pageSize: `${this.pageSize}`,
|
||||
},
|
||||
} as Location
|
||||
mergeFilterParams(this.filters, loc.query)
|
||||
this.$router.replace(loc).catch((_: any) => {
|
||||
|
|
@ -354,9 +430,18 @@ export default Vue.extend({
|
|||
async loadReadList(readListId: string) {
|
||||
this.$komgaReadLists.getOneReadList(readListId)
|
||||
.then(v => this.readList = v)
|
||||
await this.loadBooks(readListId)
|
||||
|
||||
await this.loadPage(readListId, this.page)
|
||||
},
|
||||
async loadBooks(readListId: string) {
|
||||
async loadPage(readListId: string, page: number) {
|
||||
this.selectedBooks = []
|
||||
|
||||
const pageRequest = {
|
||||
page: page - 1,
|
||||
size: this.pageSize,
|
||||
unpaged: this.unpaged,
|
||||
} as PageRequest
|
||||
|
||||
let authorsFilter = [] as AuthorDto[]
|
||||
authorRoles.forEach((role: string) => {
|
||||
if (role in this.filters) this.filters[role].forEach((name: string) => authorsFilter.push({
|
||||
|
|
@ -365,13 +450,18 @@ export default Vue.extend({
|
|||
}))
|
||||
})
|
||||
|
||||
this.books = (await this.$komgaReadLists.getBooks(readListId, {unpaged: true} as PageRequest, this.filters.library, replaceCompositeReadStatus(this.filters.readStatus), this.filters.tag, authorsFilter)).content
|
||||
const booksPage = await this.$komgaReadLists.getBooks(readListId, pageRequest, this.filters.library, replaceCompositeReadStatus(this.filters.readStatus), this.filters.tag, authorsFilter)
|
||||
|
||||
this.totalPages = booksPage.totalPages
|
||||
this.totalElements = booksPage.totalElements
|
||||
this.books = booksPage.content
|
||||
|
||||
this.books.forEach((x: BookDto) => x.context = {origin: ContextOrigin.READLIST, id: readListId})
|
||||
this.booksCopy = [...this.books]
|
||||
this.selectedBooks = []
|
||||
},
|
||||
reloadBooks: throttle(function (this: any) {
|
||||
this.loadBooks(this.readListId)
|
||||
reloadPage: throttle(function (this: any) {
|
||||
this.loadPage(this.readListId, this.page)
|
||||
}, 1000),
|
||||
editSingleBook(book: BookDto) {
|
||||
this.$store.dispatch('dialogUpdateBooks', book)
|
||||
|
|
@ -397,13 +487,17 @@ export default Vue.extend({
|
|||
addToReadList() {
|
||||
this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks)
|
||||
},
|
||||
startEditElements() {
|
||||
async startEditElements() {
|
||||
this.filters = {}
|
||||
this.unpaged = true
|
||||
await this.reloadPage()
|
||||
this.editElements = true
|
||||
},
|
||||
cancelEditElements() {
|
||||
this.editElements = false
|
||||
this.books = [...this.booksCopy]
|
||||
this.unpaged = false
|
||||
this.reloadPage()
|
||||
},
|
||||
doEditElements() {
|
||||
this.editElements = false
|
||||
|
|
@ -411,15 +505,17 @@ export default Vue.extend({
|
|||
bookIds: this.books.map(x => x.id),
|
||||
} as ReadListUpdateDto
|
||||
this.$komgaReadLists.patchReadList(this.readListId, update)
|
||||
this.unpaged = false
|
||||
this.reloadPage()
|
||||
},
|
||||
editReadList() {
|
||||
this.$store.dispatch('dialogEditReadList', this.readList)
|
||||
},
|
||||
bookChanged(event: BookSseDto) {
|
||||
if (this.books.some(b => b.id === event.bookId)) this.reloadBooks()
|
||||
if (this.books.some(b => b.id === event.bookId)) this.reloadPage()
|
||||
},
|
||||
readProgressChanged(event: ReadProgressSseDto) {
|
||||
if (this.books.some(b => b.id === event.bookId)) this.reloadBooks()
|
||||
if (this.books.some(b => b.id === event.bookId)) this.reloadPage()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue