From c3a3fa343b921ef151552ea364d865ef7b11beeb Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Fri, 31 Dec 2021 10:18:06 +0800 Subject: [PATCH] feat(webui): filter series by completeness part of #590 --- komga-webui/src/components/FilterList.vue | 18 +++++++---- komga-webui/src/functions/query-params.ts | 11 +++++-- .../src/services/komga-collections.service.ts | 30 ++++++++++--------- .../src/services/komga-series.service.ts | 6 ++-- komga-webui/src/types/filter.ts | 2 ++ komga-webui/src/views/BrowseCollection.vue | 11 +++++-- komga-webui/src/views/BrowseLibraries.vue | 14 ++++++--- 7 files changed, 63 insertions(+), 29 deletions(-) diff --git a/komga-webui/src/components/FilterList.vue b/komga-webui/src/components/FilterList.vue index cae0e1e8c..db887de04 100644 --- a/komga-webui/src/components/FilterList.vue +++ b/komga-webui/src/components/FilterList.vue @@ -6,10 +6,13 @@ {{ f.name }} - + + mdi-minus-box + + mdi-checkbox-marked @@ -38,11 +41,16 @@ export default Vue.extend({ }, }, methods: { - click (key: string, value: string) { + click(key: string, value: string, nValue?: string) { let r = this.$_.cloneDeep(this.filtersActive) if (!(key in r)) r[key] = [] - if (r[key].includes(value)) this.$_.pull(r[key], (value)) - else r[key].push(value) + if (nValue && r[key].includes(nValue)) + this.$_.pull(r[key], (nValue)) + else if (r[key].includes(value)) { + this.$_.pull(r[key], (value)) + if (nValue) + r[key].push(nValue) + } else r[key].push(value) this.$emit('update:filtersActive', r) }, diff --git a/komga-webui/src/functions/query-params.ts b/komga-webui/src/functions/query-params.ts index 5837bb009..0cfbd97db 100644 --- a/komga-webui/src/functions/query-params.ts +++ b/komga-webui/src/functions/query-params.ts @@ -1,12 +1,19 @@ const sortDirs = ['asc', 'desc'] -export function parseQuerySort (querySort: any, sortOptions: SortOption[]): SortActive | null { +export function parseQuerySort(querySort: any, sortOptions: SortOption[]): SortActive | null { let customSort = null if (querySort) { const split = querySort.split(',') if (split.length === 2 && sortOptions.map(x => x.key).includes(split[0]) && sortDirs.includes(split[1])) { - customSort = { key: split[0], order: split[1] } + customSort = {key: split[0], order: split[1]} } } return customSort } + +export function parseBooleanFilter(values?: string[]): boolean | undefined { + if (!values || values.length === 0) return undefined + if (values[0].trim().toLowerCase() === 'true') return true + if (values[0].trim().toLowerCase() === 'false') return false + return undefined +} diff --git a/komga-webui/src/services/komga-collections.service.ts b/komga-webui/src/services/komga-collections.service.ts index 0cc36d345..332495d17 100644 --- a/komga-webui/src/services/komga-collections.service.ts +++ b/komga-webui/src/services/komga-collections.service.ts @@ -9,19 +9,19 @@ const API_COLLECTIONS = '/api/v1/collections' export default class KomgaCollectionsService { private http: AxiosInstance - constructor (http: AxiosInstance) { + constructor(http: AxiosInstance) { this.http = http } - async getCollections (libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise> { + async getCollections(libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise> { try { - const params = { ...pageRequest } as any + const params = {...pageRequest} as any if (libraryIds) params.library_id = libraryIds if (search) params.search = search return (await this.http.get(API_COLLECTIONS, { params: params, - paramsSerializer: params => qs.stringify(params, { indices: false }), + paramsSerializer: params => qs.stringify(params, {indices: false}), })).data } catch (e) { let msg = 'An error occurred while trying to retrieve collections' @@ -32,7 +32,7 @@ export default class KomgaCollectionsService { } } - async getOneCollection (collectionId: string): Promise { + async getOneCollection(collectionId: string): Promise { try { return (await this.http.get(`${API_COLLECTIONS}/${collectionId}`)).data } catch (e) { @@ -44,7 +44,7 @@ export default class KomgaCollectionsService { } } - async postCollection (collection: CollectionCreationDto): Promise { + async postCollection(collection: CollectionCreationDto): Promise { try { return (await this.http.post(API_COLLECTIONS, collection)).data } catch (e) { @@ -56,7 +56,7 @@ export default class KomgaCollectionsService { } } - async patchCollection (collectionId: string, collection: CollectionUpdateDto) { + async patchCollection(collectionId: string, collection: CollectionUpdateDto) { try { await this.http.patch(`${API_COLLECTIONS}/${collectionId}`, collection) } catch (e) { @@ -68,7 +68,7 @@ export default class KomgaCollectionsService { } } - async deleteCollection (collectionId: string) { + async deleteCollection(collectionId: string) { try { await this.http.delete(`${API_COLLECTIONS}/${collectionId}`) } catch (e) { @@ -80,12 +80,13 @@ export default class KomgaCollectionsService { } } - async getSeries (collectionId: string, pageRequest?: PageRequest, - libraryId?: string[], status?: string[], - readStatus?: string[], genre?: string[], tag?: string[], language?: string[], - publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[]): Promise> { + async getSeries(collectionId: string, pageRequest?: PageRequest, + libraryId?: string[], status?: string[], + readStatus?: string[], genre?: string[], tag?: string[], language?: string[], + publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[], + complete?: boolean): Promise> { try { - const params = { ...pageRequest } as any + const params = {...pageRequest} as any if (libraryId) params.library_id = libraryId if (status) params.status = status if (readStatus) params.read_status = readStatus @@ -96,10 +97,11 @@ export default class KomgaCollectionsService { if (ageRating) params.age_rating = ageRating if (releaseDate) params.release_year = releaseDate if (authors) params.author = authors.map(a => `${a.name},${a.role}`) + if (complete !== undefined) params.complete = complete return (await this.http.get(`${API_COLLECTIONS}/${collectionId}/series`, { params: params, - paramsSerializer: params => qs.stringify(params, { indices: false }), + paramsSerializer: params => qs.stringify(params, {indices: false}), })).data } catch (e) { let msg = 'An error occurred while trying to retrieve series' diff --git a/komga-webui/src/services/komga-series.service.ts b/komga-webui/src/services/komga-series.service.ts index 6a9d1113c..c2e829213 100644 --- a/komga-webui/src/services/komga-series.service.ts +++ b/komga-webui/src/services/komga-series.service.ts @@ -16,7 +16,7 @@ export default class KomgaSeriesService { async getSeries(libraryId?: string, pageRequest?: PageRequest, search?: string, status?: string[], readStatus?: string[], genre?: string[], tag?: string[], language?: string[], publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[], - searchRegex?: string): Promise> { + searchRegex?: string, complete?: boolean): Promise> { try { const params = {...pageRequest} as any if (libraryId) params.library_id = libraryId @@ -31,6 +31,7 @@ export default class KomgaSeriesService { if (ageRating) params.age_rating = ageRating if (releaseDate) params.release_year = releaseDate if (authors) params.author = authors.map(a => `${a.name},${a.role}`) + if (complete !== undefined) params.complete = complete return (await this.http.get(API_SERIES, { params: params, @@ -47,7 +48,7 @@ export default class KomgaSeriesService { async getAlphabeticalGroups(libraryId?: string, search?: string, status?: string[], readStatus?: string[], genre?: string[], tag?: string[], language?: string[], - publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[]): Promise { + publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[], complete?: boolean): Promise { try { const params = {} as any if (libraryId) params.library_id = libraryId @@ -61,6 +62,7 @@ export default class KomgaSeriesService { if (ageRating) params.age_rating = ageRating if (releaseDate) params.release_year = releaseDate if (authors) params.author = authors.map(a => `${a.name},${a.role}`) + if (complete !== undefined) params.complete = complete return (await this.http.get(`${API_SERIES}/alphabetical-groups`, { params: params, diff --git a/komga-webui/src/types/filter.ts b/komga-webui/src/types/filter.ts index adeedbe57..358b27825 100644 --- a/komga-webui/src/types/filter.ts +++ b/komga-webui/src/types/filter.ts @@ -9,6 +9,8 @@ interface FiltersOptions { interface NameValue { name: string, value: string, + // an optional negative value + nValue?: string, } interface FiltersActive { diff --git a/komga-webui/src/views/BrowseCollection.vue b/komga-webui/src/views/BrowseCollection.vue index 8bd09ae9f..fc06d8451 100644 --- a/komga-webui/src/views/BrowseCollection.vue +++ b/komga-webui/src/views/BrowseCollection.vue @@ -142,6 +142,7 @@ import {AuthorDto} from '@/types/komga-books' import {CollectionSseDto, ReadProgressSeriesSseDto, SeriesSseDto} from '@/types/komga-sse' import {throttle} from 'lodash' import {LibraryDto} from '@/types/komga-libraries' +import {parseBooleanFilter} from '@/functions/query-params' export default Vue.extend({ name: 'BrowseCollection', @@ -231,6 +232,9 @@ export default Vue.extend({ {name: this.$t('filter.read').toString(), value: ReadStatus.READ}, ], }, + complete: { + values: [{name: 'Complete', value: 'true', nValue: 'false'}], + }, } as FiltersOptions }, filterOptionsPanel(): FiltersOptions { @@ -303,7 +307,7 @@ export default Vue.extend({ // get filter from query params or local storage and validate with available filter values let activeFilters: any - if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating || route.query.library || route.query.publisher || authorRoles.some(role => role in route.query)) { + if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating || route.query.library || route.query.publisher || authorRoles.some(role => role in route.query) || route.query.complete) { activeFilters = { status: route.query.status || [], readStatus: route.query.readStatus || [], @@ -314,6 +318,7 @@ export default Vue.extend({ language: route.query.language || [], ageRating: route.query.ageRating || [], releaseDate: route.query.releaseDate || [], + complete: route.query.complete || [], } authorRoles.forEach((role: string) => { activeFilters[role] = route.query[role] || [] @@ -334,6 +339,7 @@ export default Vue.extend({ language: filters.language?.filter(x => this.filterOptions.language.map(n => n.value).includes(x)) || [], ageRating: filters.ageRating?.filter(x => this.filterOptions.ageRating.map(n => n.value).includes(x)) || [], releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [], + complete: filters.complete?.filter(x => x === 'true' || x === 'false') || [], } as any authorRoles.forEach((role: string) => { validFilter[role] = filters[role] || [] @@ -379,7 +385,8 @@ export default Vue.extend({ })) }) - 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)).content + 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.seriesCopy = [...this.series] this.selectedSeries = [] }, diff --git a/komga-webui/src/views/BrowseLibraries.vue b/komga-webui/src/views/BrowseLibraries.vue index e7bf90012..342b579c9 100644 --- a/komga-webui/src/views/BrowseLibraries.vue +++ b/komga-webui/src/views/BrowseLibraries.vue @@ -128,7 +128,7 @@ import ItemBrowser from '@/components/ItemBrowser.vue' import LibraryNavigation from '@/components/LibraryNavigation.vue' import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue' import PageSizeSelect from '@/components/PageSizeSelect.vue' -import {parseQuerySort} from '@/functions/query-params' +import {parseBooleanFilter, parseQuerySort} from '@/functions/query-params' import {ReadStatus, replaceCompositeReadStatus} from '@/types/enum-books' import {SeriesStatus, SeriesStatusKeyValue} from '@/types/enum-series' import { @@ -281,6 +281,9 @@ export default Vue.extend({ {name: this.$t('filter.read').toString(), value: ReadStatus.READ}, ], }, + complete: { + values: [{name: 'Complete', value: 'true', nValue: 'false'}], + }, } as FiltersOptions }, filterOptionsPanel(): FiltersOptions { @@ -377,7 +380,7 @@ export default Vue.extend({ // get filter from query params or local storage and validate with available filter values let activeFilters: any - if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating || route.query.publisher || authorRoles.some(role => role in route.query)) { + if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating || route.query.publisher || authorRoles.some(role => role in route.query) || route.query.complete) { activeFilters = { status: route.query.status || [], readStatus: route.query.readStatus || [], @@ -387,6 +390,7 @@ export default Vue.extend({ language: route.query.language || [], ageRating: route.query.ageRating || [], releaseDate: route.query.releaseDate || [], + complete: route.query.complete || [], } authorRoles.forEach((role: string) => { activeFilters[role] = route.query[role] || [] @@ -406,6 +410,7 @@ export default Vue.extend({ language: filters.language?.filter(x => this.filterOptions.language.map(n => n.value).includes(x)) || [], ageRating: filters.ageRating?.filter(x => this.filterOptions.ageRating.map(n => n.value).includes(x)) || [], releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [], + complete: filters.complete?.filter(x => x === 'true' || x === 'false') || [], } as any authorRoles.forEach((role: string) => { validFilter[role] = filters[role] || [] @@ -510,13 +515,14 @@ export default Vue.extend({ }) const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined - const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, 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, searchRegex) + const complete = parseBooleanFilter(this.filters.complete) + const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, 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, searchRegex, complete) this.totalPages = seriesPage.totalPages this.totalElements = seriesPage.totalElements this.series = seriesPage.content - const seriesGroups = await this.$komgaSeries.getAlphabeticalGroups(requestLibraryId, undefined, 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) + const seriesGroups = await this.$komgaSeries.getAlphabeticalGroups(requestLibraryId, undefined, 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) const nonAlpha = seriesGroups .filter((g) => !(/[a-zA-Z]/).test(g.group)) .reduce((a, b) => a + b.count, 0)