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:
Gauthier Roebroeck 2024-12-12 16:33:42 +08:00
parent 3bfc7981e5
commit d93bc3d996
15 changed files with 665 additions and 106 deletions

View file

@ -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)
},

View file

@ -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>

View file

@ -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

View file

@ -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()

View file

@ -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
}

View file

@ -0,0 +1,3 @@
export function objIsEqual(o1: any, o2: any): boolean {
return JSON.stringify(o1) === JSON.stringify(o2)
}

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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,
}

View 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
}
}

View file

@ -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)

View file

@ -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,

View file

@ -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[],

View file

@ -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))