feat(webui): add unavailable condition in series and books filters

Closes: #1580
This commit is contained in:
Gauthier Roebroeck 2025-02-20 17:50:10 +08:00
parent 87d73cc207
commit 1b8fa45ef2
5 changed files with 54 additions and 4 deletions

View file

@ -1,4 +1,5 @@
import {SearchConditionSeries} from '@/types/komga-search' import {SearchConditionSeries} from '@/types/komga-search'
import {FiltersActive, NameValue} from '@/types/filter'
export function sortOrFilterActive(sortActive: SortActive, sortDefault: SortActive, filters: FiltersActive): boolean { export function sortOrFilterActive(sortActive: SortActive, sortDefault: SortActive, filters: FiltersActive): boolean {
const sortCustom = sortActive.key !== sortDefault.key || sortActive.order !== sortDefault.order const sortCustom = sortActive.key !== sortDefault.key || sortActive.order !== sortDefault.order

View file

@ -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 { export interface AuthorMatch {
name?: string, name?: string,
role?: string role?: string

View file

@ -158,6 +158,7 @@ import {
SearchConditionAllOfBook, SearchConditionAllOfBook,
SearchConditionAnyOfBook, SearchConditionAnyOfBook,
SearchConditionAuthor, SearchConditionAuthor,
SearchConditionDeleted,
SearchConditionLibraryId, SearchConditionLibraryId,
SearchConditionMediaProfile, SearchConditionMediaProfile,
SearchConditionOneShot, SearchConditionOneShot,
@ -348,6 +349,15 @@ export default Vue.extend({
nValue: new SearchConditionOneShot(new SearchOperatorIsFalse()), 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 } as FiltersOptions
}, },
filterOptionsPanel(): 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 // get filter from query params or local storage and validate with available filter values
let activeFilters: any 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 = { activeFilters = {
readStatus: route.query.readStatus || [], readStatus: route.query.readStatus || [],
tag: route.query.tag || [], tag: route.query.tag || [],
oneshot: route.query.oneshot || [], oneshot: route.query.oneshot || [],
deleted: route.query.deleted || [],
} }
authorRoles.forEach((role: string) => { authorRoles.forEach((role: string) => {
activeFilters[role] = route.query[role] || [] activeFilters[role] = route.query[role] || []
@ -471,6 +482,7 @@ export default Vue.extend({
readStatus: this.$_.intersectionWith(filters.readStatus, extractFilterOptionsValues(this.filterOptionsList.readStatus.values), objIsEqual) || [], readStatus: this.$_.intersectionWith(filters.readStatus, extractFilterOptionsValues(this.filterOptionsList.readStatus.values), objIsEqual) || [],
tag: this.$_.intersectionWith(filters.tag, extractFilterOptionsValues(this.filterOptions.tag), objIsEqual) || [], tag: this.$_.intersectionWith(filters.tag, extractFilterOptionsValues(this.filterOptions.tag), objIsEqual) || [],
oneshot: this.$_.intersectionWith(filters.oneshot, extractFilterOptionsValues(this.filterOptionsList.oneshot.values), 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 } as any
authorRoles.forEach((role: string) => { authorRoles.forEach((role: string) => {
validFilter[role] = filters[role] || [] 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.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.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.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) => { authorRoles.forEach((role: string) => {
if (role in this.filters) { if (role in this.filters) {
const authorConditions = this.filters[role].map((name: string) => { const authorConditions = this.filters[role].map((name: string) => {

View file

@ -172,6 +172,7 @@ import {
SearchConditionAnyOfSeries, SearchConditionAnyOfSeries,
SearchConditionAuthor, SearchConditionAuthor,
SearchConditionComplete, SearchConditionComplete,
SearchConditionDeleted,
SearchConditionGenre, SearchConditionGenre,
SearchConditionLanguage, SearchConditionLanguage,
SearchConditionLibraryId, SearchConditionLibraryId,
@ -414,6 +415,15 @@ export default Vue.extend({
nValue: new SearchConditionOneShot(new SearchOperatorIsFalse()), 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 } as FiltersOptions
}, },
filterOptionsPanel(): FiltersOptions { filterOptionsPanel(): FiltersOptions {
@ -435,7 +445,7 @@ export default Vue.extend({
}, },
...this.filterOptions.genre, ...this.filterOptions.genre,
], ],
anyAllSelector: true anyAllSelector: true,
}, },
tag: { tag: {
name: this.$t('filter.tag').toString(), 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 // get filter from query params or local storage and validate with available filter values
let activeFilters: any 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 = { activeFilters = {
status: route.query.status || [], status: route.query.status || [],
readStatus: route.query.readStatus || [], readStatus: route.query.readStatus || [],
@ -623,6 +633,7 @@ export default Vue.extend({
complete: route.query.complete || [], complete: route.query.complete || [],
oneshot: route.query.oneshot || [], oneshot: route.query.oneshot || [],
sharingLabel: route.query.sharingLabel || [], sharingLabel: route.query.sharingLabel || [],
deleted: route.query.deleted || [],
} }
authorRoles.forEach((role: string) => { authorRoles.forEach((role: string) => {
activeFilters[role] = route.query[role] || [] activeFilters[role] = route.query[role] || []
@ -661,6 +672,7 @@ export default Vue.extend({
complete: this.$_.intersectionWith(filters.complete, extractFilterOptionsValues(this.filterOptionsList.complete.values), objIsEqual) || [], complete: this.$_.intersectionWith(filters.complete, extractFilterOptionsValues(this.filterOptionsList.complete.values), objIsEqual) || [],
oneshot: this.$_.intersectionWith(filters.oneshot, extractFilterOptionsValues(this.filterOptionsList.oneshot.values), objIsEqual) || [], oneshot: this.$_.intersectionWith(filters.oneshot, extractFilterOptionsValues(this.filterOptionsList.oneshot.values), objIsEqual) || [],
sharingLabel: this.$_.intersectionWith(filters.sharingLabel, extractFilterOptionsValues(this.filterOptions.sharingLabel), objIsEqual) || [], sharingLabel: this.$_.intersectionWith(filters.sharingLabel, extractFilterOptionsValues(this.filterOptions.sharingLabel), objIsEqual) || [],
deleted: this.$_.intersectionWith(filters.deleted, extractFilterOptionsValues(this.filterOptionsList.deleted.values), objIsEqual) || [],
} as any } as any
authorRoles.forEach((role: string) => { authorRoles.forEach((role: string) => {
validFilter[role] = filters[role] || [] 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.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.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.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) => { authorRoles.forEach((role: string) => {
if (role in this.filters) { if (role in this.filters) {
const authorConditions = this.filters[role].map((name: string) => { const authorConditions = this.filters[role].map((name: string) => {

View file

@ -555,6 +555,7 @@ import {
SearchConditionAnyOfBook, SearchConditionAnyOfBook,
SearchConditionAuthor, SearchConditionAuthor,
SearchConditionBook, SearchConditionBook,
SearchConditionDeleted,
SearchConditionGenre, SearchConditionGenre,
SearchConditionLanguage, SearchConditionLanguage,
SearchConditionMediaProfile, SearchConditionMediaProfile,
@ -564,9 +565,11 @@ import {
SearchConditionSeriesStatus, SearchConditionSeriesStatus,
SearchConditionTag, SearchConditionTag,
SearchOperatorIs, SearchOperatorIs,
SearchOperatorIsFalse,
SearchOperatorIsNot, SearchOperatorIsNot,
SearchOperatorIsNotNull, SearchOperatorIsNotNull,
SearchOperatorIsNull, SearchOperatorIsNull,
SearchOperatorIsTrue,
} from '@/types/komga-search' } from '@/types/komga-search'
import {objIsEqual} from '@/functions/object' import {objIsEqual} from '@/functions/object'
import i18n from '@/i18n' 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 } as FiltersOptions
}, },
filterOptionsPanel(): FiltersOptions { filterOptionsPanel(): FiltersOptions {
@ -874,11 +886,12 @@ export default Vue.extend({
// get filter from query params and validate with available filter values // get filter from query params and validate with available filter values
let activeFilters = {} as FiltersActive 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 = { activeFilters = {
readStatus: route.query.readStatus || [], readStatus: route.query.readStatus || [],
tag: route.query.tag || [], tag: route.query.tag || [],
mediaProfile: route.query.mediaProfile || [], mediaProfile: route.query.mediaProfile || [],
deleted: route.query.deleted || [],
} }
authorRoles.forEach((role: string) => { authorRoles.forEach((role: string) => {
activeFilters[role] = route.query[role] || [] activeFilters[role] = route.query[role] || []
@ -898,6 +911,7 @@ export default Vue.extend({
readStatus: this.$_.intersectionWith(filters.readStatus, extractFilterOptionsValues(this.filterOptionsList.readStatus.values), objIsEqual) || [], readStatus: this.$_.intersectionWith(filters.readStatus, extractFilterOptionsValues(this.filterOptionsList.readStatus.values), objIsEqual) || [],
tag: this.$_.intersectionWith(filters.tag, extractFilterOptionsValues(this.filterOptions.tag), objIsEqual) || [], tag: this.$_.intersectionWith(filters.tag, extractFilterOptionsValues(this.filterOptions.tag), objIsEqual) || [],
mediaProfile: this.$_.intersectionWith(filters.mediaProfile, extractFilterOptionsValues(this.filterOptionsPanel.mediaProfile.values), 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 } as any
authorRoles.forEach((role: string) => { authorRoles.forEach((role: string) => {
validFilter[role] = filters[role] || [] 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.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.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.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) => { authorRoles.forEach((role: string) => {
if (role in this.filters) { if (role in this.filters) {
const authorConditions = this.filters[role].map((name: string) => { const authorConditions = this.filters[role].map((name: string) => {