mirror of
https://github.com/gotson/komga.git
synced 2025-12-20 15:34:17 +01:00
feat(webui): horizontal scroller infinite scroll on dashboard and search results
closes #605
This commit is contained in:
parent
24b564a707
commit
fe78f17e5e
5 changed files with 315 additions and 137 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
43
komga-webui/src/types/pageLoader.ts
Normal file
43
komga-webui/src/types/pageLoader.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
49
komga-webui/tests/unit/types/pageLoader.spec.ts
Normal file
49
komga-webui/tests/unit/types/pageLoader.spec.ts
Normal 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'))
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue