refactor(webui): use new book list API

This commit is contained in:
Gauthier Roebroeck 2025-01-17 17:30:34 +08:00
parent 1da0afe04a
commit 2ac296dd49
14 changed files with 139 additions and 72 deletions

View file

@ -132,6 +132,7 @@ import {bookPageUrl, transientBookPageUrl} from '@/functions/urls'
import {convertErrorCodes} from '@/functions/error-codes'
import FileNameChooserDialog from '@/components/dialogs/FileNameChooserDialog.vue'
import {SeriesSelected} from '@/types/series-slim'
import {BookSearch, SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'FileImportRow',
@ -262,7 +263,9 @@ export default Vue.extend({
},
async getSeriesBooks(series: SeriesSelected) {
if (series) {
this.seriesBooks = (await this.$komgaSeries.getBooks(series.seriesId, {unpaged: true})).content
this.seriesBooks = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(series.seriesId)),
} as BookSearch, {unpaged: true, sort: 'metadata.numberSort'})).content
if (series.oneshot) {
this.bookNumber = this.seriesBooks[0].metadata.numberSort
}

View file

@ -71,6 +71,7 @@ import {
ReadListRequestBookMatchSeriesDto,
} from '@/types/komga-readlists'
import BookPickerDialog from '@/components/dialogs/BookPickerDialog.vue'
import {BookSearch, SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'ReadListMatchRow',
@ -125,7 +126,9 @@ export default Vue.extend({
methods: {
openBookPicker() {
if (!this.seriesBooksCached) {
this.$komgaSeries.getBooks(this.series?.seriesId, {unpaged: true})
this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(this.series?.seriesId)),
} as BookSearch, {unpaged: true, sort: 'metadata.numberSort'})
.then(r => {
this.seriesBooks = r.content
this.seriesBooksCached = true

View file

@ -124,6 +124,7 @@ import {getReadProgress} from '@/functions/book-progress'
import {ReadStatus} from '@/types/enum-books'
import {ReadListDto} from '@/types/komga-readlists'
import {
BookSearch,
SearchConditionOneShot,
SearchOperatorIsFalse,
SeriesSearch,
@ -211,7 +212,9 @@ export default Vue.extend({
fullTextSearch: query,
condition: new SearchConditionOneShot(new SearchOperatorIsFalse()),
} as SeriesSearch, {size: this.pageSize})).content
this.books = (await this.$komgaBooks.getBooks(undefined, {size: this.pageSize}, query)).content
this.books = (await this.$komgaBooks.getBooksList({
fullTextSearch: query,
} as BookSearch, {size: this.pageSize})).content
this.collections = (await this.$komgaCollections.getCollections(undefined, {size: this.pageSize}, query)).content
this.readLists = (await this.$komgaReadLists.getReadLists(undefined, {size: this.pageSize}, query)).content
this.showResults = true

View file

@ -93,7 +93,9 @@ export default Vue.extend({
this.$store.dispatch('dialogAddSeriesToCollection', [this.seriesId])
},
async addToReadList() {
if (!this.book && !this.localBookId) this.localBookId = (await this.$komgaSeries.getBooks(this.seriesId)).content[0].id
if (!this.book && !this.localBookId) this.localBookId = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(this.seriesId)),
} as BookSearch)).content[0].id
this.$store.dispatch('dialogAddBooksToReadList', [this.book?.id || this.localBookId])
},
async markRead() {

View file

@ -8,9 +8,9 @@ import {
PageDto,
ReadProgressUpdateDto,
} from '@/types/komga-books'
import {formatISO} from 'date-fns'
import {ReadListDto} from '@/types/komga-readlists'
import {R2Progression} from '@/types/readium'
import {BookSearch} from '@/types/komga-search'
const qs = require('qs')
@ -23,26 +23,10 @@ export default class KomgaBooksService {
this.http = http
}
async getBooks(libraryId?: string, pageRequest?: PageRequest, search?: string, mediaStatus?: string[], readStatus?: string[], releasedAfter?: Date): Promise<Page<BookDto>> {
async getBooksList(search: BookSearch, pageRequest?: PageRequest): Promise<Page<BookDto>> {
try {
const params = {...pageRequest} as any
if (libraryId) {
params.library_id = libraryId
}
if (search) {
params.search = search
}
if (mediaStatus) {
params.media_status = mediaStatus
}
if (readStatus) {
params.read_status = readStatus
}
if (releasedAfter) {
params.released_after = formatISO(releasedAfter, {representation: 'date'})
}
return (await this.http.get(API_BOOKS, {
params: params,
return (await this.http.post(`${API_BOOKS}/list`, search, {
params: {...pageRequest},
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {

View file

@ -87,26 +87,6 @@ export default class KomgaSeriesService {
}
}
async getBooks(seriesId: string, pageRequest?: PageRequest, readStatus?: string[], tag?: string[], authors?: AuthorDto[]): Promise<Page<BookDto>> {
try {
const params = {...pageRequest} as any
if (readStatus) params.read_status = readStatus
if (tag) params.tag = tag
if (authors) params.author = authors.map(a => `${a.name},${a.role}`)
return (await this.http.get(`${API_SERIES}/${seriesId}/books`, {
params: params,
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve books'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async getCollections(seriesId: string): Promise<CollectionDto[]> {
try {
return (await this.http.get(`${API_SERIES}/${seriesId}/collections`)).data

View file

@ -3,18 +3,31 @@ export interface SeriesSearch {
fullTextSearch?: string,
}
export interface BookSearch {
condition?: SearchConditionBook,
fullTextSearch?: string,
}
export interface SearchConditionSeries {
}
export interface SearchConditionBook {
}
export interface SearchConditionAnyOfBook extends SearchConditionBook {
anyOf: SearchConditionBook[],
export class SearchConditionAnyOfBook implements SearchConditionBook {
anyOf: SearchConditionBook[]
constructor(conditions: SearchConditionBook[]) {
this.anyOf = conditions
}
}
export interface SearchConditionAllOfBook extends SearchConditionBook {
allOf: SearchConditionBook[],
export class SearchConditionAllOfBook implements SearchConditionBook {
allOf: SearchConditionBook[]
constructor(conditions: SearchConditionBook[]) {
this.allOf = conditions
}
}
export class SearchConditionAnyOfSeries implements SearchConditionSeries {
@ -41,6 +54,14 @@ export class SearchConditionLibraryId implements SearchConditionBook, SearchCond
}
}
export class SearchConditionSeriesId implements SearchConditionBook {
seriesId: SearchOperatorEquality
constructor(op: SearchOperatorEquality) {
this.seriesId = op
}
}
export class SearchConditionSeriesStatus implements SearchConditionSeries {
seriesStatus: SearchOperatorEquality
@ -57,6 +78,14 @@ export class SearchConditionReadStatus implements SearchConditionBook, SearchCon
}
}
export class SearchConditionMediaStatus implements SearchConditionBook {
mediaStatus: SearchOperatorEquality
constructor(op: SearchOperatorEquality) {
this.mediaStatus = op
}
}
export class SearchConditionGenre implements SearchConditionSeries {
genre: SearchOperatorEquality

View file

@ -165,6 +165,7 @@ import {LibraryDto} from '@/types/komga-libraries'
import {parseBooleanFilter} from '@/functions/query-params'
import {ContextOrigin} from '@/types/context'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
import {SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'BrowseCollection',
@ -492,14 +493,18 @@ export default Vue.extend({
},
async editSingleSeries(series: SeriesDto) {
if (series.oneshot) {
const book = (await this.$komgaSeries.getBooks(series.id)).content[0]
const book = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(series.id)),
} as BookSearch)).content[0]
this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book})
} else
this.$store.dispatch('dialogUpdateSeries', series)
},
async editMultipleSeries() {
if (this.selectedSeries.every(s => s.oneshot)) {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)),
} as BookSearch)))
const oneshots = this.selectedSeries.map((s, index) => ({series: s, book: books[index].content[0]} as Oneshot))
this.$store.dispatch('dialogUpdateOneshots', oneshots)
} else
@ -524,7 +529,9 @@ export default Vue.extend({
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries.map(s => s.id))
},
async addToReadList() {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)),
} as BookSearch)))
this.$store.dispatch('dialogAddBooksToReadList', books.map(b => b.content[0].id))
},
async startEditElements() {

View file

@ -177,7 +177,7 @@ import {
SearchConditionPublisher,
SearchConditionReadStatus,
SearchConditionReleaseDate,
SearchConditionSeries,
SearchConditionSeries, SearchConditionSeriesId,
SearchConditionSeriesStatus,
SearchConditionSharingLabel,
SearchConditionTag,
@ -719,19 +719,25 @@ export default Vue.extend({
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries.map(s => s.id))
},
async addToReadList() {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)),
} as BookSearch)))
this.$store.dispatch('dialogAddBooksToReadList', books.map(b => b.content[0].id))
},
async editSingleSeries(series: SeriesDto) {
if (series.oneshot) {
const book = (await this.$komgaSeries.getBooks(series.id)).content[0]
const book = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(series.id)),
} as BookSearch)).content[0]
this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book})
} else
this.$store.dispatch('dialogUpdateSeries', series)
},
async editMultipleSeries() {
if (this.selectedOneshots) {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)),
} as BookSearch)))
const oneshots = this.selectedSeries.map((s, index) => ({series: s, book: books[index].content[0]} as Oneshot))
this.$store.dispatch('dialogUpdateOneshots', oneshots)
} else

View file

@ -17,8 +17,8 @@
<!-- Action menu -->
<oneshot-actions-menu v-if="book"
:book="book"
:series="series"
:book="book"
:series="series"
/>
<v-btn icon @click="editBook" v-if="isAdmin">
@ -527,8 +527,9 @@ import CollectionsExpansionPanels from '@/components/CollectionsExpansionPanels.
import OneshotActionsMenu from '@/components/menus/OneshotActionsMenu.vue'
import {
SearchConditionAgeRating,
SearchConditionGenre, SearchConditionLanguage,
SearchConditionPublisher,
SearchConditionGenre,
SearchConditionLanguage,
SearchConditionPublisher, SearchConditionSeriesId,
SearchConditionTag,
SearchOperatorIs,
} from '@/types/komga-search'
@ -744,7 +745,9 @@ export default Vue.extend({
await this.loadBook(seriesId)
},
async loadBook(seriesId: string) {
this.book = (await this.$komgaSeries.getBooks(seriesId)).content[0]
this.book = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(seriesId)),
} as BookSearch)).content[0]
// parse query params to get context and contextId
if (this.$route.query.contextId && this.$route.query.context

View file

@ -232,6 +232,14 @@ import {BookSseDto, ReadProgressSeriesSseDto, ReadProgressSseDto, SeriesSseDto}
import {LibraryDto} from '@/types/komga-libraries'
import {PageLoader} from '@/types/pageLoader'
import {ItemContext} from '@/types/items'
import {
BookSearch,
SearchConditionAllOfBook,
SearchConditionBook,
SearchConditionLibraryId,
SearchConditionReadStatus, SearchConditionReleaseDate, SearchConditionSeriesId, SearchOperatorAfter,
SearchOperatorIs,
} from '@/types/komga-search'
export default Vue.extend({
name: 'DashboardView',
@ -365,9 +373,14 @@ export default Vue.extend({
this.loadAll(this.libraryId, true)
}, 5000),
setupLoaders(libraryId: string) {
const baseBookConditions = [] as SearchConditionBook[]
if (libraryId !== LIBRARIES_ALL) baseBookConditions.push(new SearchConditionLibraryId(new SearchOperatorIs(libraryId)))
this.loaderInProgressBooks = new PageLoader<BookDto>(
{sort: ['readProgress.readDate,desc']},
(pageable: PageRequest) => this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageable, undefined, undefined, [ReadStatus.IN_PROGRESS]),
(pageable: PageRequest) => this.$komgaBooks.getBooksList({
condition: new SearchConditionAllOfBook([...baseBookConditions, new SearchConditionReadStatus(new SearchOperatorIs(ReadStatus.IN_PROGRESS))]),
} as BookSearch, pageable),
)
this.loaderOnDeckBooks = new PageLoader<BookDto>(
{},
@ -375,15 +388,21 @@ export default Vue.extend({
)
this.loaderLatestBooks = new PageLoader<BookDto>(
{sort: ['createdDate,desc']},
(pageable: PageRequest) => this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageable),
(pageable: PageRequest) => this.$komgaBooks.getBooksList({
condition: new SearchConditionAllOfBook(baseBookConditions),
} as BookSearch, 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)),
(pageable: PageRequest) => this.$komgaBooks.getBooksList({
condition: new SearchConditionAllOfBook([...baseBookConditions, new SearchConditionReleaseDate(new SearchOperatorAfter(subMonths(new Date(), 1)))]),
} as BookSearch, pageable),
)
this.loaderRecentlyReadBooks = new PageLoader<BookDto>(
{sort: ['readProgress.readDate,desc']},
(pageable: PageRequest) => this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageable, undefined, undefined, [ReadStatus.READ]),
(pageable: PageRequest) => this.$komgaBooks.getBooksList({
condition: new SearchConditionAllOfBook([...baseBookConditions, new SearchConditionReadStatus(new SearchOperatorIs(ReadStatus.READ))]),
} as BookSearch, pageable),
)
this.loaderNewSeries = new PageLoader<SeriesDto>(
@ -429,7 +448,9 @@ export default Vue.extend({
},
async singleEditSeries(series: SeriesDto) {
if (series.oneshot) {
let book = (await this.$komgaSeries.getBooks(series.id)).content[0]
let book = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(series.id)),
} as BookSearch)).content[0]
this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book})
} else
this.$store.dispatch('dialogUpdateSeries', series)
@ -463,7 +484,9 @@ export default Vue.extend({
},
async editMultipleSeries() {
if (this.selectedSeries.every(s => s.oneshot)) {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)),
} as BookSearch)))
const oneshots = this.selectedSeries.map((s, index) => ({series: s, book: books[index].content[0]} as Oneshot))
this.$store.dispatch('dialogUpdateOneshots', oneshots)
} else

View file

@ -230,6 +230,7 @@ import {LIBRARIES_ALL} from '@/types/library'
import ToasterNotification from '@/components/ToasterNotification.vue'
import {MediaStatus} from '@/types/enum-books'
import {LibraryDto} from '@/types/komga-libraries'
import {BookSearch, SearchConditionAnyOfBook, SearchConditionMediaStatus, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'HomeView',
@ -245,7 +246,12 @@ export default Vue.extend({
if (this.isAdmin) {
this.$actuator.getInfo()
.then(x => this.$store.commit('setActuatorInfo', x))
this.$komgaBooks.getBooks(undefined, {size: 0} as PageRequest, undefined, [MediaStatus.ERROR, MediaStatus.UNSUPPORTED])
this.$komgaBooks.getBooksList({
condition: new SearchConditionAnyOfBook([
new SearchConditionMediaStatus(new SearchOperatorIs(MediaStatus.ERROR)),
new SearchConditionMediaStatus(new SearchOperatorIs(MediaStatus.UNSUPPORTED)),
]),
} as BookSearch, {size: 0} as PageRequest)
.then(x => this.$store.commit('setBooksToCheck', x.totalElements))
this.$komgaAnnouncements.getAnnouncements()
.then(x => this.$store.commit('setAnnouncements', x))

View file

@ -40,6 +40,7 @@ import Vue from 'vue'
import {MediaStatus} from '@/types/enum-books'
import {BookDto} from '@/types/komga-books'
import {convertErrorCodes} from '@/functions/error-codes'
import {BookSearch, SearchConditionAnyOfBook, SearchConditionMediaStatus, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'MediaAnalysis',
@ -97,7 +98,12 @@ export default Vue.extend({
pageRequest.sort!!.push(`${sortBy[i]},${sortDesc[i] ? 'desc' : 'asc'}`)
}
const booksPage = await this.$komgaBooks.getBooks(undefined, pageRequest, undefined, [MediaStatus.ERROR, MediaStatus.UNSUPPORTED])
const booksPage = await this.$komgaBooks.getBooksList({
condition: new SearchConditionAnyOfBook([
new SearchConditionMediaStatus(new SearchOperatorIs(MediaStatus.ERROR)),
new SearchConditionMediaStatus(new SearchOperatorIs(MediaStatus.UNSUPPORTED)),
]),
} as BookSearch, pageRequest)
this.totalBooks = booksPage.totalElements
this.$store.commit('setBooksToCheck', booksPage.totalElements)
this.books = booksPage.content

View file

@ -180,7 +180,13 @@ import {throttle} from 'lodash'
import {PageLoader} from '@/types/pageLoader'
import {ItemContext} from '@/types/items'
import {ReadListDto} from '@/types/komga-readlists'
import {SearchConditionOneShot, SearchOperatorIsFalse, SeriesSearch} from '@/types/komga-search'
import {
BookSearch,
SearchConditionOneShot,
SearchConditionSeriesId, SearchOperatorIs,
SearchOperatorIsFalse,
SeriesSearch,
} from '@/types/komga-search'
export default Vue.extend({
name: 'SearchView',
@ -307,7 +313,9 @@ export default Vue.extend({
},
async singleEditSeries(series: SeriesDto) {
if (series.oneshot) {
let book = (await this.$komgaSeries.getBooks(series.id)).content[0]
const book = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(series.id)),
} as BookSearch)).content[0]
this.$store.dispatch('dialogUpdateOneshots', {series: series, book: book})
} else
this.$store.dispatch('dialogUpdateSeries', series)
@ -352,7 +360,9 @@ export default Vue.extend({
},
async editMultipleSeries() {
if (this.selectedSeries.every(s => s.oneshot)) {
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.getBooks(s.id)))
const books = await Promise.all(this.selectedSeries.map(s => this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(s.id)),
} as BookSearch)))
const oneshots = this.selectedSeries.map((s, index) => ({series: s, book: books[index].content[0]} as Oneshot))
this.$store.dispatch('dialogUpdateOneshots', oneshots)
} else
@ -400,7 +410,9 @@ export default Vue.extend({
fullTextSearch: search,
condition: new SearchConditionOneShot(new SearchOperatorIsFalse()),
} as SeriesSearch, pageable))
this.loaderBooks = new PageLoader<BookDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaBooks.getBooks(undefined, pageable, search))
this.loaderBooks = new PageLoader<BookDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaBooks.getBooksList({
fullTextSearch: search,
} as BookSearch, pageable))
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))
} else {