{{ groupAllOfActive(key) ? 'mdi-filter-multiple' : '' }}
+
+ {{ groupActive(key) ? 'mdi-checkbox-marked' : '' }}
@@ -26,6 +33,28 @@
+
+
+
+
+
+ mdi-filter-outline
+
+
+ {{ $t('common.any_of') }}
+
+
+
+
+
+ mdi-filter-multiple-outline
+
+
+ {{ $t('common.all_of') }}
+
+
+
+
-
-
-
-
- mdi-checkbox-marked
-
-
- mdi-checkbox-blank-outline
-
-
- {{ v.name }}
-
-
+
+
+
+
+
+ mdi-minus-box
+
+
+ mdi-checkbox-marked
+
+
+ mdi-checkbox-blank-outline
+
+
+ {{ v.name }}
+
+
+
@@ -81,11 +112,19 @@ export default Vue.extend({
type: Object as PropType,
required: true,
},
+ filtersActiveMode: {
+ type: Object as PropType,
+ 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)
+ },
},
})
@@ -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;
+}
diff --git a/komga-webui/src/components/SearchBox.vue b/komga-webui/src/components/SearchBox.vue
index 2b16198cd..b20dbfa09 100644
--- a/komga-webui/src/components/SearchBox.vue
+++ b/komga-webui/src/components/SearchBox.vue
@@ -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
diff --git a/komga-webui/src/components/dialogs/SeriesPickerDialog.vue b/komga-webui/src/components/dialogs/SeriesPickerDialog.vue
index cd7a66b51..24ffa4e2e 100644
--- a/komga-webui/src/components/dialogs/SeriesPickerDialog.vue
+++ b/komga-webui/src/components/dialogs/SeriesPickerDialog.vue
@@ -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()
diff --git a/komga-webui/src/functions/filter.ts b/komga-webui/src/functions/filter.ts
index 7bbdd61d5..a6d1733af 100644
--- a/komga-webui/src/functions/filter.ts
+++ b/komga-webui/src/functions/filter.ts
@@ -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
}
diff --git a/komga-webui/src/functions/object.ts b/komga-webui/src/functions/object.ts
new file mode 100644
index 000000000..e302d6d46
--- /dev/null
+++ b/komga-webui/src/functions/object.ts
@@ -0,0 +1,3 @@
+export function objIsEqual(o1: any, o2: any): boolean {
+ return JSON.stringify(o1) === JSON.stringify(o2)
+}
diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json
index 14d18e578..a92b9e569 100644
--- a/komga-webui/src/locales/en.json
+++ b/komga-webui/src/locales/en.json
@@ -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",
diff --git a/komga-webui/src/plugins/persisted-state.ts b/komga-webui/src/plugins/persisted-state.ts
index 061a83f7a..36ae9c10a 100644
--- a/komga-webui/src/plugins/persisted-state.ts
+++ b/komga-webui/src/plugins/persisted-state.ts
@@ -29,7 +29,11 @@ export const persistedModule: Module = {
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 = {
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 = {
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
diff --git a/komga-webui/src/services/komga-series.service.ts b/komga-webui/src/services/komga-series.service.ts
index 49989adcd..9cdc83680 100644
--- a/komga-webui/src/services/komga-series.service.ts
+++ b/komga-webui/src/services/komga-series.service.ts
@@ -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> {
+ 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{
+ 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> {
try {
const params = {...pageRequest} as any
diff --git a/komga-webui/src/types/filter.ts b/komga-webui/src/types/filter.ts
index 358b27825..8d5cbc625 100644
--- a/komga-webui/src/types/filter.ts
+++ b/komga-webui/src/types/filter.ts
@@ -3,16 +3,25 @@ interface FiltersOptions {
name?: string,
values?: NameValue[],
search?: (search: string) => Promise,
+ 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,
}
diff --git a/komga-webui/src/types/komga-search.ts b/komga-webui/src/types/komga-search.ts
new file mode 100644
index 000000000..8572a30fa
--- /dev/null
+++ b/komga-webui/src/types/komga-search.ts
@@ -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
+ }
+}
diff --git a/komga-webui/src/views/BrowseLibraries.vue b/komga-webui/src/views/BrowseLibraries.vue
index 703afabca..2ec98abe0 100644
--- a/komga-webui/src/views/BrowseLibraries.vue
+++ b/komga-webui/src/views/BrowseLibraries.vue
@@ -58,6 +58,7 @@
@@ -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)
diff --git a/komga-webui/src/views/BrowseOneshot.vue b/komga-webui/src/views/BrowseOneshot.vue
index d979697bc..328f0fc07 100644
--- a/komga-webui/src/views/BrowseOneshot.vue
+++ b/komga-webui/src/views/BrowseOneshot.vue
@@ -153,14 +153,14 @@
{{ series.metadata.ageRating }}+
{{ languageDisplay }}
@@ -307,7 +307,7 @@