Skip to content

Commit

Permalink
feat(webui): filter series and books by any/none author role
Browse files Browse the repository at this point in the history
Refs: #1829
  • Loading branch information
gotson committed Jan 20, 2025
1 parent d07eb39 commit ffc397f
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 55 deletions.
1 change: 1 addition & 0 deletions komga-webui/src/components/FilterList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

<script lang="ts">
import Vue from 'vue'
import {FiltersActive, FiltersOptions} from '@/types/filter'
export default Vue.extend({
name: 'FilterList',
Expand Down
64 changes: 36 additions & 28 deletions komga-webui/src/components/FilterPanels.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
</search-box-base>

<div style="position: absolute; right: 0; z-index: 1">
<v-btn-toggle v-if="f.anyAllSelector || groupAllOfActive(key)" mandatory class="semi-transparent" :value="filtersActiveMode[key]?.allOf">
<v-btn-toggle v-if="f.anyAllSelector || groupAllOfActive(key)" mandatory class="semi-transparent"
:value="filtersActiveMode[key]?.allOf">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn small icon :value="false" v-on="on" @click.stop="clickFilterMode(key, false)">
Expand All @@ -56,10 +57,11 @@
</div>

<v-list
v-if="f.search"
v-if="f.search || f.values"
dense
>
<v-list-item v-for="(v, i) in filtersActive[key]"
<!-- Dynamic content from search -->
<v-list-item v-for="(v, i) in searchFiltersActive(key)"
:key="i"
@click.stop="click(key, v)"
>
Expand All @@ -68,29 +70,26 @@
</v-list-item-icon>
<v-list-item-title>{{ v }}</v-list-item-title>
</v-list-item>
</v-list>

<div v-if="f.values">
<v-list dense>
<v-list-item v-for="v in f.values"
:key="JSON.stringify(v.value)"
@click.stop="click(key, v.value, v.nValue)"
>
<v-list-item-icon>
<v-icon v-if="key in filtersActive && includes(filtersActive[key], v.nValue)" color="secondary">
mdi-minus-box
</v-icon>
<v-icon v-else-if="key in filtersActive && includes(filtersActive[key], v.value)" color="secondary">
mdi-checkbox-marked
</v-icon>
<v-icon v-else>
mdi-checkbox-blank-outline
</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ v.name }}</v-list-item-title>
</v-list-item>
</v-list>
</div>
<!-- Static content from filters options -->
<v-list-item v-for="v in f.values"
:key="JSON.stringify(v.value)"
@click.stop="click(key, v.value, v.nValue)"
>
<v-list-item-icon>
<v-icon v-if="key in filtersActive && includes(filtersActive[key], v.nValue)" color="secondary">
mdi-minus-box
</v-icon>
<v-icon v-else-if="key in filtersActive && includes(filtersActive[key], v.value)" color="secondary">
mdi-checkbox-marked
</v-icon>
<v-icon v-else>
mdi-checkbox-blank-outline
</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ v.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
Expand All @@ -99,6 +98,7 @@
<script lang="ts">
import Vue, {PropType} from 'vue'
import SearchBoxBase from '@/components/SearchBoxBase.vue'
import {FiltersActive, FiltersActiveMode, FiltersOptions} from '@/types/filter'
export default Vue.extend({
name: 'FilterPanels',
Expand All @@ -118,13 +118,19 @@ export default Vue.extend({
},
},
methods: {
// filtersActive, filtered to not show options that are in filtersOptions
searchFiltersActive(key: string): FiltersActive[] {
if (!(key in this.filtersActive)) return []
const listedOptions = this.filtersOptions[key]?.values?.flatMap(x => [x.value, x.nValue])
return this.filtersActive[key].filter((x: string) => !this.$_.includes(listedOptions, x))
},
includes(array: any[], value: any): boolean {
return this.$_.isObject(value) ? this.$_.some(array, value) : this.$_.includes(array, value)
},
clear(key: string) {
let r = this.$_.cloneDeep(this.filtersActive)
r[key] = []
if(!this.filtersOptions[key].anyAllSelector) this.clickFilterMode(key, false)
if (!this.filtersOptions[key].anyAllSelector) this.clickFilterMode(key, false)
this.$emit('update:filtersActive', r)
},
Expand Down Expand Up @@ -154,12 +160,12 @@ export default Vue.extend({
} else
r[key].push(value)
if(!this.filtersOptions[key].anyAllSelector && r[key].length == 0) this.clickFilterMode(key, false)
if (!this.filtersOptions[key].anyAllSelector && r[key].length == 0) this.clickFilterMode(key, false)
this.$emit('update:filtersActive', r)
},
clickFilterMode(key: string, value: boolean) {
if(!this.filtersActiveMode) return
if (!this.filtersActiveMode) return
let r = this.$_.cloneDeep(this.filtersActiveMode)
r[key] = {allOf: value}
Expand All @@ -173,9 +179,11 @@ export default Vue.extend({
.no-padding .v-expansion-panel-content__wrap {
padding: 0;
}
.semi-transparent {
opacity: 0.5;
}
.semi-transparent:hover {
opacity: 1;
}
Expand Down
1 change: 1 addition & 0 deletions komga-webui/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@
"filter": {
"age_rating": "age rating",
"age_rating_none": "None",
"any": "Any",
"complete": "Complete",
"genre": "genre",
"in_progress": "In Progress",
Expand Down
13 changes: 8 additions & 5 deletions komga-webui/src/types/filter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface FiltersOptions {
export interface FiltersOptions {
[key: string]: {
name?: string,
values?: NameValue[],
Expand All @@ -7,21 +7,24 @@ interface FiltersOptions {
},
}

interface NameValue {
export interface NameValue {
name: string,
value: any,
// an optional negative value
nValue?: any,
}

interface FiltersActive {
export interface FiltersActive {
[key: string]: any[],
}

interface FiltersActiveMode {
export interface FiltersActiveMode {
[key: string]: FilterMode,
}

interface FilterMode {
export interface FilterMode {
allOf: boolean,
}

export const FILTER_ANY = 'KOMGA____ANY____'
export const FILTER_NONE = 'KOMGA____NONE____'
3 changes: 2 additions & 1 deletion komga-webui/src/views/BrowseCollection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ import {LibraryDto} from '@/types/komga-libraries'
import {parseBooleanFilter} from '@/functions/query-params'
import {ContextOrigin} from '@/types/context'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
import {SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
import {BookSearch, SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
import {FiltersActive, FiltersOptions, NameValue} from '@/types/filter'
export default Vue.extend({
name: 'BrowseCollection',
Expand Down
43 changes: 35 additions & 8 deletions komga-webui/src/views/BrowseLibraries.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ import AlphabeticalNavigation from '@/components/AlphabeticalNavigation.vue'
import {LibraryDto} from '@/types/komga-libraries'
import {ItemContext} from '@/types/items'
import {
BookSearch,
SearchConditionAgeRating,
SearchConditionAllOfSeries,
SearchConditionAnyOfSeries,
Expand All @@ -177,7 +178,8 @@ import {
SearchConditionPublisher,
SearchConditionReadStatus,
SearchConditionReleaseDate,
SearchConditionSeries, SearchConditionSeriesId,
SearchConditionSeries,
SearchConditionSeriesId,
SearchConditionSeriesStatus,
SearchConditionSharingLabel,
SearchConditionTag,
Expand All @@ -196,6 +198,15 @@ import {
} from '@/types/komga-search'
import i18n from '@/i18n'
import {objIsEqual} from '@/functions/object'
import {
FILTER_ANY,
FILTER_NONE,
FilterMode,
FiltersActive,
FiltersActiveMode,
FiltersOptions,
NameValue,
} from '@/types/filter'
export default Vue.extend({
name: 'BrowseLibraries',
Expand Down Expand Up @@ -401,6 +412,11 @@ export default Vue.extend({
.content
.map(x => x.name)
},
values: [{
name: this.$t('filter.any').toString(),
value: FILTER_ANY,
nValue: FILTER_NONE,
}],
anyAllSelector: true,
}
})
Expand Down Expand Up @@ -520,7 +536,7 @@ export default Vue.extend({
// get filter mode from query params or local storage
let activeFiltersMode: any
if(route.query.filterMode) {
if (route.query.filterMode) {
activeFiltersMode = route.query.filterMode
} else {
activeFiltersMode = this.$store.getters.getLibraryFilterMode(route.params.libraryId) || {} as FiltersActiveMode
Expand Down Expand Up @@ -655,19 +671,30 @@ export default Vue.extend({
if (this.filters.readStatus && this.filters.readStatus.length > 0) conditions.push(new SearchConditionAnyOfSeries(this.filters.readStatus))
if (this.filters.genre && this.filters.genre.length > 0) this.filtersMode?.genre?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.genre)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.genre))
if (this.filters.tag && this.filters.tag.length > 0) this.filtersMode?.tag?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.tag)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.tag))
if (this.filters.language && this.filters.language.length > 0) this.filtersMode?.language?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.language)):conditions.push(new SearchConditionAnyOfSeries(this.filters.language))
if (this.filters.language && this.filters.language.length > 0) this.filtersMode?.language?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.language)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.language))
if (this.filters.publisher && this.filters.publisher.length > 0) this.filtersMode?.publisher?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.publisher)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.publisher))
if (this.filters.ageRating && this.filters.ageRating.length > 0) this.filtersMode?.ageRating?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.ageRating)):conditions.push(new SearchConditionAnyOfSeries(this.filters.ageRating))
if (this.filters.ageRating && this.filters.ageRating.length > 0) this.filtersMode?.ageRating?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.ageRating)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.ageRating))
if (this.filters.releaseDate && this.filters.releaseDate.length > 0) this.filtersMode?.releaseDate?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.releaseDate)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.releaseDate))
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)
authorRoles.forEach((role: string) => {
if (role in this.filters) {
const authorConditions = this.filters[role].map((name: string) => new SearchConditionAuthor(new SearchOperatorIs({
name: name,
role: role,
})))
const authorConditions = this.filters[role].map((name: string) => {
if (name === FILTER_ANY)
return new SearchConditionAuthor(new SearchOperatorIs({
role: role,
}))
else if (name === FILTER_NONE)
return new SearchConditionAuthor(new SearchOperatorIsNot({
role: role,
}))
else
return new SearchConditionAuthor(new SearchOperatorIs({
name: name,
role: role,
}))
})
conditions.push(this.filtersMode[role]?.allOf ? new SearchConditionAllOfSeries(authorConditions) : new SearchConditionAnyOfSeries(authorConditions))
}
})
Expand Down
1 change: 1 addition & 0 deletions komga-webui/src/views/BrowseReadList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ import PageSizeSelect from '@/components/PageSizeSelect.vue'
import EmptyState from '@/components/EmptyState.vue'
import {ReadListDto, ReadListUpdateDto} from '@/types/komga-readlists'
import {Oneshot} from '@/types/komga-series'
import {FiltersActive, FiltersOptions, NameValue} from '@/types/filter'
export default Vue.extend({
name: 'BrowseReadList',
Expand Down
43 changes: 30 additions & 13 deletions komga-webui/src/views/BrowseSeries.vue
Original file line number Diff line number Diff line change
Expand Up @@ -504,7 +504,7 @@ import {
} from '@/types/events'
import Vue from 'vue'
import {Location} from 'vue-router'
import {AuthorDto, BookDto} from '@/types/komga-books'
import {BookDto} from '@/types/komga-books'
import {SeriesStatus} from '@/types/enum-series'
import FilterDrawer from '@/components/FilterDrawer.vue'
import FilterList from '@/components/FilterList.vue'
Expand Down Expand Up @@ -546,6 +546,15 @@ import {
} from '@/types/komga-search'
import {objIsEqual} from '@/functions/object'
import i18n from '@/i18n'
import {
FILTER_ANY,
FILTER_NONE,
FilterMode,
FiltersActive,
FiltersActiveMode,
FiltersOptions,
NameValue,
} from '@/types/filter'
const tags = require('language-tags')
Expand Down Expand Up @@ -661,6 +670,11 @@ export default Vue.extend({
.content
.map(x => x.name)
},
values: [{
name: this.$t('filter.any').toString(),
value: FILTER_ANY,
nValue: FILTER_NONE,
}],
anyAllSelector: true,
}
})
Expand Down Expand Up @@ -977,25 +991,28 @@ export default Vue.extend({
pageRequest.sort = [`${sort.key},${sort.order}`]
}
let authorsFilter = [] as AuthorDto[]
authorRoles.forEach((role: string) => {
if (role in this.filters) this.filters[role].forEach((name: string) => authorsFilter.push({
name: name,
role: role,
}))
})
const conditions = [] as SearchConditionBook[]
conditions.push(new SearchConditionSeriesId(new SearchOperatorIs(seriesId)))
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))
authorRoles.forEach((role: string) => {
if (role in this.filters) {
const authorConditions = this.filters[role].map((name: string) => new SearchConditionAuthor(new SearchOperatorIs({
name: name,
role: role,
})))
const authorConditions = this.filters[role].map((name: string) => {
if (name === FILTER_ANY)
return new SearchConditionAuthor(new SearchOperatorIs({
role: role,
}))
else if (name === FILTER_NONE)
return new SearchConditionAuthor(new SearchOperatorIsNot({
role: role,
}))
else
return new SearchConditionAuthor(new SearchOperatorIs({
name: name,
role: role,
}))
})
conditions.push(this.filtersMode[role]?.allOf ? new SearchConditionAllOfBook(authorConditions) : new SearchConditionAnyOfBook(authorConditions))
}
})
Expand Down

0 comments on commit ffc397f

Please sign in to comment.