From 87d73cc2079ef5fd5de2299f4aae8b14024794ba Mon Sep 17 00:00:00 2001 From: Gauthier Roebroeck Date: Thu, 20 Feb 2025 17:11:23 +0800 Subject: [PATCH] feat(webui): add any/none filtering on more criteria Closes: #1884 --- komga-webui/src/views/BrowseBooks.vue | 15 ++++- komga-webui/src/views/BrowseLibraries.vue | 74 +++++++++++++++++++++-- komga-webui/src/views/BrowseSeries.vue | 15 ++++- 3 files changed, 96 insertions(+), 8 deletions(-) diff --git a/komga-webui/src/views/BrowseBooks.vue b/komga-webui/src/views/BrowseBooks.vue index 1f59fbc2d..c540ba0ca 100644 --- a/komga-webui/src/views/BrowseBooks.vue +++ b/komga-webui/src/views/BrowseBooks.vue @@ -167,6 +167,8 @@ import { SearchOperatorIs, SearchOperatorIsFalse, SearchOperatorIsNot, + SearchOperatorIsNotNull, + SearchOperatorIsNull, SearchOperatorIsTrue, } from '@/types/komga-search' import i18n from '@/i18n' @@ -350,7 +352,18 @@ export default Vue.extend({ }, filterOptionsPanel(): FiltersOptions { const r = { - tag: {name: this.$t('filter.tag').toString(), values: this.filterOptions.tag, anyAllSelector: true}, + tag: { + name: this.$t('filter.tag').toString(), + values: [ + { + name: this.$t('filter.any').toString(), + value: new SearchConditionTag(new SearchOperatorIsNotNull()), + nValue: new SearchConditionTag(new SearchOperatorIsNull()), + }, + ...this.filterOptions.tag, + ], + anyAllSelector: true, + }, mediaProfile: { name: this.$t('filter.media_profile').toString(), values: Object.values(MediaProfile).map(x => ({ name: i18n.t(`enums.media_profile.${x}`), diff --git a/komga-webui/src/views/BrowseLibraries.vue b/komga-webui/src/views/BrowseLibraries.vue index 03160693c..5eae2fcae 100644 --- a/komga-webui/src/views/BrowseLibraries.vue +++ b/komga-webui/src/views/BrowseLibraries.vue @@ -425,10 +425,52 @@ export default Vue.extend({ nValue: new SearchConditionSeriesStatus(new SearchOperatorIsNot(x)), } as NameValue)), }, - genre: {name: this.$t('filter.genre').toString(), values: this.filterOptions.genre, anyAllSelector: true}, - tag: {name: this.$t('filter.tag').toString(), values: this.filterOptions.tag, anyAllSelector: true}, - publisher: {name: this.$t('filter.publisher').toString(), values: this.filterOptions.publisher}, - language: {name: this.$t('filter.language').toString(), values: this.filterOptions.language}, + genre: { + name: this.$t('filter.genre').toString(), + values: [ + { + name: this.$t('filter.any').toString(), + value: new SearchConditionGenre(new SearchOperatorIsNotNull()), + nValue: new SearchConditionGenre(new SearchOperatorIsNull()), + }, + ...this.filterOptions.genre, + ], + anyAllSelector: true + }, + tag: { + name: this.$t('filter.tag').toString(), + values: [ + { + name: this.$t('filter.any').toString(), + value: new SearchConditionTag(new SearchOperatorIsNotNull()), + nValue: new SearchConditionTag(new SearchOperatorIsNull()), + }, + ...this.filterOptions.tag, + ], + anyAllSelector: true, + }, + publisher: { + name: this.$t('filter.publisher').toString(), + values: [ + { + name: this.$t('filter.any').toString(), + value: new SearchConditionPublisher(new SearchOperatorIsNot('')), + nValue: new SearchConditionPublisher(new SearchOperatorIs('')), + }, + ...this.filterOptions.publisher, + ], + }, + language: { + name: this.$t('filter.language').toString(), + values: [ + { + name: this.$t('filter.any').toString(), + value: new SearchConditionLanguage(new SearchOperatorIsNot('')), + nValue: new SearchConditionLanguage(new SearchOperatorIs('')), + }, + ...this.filterOptions.language, + ], + }, ageRating: { name: this.$t('filter.age_rating').toString(), values: this.filterOptions.ageRating.map((x: NameValue) => ({ @@ -438,7 +480,17 @@ export default Vue.extend({ } as NameValue), ), }, - releaseDate: {name: this.$t('filter.release_date').toString(), values: this.filterOptions.releaseDate}, + releaseDate: { + name: this.$t('filter.release_date').toString(), + values: [ + { + name: this.$t('filter.any').toString(), + value: new SearchConditionReleaseDate(new SearchOperatorIsNotNull()), + nValue: new SearchConditionReleaseDate(new SearchOperatorIsNull()), + }, + ...this.filterOptions.releaseDate, + ], + }, } as FiltersOptions authorRoles.forEach((role: string) => { r[role] = { @@ -456,7 +508,17 @@ export default Vue.extend({ anyAllSelector: true, } }) - r['sharingLabel'] = {name: this.$t('filter.sharing_label').toString(), values: this.filterOptions.sharingLabel} + r['sharingLabel'] = { + name: this.$t('filter.sharing_label').toString(), + values: [ + { + name: this.$t('filter.any').toString(), + value: new SearchConditionSharingLabel(new SearchOperatorIsNotNull()), + nValue: new SearchConditionSharingLabel(new SearchOperatorIsNull()), + }, + ...this.filterOptions.sharingLabel, + ], + } return r }, isAdmin(): boolean { diff --git a/komga-webui/src/views/BrowseSeries.vue b/komga-webui/src/views/BrowseSeries.vue index a1df0bd3f..d348497ec 100644 --- a/komga-webui/src/views/BrowseSeries.vue +++ b/komga-webui/src/views/BrowseSeries.vue @@ -565,6 +565,8 @@ import { SearchConditionTag, SearchOperatorIs, SearchOperatorIsNot, + SearchOperatorIsNotNull, + SearchOperatorIsNull, } from '@/types/komga-search' import {objIsEqual} from '@/functions/object' import i18n from '@/i18n' @@ -681,7 +683,18 @@ export default Vue.extend({ }, filterOptionsPanel(): FiltersOptions { const r = { - tag: {name: this.$t('filter.tag').toString(), values: this.filterOptions.tag, anyAllSelector: true}, + tag: { + name: this.$t('filter.tag').toString(), + values: [ + { + name: this.$t('filter.any').toString(), + value: new SearchConditionTag(new SearchOperatorIsNotNull()), + nValue: new SearchConditionTag(new SearchOperatorIsNull()), + }, + ...this.filterOptions.tag, + ], + anyAllSelector: true, + }, mediaProfile: { name: this.$t('filter.media_profile').toString(), values: Object.values(MediaProfile).map(x => ({ name: i18n.t(`enums.media_profile.${x}`),