mirror of
https://github.com/gotson/komga.git
synced 2025-12-22 00:13:30 +01:00
feat(webui): more filter criteria
filter libraries by: genre, tag, publisher, language filter series by: tag closes #283, closes #34
This commit is contained in:
parent
940d5d3410
commit
4d22d9c8e8
10 changed files with 155 additions and 89 deletions
|
|
@ -1,27 +1,25 @@
|
|||
<template>
|
||||
<v-list dense>
|
||||
<div v-for="(f, key) in filtersOptions"
|
||||
:key="key"
|
||||
<v-list dense>
|
||||
<div v-for="(f, key) in filtersOptions"
|
||||
:key="key"
|
||||
>
|
||||
<v-subheader v-if="f.name">{{ f.name }}</v-subheader>
|
||||
<v-list-item v-for="v in f.values"
|
||||
:key="v.value"
|
||||
@click.stop="click(key, v.value)"
|
||||
>
|
||||
<v-subheader v-if="f.name">{{ f.name }}</v-subheader>
|
||||
<v-list-item v-for="v in f.values"
|
||||
:key="v"
|
||||
@click.stop="click(key, v)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="filtersActive[key].includes(v)" 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 class="text-capitalize">
|
||||
{{ v.toString().toLowerCase().replace('_', ' ') }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</v-list>
|
||||
<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>
|
||||
</div>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
|
@ -42,6 +40,7 @@ export default Vue.extend({
|
|||
methods: {
|
||||
click (key: string, value: string) {
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-expansion-panels accordion multiple flat tile hover>
|
||||
<v-expansion-panels accordion flat tile hover>
|
||||
<v-expansion-panel
|
||||
v-for="(f, key) in filtersOptions"
|
||||
:key="key"
|
||||
|
|
@ -17,20 +17,18 @@
|
|||
<v-expansion-panel-content class="no-padding">
|
||||
<v-list dense>
|
||||
<v-list-item v-for="v in f.values"
|
||||
:key="v"
|
||||
@click.stop="click(key, v)"
|
||||
:key="v.value"
|
||||
@click.stop="click(key, v.value)"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon v-if="filtersActive[key].includes(v)" color="secondary">
|
||||
<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 class="text-capitalize">
|
||||
{{ v.toString().toLowerCase().replace('_', ' ') }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-title>{{ v.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-expansion-panel-content>
|
||||
|
|
@ -61,8 +59,9 @@ export default Vue.extend({
|
|||
this.$emit('update:filtersActive', r)
|
||||
},
|
||||
groupActive (key: string): boolean {
|
||||
if (!(key in this.filtersActive)) return false
|
||||
for (let v of this.filtersOptions[key].values) {
|
||||
if (this.filtersActive[key].includes(v)) {
|
||||
if (this.filtersActive[key].includes(v.value)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -70,6 +69,7 @@ export default Vue.extend({
|
|||
},
|
||||
click (key: string, value: string) {
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,3 +3,13 @@ export function sortOrFilterActive (sortActive: SortActive, sortDefault: SortAct
|
|||
const filterCustom = Object.keys(filters).some(x => filters[x].length !== 0)
|
||||
return sortCustom || filterCustom
|
||||
}
|
||||
|
||||
export function mergeFilterParams (filter: FiltersActive, query: any) {
|
||||
for (const f of Object.keys(filter)) {
|
||||
if (filter[f].length !== 0) query[f] = `${filter[f]}`
|
||||
}
|
||||
}
|
||||
|
||||
export function toNameValue (list: string[]): NameValue[] {
|
||||
return list.map(x => ({ name: x, value: x } as NameValue))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ export function parseQuerySort (querySort: any, sortOptions: SortOption[]): Sort
|
|||
return customSort
|
||||
}
|
||||
|
||||
export function parseQueryFilter (queryStatus: any, enumeration: any): string[] {
|
||||
return queryStatus ? queryStatus.toString().split(',').filter((x: string) => Object.keys(enumeration).includes(x)) : []
|
||||
export function parseQueryFilter (queryStatus: any, enumeration: string[]): string[] {
|
||||
return queryStatus ? queryStatus.toString().split(',').filter((x: string) => enumeration.includes(x)) : []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { AxiosInstance } from 'axios'
|
||||
|
||||
const qs = require('qs')
|
||||
const tags = require('language-tags')
|
||||
|
||||
export default class KomgaReferentialService {
|
||||
private http: AxiosInstance
|
||||
|
|
@ -51,4 +52,37 @@ export default class KomgaReferentialService {
|
|||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getPublishers (): Promise<string[]> {
|
||||
try {
|
||||
return (await this.http.get('/api/v1/publishers')).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve publishers'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getLanguages (): Promise<NameValue[]> {
|
||||
try {
|
||||
const data = (await this.http.get('/api/v1/languages')).data
|
||||
const ret = [] as NameValue[]
|
||||
for (const code of data) {
|
||||
const tag = tags(code)
|
||||
if (tag.valid()) {
|
||||
const name = tag.language().descriptions()[0] + ` (${code})`
|
||||
ret.push({ name: name, value: code })
|
||||
}
|
||||
}
|
||||
return ret
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve publishers'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,21 +11,18 @@ export default class KomgaSeriesService {
|
|||
this.http = http
|
||||
}
|
||||
|
||||
async getSeries (libraryId?: string, pageRequest?: PageRequest, search?: string, status?: string[], readStatus?: string[]): Promise<Page<SeriesDto>> {
|
||||
async getSeries (libraryId?: string, pageRequest?: PageRequest, search?: string, status?: string[], readStatus?: string[], genre?: string[], tag?: string[], language?: string[], publisher?: string[]): Promise<Page<SeriesDto>> {
|
||||
try {
|
||||
const params = { ...pageRequest } as any
|
||||
if (libraryId) {
|
||||
params.library_id = libraryId
|
||||
}
|
||||
if (search) {
|
||||
params.search = search
|
||||
}
|
||||
if (status) {
|
||||
params.status = status
|
||||
}
|
||||
if (readStatus) {
|
||||
params.read_status = readStatus
|
||||
}
|
||||
if (libraryId) params.library_id = libraryId
|
||||
if (search) params.search = search
|
||||
if (status) params.status = status
|
||||
if (readStatus) params.read_status = readStatus
|
||||
if (genre) params.genre = genre
|
||||
if (tag) params.tag = tag
|
||||
if (language) params.language = language
|
||||
if (publisher) params.publisher = publisher
|
||||
|
||||
return (await this.http.get(API_SERIES, {
|
||||
params: params,
|
||||
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
||||
|
|
@ -81,12 +78,12 @@ export default class KomgaSeriesService {
|
|||
}
|
||||
}
|
||||
|
||||
async getBooks (seriesId: string, pageRequest?: PageRequest, readStatus?: string[]): Promise<Page<BookDto>> {
|
||||
async getBooks (seriesId: string, pageRequest?: PageRequest, readStatus?: string[], tag?: string[]): Promise<Page<BookDto>> {
|
||||
try {
|
||||
const params = { ...pageRequest } as any
|
||||
if (readStatus) {
|
||||
params.read_status = readStatus
|
||||
}
|
||||
if (readStatus) params.read_status = readStatus
|
||||
if (tag) params.tag = tag
|
||||
|
||||
return (await this.http.get(`${API_SERIES}/${seriesId}/books`, {
|
||||
params: params,
|
||||
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { capitalize } from 'lodash'
|
||||
|
||||
export enum SeriesStatus {
|
||||
ENDED = 'ENDED',
|
||||
ONGOING = 'ONGOING',
|
||||
ABANDONED = 'ABANDONED',
|
||||
HIATUS = 'HIATUS'
|
||||
}
|
||||
|
||||
export const SeriesStatusKeyValue = Object.values(SeriesStatus).map(x => ({
|
||||
name: capitalize(x),
|
||||
value: x,
|
||||
} as NameValue))
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
interface FiltersOptions {
|
||||
[key: string]: {
|
||||
name?: string,
|
||||
values: string[],
|
||||
values: NameValue[],
|
||||
},
|
||||
}
|
||||
|
||||
interface NameValue {
|
||||
name: string,
|
||||
value: string,
|
||||
}
|
||||
|
||||
interface FiltersActive {
|
||||
[key: string]: string[],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue'
|
|||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
import { parseQueryFilter, parseQuerySort } from '@/functions/query-params'
|
||||
import { ReadStatus } from '@/types/enum-books'
|
||||
import { SeriesStatus } from '@/types/enum-series'
|
||||
import { SeriesStatus, SeriesStatusKeyValue } from '@/types/enum-series'
|
||||
import { LIBRARY_CHANGED, LIBRARY_DELETED, SERIES_CHANGED } from '@/types/events'
|
||||
import Vue from 'vue'
|
||||
import { Location } from 'vue-router'
|
||||
|
|
@ -104,7 +104,7 @@ 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 { sortOrFilterActive } from '@/functions/filter'
|
||||
import { mergeFilterParams, sortOrFilterActive, toNameValue } from '@/functions/filter'
|
||||
|
||||
const cookiePageSize = 'pagesize'
|
||||
|
||||
|
|
@ -140,17 +140,16 @@ export default Vue.extend({
|
|||
sortActive: {} as SortActive,
|
||||
sortDefault: { key: 'metadata.titleSort', order: 'asc' } as SortActive,
|
||||
filterOptionsList: {
|
||||
readStatus: {
|
||||
values: [ReadStatus.UNREAD],
|
||||
},
|
||||
readStatus: { values: [{ name: 'Unread', value: ReadStatus.UNREAD }] },
|
||||
} as FiltersOptions,
|
||||
filterOptionsPanel: {
|
||||
status: {
|
||||
name: 'STATUS',
|
||||
values: Object.values(SeriesStatus),
|
||||
},
|
||||
status: { name: 'STATUS', values: SeriesStatusKeyValue },
|
||||
genre: { name: 'GENRE', values: [] },
|
||||
tag: { name: 'TAG', values: [] },
|
||||
publisher: { name: 'PUBLISHER', values: [] },
|
||||
language: { name: 'LANGUAGE', values: [] },
|
||||
} as FiltersOptions,
|
||||
filters: { status: [], readStatus: [] } as FiltersActive,
|
||||
filters: {} as FiltersActive,
|
||||
sortUnwatch: null as any,
|
||||
filterUnwatch: null as any,
|
||||
pageUnwatch: null as any,
|
||||
|
|
@ -184,11 +183,16 @@ export default Vue.extend({
|
|||
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
|
||||
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
|
||||
},
|
||||
mounted () {
|
||||
async mounted () {
|
||||
if (this.$cookies.isKey(cookiePageSize)) {
|
||||
this.pageSize = Number(this.$cookies.get(cookiePageSize))
|
||||
}
|
||||
|
||||
this.filterOptionsPanel.genre.values.push(...toNameValue(await this.$komgaReferential.getGenres()))
|
||||
this.filterOptionsPanel.tag.values.push(...toNameValue(await this.$komgaReferential.getTags()))
|
||||
this.filterOptionsPanel.publisher.values.push(...toNameValue(await this.$komgaReferential.getPublishers()))
|
||||
this.filterOptionsPanel.language.values.push(...(await this.$komgaReferential.getLanguages()))
|
||||
|
||||
// restore from query param
|
||||
this.resetParams(this.$route)
|
||||
if (this.$route.query.page) this.page = Number(this.$route.query.page)
|
||||
|
|
@ -249,12 +253,13 @@ export default Vue.extend({
|
|||
this.$cookies.get(this.cookieSort(route.params.libraryId)) ||
|
||||
this.$_.clone(this.sortDefault)
|
||||
|
||||
if (route.query.status || route.query.readStatus) {
|
||||
this.filters.status = parseQueryFilter(route.query.status, SeriesStatus)
|
||||
this.filters.readStatus = parseQueryFilter(route.query.readStatus, ReadStatus)
|
||||
if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag) {
|
||||
this.filters.status = parseQueryFilter(route.query.status, Object.keys(SeriesStatus))
|
||||
this.filters.readStatus = parseQueryFilter(route.query.readStatus, Object.keys(ReadStatus))
|
||||
this.filters.genre = parseQueryFilter(route.query.genre, this.filterOptionsPanel.genre.values.map(x => x.value))
|
||||
this.filters.tag = parseQueryFilter(route.query.tag, this.filterOptionsPanel.tag.values.map(x => x.value))
|
||||
} else {
|
||||
this.filters = this.$cookies.get(this.cookieFilter(route.params.libraryId)) ||
|
||||
{ status: [], readStatus: [] } as FiltersActive
|
||||
this.filters = this.$cookies.get(this.cookieFilter(route.params.libraryId)) || {} as FiltersActive
|
||||
}
|
||||
},
|
||||
libraryDeleted (event: EventLibraryDeleted) {
|
||||
|
|
@ -316,17 +321,17 @@ export default Vue.extend({
|
|||
await this.loadPage(libraryId, this.page, this.sortActive)
|
||||
},
|
||||
updateRoute () {
|
||||
this.$router.replace({
|
||||
const loc = {
|
||||
name: this.$route.name,
|
||||
params: { libraryId: this.$route.params.libraryId },
|
||||
query: {
|
||||
page: `${this.page}`,
|
||||
pageSize: `${this.pageSize}`,
|
||||
sort: `${this.sortActive.key},${this.sortActive.order}`,
|
||||
status: `${this.filters.status}`,
|
||||
readStatus: `${this.filters.readStatus}`,
|
||||
},
|
||||
} as Location).catch((_: any) => {
|
||||
} as Location
|
||||
mergeFilterParams(this.filters, loc.query)
|
||||
this.$router.replace(loc).catch((_: any) => {
|
||||
})
|
||||
},
|
||||
async loadPage (libraryId: string, page: number, sort: SortActive) {
|
||||
|
|
@ -340,7 +345,7 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined
|
||||
const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filters.status, this.filters.readStatus)
|
||||
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.totalPages = seriesPage.totalPages
|
||||
this.totalElements = seriesPage.totalElements
|
||||
|
|
|
|||
|
|
@ -50,12 +50,12 @@
|
|||
/>
|
||||
</template>
|
||||
|
||||
<!-- <template v-slot:filter>-->
|
||||
<!-- <filter-panels-->
|
||||
<!-- :filters-options="filterOptionsPanels"-->
|
||||
<!-- :filters-active.sync="filters"-->
|
||||
<!-- />-->
|
||||
<!-- </template>-->
|
||||
<template v-slot:filter>
|
||||
<filter-panels
|
||||
:filters-options="filterOptionsPanel"
|
||||
:filters-active.sync="filters"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:sort>
|
||||
<sort-list
|
||||
|
|
@ -209,7 +209,8 @@ import { SeriesStatus } from '@/types/enum-series'
|
|||
import FilterDrawer from '@/components/FilterDrawer.vue'
|
||||
import FilterList from '@/components/FilterList.vue'
|
||||
import SortList from '@/components/SortList.vue'
|
||||
import { sortOrFilterActive } from '@/functions/filter'
|
||||
import { mergeFilterParams, sortOrFilterActive, toNameValue } from '@/functions/filter'
|
||||
import FilterPanels from '@/components/FilterPanels.vue'
|
||||
|
||||
const tags = require('language-tags')
|
||||
|
||||
|
|
@ -228,6 +229,7 @@ export default Vue.extend({
|
|||
CollectionsExpansionPanels,
|
||||
FilterDrawer,
|
||||
FilterList,
|
||||
FilterPanels,
|
||||
SortList,
|
||||
},
|
||||
data: () => {
|
||||
|
|
@ -246,11 +248,12 @@ export default Vue.extend({
|
|||
sortActive: {} as SortActive,
|
||||
sortDefault: { key: 'metadata.numberSort', order: 'asc' } as SortActive,
|
||||
filterOptionsList: {
|
||||
readStatus: {
|
||||
values: [ReadStatus.UNREAD],
|
||||
},
|
||||
readStatus: { values: [{ name: 'Unread', value: ReadStatus.UNREAD }] },
|
||||
} as FiltersOptions,
|
||||
filters: { readStatus: [] } as FiltersActive,
|
||||
filterOptionsPanel: {
|
||||
tag: { name: 'TAG', values: [] },
|
||||
} as FiltersOptions,
|
||||
filters: {} as FiltersActive,
|
||||
sortUnwatch: null as any,
|
||||
filterUnwatch: null as any,
|
||||
pageUnwatch: null as any,
|
||||
|
|
@ -325,14 +328,19 @@ export default Vue.extend({
|
|||
this.$eventHub.$off(BOOK_CHANGED, this.reloadBooks)
|
||||
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
|
||||
},
|
||||
mounted () {
|
||||
async mounted () {
|
||||
if (this.$cookies.isKey(cookiePageSize)) {
|
||||
this.pageSize = Number(this.$cookies.get(cookiePageSize))
|
||||
}
|
||||
|
||||
this.filterOptionsPanel.tag.values.push(...toNameValue(await this.$komgaReferential.getTags()))
|
||||
|
||||
// restore from query param
|
||||
this.sortActive = this.parseQuerySortOrDefault(this.$route.query.sort)
|
||||
this.filters.readStatus = parseQueryFilter(this.$route.query.readStatus, ReadStatus)
|
||||
|
||||
this.filters.readStatus = parseQueryFilter(this.$route.query.readStatus, Object.keys(ReadStatus))
|
||||
this.filters.tag = parseQueryFilter(this.$route.query.tag, this.filterOptionsPanel.tag.values.map(x => x.value))
|
||||
|
||||
if (this.$route.query.page) this.page = Number(this.$route.query.page)
|
||||
if (this.$route.query.pageSize) this.pageSize = Number(this.$route.query.pageSize)
|
||||
|
||||
|
|
@ -346,7 +354,7 @@ export default Vue.extend({
|
|||
|
||||
// reset
|
||||
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
|
||||
this.filters.readStatus = parseQueryFilter(to.query.readStatus, ReadStatus)
|
||||
this.filters.readStatus = parseQueryFilter(to.query.readStatus, Object.keys(ReadStatus))
|
||||
this.page = 1
|
||||
this.totalPages = 1
|
||||
this.totalElements = null
|
||||
|
|
@ -414,16 +422,17 @@ export default Vue.extend({
|
|||
return queryStatus ? queryStatus.toString().split(',').filter((x: string) => Object.keys(ReadStatus).includes(x)) : []
|
||||
},
|
||||
updateRoute () {
|
||||
this.$router.replace({
|
||||
const loc = {
|
||||
name: this.$route.name,
|
||||
params: { seriesId: this.$route.params.seriesId },
|
||||
query: {
|
||||
page: `${this.page}`,
|
||||
pageSize: `${this.pageSize}`,
|
||||
sort: `${this.sortActive.key},${this.sortActive.order}`,
|
||||
readStatus: `${this.filters.readStatus}`,
|
||||
},
|
||||
} as Location).catch((_: any) => {
|
||||
} as Location
|
||||
mergeFilterParams(this.filters, loc.query)
|
||||
this.$router.replace(loc).catch((_: any) => {
|
||||
})
|
||||
},
|
||||
async loadPage (seriesId: string, page: number, sort: SortActive) {
|
||||
|
|
@ -435,7 +444,7 @@ export default Vue.extend({
|
|||
if (sort) {
|
||||
pageRequest.sort = [`${sort.key},${sort.order}`]
|
||||
}
|
||||
const booksPage = await this.$komgaSeries.getBooks(seriesId, pageRequest, this.filters.readStatus)
|
||||
const booksPage = await this.$komgaSeries.getBooks(seriesId, pageRequest, this.filters.readStatus, this.filters.tag)
|
||||
|
||||
this.totalPages = booksPage.totalPages
|
||||
this.totalElements = booksPage.totalElements
|
||||
|
|
|
|||
Loading…
Reference in a new issue