mirror of
https://github.com/gotson/komga.git
synced 2025-12-24 17:35:03 +01:00
feat(webui): better filtering for Series
use the new conditions API to allow negative filters as well as any/all of filters Closes: #1169 Closes: #1523 Closes: #1552
This commit is contained in:
parent
3bfc7981e5
commit
d93bc3d996
15 changed files with 665 additions and 106 deletions
|
|
@ -5,14 +5,14 @@
|
|||
>
|
||||
<v-subheader v-if="f.name">{{ f.name }}</v-subheader>
|
||||
<v-list-item v-for="v in f.values"
|
||||
:key="v.value"
|
||||
:key="JSON.stringify(v.value)"
|
||||
@click.stop="click(key, v.value, v.nValue)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="key in filtersActive && filtersActive[key].includes(v.nValue)" color="secondary">
|
||||
<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 && filtersActive[key].includes(v.value)" color="secondary">
|
||||
<v-icon v-else-if="key in filtersActive && includes(filtersActive[key], v.value)" color="secondary">
|
||||
mdi-checkbox-marked
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
|
|
@ -41,16 +41,24 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
click(key: string, value: string, nValue?: string) {
|
||||
includes(array: any[], value: any): boolean {
|
||||
return this.$_.isObject(value) ? this.$_.some(array, value) : this.$_.includes(array, value)
|
||||
},
|
||||
click(key: string, value: any, nValue?: any) {
|
||||
let r = this.$_.cloneDeep(this.filtersActive)
|
||||
if (!(key in r)) r[key] = []
|
||||
if (nValue && r[key].includes(nValue))
|
||||
this.$_.pull(r[key], (nValue))
|
||||
else if (r[key].includes(value)) {
|
||||
this.$_.pull(r[key], (value))
|
||||
|
||||
const pull = this.$_.isObject(value) ? this.$_.remove : this.$_.pull
|
||||
const includes = this.$_.isObject(value) ? this.$_.some : this.$_.includes
|
||||
|
||||
if (nValue && includes(r[key], nValue))
|
||||
pull(r[key], nValue)
|
||||
else if (includes(r[key], value)) {
|
||||
pull(r[key], value)
|
||||
if (nValue)
|
||||
r[key].push(nValue)
|
||||
} else r[key].push(value)
|
||||
} else
|
||||
r[key].push(value)
|
||||
|
||||
this.$emit('update:filtersActive', r)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,11 +5,18 @@
|
|||
:key="key"
|
||||
:disabled="(f.values && f.values.length === 0) && !f.search"
|
||||
>
|
||||
<v-expansion-panel-header class="text-uppercase">
|
||||
<v-expansion-panel-header class="text-uppercase ps-1">
|
||||
<v-icon
|
||||
color="secondary"
|
||||
style="max-width: 24px"
|
||||
class="mx-2"
|
||||
class="mx-0"
|
||||
@click.stop="clickFilterMode(key, false)"
|
||||
>{{ groupAllOfActive(key) ? 'mdi-filter-multiple' : '' }}
|
||||
</v-icon>
|
||||
<v-icon
|
||||
color="secondary"
|
||||
style="max-width: 24px"
|
||||
class="me-2"
|
||||
@click.stop="clear(key)"
|
||||
>{{ groupActive(key) ? 'mdi-checkbox-marked' : '' }}
|
||||
</v-icon>
|
||||
|
|
@ -26,6 +33,28 @@
|
|||
</template>
|
||||
</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-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn small icon :value="false" v-on="on" @click.stop="clickFilterMode(key, false)">
|
||||
<v-icon small>mdi-filter-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t('common.any_of') }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn small icon :value="true" v-on="on" @click.stop="clickFilterMode(key, true)">
|
||||
<v-icon small>mdi-filter-multiple-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t('common.all_of') }}</span>
|
||||
</v-tooltip>
|
||||
</v-btn-toggle>
|
||||
</div>
|
||||
|
||||
<v-list
|
||||
v-if="f.search"
|
||||
dense
|
||||
|
|
@ -41,25 +70,27 @@
|
|||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-list
|
||||
v-if="f.values"
|
||||
dense
|
||||
>
|
||||
<v-list-item v-for="v in f.values"
|
||||
:key="v.value"
|
||||
@click.stop="click(key, v.value)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="key in filtersActive && filtersActive[key].includes(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 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>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
|
|
@ -81,11 +112,19 @@ export default Vue.extend({
|
|||
type: Object as PropType<FiltersActive>,
|
||||
required: true,
|
||||
},
|
||||
filtersActiveMode: {
|
||||
type: Object as PropType<FiltersActiveMode>,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
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)
|
||||
|
||||
this.$emit('update:filtersActive', r)
|
||||
},
|
||||
|
|
@ -93,14 +132,39 @@ export default Vue.extend({
|
|||
if (!(key in this.filtersActive)) return false
|
||||
return this.filtersActive[key].length > 0
|
||||
},
|
||||
click(key: string, value: string) {
|
||||
groupAllOfActive(key: string): boolean {
|
||||
if (!this.filtersActiveMode || !(key in this.filtersActiveMode)) return false
|
||||
return this.filtersActiveMode[key].allOf
|
||||
},
|
||||
click(key: string, value: any, nValue?: any) {
|
||||
let r = this.$_.cloneDeep(this.filtersActive)
|
||||
if (!(key in r)) r[key] = []
|
||||
if (r[key].includes(value)) this.$_.pull(r[key], (value))
|
||||
else r[key].push(value)
|
||||
|
||||
const pull = this.$_.isObject(value) ? this.$_.remove : this.$_.pull
|
||||
const includes = this.$_.isObject(value) ? this.$_.some : this.$_.includes
|
||||
|
||||
if (nValue && includes(r[key], nValue))
|
||||
pull(r[key], nValue)
|
||||
else if (includes(r[key], value)) {
|
||||
pull(r[key], value)
|
||||
if (nValue) {
|
||||
r[key].push(nValue)
|
||||
this.clickFilterMode(key, true)
|
||||
}
|
||||
} else
|
||||
r[key].push(value)
|
||||
|
||||
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
|
||||
let r = this.$_.cloneDeep(this.filtersActiveMode)
|
||||
r[key] = {allOf: value}
|
||||
|
||||
this.$emit('update:filtersActiveMode', r)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
|
@ -109,4 +173,10 @@ export default Vue.extend({
|
|||
.no-padding .v-expansion-panel-content__wrap {
|
||||
padding: 0;
|
||||
}
|
||||
.semi-transparent {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.semi-transparent:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -123,6 +123,11 @@ import {SeriesDto} from '@/types/komga-series'
|
|||
import {getReadProgress} from '@/functions/book-progress'
|
||||
import {ReadStatus} from '@/types/enum-books'
|
||||
import {ReadListDto} from '@/types/komga-readlists'
|
||||
import {
|
||||
SearchConditionOneShot,
|
||||
SearchOperatorIsFalse,
|
||||
SeriesSearch,
|
||||
} from '@/types/komga-search'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SearchBox',
|
||||
|
|
@ -202,7 +207,10 @@ export default Vue.extend({
|
|||
searchItems: debounce(async function (this: any, query: string) {
|
||||
if (query) {
|
||||
this.loading = true
|
||||
this.series = (await this.$komgaSeries.getSeries(undefined, {size: this.pageSize}, query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, false)).content
|
||||
this.series = (await this.$komgaSeries.getSeriesList({
|
||||
fullTextSearch: query,
|
||||
condition: new SearchConditionOneShot(new SearchOperatorIsFalse()),
|
||||
} as SeriesSearch, {size: this.pageSize})).content
|
||||
this.books = (await this.$komgaBooks.getBooks(undefined, {size: this.pageSize}, query)).content
|
||||
this.collections = (await this.$komgaCollections.getCollections(undefined, {size: this.pageSize}, query)).content
|
||||
this.readLists = (await this.$komgaReadLists.getReadLists(undefined, {size: this.pageSize}, query)).content
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ import Vue, {PropType} from 'vue'
|
|||
import {SeriesDto} from '@/types/komga-series'
|
||||
import {debounce} from 'lodash'
|
||||
import {seriesThumbnailUrl} from '@/functions/urls'
|
||||
import {SearchConditionOneShot, SearchOperatorIsFalse, SeriesSearch} from '@/types/komga-search'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SeriesPickerDialog',
|
||||
|
|
@ -123,7 +124,10 @@ export default Vue.extend({
|
|||
searchItems: debounce(async function (this: any, query: string) {
|
||||
if (query) {
|
||||
this.showResults = false
|
||||
this.results = (await this.$komgaSeries.getSeries(undefined, {unpaged: true}, query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, this.includeOneshots ? undefined : false)).content
|
||||
this.results = (await this.$komgaSeries.getSeriesList({
|
||||
fullTextSearch: query,
|
||||
condition: this.includeOneshots ? undefined : new SearchConditionOneShot(new SearchOperatorIsFalse()),
|
||||
} as SeriesSearch, {unpaged: true})).content
|
||||
this.showResults = true
|
||||
} else {
|
||||
this.clear()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
export function sortOrFilterActive (sortActive: SortActive, sortDefault: SortActive, filters: FiltersActive): boolean {
|
||||
import {SearchConditionSeries} from '@/types/komga-search'
|
||||
|
||||
export function sortOrFilterActive(sortActive: SortActive, sortDefault: SortActive, filters: FiltersActive): boolean {
|
||||
const sortCustom = sortActive.key !== sortDefault.key || sortActive.order !== sortDefault.order
|
||||
const filterCustom = Object.keys(filters).some(x => filters[x].length !== 0)
|
||||
return sortCustom || filterCustom
|
||||
|
|
@ -10,6 +12,19 @@ export function mergeFilterParams (filter: FiltersActive, query: any) {
|
|||
}
|
||||
}
|
||||
|
||||
export function toNameValue (list: string[]): NameValue[] {
|
||||
return list.map(x => ({ name: x, value: x } as NameValue))
|
||||
export function toNameValue(list: string[]): NameValue[] {
|
||||
return list.map(x => ({name: x, value: x} as NameValue))
|
||||
}
|
||||
|
||||
export function toNameValueCondition(list: string[], valueSupplier: (x: any) => SearchConditionSeries, nValueSupplier?: (x: any) => SearchConditionSeries): NameValue[] {
|
||||
return list.map(x => ({name: x, value: valueSupplier(x), nValue: nValueSupplier ? nValueSupplier(x) : undefined} as NameValue))
|
||||
}
|
||||
|
||||
export function extractFilterOptionsValues(options: NameValue[] | undefined): any[] {
|
||||
const r: any[] = []
|
||||
options?.forEach(x => {
|
||||
r.push(x.value)
|
||||
if (x.nValue) r.push(x.nValue)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
|
|
|||
3
komga-webui/src/functions/object.ts
Normal file
3
komga-webui/src/functions/object.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function objIsEqual(o1: any, o2: any): boolean {
|
||||
return JSON.stringify(o1) === JSON.stringify(o2)
|
||||
}
|
||||
|
|
@ -201,6 +201,8 @@
|
|||
"common": {
|
||||
"age": "Age",
|
||||
"all_libraries": "All Libraries",
|
||||
"all_of": "All of",
|
||||
"any_of": "Any of",
|
||||
"book": "Book",
|
||||
"books": "Books",
|
||||
"books_n": "No book | 1 book | {count} books",
|
||||
|
|
|
|||
|
|
@ -29,7 +29,11 @@ export const persistedModule: Module<any, any> = {
|
|||
filter: {},
|
||||
},
|
||||
library: {
|
||||
// DEPRECATED: this is the old filter, before criteria-dsl was introduced
|
||||
filter: {},
|
||||
// this is the criteria-dsl filter, incompatible with the previous one
|
||||
filterDsl: {},
|
||||
filterMode: {},
|
||||
sort: {},
|
||||
route: {},
|
||||
},
|
||||
|
|
@ -63,7 +67,10 @@ export const persistedModule: Module<any, any> = {
|
|||
return state.readList.filter[id]
|
||||
},
|
||||
getLibraryFilter: (state) => (id: string) => {
|
||||
return state.library.filter[id]
|
||||
return state.library.filterDsl[id]
|
||||
},
|
||||
getLibraryFilterMode: (state) => (id: string) => {
|
||||
return state.library.filterMode[id]
|
||||
},
|
||||
getLibrarySort: (state) => (id: string) => {
|
||||
return state.library.sort[id]
|
||||
|
|
@ -119,7 +126,10 @@ export const persistedModule: Module<any, any> = {
|
|||
state.readList.filter[id] = filter
|
||||
},
|
||||
setLibraryFilter(state, {id, filter}) {
|
||||
state.library.filter[id] = filter
|
||||
state.library.filterDsl[id] = filter
|
||||
},
|
||||
setLibraryFilterMode(state, {id, filterMode: filterMode}) {
|
||||
state.library.filterMode[id] = filterMode
|
||||
},
|
||||
setLibrarySort(state, {id, sort}) {
|
||||
state.library.sort[id] = sort
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {AxiosInstance} from 'axios'
|
||||
import {AuthorDto, BookDto} from '@/types/komga-books'
|
||||
import {GroupCountDto, SeriesDto, SeriesMetadataUpdateDto, SeriesThumbnailDto} from '@/types/komga-series'
|
||||
import {SeriesSearch} from '@/types/komga-search'
|
||||
|
||||
const qs = require('qs')
|
||||
|
||||
|
|
@ -48,6 +49,21 @@ export default class KomgaSeriesService {
|
|||
}
|
||||
}
|
||||
|
||||
async getSeriesList(search: SeriesSearch, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
|
||||
try {
|
||||
return (await this.http.post(`${API_SERIES}/list`, search, {
|
||||
params: {...pageRequest},
|
||||
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve series'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getAlphabeticalGroups(libraryId?: string, search?: string, status?: string[],
|
||||
readStatus?: string[], genre?: string[], tag?: string[], language?: string[],
|
||||
publisher?: string[], ageRating?: string[], releaseDate?: string[], authors?: AuthorDto[],
|
||||
|
|
@ -82,6 +98,18 @@ export default class KomgaSeriesService {
|
|||
}
|
||||
}
|
||||
|
||||
async getSeriesListByAlphabeticalGroups(search: SeriesSearch): Promise<GroupCountDto[]>{
|
||||
try {
|
||||
return (await this.http.post(`${API_SERIES}/list/alphabetical-groups`, search)).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve series alphabetical groups'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getNewSeries(libraryId?: string, oneshot?: boolean, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
|
||||
try {
|
||||
const params = {...pageRequest} as any
|
||||
|
|
|
|||
|
|
@ -3,16 +3,25 @@ interface FiltersOptions {
|
|||
name?: string,
|
||||
values?: NameValue[],
|
||||
search?: (search: string) => Promise<string[]>,
|
||||
anyAllSelector?: boolean,
|
||||
},
|
||||
}
|
||||
|
||||
interface NameValue {
|
||||
name: string,
|
||||
value: string,
|
||||
value: any,
|
||||
// an optional negative value
|
||||
nValue?: string,
|
||||
nValue?: any,
|
||||
}
|
||||
|
||||
interface FiltersActive {
|
||||
[key: string]: string[],
|
||||
[key: string]: any[],
|
||||
}
|
||||
|
||||
interface FiltersActiveMode {
|
||||
[key: string]: FilterMode,
|
||||
}
|
||||
|
||||
interface FilterMode {
|
||||
allOf: boolean,
|
||||
}
|
||||
|
|
|
|||
236
komga-webui/src/types/komga-search.ts
Normal file
236
komga-webui/src/types/komga-search.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
export interface SeriesSearch {
|
||||
condition?: SearchConditionSeries,
|
||||
fullTextSearch?: string,
|
||||
}
|
||||
|
||||
export interface SearchConditionSeries {
|
||||
}
|
||||
|
||||
export interface SearchConditionBook {
|
||||
}
|
||||
|
||||
export interface SearchConditionAnyOfBook extends SearchConditionBook {
|
||||
anyOf: SearchConditionBook[],
|
||||
}
|
||||
|
||||
export interface SearchConditionAllOfBook extends SearchConditionBook {
|
||||
allOf: SearchConditionBook[],
|
||||
}
|
||||
|
||||
export class SearchConditionAnyOfSeries implements SearchConditionSeries {
|
||||
anyOf: SearchConditionSeries[]
|
||||
|
||||
constructor(conditions: SearchConditionSeries[]) {
|
||||
this.anyOf = conditions
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionAllOfSeries implements SearchConditionSeries {
|
||||
allOf: SearchConditionSeries[]
|
||||
|
||||
constructor(conditions: SearchConditionSeries[]) {
|
||||
this.allOf = conditions
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionLibraryId implements SearchConditionBook, SearchConditionSeries {
|
||||
libraryId: SearchOperatorEquality
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.libraryId = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionSeriesStatus implements SearchConditionSeries {
|
||||
seriesStatus: SearchOperatorEquality
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.seriesStatus = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionReadStatus implements SearchConditionBook, SearchConditionSeries {
|
||||
readStatus: SearchOperatorEquality
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.readStatus = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionGenre implements SearchConditionSeries {
|
||||
genre: SearchOperatorEquality
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.genre = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionTag implements SearchConditionBook, SearchConditionSeries {
|
||||
tag: SearchOperatorEquality
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.tag = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionLanguage implements SearchConditionSeries {
|
||||
language: SearchOperatorEquality
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.language = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionPublisher implements SearchConditionSeries {
|
||||
publisher: SearchOperatorEquality
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.publisher = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionAgeRating implements SearchConditionSeries {
|
||||
ageRating: SearchOperatorNumericNullable
|
||||
|
||||
constructor(op: SearchOperatorNumericNullable) {
|
||||
this.ageRating = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionReleaseDate implements SearchConditionBook, SearchConditionSeries {
|
||||
releaseDate: SearchOperatorDate
|
||||
|
||||
constructor(op: SearchOperatorDate) {
|
||||
this.releaseDate = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionSharingLabel implements SearchConditionBook, SearchConditionSeries {
|
||||
sharingLabel: SearchOperatorEquality
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.sharingLabel = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionComplete implements SearchConditionSeries {
|
||||
complete: SearchOperatorBoolean
|
||||
|
||||
constructor(op: SearchOperatorBoolean) {
|
||||
this.complete = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionOneShot implements SearchConditionBook, SearchConditionSeries {
|
||||
oneShot: SearchOperatorBoolean
|
||||
|
||||
constructor(op: SearchOperatorBoolean) {
|
||||
this.oneShot = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionAuthor implements SearchConditionBook, SearchConditionSeries {
|
||||
author: SearchOperatorBoolean
|
||||
|
||||
constructor(op: SearchOperatorEquality) {
|
||||
this.author = op
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchConditionTitleSort implements SearchConditionSeries {
|
||||
titleSort: SearchOperatorString
|
||||
|
||||
constructor(op: SearchOperatorString) {
|
||||
this.titleSort = op
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuthorMatch {
|
||||
name?: string,
|
||||
role?: string
|
||||
}
|
||||
|
||||
export interface SearchOperatorEquality {
|
||||
}
|
||||
|
||||
export interface SearchOperatorBoolean {
|
||||
}
|
||||
|
||||
export interface SearchOperatorNumericNullable {
|
||||
}
|
||||
|
||||
export interface SearchOperatorDate {
|
||||
}
|
||||
|
||||
export interface SearchOperatorString {
|
||||
}
|
||||
|
||||
export class SearchOperatorIs implements SearchOperatorEquality, SearchOperatorNumericNullable, SearchOperatorDate {
|
||||
readonly operator: string = 'is'
|
||||
value: any
|
||||
|
||||
constructor(value: any) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchOperatorIsNot implements SearchOperatorEquality, SearchOperatorNumericNullable, SearchOperatorDate {
|
||||
readonly operator: string = 'isNot'
|
||||
value: any
|
||||
|
||||
constructor(value: any) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchOperatorBefore implements SearchOperatorDate {
|
||||
readonly operator: string = 'before'
|
||||
dateTime: any
|
||||
|
||||
constructor(value: any) {
|
||||
this.dateTime = value
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchOperatorAfter implements SearchOperatorDate {
|
||||
readonly operator: string = 'after'
|
||||
dateTime: any
|
||||
|
||||
constructor(value: any) {
|
||||
this.dateTime = value
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchOperatorIsNull implements SearchOperatorNumericNullable, SearchOperatorDate {
|
||||
readonly operator: string = 'isNull'
|
||||
}
|
||||
|
||||
export class SearchOperatorIsNotNull implements SearchOperatorNumericNullable, SearchOperatorDate {
|
||||
readonly operator: string = 'isNotNull'
|
||||
}
|
||||
|
||||
export class SearchOperatorIsTrue implements SearchOperatorBoolean {
|
||||
readonly operator: string = 'isTrue'
|
||||
}
|
||||
|
||||
export class SearchOperatorIsFalse implements SearchOperatorBoolean {
|
||||
readonly operator: string = 'isFalse'
|
||||
}
|
||||
|
||||
export class SearchOperatorBeginsWith implements SearchOperatorString {
|
||||
readonly operator: string = 'beginsWith'
|
||||
value: any
|
||||
|
||||
constructor(value: any) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchOperatorDoesNotBeginWith implements SearchOperatorString {
|
||||
readonly operator: string = 'doesNotBeginWith'
|
||||
value: any
|
||||
|
||||
constructor(value: any) {
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
|
@ -58,6 +58,7 @@
|
|||
<filter-panels
|
||||
:filters-options="filterOptionsPanel"
|
||||
:filters-active.sync="filters"
|
||||
:filters-active-mode.sync="filtersMode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -131,9 +132,9 @@ import ItemBrowser from '@/components/ItemBrowser.vue'
|
|||
import LibraryNavigation from '@/components/LibraryNavigation.vue'
|
||||
import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue'
|
||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
import {parseBooleanFilter, parseQuerySort} from '@/functions/query-params'
|
||||
import {parseQuerySort} from '@/functions/query-params'
|
||||
import {ReadStatus} from '@/types/enum-books'
|
||||
import {SeriesStatus, SeriesStatusKeyValue} from '@/types/enum-series'
|
||||
import {SeriesStatus} from '@/types/enum-series'
|
||||
import {
|
||||
LIBRARY_CHANGED,
|
||||
LIBRARY_DELETED,
|
||||
|
|
@ -150,15 +151,51 @@ import FilterDrawer from '@/components/FilterDrawer.vue'
|
|||
import SortList from '@/components/SortList.vue'
|
||||
import FilterPanels from '@/components/FilterPanels.vue'
|
||||
import FilterList from '@/components/FilterList.vue'
|
||||
import {mergeFilterParams, sortOrFilterActive, toNameValue} from '@/functions/filter'
|
||||
import {
|
||||
extractFilterOptionsValues,
|
||||
mergeFilterParams,
|
||||
sortOrFilterActive,
|
||||
toNameValueCondition,
|
||||
} from '@/functions/filter'
|
||||
import {GroupCountDto, Oneshot, SeriesDto} from '@/types/komga-series'
|
||||
import {AuthorDto} from '@/types/komga-books'
|
||||
import {authorRoles} from '@/types/author-roles'
|
||||
import {LibrarySseDto, ReadProgressSeriesSseDto, SeriesSseDto} from '@/types/komga-sse'
|
||||
import {throttle} from 'lodash'
|
||||
import AlphabeticalNavigation from '@/components/AlphabeticalNavigation.vue'
|
||||
import {LibraryDto} from '@/types/komga-libraries'
|
||||
import {ItemContext} from '@/types/items'
|
||||
import {
|
||||
SearchConditionAgeRating,
|
||||
SearchConditionAllOfSeries,
|
||||
SearchConditionAnyOfSeries,
|
||||
SearchConditionAuthor,
|
||||
SearchConditionComplete,
|
||||
SearchConditionGenre,
|
||||
SearchConditionLanguage,
|
||||
SearchConditionLibraryId,
|
||||
SearchConditionOneShot,
|
||||
SearchConditionPublisher,
|
||||
SearchConditionReadStatus,
|
||||
SearchConditionReleaseDate,
|
||||
SearchConditionSeries,
|
||||
SearchConditionSeriesStatus,
|
||||
SearchConditionSharingLabel,
|
||||
SearchConditionTag,
|
||||
SearchConditionTitleSort,
|
||||
SearchOperatorAfter,
|
||||
SearchOperatorBefore,
|
||||
SearchOperatorBeginsWith,
|
||||
SearchOperatorDoesNotBeginWith,
|
||||
SearchOperatorIs,
|
||||
SearchOperatorIsFalse,
|
||||
SearchOperatorIsNot,
|
||||
SearchOperatorIsNotNull,
|
||||
SearchOperatorIsNull,
|
||||
SearchOperatorIsTrue,
|
||||
SeriesSearch,
|
||||
} from '@/types/komga-search'
|
||||
import i18n from '@/i18n'
|
||||
import {objIsEqual} from '@/functions/object'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseLibraries',
|
||||
|
|
@ -191,8 +228,10 @@ export default Vue.extend({
|
|||
sortActive: {} as SortActive,
|
||||
sortDefault: {key: 'metadata.titleSort', order: 'asc'} as SortActive,
|
||||
filters: {} as FiltersActive,
|
||||
filtersMode: {} as FiltersActiveMode,
|
||||
sortUnwatch: null as any,
|
||||
filterUnwatch: null as any,
|
||||
filterModeUnwatch: null as any,
|
||||
pageUnwatch: null as any,
|
||||
pageSizeUnwatch: null as any,
|
||||
drawer: false,
|
||||
|
|
@ -266,10 +305,14 @@ export default Vue.extend({
|
|||
next()
|
||||
},
|
||||
computed: {
|
||||
searchRegex(): string | undefined {
|
||||
symbolCondition(): SearchConditionSeries | undefined {
|
||||
if (this.selectedSymbol === 'ALL') return undefined
|
||||
if (this.selectedSymbol === '#') return '^[^a-z],title_sort'
|
||||
return `^${this.selectedSymbol},title_sort`
|
||||
if (this.selectedSymbol === '#') return new SearchConditionAllOfSeries(
|
||||
this.alphabeticalNavigation
|
||||
.filter(it => it !== 'ALL' && it !== '#')
|
||||
.map(it => new SearchConditionTitleSort(new SearchOperatorDoesNotBeginWith(it))),
|
||||
)
|
||||
return new SearchConditionTitleSort(new SearchOperatorBeginsWith(this.selectedSymbol))
|
||||
},
|
||||
itemContext(): ItemContext[] {
|
||||
if (this.sortActive.key === 'booksMetadata.releaseDate') return [ItemContext.RELEASE_DATE]
|
||||
|
|
@ -291,24 +334,50 @@ export default Vue.extend({
|
|||
return {
|
||||
readStatus: {
|
||||
values: [
|
||||
{name: this.$t('filter.unread').toString(), value: ReadStatus.UNREAD},
|
||||
{name: this.$t('filter.in_progress').toString(), value: ReadStatus.IN_PROGRESS},
|
||||
{name: this.$t('filter.read').toString(), value: ReadStatus.READ},
|
||||
{
|
||||
name: this.$t('filter.unread').toString(),
|
||||
value: new SearchConditionReadStatus(new SearchOperatorIs(ReadStatus.UNREAD)),
|
||||
nValue: new SearchConditionReadStatus(new SearchOperatorIsNot(ReadStatus.UNREAD)),
|
||||
},
|
||||
{
|
||||
name: this.$t('filter.in_progress').toString(),
|
||||
value: new SearchConditionReadStatus(new SearchOperatorIs(ReadStatus.IN_PROGRESS)),
|
||||
nValue: new SearchConditionReadStatus(new SearchOperatorIsNot(ReadStatus.IN_PROGRESS)),
|
||||
},
|
||||
{
|
||||
name: this.$t('filter.read').toString(),
|
||||
value: new SearchConditionReadStatus(new SearchOperatorIs(ReadStatus.READ)),
|
||||
nValue: new SearchConditionReadStatus(new SearchOperatorIsNot(ReadStatus.READ)),
|
||||
},
|
||||
],
|
||||
},
|
||||
complete: {
|
||||
values: [{name: this.$t('filter.complete').toString(), value: 'true', nValue: 'false'}],
|
||||
values: [{
|
||||
name: this.$t('filter.complete').toString(),
|
||||
value: new SearchConditionComplete(new SearchOperatorIsTrue()),
|
||||
nValue: new SearchConditionComplete(new SearchOperatorIsFalse()),
|
||||
}],
|
||||
},
|
||||
oneshot: {
|
||||
values: [{name: this.$t('filter.oneshot').toString(), value: 'true', nValue: 'false'}],
|
||||
values: [{
|
||||
name: this.$t('filter.oneshot').toString(),
|
||||
value: new SearchConditionOneShot(new SearchOperatorIsTrue()),
|
||||
nValue: new SearchConditionOneShot(new SearchOperatorIsFalse()),
|
||||
}],
|
||||
},
|
||||
} as FiltersOptions
|
||||
},
|
||||
filterOptionsPanel(): FiltersOptions {
|
||||
const r = {
|
||||
status: {name: this.$t('filter.status').toString(), values: SeriesStatusKeyValue()},
|
||||
genre: {name: this.$t('filter.genre').toString(), values: this.filterOptions.genre},
|
||||
tag: {name: this.$t('filter.tag').toString(), values: this.filterOptions.tag},
|
||||
status: {
|
||||
name: this.$t('filter.status').toString(), values: Object.values(SeriesStatus).map(x => ({
|
||||
name: i18n.t(`enums.series_status.${x}`),
|
||||
value: new SearchConditionSeriesStatus(new SearchOperatorIs(x)),
|
||||
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},
|
||||
ageRating: {
|
||||
|
|
@ -316,6 +385,7 @@ export default Vue.extend({
|
|||
values: this.filterOptions.ageRating.map((x: NameValue) => ({
|
||||
name: (x.value === 'None' ? this.$t('filter.age_rating_none').toString() : x.name),
|
||||
value: x.value,
|
||||
nValue: x.nValue,
|
||||
} as NameValue),
|
||||
),
|
||||
},
|
||||
|
|
@ -329,6 +399,7 @@ export default Vue.extend({
|
|||
.content
|
||||
.map(x => x.name)
|
||||
},
|
||||
anyAllSelector: true,
|
||||
}
|
||||
})
|
||||
r['sharingLabel'] = {name: this.$t('filter.sharing_label').toString(), values: this.filterOptions.sharingLabel}
|
||||
|
|
@ -362,7 +433,7 @@ export default Vue.extend({
|
|||
this.selectedSymbol = symbol
|
||||
this.page = 1
|
||||
this.updateRoute()
|
||||
this.loadPage(this.libraryId, 1, this.sortActive, this.searchRegex)
|
||||
this.loadPage(this.libraryId, 1, this.sortActive, this.symbolCondition)
|
||||
},
|
||||
resetSortAndFilters() {
|
||||
this.drawer = false
|
||||
|
|
@ -391,13 +462,35 @@ export default Vue.extend({
|
|||
this.$komgaReferential.getSeriesReleaseDates(requestLibraryId),
|
||||
this.$komgaReferential.getSharingLabels(requestLibraryId),
|
||||
])
|
||||
this.$set(this.filterOptions, 'genre', toNameValue(genres))
|
||||
this.$set(this.filterOptions, 'tag', toNameValue(tags))
|
||||
this.$set(this.filterOptions, 'publisher', toNameValue(publishers))
|
||||
this.$set(this.filterOptions, 'language', (languages))
|
||||
this.$set(this.filterOptions, 'ageRating', toNameValue(ageRatings))
|
||||
this.$set(this.filterOptions, 'releaseDate', toNameValue(releaseDates))
|
||||
this.$set(this.filterOptions, 'sharingLabel', toNameValue(sharingLabels))
|
||||
this.$set(this.filterOptions, 'genre', toNameValueCondition(genres, x => new SearchConditionGenre(new SearchOperatorIs(x)), x => new SearchConditionGenre(new SearchOperatorIsNot(x))))
|
||||
this.$set(this.filterOptions, 'tag', toNameValueCondition(tags, x => new SearchConditionTag(new SearchOperatorIs(x)), x => new SearchConditionTag(new SearchOperatorIsNot(x))))
|
||||
this.$set(this.filterOptions, 'publisher', toNameValueCondition(publishers, x => new SearchConditionPublisher(new SearchOperatorIs(x)), x => new SearchConditionPublisher(new SearchOperatorIsNot(x))))
|
||||
this.$set(this.filterOptions, 'language', languages.map((x: NameValue) => {
|
||||
return {
|
||||
name: x.name,
|
||||
value: new SearchConditionLanguage(new SearchOperatorIs(x.value)),
|
||||
nValue: new SearchConditionLanguage(new SearchOperatorIsNot(x.value)),
|
||||
} as NameValue
|
||||
}))
|
||||
this.$set(this.filterOptions, 'ageRating', toNameValueCondition(ageRatings, x => new SearchConditionAgeRating(isFinite(x) ? new SearchOperatorIs(x) : new SearchOperatorIsNull()), x => new SearchConditionAgeRating(isFinite(x) ? new SearchOperatorIsNot(x) : new SearchOperatorIsNotNull())))
|
||||
this.$set(this.filterOptions, 'releaseDate', toNameValueCondition(
|
||||
releaseDates,
|
||||
x => {
|
||||
const year = Number.parseInt(x)
|
||||
return year ? new SearchConditionAllOfSeries([
|
||||
new SearchConditionReleaseDate(new SearchOperatorAfter(`${year - 1}-12-31T12:00:00Z`)),
|
||||
new SearchConditionReleaseDate(new SearchOperatorBefore(`${year + 1}-01-01T12:00:00Z`)),
|
||||
]) : new SearchConditionAllOfSeries([])
|
||||
},
|
||||
year =>
|
||||
new SearchConditionAnyOfSeries([
|
||||
new SearchConditionReleaseDate(new SearchOperatorAfter(`${year}-12-31T12:00:00Z`)),
|
||||
new SearchConditionReleaseDate(new SearchOperatorBefore(`${year}-01-01T12:00:00Z`)),
|
||||
new SearchConditionReleaseDate(new SearchOperatorIsNull()),
|
||||
],
|
||||
),
|
||||
))
|
||||
this.$set(this.filterOptions, 'sharingLabel', toNameValueCondition(sharingLabels, x => new SearchConditionSharingLabel(new SearchOperatorIs(x)), x => new SearchConditionSharingLabel(new SearchOperatorIsNot(x))))
|
||||
|
||||
// get filter from query params or local storage and validate with available filter values
|
||||
let activeFilters: any
|
||||
|
|
@ -422,20 +515,36 @@ export default Vue.extend({
|
|||
activeFilters = this.$store.getters.getLibraryFilter(route.params.libraryId) || {} as FiltersActive
|
||||
}
|
||||
this.filters = this.validateFilters(activeFilters)
|
||||
|
||||
// get filter mode from query params or local storage
|
||||
let activeFiltersMode: any
|
||||
if(route.query.filterMode) {
|
||||
activeFiltersMode = route.query.filterMode
|
||||
} else {
|
||||
activeFiltersMode = this.$store.getters.getLibraryFilterMode(route.params.libraryId) || {} as FiltersActiveMode
|
||||
}
|
||||
this.filtersMode = this.validateFiltersMode(activeFiltersMode)
|
||||
},
|
||||
validateFiltersMode(filtersMode: any): FiltersActiveMode {
|
||||
const validFilterMode = {} as FiltersActiveMode
|
||||
for (let key in filtersMode) {
|
||||
if (filtersMode[key].allOf == 'true' || filtersMode[key].allOf == true) validFilterMode[key] = {allOf: true} as FilterMode
|
||||
}
|
||||
return validFilterMode
|
||||
},
|
||||
validateFilters(filters: FiltersActive): FiltersActive {
|
||||
const validFilter = {
|
||||
status: filters.status?.filter(x => Object.keys(SeriesStatus).includes(x)) || [],
|
||||
readStatus: filters.readStatus?.filter(x => Object.keys(ReadStatus).includes(x)) || [],
|
||||
genre: filters.genre?.filter(x => this.filterOptions.genre.map(n => n.value).includes(x)) || [],
|
||||
tag: filters.tag?.filter(x => this.filterOptions.tag.map(n => n.value).includes(x)) || [],
|
||||
publisher: filters.publisher?.filter(x => this.filterOptions.publisher.map(n => n.value).includes(x)) || [],
|
||||
language: filters.language?.filter(x => this.filterOptions.language.map(n => n.value).includes(x)) || [],
|
||||
ageRating: filters.ageRating?.filter(x => this.filterOptions.ageRating.map(n => n.value).includes(x)) || [],
|
||||
releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [],
|
||||
complete: filters.complete?.filter(x => x === 'true' || x === 'false') || [],
|
||||
oneshot: filters.oneshot?.filter(x => x === 'true' || x === 'false') || [],
|
||||
sharingLabel: filters.sharingLabel?.filter(x => this.filterOptions.sharingLabel.map(n => n.value).includes(x)) || [],
|
||||
status: this.$_.intersectionWith(filters.status, extractFilterOptionsValues(this.filterOptionsPanel.status.values), objIsEqual) || [],
|
||||
readStatus: this.$_.intersectionWith(filters.readStatus, extractFilterOptionsValues(this.filterOptionsList.readStatus.values), objIsEqual) || [],
|
||||
genre: this.$_.intersectionWith(filters.genre, extractFilterOptionsValues(this.filterOptions.genre), objIsEqual) || [],
|
||||
tag: this.$_.intersectionWith(filters.tag, extractFilterOptionsValues(this.filterOptions.tag), objIsEqual) || [],
|
||||
publisher: this.$_.intersectionWith(filters.publisher, extractFilterOptionsValues(this.filterOptions.publisher), objIsEqual) || [],
|
||||
language: this.$_.intersectionWith(filters.language, extractFilterOptionsValues(this.filterOptions.language), objIsEqual) || [],
|
||||
ageRating: this.$_.intersectionWith(filters.ageRating, extractFilterOptionsValues(this.filterOptions.ageRating), objIsEqual) || [],
|
||||
releaseDate: this.$_.intersectionWith(filters.releaseDate, extractFilterOptionsValues(this.filterOptions.releaseDate), objIsEqual) || [],
|
||||
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) || [],
|
||||
} as any
|
||||
authorRoles.forEach((role: string) => {
|
||||
validFilter[role] = filters[role] || []
|
||||
|
|
@ -458,6 +567,10 @@ export default Vue.extend({
|
|||
this.$store.commit('setLibraryFilter', {id: this.libraryId, filter: val})
|
||||
this.updateRouteAndReload()
|
||||
})
|
||||
this.filterModeUnwatch = this.$watch('filtersMode', (val) => {
|
||||
this.$store.commit('setLibraryFilterMode', {id: this.libraryId, filterMode: val})
|
||||
this.updateRouteAndReload()
|
||||
})
|
||||
this.pageSizeUnwatch = this.$watch('pageSize', (val) => {
|
||||
this.$store.commit('setBrowsingPageSize', val)
|
||||
this.updateRouteAndReload()
|
||||
|
|
@ -465,12 +578,13 @@ export default Vue.extend({
|
|||
|
||||
this.pageUnwatch = this.$watch('page', (val) => {
|
||||
this.updateRoute()
|
||||
this.loadPage(this.libraryId, val, this.sortActive, this.searchRegex)
|
||||
this.loadPage(this.libraryId, val, this.sortActive, this.symbolCondition)
|
||||
})
|
||||
},
|
||||
unsetWatches() {
|
||||
this.sortUnwatch()
|
||||
this.filterUnwatch()
|
||||
this.filterModeUnwatch()
|
||||
this.pageUnwatch()
|
||||
this.pageSizeUnwatch()
|
||||
},
|
||||
|
|
@ -480,7 +594,7 @@ export default Vue.extend({
|
|||
this.page = 1
|
||||
|
||||
this.updateRoute()
|
||||
this.loadPage(this.libraryId, this.page, this.sortActive, this.searchRegex)
|
||||
this.loadPage(this.libraryId, this.page, this.sortActive, this.symbolCondition)
|
||||
|
||||
this.setWatches()
|
||||
},
|
||||
|
|
@ -500,7 +614,7 @@ export default Vue.extend({
|
|||
async loadLibrary(libraryId: string) {
|
||||
this.library = this.getLibraryLazy(libraryId)
|
||||
|
||||
await this.loadPage(libraryId, this.page, this.sortActive, this.searchRegex)
|
||||
await this.loadPage(libraryId, this.page, this.sortActive, this.symbolCondition)
|
||||
},
|
||||
updateRoute() {
|
||||
const loc = {
|
||||
|
|
@ -514,13 +628,14 @@ export default Vue.extend({
|
|||
},
|
||||
} as Location
|
||||
mergeFilterParams(this.filters, loc.query)
|
||||
loc.query['filterMode'] = this.validateFiltersMode(this.filtersMode)
|
||||
this.$router.replace(loc).catch((_: any) => {
|
||||
})
|
||||
},
|
||||
reloadPage: throttle(function (this: any) {
|
||||
this.loadPage(this.libraryId, this.page, this.sortActive, this.searchRegex)
|
||||
this.loadPage(this.libraryId, this.page, this.sortActive, this.symbolCondition)
|
||||
}, 1000),
|
||||
async loadPage(libraryId: string, page: number, sort: SortActive, searchRegex?: string) {
|
||||
async loadPage(libraryId: string, page: number, sort: SortActive, symbolCondition?: SearchConditionSeries) {
|
||||
this.selectedSeries = []
|
||||
|
||||
const pageRequest = {
|
||||
|
|
@ -532,24 +647,43 @@ export default Vue.extend({
|
|||
pageRequest.sort = [`${sort.key},${sort.order}`]
|
||||
}
|
||||
|
||||
let authorsFilter = [] as AuthorDto[]
|
||||
const conditions = [] as SearchConditionSeries[]
|
||||
if (libraryId !== LIBRARIES_ALL) conditions.push(new SearchConditionLibraryId(new SearchOperatorIs(libraryId)))
|
||||
if (this.filters.status && this.filters.status.length > 0) this.filtersMode?.status?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.status)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.status))
|
||||
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.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.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) this.filters[role].forEach((name: string) => authorsFilter.push({
|
||||
name: name,
|
||||
role: role,
|
||||
}))
|
||||
if (role in this.filters) {
|
||||
const authorConditions = this.filters[role].map((name: string) => new SearchConditionAuthor(new SearchOperatorIs({
|
||||
name: name,
|
||||
role: role,
|
||||
})))
|
||||
conditions.push(this.filtersMode[role]?.allOf ? new SearchConditionAllOfSeries(authorConditions) : new SearchConditionAnyOfSeries(authorConditions))
|
||||
}
|
||||
})
|
||||
|
||||
const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined
|
||||
const complete = parseBooleanFilter(this.filters.complete)
|
||||
const oneshot = parseBooleanFilter(this.filters.oneshot)
|
||||
const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filters.status, this.filters.readStatus, this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, searchRegex, complete, this.filters.sharingLabel, oneshot)
|
||||
const groupConditions = this.$_.cloneDeep(conditions)
|
||||
if (symbolCondition) conditions.push(symbolCondition)
|
||||
|
||||
const seriesPage = await this.$komgaSeries.getSeriesList({
|
||||
condition: new SearchConditionAllOfSeries(conditions),
|
||||
} as SeriesSearch, pageRequest)
|
||||
|
||||
this.totalPages = seriesPage.totalPages
|
||||
this.totalElements = seriesPage.totalElements
|
||||
this.series = seriesPage.content
|
||||
|
||||
const seriesGroups = await this.$komgaSeries.getAlphabeticalGroups(requestLibraryId, undefined, this.filters.status, this.filters.readStatus, this.filters.genre, this.filters.tag, this.filters.language, this.filters.publisher, this.filters.ageRating, this.filters.releaseDate, authorsFilter, complete, this.filters.sharingLabel, oneshot)
|
||||
const seriesGroups = await this.$komgaSeries.getSeriesListByAlphabeticalGroups({
|
||||
condition: new SearchConditionAllOfSeries(groupConditions),
|
||||
} as SeriesSearch)
|
||||
const nonAlpha = seriesGroups
|
||||
.filter((g) => !(/[a-zA-Z]/).test(g.group))
|
||||
.reduce((a, b) => a + b.count, 0)
|
||||
|
|
|
|||
|
|
@ -153,14 +153,14 @@
|
|||
<v-row class="text-body-2">
|
||||
<v-col class="py-1 pe-0" cols="auto" v-if="series.metadata && series.metadata.ageRating">
|
||||
<v-chip label small link
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {ageRating: [series.metadata.ageRating]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {ageRating: [new SearchConditionAgeRating(new SearchOperatorIs(series.metadata.ageRating.toString()))]}}"
|
||||
>
|
||||
{{ series.metadata.ageRating }}+
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col class="py-1 pe-0" cols="auto" v-if="series.metadata.language">
|
||||
<v-chip label small link
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {language: [series.metadata.language]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {language: [new SearchConditionLanguage(new SearchOperatorIs(series.metadata.language))]}}"
|
||||
>
|
||||
{{ languageDisplay }}
|
||||
</v-chip>
|
||||
|
|
@ -307,7 +307,7 @@
|
|||
<v-chip
|
||||
class="me-2"
|
||||
:title="series.metadata.publisher"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {publisher: [series.metadata.publisher]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {publisher: [new SearchConditionPublisher(new SearchOperatorIs(series.metadata.publisher))]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
|
|
@ -337,7 +337,7 @@
|
|||
:key="i"
|
||||
class="me-2"
|
||||
:title="t"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {genre: [t]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {genre: [new SearchConditionGenre(new SearchOperatorIs(t))]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
|
|
@ -402,7 +402,7 @@
|
|||
:key="i"
|
||||
class="me-2"
|
||||
:title="t"
|
||||
:to="{name:'browse-libraries', params: {libraryId: book.libraryId}, query: {tag: [t]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: book.libraryId}, query: {tag: [new SearchConditionTag(new SearchOperatorIs(t))]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
|
|
@ -480,7 +480,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import BookActionsMenu from '@/components/menus/BookActionsMenu.vue'
|
||||
import ItemCard from '@/components/ItemCard.vue'
|
||||
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
|
||||
import {groupAuthorsByRole} from '@/functions/authors'
|
||||
|
|
@ -526,6 +525,13 @@ import {ReadListDto} from '@/types/komga-readlists'
|
|||
import {Oneshot, SeriesDto} from '@/types/komga-series'
|
||||
import CollectionsExpansionPanels from '@/components/CollectionsExpansionPanels.vue'
|
||||
import OneshotActionsMenu from '@/components/menus/OneshotActionsMenu.vue'
|
||||
import {
|
||||
SearchConditionAgeRating,
|
||||
SearchConditionGenre, SearchConditionLanguage,
|
||||
SearchConditionPublisher,
|
||||
SearchConditionTag,
|
||||
SearchOperatorIs,
|
||||
} from '@/types/komga-search'
|
||||
|
||||
const tags = require('language-tags')
|
||||
|
||||
|
|
@ -538,6 +544,12 @@ export default Vue.extend({
|
|||
},
|
||||
data: () => {
|
||||
return {
|
||||
SearchConditionPublisher,
|
||||
SearchConditionGenre,
|
||||
SearchConditionTag,
|
||||
SearchConditionLanguage,
|
||||
SearchConditionAgeRating,
|
||||
SearchOperatorIs,
|
||||
MediaStatus,
|
||||
ContextOrigin,
|
||||
book: {} as BookDto,
|
||||
|
|
|
|||
|
|
@ -127,20 +127,20 @@
|
|||
<v-row class="text-body-2">
|
||||
<v-col class="py-1 pe-0" cols="auto">
|
||||
<v-chip label small link :color="statusChip.color" :text-color="statusChip.text"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {status: [series.metadata.status]}}">
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {status: [new SearchConditionSeriesStatus(new SearchOperatorIs(series.metadata.status))]}}">
|
||||
{{ $t(`enums.series_status.${series.metadata.status}`) }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col class="py-1 pe-0" cols="auto" v-if="series.metadata.ageRating">
|
||||
<v-chip label small link
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {ageRating: [series.metadata.ageRating]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {ageRating: [new SearchConditionAgeRating(new SearchOperatorIs(series.metadata.ageRating.toString()))]}}"
|
||||
>
|
||||
{{ series.metadata.ageRating }}+
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col class="py-1 pe-0" cols="auto" v-if="series.metadata.language">
|
||||
<v-chip label small link
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {language: [series.metadata.language]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {language: [new SearchConditionLanguage(new SearchOperatorIs(series.metadata.language))]}}"
|
||||
>
|
||||
{{ languageDisplay }}
|
||||
</v-chip>
|
||||
|
|
@ -285,7 +285,7 @@
|
|||
<v-chip
|
||||
class="me-2"
|
||||
:title="series.metadata.publisher"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {publisher: [series.metadata.publisher]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {publisher: [new SearchConditionPublisher(new SearchOperatorIs(series.metadata.publisher))]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
|
|
@ -315,7 +315,7 @@
|
|||
:key="i"
|
||||
class="me-2"
|
||||
:title="t"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {genre: [t]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {genre: [new SearchConditionGenre(new SearchOperatorIs(t))]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
|
|
@ -347,7 +347,7 @@
|
|||
:key="`series_${i}`"
|
||||
class="me-2"
|
||||
:title="t"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {tag: [t]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {tag: [new SearchConditionTag(new SearchOperatorIs(t))]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
|
|
@ -358,7 +358,7 @@
|
|||
:key="`book_${i}`"
|
||||
class="me-2"
|
||||
:title="t"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {tag: [t]}}"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }, query: {tag: [new SearchConditionTag(new SearchOperatorIs(t))]}}"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
|
|
@ -521,6 +521,15 @@ import {BookSseDto, CollectionSseDto, LibrarySseDto, ReadProgressSseDto, SeriesS
|
|||
import {ItemContext} from '@/types/items'
|
||||
import {Context, ContextOrigin} from '@/types/context'
|
||||
import {RawLocation} from 'vue-router/types/router'
|
||||
import {
|
||||
SearchConditionAgeRating,
|
||||
SearchConditionGenre,
|
||||
SearchConditionLanguage,
|
||||
SearchConditionPublisher,
|
||||
SearchConditionSeriesStatus,
|
||||
SearchConditionTag,
|
||||
SearchOperatorIs,
|
||||
} from '@/types/komga-search'
|
||||
|
||||
const tags = require('language-tags')
|
||||
|
||||
|
|
@ -545,6 +554,13 @@ export default Vue.extend({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
SearchConditionSeriesStatus,
|
||||
SearchConditionPublisher,
|
||||
SearchConditionGenre,
|
||||
SearchConditionTag,
|
||||
SearchConditionLanguage,
|
||||
SearchConditionAgeRating,
|
||||
SearchOperatorIs,
|
||||
series: {} as SeriesDto,
|
||||
context: {} as Context,
|
||||
books: [] as BookDto[],
|
||||
|
|
|
|||
|
|
@ -180,6 +180,7 @@ import {throttle} from 'lodash'
|
|||
import {PageLoader} from '@/types/pageLoader'
|
||||
import {ItemContext} from '@/types/items'
|
||||
import {ReadListDto} from '@/types/komga-readlists'
|
||||
import {SearchConditionOneShot, SearchOperatorIsFalse, SeriesSearch} from '@/types/komga-search'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SearchView',
|
||||
|
|
@ -395,7 +396,10 @@ export default Vue.extend({
|
|||
}, 500),
|
||||
setupLoaders(search: string) {
|
||||
if (search) {
|
||||
this.loaderSeries = new PageLoader<SeriesDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaSeries.getSeries(undefined, pageable, search, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, false))
|
||||
this.loaderSeries = new PageLoader<SeriesDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaSeries.getSeriesList({
|
||||
fullTextSearch: search,
|
||||
condition: new SearchConditionOneShot(new SearchOperatorIsFalse()),
|
||||
} as SeriesSearch, pageable))
|
||||
this.loaderBooks = new PageLoader<BookDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaBooks.getBooks(undefined, pageable, search))
|
||||
this.loaderCollections = new PageLoader<CollectionDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaCollections.getCollections(undefined, pageable, search))
|
||||
this.loaderReadLists = new PageLoader<ReadListDto>({size: this.pageSize}, (pageable: PageRequest) => this.$komgaReadLists.getReadLists(undefined, pageable, search))
|
||||
|
|
|
|||
Loading…
Reference in a new issue