feat(webui): horizontal scroller infinite scroll on dashboard and search results

closes #605
This commit is contained in:
Gauthier Roebroeck 2021-09-10 15:22:14 +08:00
parent 24b564a707
commit fe78f17e5e
5 changed files with 315 additions and 137 deletions

View file

@ -22,7 +22,10 @@
@scroll="computeScrollability"
v-resize="computeScrollability"
>
<slot name="content" class="content"/>
<div class="d-inline-flex">
<slot name="content" class="content"/>
<slot name="content-append"/>
</div>
</div>
</div>
</template>
@ -44,20 +47,35 @@ export default Vue.extend({
adjustment: 100,
}
},
props: {
tick: {
type: Number,
default: 0,
},
},
mounted() {
this.container = this.$refs[this.id] as HTMLElement
this.computeScrollability()
},
watch: {
tick() {
setTimeout(this.computeScrollability, 200)
},
},
methods: {
computeScrollability() {
if (this.container !== undefined) {
if (this.container) {
let scrollPercent: number
if (this.$vuetify.rtl) {
this.canScrollBackward = Math.round(this.container.scrollLeft) < 0
this.canScrollForward = (Math.round(this.container.scrollLeft) - this.container.clientWidth) > -this.container.scrollWidth
scrollPercent = (Math.round(this.container.scrollLeft) - this.container.clientWidth) / -this.container.scrollWidth
} else {
this.canScrollBackward = Math.round(this.container.scrollLeft) > 0
this.canScrollForward = (Math.round(this.container.scrollLeft) + this.container.clientWidth) < this.container.scrollWidth
scrollPercent = (Math.round(this.container.scrollLeft) + this.container.clientWidth) / this.container.scrollWidth
}
this.$emit('scroll-changed', scrollPercent)
}
},
doScroll(direction: string) {

View file

@ -0,0 +1,43 @@
export class PageLoader<T> {
private readonly pageable: PageRequest
private readonly loader: (pageRequest: PageRequest) => Promise<Page<T>>
private currentPage = undefined as unknown as Page<T>
private loadedPages: number[] = []
public readonly items: T[] = []
get page() {
return this.currentPage?.number || 0
}
get hasNextPage() {
return !this.currentPage ? false : !this.currentPage.last
}
constructor(pageable: PageRequest, loader: (pageRequest: PageRequest) => Promise<Page<T>>) {
this.pageable = pageable
this.loader = loader
}
async loadNext(): Promise<boolean> {
// load first page if nothing has been loaded yet
if (!this.currentPage) {
this.loadedPages.push(this.pageable.page || 0)
this.currentPage = await this.loader(this.pageable)
this.items.push(...this.currentPage.content)
return true
}
// if the last page has been loaded, do nothing
else if (this.currentPage.last) return false
else {
const nextPage = this.currentPage.number + 1
if (!this.loadedPages.includes(nextPage)) {
this.loadedPages.push(nextPage)
const pageable = Object.assign({}, this.pageable, {page: nextPage})
this.currentPage = await this.loader(pageable)
this.items.push(...this.currentPage.content)
return true
}
return false
}
}
}

View file

@ -49,12 +49,17 @@
>
</empty-state>
<horizontal-scroller v-if="inProgressBooks.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderInProgressBooks && loaderInProgressBooks.items.length !== 0"
class="mb-4"
:tick="loaderInProgressBooks.page"
@scroll-changed="(percent) => scrollChanged(loaderInProgressBooks, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('dashboard.keep_reading') }}</div>
</template>
<template v-slot:content>
<item-browser :items="inProgressBooks"
<item-browser :items="loaderInProgressBooks.items"
nowrap
:edit-function="isAdmin ? singleEditBook : undefined"
:selected.sync="selectedBooks"
@ -64,12 +69,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="onDeckBooks.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderOnDeckBooks && loaderOnDeckBooks.items.length !== 0"
class="mb-4"
:tick="loaderOnDeckBooks.page"
@scroll-changed="(percent) => scrollChanged(loaderOnDeckBooks, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('dashboard.on_deck') }}</div>
</template>
<template v-slot:content>
<item-browser :items="onDeckBooks"
<item-browser :items="loaderOnDeckBooks.items"
nowrap
:edit-function="isAdmin ? singleEditBook : undefined"
:selected.sync="selectedBooks"
@ -79,12 +89,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="recentlyReleasedBooks.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderRecentlyReleasedBooks && loaderRecentlyReleasedBooks.items.length !== 0"
class="mb-4"
:tick="loaderRecentlyReleasedBooks.page"
@scroll-changed="(percent) => scrollChanged(loaderRecentlyReleasedBooks, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('dashboard.recently_released_books') }}</div>
</template>
<template v-slot:content>
<item-browser :items="recentlyReleasedBooks"
<item-browser :items="loaderRecentlyReleasedBooks.items"
nowrap
:edit-function="isAdmin ? singleEditBook : undefined"
:selected.sync="selectedBooks"
@ -94,12 +109,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="latestBooks.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderLatestBooks && loaderLatestBooks.items.length !== 0"
class="mb-4"
:tick="loaderLatestBooks.page"
@scroll-changed="(percent) => scrollChanged(loaderLatestBooks, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('dashboard.recently_added_books') }}</div>
</template>
<template v-slot:content>
<item-browser :items="latestBooks"
<item-browser :items="loaderLatestBooks.items"
nowrap
:edit-function="isAdmin ? singleEditBook : undefined"
:selected.sync="selectedBooks"
@ -109,12 +129,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="newSeries.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderNewSeries && loaderNewSeries.items.length !== 0"
class="mb-4"
:tick="loaderNewSeries.page"
@scroll-changed="(percent) => scrollChanged(loaderNewSeries, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('dashboard.recently_added_series') }}</div>
</template>
<template v-slot:content>
<item-browser :items="newSeries"
<item-browser :items="loaderNewSeries.items"
nowrap
:edit-function="isAdmin ? singleEditSeries : undefined"
:selected.sync="selectedSeries"
@ -124,12 +149,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="updatedSeries.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderUpdatedSeries && loaderUpdatedSeries.items.length !== 0"
class="mb-4"
:tick="loaderUpdatedSeries.page"
@scroll-changed="(percent) => scrollChanged(loaderUpdatedSeries, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('dashboard.recently_updated_series') }}</div>
</template>
<template v-slot:content>
<item-browser :items="updatedSeries"
<item-browser :items="loaderUpdatedSeries.items"
nowrap
:edit-function="isAdmin ? singleEditSeries : undefined"
:selected.sync="selectedSeries"
@ -139,12 +169,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="recentlyReadBooks.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderRecentlyReadBooks && loaderRecentlyReadBooks.items.length !== 0"
class="mb-4"
:tick="loaderRecentlyReadBooks.page"
@scroll-changed="(percent) => scrollChanged(loaderRecentlyReadBooks, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('dashboard.recently_read_books') }}</div>
</template>
<template v-slot:content>
<item-browser :items="recentlyReadBooks"
<item-browser :items="loaderRecentlyReadBooks.items"
nowrap
:edit-function="isAdmin ? singleEditBook : undefined"
:selected.sync="selectedBooks"
@ -184,6 +219,7 @@ import {throttle} from 'lodash'
import {subMonths} from 'date-fns'
import {BookSseDto, ReadProgressSseDto, SeriesSseDto} from '@/types/komga-sse'
import {LibraryDto} from '@/types/komga-libraries'
import {PageLoader} from '@/types/pageLoader'
export default Vue.extend({
name: 'Dashboard',
@ -200,13 +236,13 @@ export default Vue.extend({
return {
loading: false,
library: undefined as LibraryDto | undefined,
newSeries: [] as SeriesDto[],
updatedSeries: [] as SeriesDto[],
latestBooks: [] as BookDto[],
inProgressBooks: [] as BookDto[],
onDeckBooks: [] as BookDto[],
recentlyReleasedBooks: [] as BookDto[],
recentlyReadBooks: [] as BookDto[],
loaderNewSeries: undefined as unknown as PageLoader<SeriesDto>,
loaderUpdatedSeries: undefined as unknown as PageLoader<SeriesDto>,
loaderLatestBooks: undefined as unknown as PageLoader<BookDto>,
loaderInProgressBooks: undefined as unknown as PageLoader<BookDto>,
loaderOnDeckBooks: undefined as unknown as PageLoader<BookDto>,
loaderRecentlyReleasedBooks: undefined as unknown as PageLoader<BookDto>,
loaderRecentlyReadBooks: undefined as unknown as PageLoader<BookDto>,
selectedSeries: [] as SeriesDto[],
selectedBooks: [] as BookDto[],
}
@ -263,19 +299,22 @@ export default Vue.extend({
return this.$vuetify.breakpoint.name === 'xs' ? 120 : 150
},
allEmpty(): boolean {
return this.newSeries.length === 0 &&
this.updatedSeries.length === 0 &&
this.latestBooks.length === 0 &&
this.inProgressBooks.length === 0 &&
this.onDeckBooks.length === 0 &&
this.recentlyReleasedBooks.length === 0 &&
this.recentlyReadBooks.length === 0
return this.loaderNewSeries?.items.length === 0 &&
this.loaderUpdatedSeries?.items.length === 0 &&
this.loaderLatestBooks?.items.length === 0 &&
this.loaderInProgressBooks?.items.length === 0 &&
this.loaderOnDeckBooks?.items.length === 0 &&
this.loaderRecentlyReleasedBooks?.items.length === 0 &&
this.loaderRecentlyReadBooks?.items.length === 0
},
individualLibrary(): boolean {
return this.libraryId !== LIBRARIES_ALL
},
},
methods: {
async scrollChanged(loader: PageLoader<any>, percent: number) {
if (percent > 0.95) await loader.loadNext()
},
getRequestLibraryId(libraryId: string): string | undefined {
return libraryId !== LIBRARIES_ALL ? libraryId : undefined
},
@ -290,11 +329,11 @@ export default Vue.extend({
}
},
readProgressChanged(event: ReadProgressSseDto) {
if (this.inProgressBooks.some(b => b.id === event.bookId)) this.reload()
else if (this.latestBooks.some(b => b.id === event.bookId)) this.reload()
else if (this.onDeckBooks.some(b => b.id === event.bookId)) this.reload()
else if (this.recentlyReleasedBooks.some(b => b.id === event.bookId)) this.reload()
else if (this.recentlyReadBooks.some(b => b.id === event.bookId)) this.reload()
if (this.loaderInProgressBooks?.items.some(b => b.id === event.bookId)) this.reload()
else if (this.loaderLatestBooks?.items.some(b => b.id === event.bookId)) this.reload()
else if (this.loaderOnDeckBooks?.items.some(b => b.id === event.bookId)) this.reload()
else if (this.loaderRecentlyReleasedBooks?.items.some(b => b.id === event.bookId)) this.reload()
else if (this.loaderRecentlyReadBooks?.items.some(b => b.id === event.bookId)) this.reload()
},
reload: throttle(function (this: any) {
this.loadAll(this.libraryId)
@ -304,55 +343,49 @@ export default Vue.extend({
this.library = this.getLibraryLazy(libraryId)
this.selectedSeries = []
this.selectedBooks = []
Promise.all([this.loadInProgressBooks(libraryId),
this.loadOnDeckBooks(libraryId),
this.loadRecentlyReleasedBooks(libraryId),
this.loadLatestBooks(libraryId),
this.loadNewSeries(libraryId),
this.loadUpdatedSeries(libraryId),
this.loadRecentlyReadBooks(libraryId),
]).then(x => {
this.loaderInProgressBooks = new PageLoader<BookDto>(
{sort: ['readProgress.lastModified,desc']},
(pageable: PageRequest) => this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageable, undefined, undefined, [ReadStatus.IN_PROGRESS]),
)
this.loaderOnDeckBooks = new PageLoader<BookDto>(
{},
() => this.$komgaBooks.getBooksOnDeck(this.getRequestLibraryId(libraryId)),
)
this.loaderLatestBooks = new PageLoader<BookDto>(
{sort: ['metadata.title,desc']},
(pageable: PageRequest) => this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageable),
)
this.loaderRecentlyReleasedBooks = new PageLoader<BookDto>(
{sort: ['metadata.releaseDate,desc']},
(pageable: PageRequest) => this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageable, undefined, undefined, undefined, subMonths(new Date(), 1)),
)
this.loaderRecentlyReadBooks = new PageLoader<BookDto>(
{sort: ['readProgress.lastModified,desc']},
(pageable: PageRequest) => this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageable, undefined, undefined, [ReadStatus.READ]),
)
this.loaderNewSeries = new PageLoader<SeriesDto>(
{},
() => this.$komgaSeries.getNewSeries(this.getRequestLibraryId(libraryId)),
)
this.loaderUpdatedSeries = new PageLoader<SeriesDto>(
{},
() => this.$komgaSeries.getUpdatedSeries(this.getRequestLibraryId(libraryId)),
)
Promise.all([
this.loaderInProgressBooks.loadNext(),
this.loaderOnDeckBooks.loadNext(),
this.loaderRecentlyReleasedBooks.loadNext(),
this.loaderLatestBooks.loadNext(),
this.loaderNewSeries.loadNext(),
this.loaderUpdatedSeries.loadNext(),
this.loaderRecentlyReadBooks.loadNext(),
]).then(() => {
this.loading = false
})
},
async loadNewSeries(libraryId: string) {
this.newSeries = (await this.$komgaSeries.getNewSeries(this.getRequestLibraryId(libraryId))).content
},
async loadUpdatedSeries(libraryId: string) {
this.updatedSeries = (await this.$komgaSeries.getUpdatedSeries(this.getRequestLibraryId(libraryId))).content
},
async loadLatestBooks(libraryId: string) {
const pageRequest = {
sort: ['createdDate,desc'],
} as PageRequest
this.latestBooks = (await this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageRequest)).content
},
async loadRecentlyReleasedBooks(libraryId: string) {
const pageRequest = {
sort: ['metadata.releaseDate,desc'],
} as PageRequest
const releasedAfter = subMonths(new Date(), 1)
this.recentlyReleasedBooks = (await this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageRequest, undefined, undefined, undefined, releasedAfter)).content
},
async loadRecentlyReadBooks(libraryId: string) {
const pageRequest = {
sort: ['readProgress.lastModified,desc'],
} as PageRequest
this.recentlyReadBooks = (await this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageRequest, undefined, undefined, [ReadStatus.READ])).content
},
async loadInProgressBooks(libraryId: string) {
const pageRequest = {
sort: ['readProgress.lastModified,desc'],
} as PageRequest
this.inProgressBooks = (await this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageRequest, undefined, undefined, [ReadStatus.IN_PROGRESS])).content
},
async loadOnDeckBooks(libraryId: string) {
this.onDeckBooks = (await this.$komgaBooks.getBooksOnDeck(this.getRequestLibraryId(libraryId))).content
},
singleEditSeries(series: SeriesDto) {
this.$store.dispatch('dialogUpdateSeries', series)
},

View file

@ -53,12 +53,17 @@
</empty-state>
<template v-else>
<horizontal-scroller v-if="series.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderSeries && loaderSeries.items.length !== 0"
class="mb-4"
:tick="loaderSeries.page"
@scroll-changed="(percent) => scrollChanged(loaderSeries, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('common.series') }}</div>
</template>
<template v-slot:content>
<item-browser :items="series"
<item-browser :items="loaderSeries.items"
nowrap
:edit-function="isAdmin ? singleEditSeries : undefined"
:selected.sync="selectedSeries"
@ -68,12 +73,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="books.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderBooks && loaderBooks.items.length !== 0"
class="mb-4"
:tick="loaderBooks.page"
@scroll-changed="(percent) => scrollChanged(loaderBooks, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('common.books') }}</div>
</template>
<template v-slot:content>
<item-browser :items="books"
<item-browser :items="loaderBooks.items"
nowrap
:edit-function="isAdmin ? singleEditBook : undefined"
:selected.sync="selectedBooks"
@ -83,12 +93,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="collections.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderCollections && loaderCollections.items.length !== 0"
class="mb-4"
:tick="loaderCollections.page"
@scroll-changed="(percent) => scrollChanged(loaderCollections, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('common.collections') }}</div>
</template>
<template v-slot:content>
<item-browser :items="collections"
<item-browser :items="loaderCollections.items"
nowrap
:edit-function="isAdmin ? singleEditCollection : undefined"
:selected.sync="selectedCollections"
@ -98,12 +113,17 @@
</template>
</horizontal-scroller>
<horizontal-scroller v-if="readLists.length !== 0" class="mb-4">
<horizontal-scroller
v-if="loaderReadLists && loaderReadLists.items.length !== 0"
class="mb-4"
:tick="loaderReadLists.page"
@scroll-changed="(percent) => scrollChanged(loaderReadLists, percent)"
>
<template v-slot:prepend>
<div class="title">{{ $t('common.readlists') }}</div>
</template>
<template v-slot:content>
<item-browser :items="readLists"
<item-browser :items="loaderReadLists.items"
nowrap
:edit-function="isAdmin ? singleEditReadList : undefined"
:selected.sync="selectedReadLists"
@ -152,6 +172,7 @@ import {
SeriesSseDto,
} from '@/types/komga-sse'
import {throttle} from 'lodash'
import {PageLoader} from '@/types/pageLoader'
export default Vue.extend({
name: 'Search',
@ -164,11 +185,11 @@ export default Vue.extend({
},
data: () => {
return {
series: [] as SeriesDto[],
books: [] as BookDto[],
collections: [] as CollectionDto[],
readLists: [] as ReadListDto[],
pageSize: 50,
loaderSeries: undefined as unknown as PageLoader<SeriesDto>,
loaderBooks: undefined as unknown as PageLoader<BookDto>,
loaderCollections: undefined as unknown as PageLoader<CollectionDto>,
loaderReadLists: undefined as unknown as PageLoader<ReadListDto>,
pageSize: 20,
loading: false,
selectedSeries: [] as SeriesDto[],
selectedBooks: [] as BookDto[],
@ -176,7 +197,7 @@ export default Vue.extend({
selectedReadLists: [] as ReadListDto[],
}
},
created () {
created() {
this.$eventHub.$on(LIBRARY_DELETED, this.reloadResults)
this.$eventHub.$on(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$on(SERIES_DELETED, this.seriesChanged)
@ -191,7 +212,7 @@ export default Vue.extend({
this.$eventHub.$on(READPROGRESS_SERIES_CHANGED, this.readProgressSeriesChanged)
this.$eventHub.$on(READPROGRESS_SERIES_DELETED, this.readProgressSeriesChanged)
},
beforeDestroy () {
beforeDestroy() {
this.$eventHub.$off(LIBRARY_DELETED, this.reloadResults)
this.$eventHub.$off(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$off(SERIES_DELETED, this.seriesChanged)
@ -223,60 +244,67 @@ export default Vue.extend({
isAdmin(): boolean {
return this.$store.getters.meAdmin
},
fixedCardWidth (): number {
fixedCardWidth(): number {
return this.$vuetify.breakpoint.name === 'xs' ? 120 : 150
},
showToolbar (): boolean {
showToolbar(): boolean {
return this.selectedSeries.length === 0 && this.selectedBooks.length === 0 && this.selectedCollections.length === 0 && this.selectedReadLists.length === 0
},
emptyResults (): boolean {
return !this.loading && this.series.length === 0 && this.books.length === 0 && this.collections.length === 0 && this.readLists.length === 0
emptyResults(): boolean {
return !this.loading &&
this.loaderSeries?.items.length === 0 &&
this.loaderBooks?.items.length === 0 &&
this.loaderCollections?.items.length === 0 &&
this.loaderReadLists?.items.length === 0
},
},
methods: {
seriesChanged(event: SeriesSseDto){
if(this.series.map(x => x.id).includes(event.seriesId)){
async scrollChanged(loader: PageLoader<any>, percent: number) {
if (percent > 0.95) await loader.loadNext()
},
seriesChanged(event: SeriesSseDto) {
if (this.loaderSeries?.items.map(x => x.id).includes(event.seriesId)) {
this.reloadResults()
}
},
bookChanged(event: BookSseDto){
if(this.books.map(x => x.id).includes(event.bookId)){
bookChanged(event: BookSseDto) {
if (this.loaderBooks?.items.map(x => x.id).includes(event.bookId)) {
this.reloadResults()
}
},
readProgressChanged(event: ReadProgressSseDto){
if(this.books.map(x => x.id).includes(event.bookId)){
readProgressChanged(event: ReadProgressSseDto) {
if (this.loaderBooks?.items.map(x => x.id).includes(event.bookId)) {
this.reloadResults()
}
},
readProgressSeriesChanged(event: ReadProgressSeriesSseDto){
if(this.series.map(x => x.id).includes(event.seriesId)){
readProgressSeriesChanged(event: ReadProgressSeriesSseDto) {
if (this.loaderSeries?.items.map(x => x.id).includes(event.seriesId)) {
this.reloadResults()
}
},
collectionChanged (event: CollectionSseDto) {
if (this.collections.map(x => x.id).includes(event.collectionId)) {
collectionChanged(event: CollectionSseDto) {
if (this.loaderCollections?.items.map(x => x.id).includes(event.collectionId)) {
this.reloadResults()
}
},
readListChanged (event: ReadListSseDto) {
if (this.readLists.map(x => x.id).includes(event.readListId)) {
readListChanged(event: ReadListSseDto) {
if (this.loaderReadLists?.items.map(x => x.id).includes(event.readListId)) {
this.reloadResults()
}
},
singleEditSeries (series: SeriesDto) {
singleEditSeries(series: SeriesDto) {
this.$store.dispatch('dialogUpdateSeries', series)
},
singleEditBook (book: BookDto) {
singleEditBook(book: BookDto) {
this.$store.dispatch('dialogUpdateBooks', book)
},
singleEditCollection (collection: CollectionDto) {
singleEditCollection(collection: CollectionDto) {
this.$store.dispatch('dialogEditCollection', collection)
},
singleEditReadList (readList: ReadListDto) {
singleEditReadList(readList: ReadListDto) {
this.$store.dispatch('dialogEditReadList', readList)
},
async markSelectedSeriesRead () {
async markSelectedSeriesRead() {
await Promise.all(this.selectedSeries.map(s =>
this.$komgaSeries.markAsRead(s.id),
))
@ -284,7 +312,7 @@ export default Vue.extend({
this.$komgaSeries.getOneSeries(s.id),
))
},
async markSelectedSeriesUnread () {
async markSelectedSeriesUnread() {
await Promise.all(this.selectedSeries.map(s =>
this.$komgaSeries.markAsUnread(s.id),
))
@ -292,33 +320,33 @@ export default Vue.extend({
this.$komgaSeries.getOneSeries(s.id),
))
},
addToCollection () {
addToCollection() {
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries)
},
addToReadList () {
addToReadList() {
this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks)
},
editMultipleSeries () {
editMultipleSeries() {
this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
},
editMultipleBooks () {
editMultipleBooks() {
this.$store.dispatch('dialogUpdateBooks', this.selectedBooks)
},
bulkEditMultipleBooks() {
this.$store.dispatch('dialogUpdateBulkBooks', this.selectedBooks)
},
deleteCollections () {
deleteCollections() {
this.$store.dispatch('dialogDeleteCollection', this.selectedCollections)
},
deleteReadLists () {
deleteReadLists() {
this.$store.dispatch('dialogDeleteReadList', this.selectedReadLists)
},
async markSelectedBooksRead () {
async markSelectedBooksRead() {
await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.updateReadProgress(b.id, { completed: true }),
this.$komgaBooks.updateReadProgress(b.id, {completed: true}),
))
},
async markSelectedBooksUnread () {
async markSelectedBooksUnread() {
await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.deleteReadProgress(b.id),
))
@ -326,7 +354,7 @@ export default Vue.extend({
reloadResults: throttle(function (this: any) {
this.loadResults(this.$route.query.q.toString())
}, 500),
async loadResults (search: string) {
async loadResults(search: string) {
this.selectedBooks = []
this.selectedSeries = []
this.selectedCollections = []
@ -334,17 +362,24 @@ export default Vue.extend({
if (search) {
this.loading = true
this.series = (await this.$komgaSeries.getSeries(undefined, { size: this.pageSize }, search)).content
this.books = (await this.$komgaBooks.getBooks(undefined, { size: this.pageSize }, search)).content
this.collections = (await this.$komgaCollections.getCollections(undefined, { size: this.pageSize }, search)).content
this.readLists = (await this.$komgaReadLists.getReadLists(undefined, { size: this.pageSize }, search)).content
this.loaderSeries = new PageLoader<SeriesDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaSeries.getSeries(undefined, pageable, search))
this.loaderBooks = new PageLoader<BookDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaBooks.getBooks(undefined, pageable, search))
this.loaderCollections = new PageLoader<CollectionDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaCollections.getCollections(undefined, pageable, search))
this.loaderReadLists = new PageLoader<ReadListDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaReadLists.getReadLists(undefined, pageable, search))
this.loading = false
Promise.all([
this.loaderSeries.loadNext(),
this.loaderBooks.loadNext(),
this.loaderCollections.loadNext(),
this.loaderReadLists.loadNext(),
]).then(() => {
this.loading = false
})
} else {
this.series = []
this.books = []
this.collections = []
this.readLists = []
this.loaderSeries = null as unknown as PageLoader<SeriesDto>
this.loaderBooks = null as unknown as PageLoader<BookDto>
this.loaderCollections = null as unknown as PageLoader<CollectionDto>
this.loaderReadLists = null as unknown as PageLoader<ReadListDto>
}
},
},

View file

@ -0,0 +1,49 @@
import {PageLoader} from '@/types/pageLoader'
describe('PageLoader', () => {
const pageRequest = {} as PageRequest
const loader = (pageable: PageRequest) => Promise.resolve({
content: [1, 2, 3],
last: true,
number: 0,
} as Page<number>)
test('given page loader when loading next then it should return true', async () => {
const pageLoader = new PageLoader(pageRequest, loader)
expect(await pageLoader.loadNext()).toBeTruthy()
expect(pageLoader.items).toStrictEqual([1, 2, 3])
})
test('given page loader on last page when loading next then it should return false', async () => {
const pageLoader = new PageLoader(pageRequest, loader)
expect(await pageLoader.loadNext()).toBeTruthy()
expect(pageLoader.items).toStrictEqual([1, 2, 3])
expect(await pageLoader.loadNext()).toBeFalsy()
})
test('given page loader when loading next then it should return true until last page is reached', async () => {
const loader = (pageable: PageRequest) => Promise.resolve({
content: pageable.page === 1 ? [4, 5, 6] : [1, 2, 3],
last: pageable.page === 1,
number: pageable.page || 0,
} as Page<number>)
const pageLoader = new PageLoader(pageRequest, loader)
expect(await pageLoader.loadNext()).toBeTruthy()
expect(pageLoader.items).toStrictEqual([1, 2, 3])
expect(await pageLoader.loadNext()).toBeTruthy()
expect(pageLoader.items).toStrictEqual([1, 2, 3, 4, 5, 6])
expect(await pageLoader.loadNext()).toBeFalsy()
})
test('given exception when loading next then exception is rethrown', async () => {
const loader = (pageable: PageRequest) => {
throw new Error('boom')
}
const pageLoader = new PageLoader(pageRequest, loader)
await expect(pageLoader.loadNext()).rejects.toEqual(new Error('boom'))
})
})