Skip to content

Commit c3a3fa3

Browse files
committed
feat(webui): filter series by completeness
part of #590
1 parent 494bdf2 commit c3a3fa3

File tree

7 files changed

+63
-29
lines changed

7 files changed

+63
-29
lines changed

Diff for: komga-webui/src/components/FilterList.vue

+13-5
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66
<v-subheader v-if="f.name">{{ f.name }}</v-subheader>
77
<v-list-item v-for="v in f.values"
88
:key="v.value"
9-
@click.stop="click(key, v.value)"
9+
@click.stop="click(key, v.value, v.nValue)"
1010
>
1111
<v-list-item-icon>
12-
<v-icon v-if="key in filtersActive && filtersActive[key].includes(v.value)" color="secondary">
12+
<v-icon v-if="key in filtersActive && filtersActive[key].includes(v.nValue)" color="secondary">
13+
mdi-minus-box
14+
</v-icon>
15+
<v-icon v-else-if="key in filtersActive && filtersActive[key].includes(v.value)" color="secondary">
1316
mdi-checkbox-marked
1417
</v-icon>
1518
<v-icon v-else>
@@ -38,11 +41,16 @@ export default Vue.extend({
3841
},
3942
},
4043
methods: {
41-
click (key: string, value: string) {
44+
click(key: string, value: string, nValue?: string) {
4245
let r = this.$_.cloneDeep(this.filtersActive)
4346
if (!(key in r)) r[key] = []
44-
if (r[key].includes(value)) this.$_.pull(r[key], (value))
45-
else r[key].push(value)
47+
if (nValue && r[key].includes(nValue))
48+
this.$_.pull(r[key], (nValue))
49+
else if (r[key].includes(value)) {
50+
this.$_.pull(r[key], (value))
51+
if (nValue)
52+
r[key].push(nValue)
53+
} else r[key].push(value)
4654
4755
this.$emit('update:filtersActive', r)
4856
},

Diff for: komga-webui/src/functions/query-params.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
const sortDirs = ['asc', 'desc']
22

3-
export function parseQuerySort (querySort: any, sortOptions: SortOption[]): SortActive | null {
3+
export function parseQuerySort(querySort: any, sortOptions: SortOption[]): SortActive | null {
44
let customSort = null
55
if (querySort) {
66
const split = querySort.split(',')
77
if (split.length === 2 && sortOptions.map(x => x.key).includes(split[0]) && sortDirs.includes(split[1])) {
8-
customSort = { key: split[0], order: split[1] }
8+
customSort = {key: split[0], order: split[1]}
99
}
1010
}
1111
return customSort
1212
}
13+
14+
export function parseBooleanFilter(values?: string[]): boolean | undefined {
15+
if (!values || values.length === 0) return undefined
16+
if (values[0].trim().toLowerCase() === 'true') return true
17+
if (values[0].trim().toLowerCase() === 'false') return false
18+
return undefined
19+
}

Diff for: komga-webui/src/services/komga-collections.service.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ const API_COLLECTIONS = '/api/v1/collections'
99
export default class KomgaCollectionsService {
1010
private http: AxiosInstance
1111

12-
constructor (http: AxiosInstance) {
12+
constructor(http: AxiosInstance) {
1313
this.http = http
1414
}
1515

16-
async getCollections (libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise<Page<CollectionDto>> {
16+
async getCollections(libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise<Page<CollectionDto>> {
1717
try {
18-
const params = { ...pageRequest } as any
18+
const params = {...pageRequest} as any
1919
if (libraryIds) params.library_id = libraryIds
2020
if (search) params.search = search
2121

2222
return (await this.http.get(API_COLLECTIONS, {
2323
params: params,
24-
paramsSerializer: params => qs.stringify(params, { indices: false }),
24+
paramsSerializer: params => qs.stringify(params, {indices: false}),
2525
})).data
2626
} catch (e) {
2727
let msg = 'An error occurred while trying to retrieve collections'
@@ -32,7 +32,7 @@ export default class KomgaCollectionsService {
3232
}
3333
}
3434

35-
async getOneCollection (collectionId: string): Promise<CollectionDto> {
35+
async getOneCollection(collectionId: string): Promise<CollectionDto> {
3636
try {
3737
return (await this.http.get(`${API_COLLECTIONS}/${collectionId}`)).data
3838
} catch (e) {
@@ -44,7 +44,7 @@ export default class KomgaCollectionsService {
4444
}
4545
}
4646

47-
async postCollection (collection: CollectionCreationDto): Promise<CollectionDto> {
47+
async postCollection(collection: CollectionCreationDto): Promise<CollectionDto> {
4848
try {
4949
return (await this.http.post(API_COLLECTIONS, collection)).data
5050
} catch (e) {
@@ -56,7 +56,7 @@ export default class KomgaCollectionsService {
5656
}
5757
}
5858

59-
async patchCollection (collectionId: string, collection: CollectionUpdateDto) {
59+
async patchCollection(collectionId: string, collection: CollectionUpdateDto) {
6060
try {
6161
await this.http.patch(`${API_COLLECTIONS}/${collectionId}`, collection)
6262
} catch (e) {
@@ -68,7 +68,7 @@ export default class KomgaCollectionsService {
6868
}
6969
}
7070

71-
async deleteCollection (collectionId: string) {
71+
async deleteCollection(collectionId: string) {
7272
try {
7373
await this.http.delete(`${API_COLLECTIONS}/${collectionId}`)
7474
} catch (e) {
@@ -80,12 +80,13 @@ export default class KomgaCollectionsService {
8080
}
8181
}
8282

83-
async getSeries (collectionId: string, pageRequest?: PageRequest,
84-
libraryId?: string[], status?: string[],
85-
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
86-
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[]): Promise<Page<SeriesDto>> {
83+
async getSeries(collectionId: string, pageRequest?: PageRequest,
84+
libraryId?: string[], status?: string[],
85+
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
86+
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[],
87+
complete?: boolean): Promise<Page<SeriesDto>> {
8788
try {
88-
const params = { ...pageRequest } as any
89+
const params = {...pageRequest} as any
8990
if (libraryId) params.library_id = libraryId
9091
if (status) params.status = status
9192
if (readStatus) params.read_status = readStatus
@@ -96,10 +97,11 @@ export default class KomgaCollectionsService {
9697
if (ageRating) params.age_rating = ageRating
9798
if (releaseDate) params.release_year = releaseDate
9899
if (authors) params.author = authors.map(a => `${a.name},${a.role}`)
100+
if (complete !== undefined) params.complete = complete
99101

100102
return (await this.http.get(`${API_COLLECTIONS}/${collectionId}/series`, {
101103
params: params,
102-
paramsSerializer: params => qs.stringify(params, { indices: false }),
104+
paramsSerializer: params => qs.stringify(params, {indices: false}),
103105
})).data
104106
} catch (e) {
105107
let msg = 'An error occurred while trying to retrieve series'

Diff for: komga-webui/src/services/komga-series.service.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default class KomgaSeriesService {
1616
async getSeries(libraryId?: string, pageRequest?: PageRequest, search?: string, status?: string[],
1717
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
1818
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[],
19-
searchRegex?: string): Promise<Page<SeriesDto>> {
19+
searchRegex?: string, complete?: boolean): Promise<Page<SeriesDto>> {
2020
try {
2121
const params = {...pageRequest} as any
2222
if (libraryId) params.library_id = libraryId
@@ -31,6 +31,7 @@ export default class KomgaSeriesService {
3131
if (ageRating) params.age_rating = ageRating
3232
if (releaseDate) params.release_year = releaseDate
3333
if (authors) params.author = authors.map(a => `${a.name},${a.role}`)
34+
if (complete !== undefined) params.complete = complete
3435

3536
return (await this.http.get(API_SERIES, {
3637
params: params,
@@ -47,7 +48,7 @@ export default class KomgaSeriesService {
4748

4849
async getAlphabeticalGroups(libraryId?: string, search?: string, status?: string[],
4950
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
50-
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[]): Promise<GroupCountDto[]> {
51+
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[], complete?: boolean): Promise<GroupCountDto[]> {
5152
try {
5253
const params = {} as any
5354
if (libraryId) params.library_id = libraryId
@@ -61,6 +62,7 @@ export default class KomgaSeriesService {
6162
if (ageRating) params.age_rating = ageRating
6263
if (releaseDate) params.release_year = releaseDate
6364
if (authors) params.author = authors.map(a => `${a.name},${a.role}`)
65+
if (complete !== undefined) params.complete = complete
6466

6567
return (await this.http.get(`${API_SERIES}/alphabetical-groups`, {
6668
params: params,

Diff for: komga-webui/src/types/filter.ts

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface FiltersOptions {
99
interface NameValue {
1010
name: string,
1111
value: string,
12+
// an optional negative value
13+
nValue?: string,
1214
}
1315

1416
interface FiltersActive {

Diff for: komga-webui/src/views/BrowseCollection.vue

+9-2
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ import {AuthorDto} from '@/types/komga-books'
142142
import {CollectionSseDto, ReadProgressSeriesSseDto, SeriesSseDto} from '@/types/komga-sse'
143143
import {throttle} from 'lodash'
144144
import {LibraryDto} from '@/types/komga-libraries'
145+
import {parseBooleanFilter} from '@/functions/query-params'
145146
146147
export default Vue.extend({
147148
name: 'BrowseCollection',
@@ -231,6 +232,9 @@ export default Vue.extend({
231232
{name: this.$t('filter.read').toString(), value: ReadStatus.READ},
232233
],
233234
},
235+
complete: {
236+
values: [{name: 'Complete', value: 'true', nValue: 'false'}],
237+
},
234238
} as FiltersOptions
235239
},
236240
filterOptionsPanel(): FiltersOptions {
@@ -303,7 +307,7 @@ export default Vue.extend({
303307
304308
// get filter from query params or local storage and validate with available filter values
305309
let activeFilters: any
306-
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)) {
310+
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) {
307311
activeFilters = {
308312
status: route.query.status || [],
309313
readStatus: route.query.readStatus || [],
@@ -314,6 +318,7 @@ export default Vue.extend({
314318
language: route.query.language || [],
315319
ageRating: route.query.ageRating || [],
316320
releaseDate: route.query.releaseDate || [],
321+
complete: route.query.complete || [],
317322
}
318323
authorRoles.forEach((role: string) => {
319324
activeFilters[role] = route.query[role] || []
@@ -334,6 +339,7 @@ export default Vue.extend({
334339
language: filters.language?.filter(x => this.filterOptions.language.map(n => n.value).includes(x)) || [],
335340
ageRating: filters.ageRating?.filter(x => this.filterOptions.ageRating.map(n => n.value).includes(x)) || [],
336341
releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [],
342+
complete: filters.complete?.filter(x => x === 'true' || x === 'false') || [],
337343
} as any
338344
authorRoles.forEach((role: string) => {
339345
validFilter[role] = filters[role] || []
@@ -379,7 +385,8 @@ export default Vue.extend({
379385
}))
380386
})
381387
382-
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
388+
const complete = parseBooleanFilter(this.filters.complete)
389+
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
383390
this.seriesCopy = [...this.series]
384391
this.selectedSeries = []
385392
},

Diff for: komga-webui/src/views/BrowseLibraries.vue

+10-4
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ import ItemBrowser from '@/components/ItemBrowser.vue'
128128
import LibraryNavigation from '@/components/LibraryNavigation.vue'
129129
import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue'
130130
import PageSizeSelect from '@/components/PageSizeSelect.vue'
131-
import {parseQuerySort} from '@/functions/query-params'
131+
import {parseBooleanFilter, parseQuerySort} from '@/functions/query-params'
132132
import {ReadStatus, replaceCompositeReadStatus} from '@/types/enum-books'
133133
import {SeriesStatus, SeriesStatusKeyValue} from '@/types/enum-series'
134134
import {
@@ -281,6 +281,9 @@ export default Vue.extend({
281281
{name: this.$t('filter.read').toString(), value: ReadStatus.READ},
282282
],
283283
},
284+
complete: {
285+
values: [{name: 'Complete', value: 'true', nValue: 'false'}],
286+
},
284287
} as FiltersOptions
285288
},
286289
filterOptionsPanel(): FiltersOptions {
@@ -377,7 +380,7 @@ export default Vue.extend({
377380
378381
// get filter from query params or local storage and validate with available filter values
379382
let activeFilters: any
380-
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)) {
383+
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) {
381384
activeFilters = {
382385
status: route.query.status || [],
383386
readStatus: route.query.readStatus || [],
@@ -387,6 +390,7 @@ export default Vue.extend({
387390
language: route.query.language || [],
388391
ageRating: route.query.ageRating || [],
389392
releaseDate: route.query.releaseDate || [],
393+
complete: route.query.complete || [],
390394
}
391395
authorRoles.forEach((role: string) => {
392396
activeFilters[role] = route.query[role] || []
@@ -406,6 +410,7 @@ export default Vue.extend({
406410
language: filters.language?.filter(x => this.filterOptions.language.map(n => n.value).includes(x)) || [],
407411
ageRating: filters.ageRating?.filter(x => this.filterOptions.ageRating.map(n => n.value).includes(x)) || [],
408412
releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [],
413+
complete: filters.complete?.filter(x => x === 'true' || x === 'false') || [],
409414
} as any
410415
authorRoles.forEach((role: string) => {
411416
validFilter[role] = filters[role] || []
@@ -510,13 +515,14 @@ export default Vue.extend({
510515
})
511516
512517
const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined
513-
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)
518+
const complete = parseBooleanFilter(this.filters.complete)
519+
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)
514520
515521
this.totalPages = seriesPage.totalPages
516522
this.totalElements = seriesPage.totalElements
517523
this.series = seriesPage.content
518524
519-
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)
525+
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)
520526
const nonAlpha = seriesGroups
521527
.filter((g) => !(/[a-zA-Z]/).test(g.group))
522528
.reduce((a, b) => a + b.count, 0)

0 commit comments

Comments
 (0)