mirror of
https://github.com/gotson/komga.git
synced 2025-12-15 21:12:27 +01:00
feat(webui): filter series and books by any/none author role
Refs: #1829
This commit is contained in:
parent
d07eb39181
commit
ffc397f119
8 changed files with 114 additions and 55 deletions
|
|
@ -27,6 +27,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {FiltersActive, FiltersOptions} from '@/types/filter'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FilterList',
|
||||
|
|
|
|||
|
|
@ -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)">
|
||||
|
|
@ -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)"
|
||||
>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
|
|
@ -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)
|
||||
},
|
||||
|
|
@ -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}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -804,6 +804,7 @@
|
|||
"filter": {
|
||||
"age_rating": "age rating",
|
||||
"age_rating_none": "None",
|
||||
"any": "Any",
|
||||
"complete": "Complete",
|
||||
"genre": "genre",
|
||||
"in_progress": "In Progress",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
interface FiltersOptions {
|
||||
export interface FiltersOptions {
|
||||
[key: string]: {
|
||||
name?: string,
|
||||
values?: NameValue[],
|
||||
|
|
@ -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____'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -177,7 +178,8 @@ import {
|
|||
SearchConditionPublisher,
|
||||
SearchConditionReadStatus,
|
||||
SearchConditionReleaseDate,
|
||||
SearchConditionSeries, SearchConditionSeriesId,
|
||||
SearchConditionSeries,
|
||||
SearchConditionSeriesId,
|
||||
SearchConditionSeriesStatus,
|
||||
SearchConditionSharingLabel,
|
||||
SearchConditionTag,
|
||||
|
|
@ -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',
|
||||
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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')
|
||||
|
||||
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
|
|
@ -977,14 +991,6 @@ 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))
|
||||
|
|
@ -992,10 +998,21 @@ export default Vue.extend({
|
|||
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))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue