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)