From 1b8fa45ef27d37f074eb5e99287300c2490a4d68 Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Thu, 20 Feb 2025 17:50:10 +0800 Subject: [PATCH] feat(webui): add unavailable condition in series and books filters Closes: #1580 --- komga-webui/src/functions/filter.ts | 1 + komga-webui/src/types/komga-search.ts | 8 ++++++++ komga-webui/src/views/BrowseBooks.vue | 15 ++++++++++++++- komga-webui/src/views/BrowseLibraries.vue | 17 +++++++++++++++-- komga-webui/src/views/BrowseSeries.vue | 17 ++++++++++++++++- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/komga-webui/src/functions/filter.ts b/komga-webui/src/functions/filter.ts index a6d1733af..2b3193aff 100644 --- a/komga-webui/src/functions/filter.ts +++ b/komga-webui/src/functions/filter.ts @@ -1,4 +1,5 @@ import {SearchConditionSeries} from '@/types/komga-search' +import {FiltersActive, NameValue} from '@/types/filter' export function sortOrFilterActive(sortActive: SortActive, sortDefault: SortActive, filters: FiltersActive): boolean { const sortCustom = sortActive.key !== sortDefault.key || sortActive.order !== sortDefault.order diff --git a/komga-webui/src/types/komga-search.ts b/komga-webui/src/types/komga-search.ts index 9c1a89b67..804d315fa 100644 --- a/komga-webui/src/types/komga-search.ts +++ b/komga-webui/src/types/komga-search.ts @@ -190,6 +190,14 @@ export class SearchConditionTitleSort implements SearchConditionSeries { } } +export class SearchConditionDeleted implements SearchConditionBook, SearchConditionSeries { + deleted: SearchOperatorBoolean + + constructor(op: SearchOperatorBoolean) { + this.deleted = op + } +} + export interface AuthorMatch { name?: string, role?: string diff --git a/komga-webui/src/views/BrowseBooks.vue b/komga-webui/src/views/BrowseBooks.vue index c540ba0ca..d2aff8b25 100644 --- a/komga-webui/src/views/BrowseBooks.vue +++ b/komga-webui/src/views/BrowseBooks.vue @@ -158,6 +158,7 @@ import { SearchConditionAllOfBook, SearchConditionAnyOfBook, SearchConditionAuthor, + SearchConditionDeleted, SearchConditionLibraryId, SearchConditionMediaProfile, SearchConditionOneShot, @@ -348,6 +349,15 @@ export default Vue.extend({ nValue: new SearchConditionOneShot(new SearchOperatorIsFalse()), }], }, + deleted: { + values: [ + { + name: this.$t('common.unavailable').toString(), + value: new SearchConditionDeleted(new SearchOperatorIsTrue()), + nValue: new SearchConditionDeleted(new SearchOperatorIsFalse()), + }, + ], + }, } as FiltersOptions }, filterOptionsPanel(): FiltersOptions { @@ -436,11 +446,12 @@ export default Vue.extend({ // get filter from query params or local storage and validate with available filter values let activeFilters: any - if (route.query.readStatus || route.query.tag || authorRoles.some(role => role in route.query) || route.query.oneshot) { + if (route.query.readStatus || route.query.tag || authorRoles.some(role => role in route.query) || route.query.oneshot || route.query.deleted) { activeFilters = { readStatus: route.query.readStatus || [], tag: route.query.tag || [], oneshot: route.query.oneshot || [], + deleted: route.query.deleted || [], } authorRoles.forEach((role: string) => { activeFilters[role] = route.query[role] || [] @@ -471,6 +482,7 @@ export default Vue.extend({ readStatus: this.$_.intersectionWith(filters.readStatus, extractFilterOptionsValues(this.filterOptionsList.readStatus.values), objIsEqual) || [], tag: this.$_.intersectionWith(filters.tag, extractFilterOptionsValues(this.filterOptions.tag), objIsEqual) || [], oneshot: this.$_.intersectionWith(filters.oneshot, extractFilterOptionsValues(this.filterOptionsList.oneshot.values), objIsEqual) || [], + deleted: this.$_.intersectionWith(filters.deleted, extractFilterOptionsValues(this.filterOptionsList.deleted.values), objIsEqual) || [], } as any authorRoles.forEach((role: string) => { validFilter[role] = filters[role] || [] @@ -581,6 +593,7 @@ export default Vue.extend({ if (this.filters.tag && this.filters.tag.length > 0) this.filtersMode?.tag?.allOf ? conditions.push(new SearchConditionAllOfBook(this.filters.tag)) : conditions.push(new SearchConditionAnyOfBook(this.filters.tag)) if (this.filters.oneshot && this.filters.oneshot.length > 0) conditions.push(...this.filters.oneshot) if (this.filters.mediaProfile && this.filters.mediaProfile.length > 0) this.filtersMode?.mediaProfile?.allOf ? conditions.push(new SearchConditionAllOfBook(this.filters.mediaProfile)) : conditions.push(new SearchConditionAnyOfBook(this.filters.mediaProfile)) + if (this.filters.deleted && this.filters.deleted.length > 0) conditions.push(...this.filters.deleted) authorRoles.forEach((role: string) => { if (role in this.filters) { const authorConditions = this.filters[role].map((name: string) => { diff --git a/komga-webui/src/views/BrowseLibraries.vue b/komga-webui/src/views/BrowseLibraries.vue index 5eae2fcae..ca3c2ec25 100644 --- a/komga-webui/src/views/BrowseLibraries.vue +++ b/komga-webui/src/views/BrowseLibraries.vue @@ -172,6 +172,7 @@ import { SearchConditionAnyOfSeries, SearchConditionAuthor, SearchConditionComplete, + SearchConditionDeleted, SearchConditionGenre, SearchConditionLanguage, SearchConditionLibraryId, @@ -414,6 +415,15 @@ export default Vue.extend({ nValue: new SearchConditionOneShot(new SearchOperatorIsFalse()), }], }, + deleted: { + values: [ + { + name: this.$t('common.unavailable').toString(), + value: new SearchConditionDeleted(new SearchOperatorIsTrue()), + nValue: new SearchConditionDeleted(new SearchOperatorIsFalse()), + }, + ], + }, } as FiltersOptions }, filterOptionsPanel(): FiltersOptions { @@ -435,7 +445,7 @@ export default Vue.extend({ }, ...this.filterOptions.genre, ], - anyAllSelector: true + anyAllSelector: true, }, tag: { name: this.$t('filter.tag').toString(), @@ -610,7 +620,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) || route.query.complete || route.query.oneshot || route.query.sharingLabel) { + 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 || route.query.oneshot || route.query.sharingLabel || route.query.deleted) { activeFilters = { status: route.query.status || [], readStatus: route.query.readStatus || [], @@ -623,6 +633,7 @@ export default Vue.extend({ complete: route.query.complete || [], oneshot: route.query.oneshot || [], sharingLabel: route.query.sharingLabel || [], + deleted: route.query.deleted || [], } authorRoles.forEach((role: string) => { activeFilters[role] = route.query[role] || [] @@ -661,6 +672,7 @@ export default Vue.extend({ complete: this.$_.intersectionWith(filters.complete, extractFilterOptionsValues(this.filterOptionsList.complete.values), objIsEqual) || [], oneshot: this.$_.intersectionWith(filters.oneshot, extractFilterOptionsValues(this.filterOptionsList.oneshot.values), objIsEqual) || [], sharingLabel: this.$_.intersectionWith(filters.sharingLabel, extractFilterOptionsValues(this.filterOptions.sharingLabel), objIsEqual) || [], + deleted: this.$_.intersectionWith(filters.deleted, extractFilterOptionsValues(this.filterOptionsList.deleted.values), objIsEqual) || [], } as any authorRoles.forEach((role: string) => { validFilter[role] = filters[role] || [] @@ -781,6 +793,7 @@ export default Vue.extend({ if (this.filters.sharingLabel && this.filters.sharingLabel.length > 0) this.filtersMode?.sharingLabel?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.sharingLabel)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.sharingLabel)) if (this.filters.complete && this.filters.complete.length > 0) conditions.push(...this.filters.complete) if (this.filters.oneshot && this.filters.oneshot.length > 0) conditions.push(...this.filters.oneshot) + if (this.filters.deleted && this.filters.deleted.length > 0) conditions.push(...this.filters.deleted) authorRoles.forEach((role: string) => { if (role in this.filters) { const authorConditions = this.filters[role].map((name: string) => { diff --git a/komga-webui/src/views/BrowseSeries.vue b/komga-webui/src/views/BrowseSeries.vue index d348497ec..198c51228 100644 --- a/komga-webui/src/views/BrowseSeries.vue +++ b/komga-webui/src/views/BrowseSeries.vue @@ -555,6 +555,7 @@ import { SearchConditionAnyOfBook, SearchConditionAuthor, SearchConditionBook, + SearchConditionDeleted, SearchConditionGenre, SearchConditionLanguage, SearchConditionMediaProfile, @@ -564,9 +565,11 @@ import { SearchConditionSeriesStatus, SearchConditionTag, SearchOperatorIs, + SearchOperatorIsFalse, SearchOperatorIsNot, SearchOperatorIsNotNull, SearchOperatorIsNull, + SearchOperatorIsTrue, } from '@/types/komga-search' import {objIsEqual} from '@/functions/object' import i18n from '@/i18n' @@ -679,6 +682,15 @@ export default Vue.extend({ }, ], }, + deleted: { + values: [ + { + name: this.$t('common.unavailable').toString(), + value: new SearchConditionDeleted(new SearchOperatorIsTrue()), + nValue: new SearchConditionDeleted(new SearchOperatorIsFalse()), + }, + ], + }, } as FiltersOptions }, filterOptionsPanel(): FiltersOptions { @@ -874,11 +886,12 @@ export default Vue.extend({ // get filter from query params and validate with available filter values let activeFilters = {} as FiltersActive - if (route.query.readStatus || route.query.tag || route.query.mediaProfile || authorRoles.some(role => role in route.query)) { + if (route.query.readStatus || route.query.tag || route.query.mediaProfile || authorRoles.some(role => role in route.query) || route.query.deleted) { activeFilters = { readStatus: route.query.readStatus || [], tag: route.query.tag || [], mediaProfile: route.query.mediaProfile || [], + deleted: route.query.deleted || [], } authorRoles.forEach((role: string) => { activeFilters[role] = route.query[role] || [] @@ -898,6 +911,7 @@ export default Vue.extend({ readStatus: this.$_.intersectionWith(filters.readStatus, extractFilterOptionsValues(this.filterOptionsList.readStatus.values), objIsEqual) || [], tag: this.$_.intersectionWith(filters.tag, extractFilterOptionsValues(this.filterOptions.tag), objIsEqual) || [], mediaProfile: this.$_.intersectionWith(filters.mediaProfile, extractFilterOptionsValues(this.filterOptionsPanel.mediaProfile.values), objIsEqual) || [], + deleted: this.$_.intersectionWith(filters.deleted, extractFilterOptionsValues(this.filterOptionsList.deleted.values), objIsEqual) || [], } as any authorRoles.forEach((role: string) => { validFilter[role] = filters[role] || [] @@ -1039,6 +1053,7 @@ export default Vue.extend({ if (this.filters.readStatus && this.filters.readStatus.length > 0) conditions.push(new SearchConditionAnyOfBook(this.filters.readStatus)) if (this.filters.tag && this.filters.tag.length > 0) this.filtersMode?.tag?.allOf ? conditions.push(new SearchConditionAllOfBook(this.filters.tag)) : conditions.push(new SearchConditionAnyOfBook(this.filters.tag)) if (this.filters.mediaProfile && this.filters.mediaProfile.length > 0) this.filtersMode?.mediaProfile?.allOf ? conditions.push(new SearchConditionAllOfBook(this.filters.mediaProfile)) : conditions.push(new SearchConditionAnyOfBook(this.filters.mediaProfile)) + if (this.filters.deleted && this.filters.deleted.length > 0) conditions.push(...this.filters.deleted) authorRoles.forEach((role: string) => { if (role in this.filters) { const authorConditions = this.filters[role].map((name: string) => {