feat(webui): add pagination to readlist/collection browse view

Closes: #817
This commit is contained in:
Gauthier Roebroeck 2023-01-18 16:22:18 +08:00
parent 17ca7f74eb
commit ff70fea71a
2 changed files with 235 additions and 39 deletions

View file

@ -36,6 +36,8 @@
</v-tooltip> </v-tooltip>
</v-btn> </v-btn>
<page-size-select v-model="pageSize"/>
<v-btn icon @click="drawer = !drawer"> <v-btn icon @click="drawer = !drawer">
<v-icon :color="filterActive ? 'secondary' : ''">mdi-filter-variant</v-icon> <v-icon :color="filterActive ? 'secondary' : ''">mdi-filter-variant</v-icon>
</v-btn> </v-btn>
@ -90,7 +92,7 @@
<v-container fluid> <v-container fluid>
<empty-state <empty-state
v-if="series.length === 0" v-if="totalPages === 0"
:title="$t('common.filter_no_matches')" :title="$t('common.filter_no_matches')"
:sub-title="$t('common.use_filter_panel_to_change_filter')" :sub-title="$t('common.use_filter_panel_to_change_filter')"
icon="mdi-book-multiple" icon="mdi-book-multiple"
@ -99,14 +101,29 @@
<v-btn @click="resetFilters">{{ $t('common.reset_filters') }}</v-btn> <v-btn @click="resetFilters">{{ $t('common.reset_filters') }}</v-btn>
</empty-state> </empty-state>
<item-browser <template v-else>
v-else <v-pagination
:items.sync="series" v-if="totalPages > 1"
:selected.sync="selectedSeries" v-model="page"
:edit-function="isAdmin ? editSingleSeries : undefined" :total-visible="paginationVisible"
:draggable="editElements && collection.ordered" :length="totalPages"
:deletable="editElements" />
/>
<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> </v-container>
@ -138,16 +155,18 @@ import {Location} from 'vue-router'
import EmptyState from '@/components/EmptyState.vue' import EmptyState from '@/components/EmptyState.vue'
import {SeriesDto} from '@/types/komga-series' import {SeriesDto} from '@/types/komga-series'
import {authorRoles} from '@/types/author-roles' 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 {CollectionSseDto, ReadProgressSeriesSseDto, SeriesSseDto} from '@/types/komga-sse'
import {throttle} from 'lodash' import {throttle} from 'lodash'
import {LibraryDto} from '@/types/komga-libraries' import {LibraryDto} from '@/types/komga-libraries'
import {parseBooleanFilter} from '@/functions/query-params' import {parseBooleanFilter} from '@/functions/query-params'
import {ContextOrigin} from '@/types/context' import {ContextOrigin} from '@/types/context'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
export default Vue.extend({ export default Vue.extend({
name: 'BrowseCollection', name: 'BrowseCollection',
components: { components: {
PageSizeSelect,
ToolbarSticky, ToolbarSticky,
ItemBrowser, ItemBrowser,
CollectionActionsMenu, CollectionActionsMenu,
@ -163,9 +182,16 @@ export default Vue.extend({
series: [] as SeriesDto[], series: [] as SeriesDto[],
seriesCopy: [] as SeriesDto[], seriesCopy: [] as SeriesDto[],
selectedSeries: [] as SeriesDto[], selectedSeries: [] as SeriesDto[],
page: 1,
pageSize: 20,
unpaged: false,
totalPages: 1,
totalElements: null as number | null,
editElements: false, editElements: false,
filters: {} as FiltersActive, filters: {} as FiltersActive,
filterUnwatch: null as any, filterUnwatch: null as any,
pageUnwatch: null as any,
pageSizeUnwatch: null as any,
drawer: false, drawer: false,
filterOptions: { filterOptions: {
library: [] as NameValue[], library: [] as NameValue[],
@ -201,7 +227,12 @@ export default Vue.extend({
this.$eventHub.$off(READPROGRESS_SERIES_DELETED, this.readProgressChanged) this.$eventHub.$off(READPROGRESS_SERIES_DELETED, this.readProgressChanged)
}, },
async mounted() { async mounted() {
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
// restore from query param
await this.resetParams(this.$route, this.collectionId) 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) this.loadCollection(this.collectionId)
@ -213,6 +244,9 @@ export default Vue.extend({
// reset // reset
await this.resetParams(this.$route, to.params.collectionId) await this.resetParams(this.$route, to.params.collectionId)
this.page = 1
this.totalPages = 1
this.totalElements = null
this.series = [] this.series = []
this.editElements = false this.editElements = false
@ -224,6 +258,19 @@ export default Vue.extend({
next() next()
}, },
computed: { 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 { filterOptionsList(): FiltersOptions {
return { return {
readStatus: { readStatus: {
@ -352,9 +399,20 @@ export default Vue.extend({
this.$store.commit('setCollectionFilter', {id: this.collectionId, filter: val}) this.$store.commit('setCollectionFilter', {id: this.collectionId, filter: val})
this.updateRouteAndReload() 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() { unsetWatches() {
this.filterUnwatch() this.filterUnwatch()
this.pageUnwatch()
this.pageSizeUnwatch()
}, },
collectionChanged(event: CollectionSseDto) { collectionChanged(event: CollectionSseDto) {
if (event.collectionId === this.collectionId) { if (event.collectionId === this.collectionId) {
@ -369,15 +427,25 @@ export default Vue.extend({
updateRouteAndReload() { updateRouteAndReload() {
this.unsetWatches() this.unsetWatches()
this.page = 1
this.updateRoute() this.updateRoute()
this.loadSeries(this.collectionId) this.loadPage(this.collectionId, this.page)
this.setWatches() this.setWatches()
}, },
reloadSeries: throttle(function (this: any) { // reloadSeries: throttle(function (this: any) {
this.loadSeries(this.collectionId) // this.loadSeries(this.collectionId)
}, 1000), // }, 1000),
async loadSeries(collectionId: string) { 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[] let authorsFilter = [] as AuthorDto[]
authorRoles.forEach((role: string) => { authorRoles.forEach((role: string) => {
if (role in this.filters) this.filters[role].forEach((name: string) => authorsFilter.push({ 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) 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.series.forEach((x: SeriesDto) => x.context = {origin: ContextOrigin.COLLECTION, id: collectionId})
this.seriesCopy = [...this.series] this.seriesCopy = [...this.series]
this.selectedSeries = [] 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) { async loadCollection(collectionId: string) {
this.$komgaCollections.getOneCollection(collectionId) this.$komgaCollections.getOneCollection(collectionId)
.then(v => this.collection = v) .then(v => this.collection = v)
await this.loadSeries(collectionId) await this.loadPage(collectionId, this.page)
}, },
updateRoute() { updateRoute() {
const loc = { const loc = {
name: this.$route.name, name: this.$route.name,
params: {collectionId: this.$route.params.collectionId}, params: {collectionId: this.$route.params.collectionId},
query: {}, query: {
page: `${this.page}`,
pageSize: `${this.pageSize}`,
},
} as Location } as Location
mergeFilterParams(this.filters, loc.query) mergeFilterParams(this.filters, loc.query)
this.$router.replace(loc).catch((_: any) => { this.$router.replace(loc).catch((_: any) => {
@ -429,13 +523,17 @@ export default Vue.extend({
addToCollection() { addToCollection() {
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries) this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries)
}, },
startEditElements() { async startEditElements() {
this.filters = {} this.filters = {}
this.unpaged = true
await this.reloadPage()
this.editElements = true this.editElements = true
}, },
cancelEditElements() { cancelEditElements() {
this.editElements = false this.editElements = false
this.series = [...this.seriesCopy] this.series = [...this.seriesCopy]
this.unpaged = false
this.reloadPage()
}, },
doEditElements() { doEditElements() {
this.editElements = false this.editElements = false
@ -443,15 +541,17 @@ export default Vue.extend({
seriesIds: this.series.map(x => x.id), seriesIds: this.series.map(x => x.id),
} as CollectionUpdateDto } as CollectionUpdateDto
this.$komgaCollections.patchCollection(this.collectionId, update) this.$komgaCollections.patchCollection(this.collectionId, update)
this.unpaged = false
this.reloadPage()
}, },
editCollection() { editCollection() {
this.$store.dispatch('dialogEditCollection', this.collection) this.$store.dispatch('dialogEditCollection', this.collection)
}, },
seriesChanged(event: SeriesSseDto) { 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) { 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()
}, },
}, },
}) })

View file

@ -33,6 +33,8 @@
</v-tooltip> </v-tooltip>
</v-btn> </v-btn>
<page-size-select v-model="pageSize"/>
<v-btn icon @click="drawer = !drawer"> <v-btn icon @click="drawer = !drawer">
<v-icon :color="filterActive ? 'secondary' : ''">mdi-filter-variant</v-icon> <v-icon :color="filterActive ? 'secondary' : ''">mdi-filter-variant</v-icon>
</v-btn> </v-btn>
@ -108,14 +110,40 @@
<v-divider class="my-3"/> <v-divider class="my-3"/>
<item-browser <empty-state
:items.sync="books" v-if="totalPages === 0"
:item-context="[ItemContext.SHOW_SERIES]" :title="$t('common.filter_no_matches')"
:selected.sync="selectedBooks" :sub-title="$t('common.use_filter_panel_to_change_filter')"
:edit-function="editSingleBook" icon="mdi-book-multiple"
:draggable="editElements" icon-color="secondary"
:deletable="editElements" >
/> <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> </v-container>
@ -151,10 +179,14 @@ import {mergeFilterParams, toNameValue} from '@/functions/filter'
import {Location} from 'vue-router' import {Location} from 'vue-router'
import {readListFileUrl} from '@/functions/urls' import {readListFileUrl} from '@/functions/urls'
import {ItemContext} from '@/types/items' import {ItemContext} from '@/types/items'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
import EmptyState from '@/components/EmptyState.vue'
export default Vue.extend({ export default Vue.extend({
name: 'BrowseReadList', name: 'BrowseReadList',
components: { components: {
EmptyState,
PageSizeSelect,
ToolbarSticky, ToolbarSticky,
ItemBrowser, ItemBrowser,
ReadListActionsMenu, ReadListActionsMenu,
@ -171,9 +203,16 @@ export default Vue.extend({
books: [] as BookDto[], books: [] as BookDto[],
booksCopy: [] as BookDto[], booksCopy: [] as BookDto[],
selectedBooks: [] as BookDto[], selectedBooks: [] as BookDto[],
page: 1,
pageSize: 20,
unpaged: false,
totalPages: 1,
totalElements: null as number | null,
editElements: false, editElements: false,
filters: {} as FiltersActive, filters: {} as FiltersActive,
filterUnwatch: null as any, filterUnwatch: null as any,
pageUnwatch: null as any,
pageSizeUnwatch: null as any,
drawer: false, drawer: false,
filterOptions: { filterOptions: {
library: [] as NameValue[], library: [] as NameValue[],
@ -204,7 +243,12 @@ export default Vue.extend({
this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged) this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged)
}, },
async mounted() { async mounted() {
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
// restore from query param
await this.resetParams(this.$route, this.readListId) 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) this.loadReadList(this.readListId)
@ -216,6 +260,9 @@ export default Vue.extend({
// reset // reset
await this.resetParams(this.$route, this.readListId) await this.resetParams(this.$route, this.readListId)
this.page = 1
this.totalPages = 1
this.totalElements = null
this.books = [] this.books = []
this.editElements = false this.editElements = false
@ -227,6 +274,19 @@ export default Vue.extend({
next() next()
}, },
computed: { 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 { filterOptionsList(): FiltersOptions {
return { return {
readStatus: { readStatus: {
@ -319,15 +379,28 @@ export default Vue.extend({
this.$store.commit('setReadListFilter', {id: this.readListId, filter: val}) this.$store.commit('setReadListFilter', {id: this.readListId, filter: val})
this.updateRouteAndReload() 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() { unsetWatches() {
this.filterUnwatch() this.filterUnwatch()
this.pageUnwatch()
this.pageSizeUnwatch()
}, },
updateRouteAndReload() { updateRouteAndReload() {
this.unsetWatches() this.unsetWatches()
this.page = 1
this.updateRoute() this.updateRoute()
this.loadBooks(this.readListId) this.loadPage(this.readListId, this.page)
this.setWatches() this.setWatches()
}, },
@ -335,7 +408,10 @@ export default Vue.extend({
const loc = { const loc = {
name: this.$route.name, name: this.$route.name,
params: {readListId: this.$route.params.readListId}, params: {readListId: this.$route.params.readListId},
query: {}, query: {
page: `${this.page}`,
pageSize: `${this.pageSize}`,
},
} as Location } as Location
mergeFilterParams(this.filters, loc.query) mergeFilterParams(this.filters, loc.query)
this.$router.replace(loc).catch((_: any) => { this.$router.replace(loc).catch((_: any) => {
@ -354,9 +430,18 @@ export default Vue.extend({
async loadReadList(readListId: string) { async loadReadList(readListId: string) {
this.$komgaReadLists.getOneReadList(readListId) this.$komgaReadLists.getOneReadList(readListId)
.then(v => this.readList = v) .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[] let authorsFilter = [] as AuthorDto[]
authorRoles.forEach((role: string) => { authorRoles.forEach((role: string) => {
if (role in this.filters) this.filters[role].forEach((name: string) => authorsFilter.push({ 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.books.forEach((x: BookDto) => x.context = {origin: ContextOrigin.READLIST, id: readListId})
this.booksCopy = [...this.books] this.booksCopy = [...this.books]
this.selectedBooks = [] this.selectedBooks = []
}, },
reloadBooks: throttle(function (this: any) { reloadPage: throttle(function (this: any) {
this.loadBooks(this.readListId) this.loadPage(this.readListId, this.page)
}, 1000), }, 1000),
editSingleBook(book: BookDto) { editSingleBook(book: BookDto) {
this.$store.dispatch('dialogUpdateBooks', book) this.$store.dispatch('dialogUpdateBooks', book)
@ -397,13 +487,17 @@ export default Vue.extend({
addToReadList() { addToReadList() {
this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks) this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks)
}, },
startEditElements() { async startEditElements() {
this.filters = {} this.filters = {}
this.unpaged = true
await this.reloadPage()
this.editElements = true this.editElements = true
}, },
cancelEditElements() { cancelEditElements() {
this.editElements = false this.editElements = false
this.books = [...this.booksCopy] this.books = [...this.booksCopy]
this.unpaged = false
this.reloadPage()
}, },
doEditElements() { doEditElements() {
this.editElements = false this.editElements = false
@ -411,15 +505,17 @@ export default Vue.extend({
bookIds: this.books.map(x => x.id), bookIds: this.books.map(x => x.id),
} as ReadListUpdateDto } as ReadListUpdateDto
this.$komgaReadLists.patchReadList(this.readListId, update) this.$komgaReadLists.patchReadList(this.readListId, update)
this.unpaged = false
this.reloadPage()
}, },
editReadList() { editReadList() {
this.$store.dispatch('dialogEditReadList', this.readList) this.$store.dispatch('dialogEditReadList', this.readList)
}, },
bookChanged(event: BookSseDto) { 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) { 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()
}, },
}, },
}) })