feat(webui): filter series by completeness

part of #590
This commit is contained in:
Gauthier Roebroeck 2021-12-31 10:18:06 +08:00
parent 494bdf28a1
commit c3a3fa343b
7 changed files with 63 additions and 29 deletions

View file

@ -6,10 +6,13 @@
<v-subheader v-if="f.name">{{ f.name }}</v-subheader>
<v-list-item v-for="v in f.values"
:key="v.value"
@click.stop="click(key, v.value)"
@click.stop="click(key, v.value, v.nValue)"
>
<v-list-item-icon>
<v-icon v-if="key in filtersActive && filtersActive[key].includes(v.value)" color="secondary">
<v-icon v-if="key in filtersActive && filtersActive[key].includes(v.nValue)" color="secondary">
mdi-minus-box
</v-icon>
<v-icon v-else-if="key in filtersActive && filtersActive[key].includes(v.value)" color="secondary">
mdi-checkbox-marked
</v-icon>
<v-icon v-else>
@ -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)
},

View file

@ -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
}

View file

@ -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<Page<CollectionDto>> {
async getCollections(libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise<Page<CollectionDto>> {
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<CollectionDto> {
async getOneCollection(collectionId: string): Promise<CollectionDto> {
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<CollectionDto> {
async postCollection(collection: CollectionCreationDto): Promise<CollectionDto> {
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<Page<SeriesDto>> {
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<Page<SeriesDto>> {
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'

View file

@ -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<Page<SeriesDto>> {
searchRegex?: string, complete?: boolean): Promise<Page<SeriesDto>> {
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<GroupCountDto[]> {
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[], complete?: boolean): Promise<GroupCountDto[]> {
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,

View file

@ -9,6 +9,8 @@ interface FiltersOptions {
interface NameValue {
name: string,
value: string,
// an optional negative value
nValue?: string,
}
interface FiltersActive {

View file

@ -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 = []
},

View file

@ -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)