mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02:00
feat(webui): add read status filter when browsing Series
related to #25
This commit is contained in:
parent
fc5c50240e
commit
cc39ce8b44
5 changed files with 145 additions and 21 deletions
94
komga-webui/src/components/FilterMenuButton.vue
Normal file
94
komga-webui/src/components/FilterMenuButton.vue
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<template>
|
||||||
|
<v-menu offset-y>
|
||||||
|
<template v-slot:activator="{on}">
|
||||||
|
<v-btn icon v-on="on">
|
||||||
|
<v-icon :color="filterCustom ? 'secondary' : null"
|
||||||
|
>mdi-filter-variant
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<div v-for="(f, i) in filtersOptions"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<v-subheader>{{ f.name }}</v-subheader>
|
||||||
|
<v-list-item v-for="v in f.values"
|
||||||
|
:key="v"
|
||||||
|
@click.stop="click(i, v)"
|
||||||
|
>
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon v-if="filtersActive[i].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-item v-if="filterCustom"
|
||||||
|
@click="clearAll()"
|
||||||
|
dense
|
||||||
|
>
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
<v-list-item-title>Clear</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'FilterMenuButton',
|
||||||
|
props: {
|
||||||
|
// array of object: name, values[]
|
||||||
|
filtersOptions: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
// array of arrays containing the selection, use with sync
|
||||||
|
filtersActive: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filterCustom (): boolean {
|
||||||
|
let r = false
|
||||||
|
this.filtersActive.forEach(x => {
|
||||||
|
if (!this.$_.isEmpty(x)) r = true
|
||||||
|
})
|
||||||
|
return r
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clearAll () {
|
||||||
|
let r = [] as any[]
|
||||||
|
this.$_.times(this.filtersActive.length, x => {
|
||||||
|
r.push([])
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('update:filtersActive', r)
|
||||||
|
},
|
||||||
|
click (index: number, value: string) {
|
||||||
|
let r = this.$_.cloneDeep(this.filtersActive) as any[]
|
||||||
|
if (r[index].includes(value)) this.$_.pull(r[index], (value))
|
||||||
|
else r[index].push(value)
|
||||||
|
|
||||||
|
this.$emit('update:filtersActive', r)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -78,10 +78,14 @@ export default class KomgaSeriesService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBooks (seriesId: number, pageRequest?: PageRequest): Promise<Page<BookDto>> {
|
async getBooks (seriesId: number, pageRequest?: PageRequest, readStatus?: string[]): Promise<Page<BookDto>> {
|
||||||
try {
|
try {
|
||||||
|
const params = { ...pageRequest } as any
|
||||||
|
if (readStatus) {
|
||||||
|
params.read_status = readStatus
|
||||||
|
}
|
||||||
return (await this.http.get(`${API_SERIES}/${seriesId}/books`, {
|
return (await this.http.get(`${API_SERIES}/${seriesId}/books`, {
|
||||||
params: { ...pageRequest },
|
params: params,
|
||||||
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
||||||
})).data
|
})).data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,6 @@ export enum MediaStatus {
|
||||||
|
|
||||||
export enum ReadStatus {
|
export enum ReadStatus {
|
||||||
UNREAD = 'UNREAD',
|
UNREAD = 'UNREAD',
|
||||||
READ = 'READ',
|
IN_PROGRESS = 'IN_PROGRESS',
|
||||||
IN_PROGRESS = 'IN_PROGRESS'
|
READ = 'READ'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,11 @@
|
||||||
<v-icon>mdi-pencil</v-icon>
|
<v-icon>mdi-pencil</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
|
||||||
|
<!-- Filter menu -->
|
||||||
|
<filter-menu-button :filters-options="filterOptions"
|
||||||
|
:filters-active.sync="filters"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Sort menu -->
|
<!-- Sort menu -->
|
||||||
<sort-menu-button :sort-default="sortDefault"
|
<sort-menu-button :sort-default="sortDefault"
|
||||||
:sort-options="sortOptions"
|
:sort-options="sortOptions"
|
||||||
|
|
@ -128,13 +133,24 @@
|
||||||
|
|
||||||
<v-divider class="my-4"/>
|
<v-divider class="my-4"/>
|
||||||
|
|
||||||
<v-pagination
|
<empty-state
|
||||||
v-model="page"
|
v-if="totalPages === 0"
|
||||||
:total-visible="paginationVisible"
|
title="The active filter has no matches"
|
||||||
:length="totalPages"
|
sub-title="Use the menu above to change the active filter"
|
||||||
/>
|
icon="mdi-book-multiple"
|
||||||
|
icon-color="secondary"
|
||||||
|
>
|
||||||
|
</empty-state>
|
||||||
|
|
||||||
<item-browser :items="books" :selected.sync="selected" :edit-function="this.singleEdit" class="px-4"/>
|
<template v-else>
|
||||||
|
<v-pagination
|
||||||
|
v-model="page"
|
||||||
|
:total-visible="paginationVisible"
|
||||||
|
:length="totalPages"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<item-browser :items="books" :selected.sync="selected" :edit-function="this.singleEdit" class="px-4"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
|
|
@ -146,6 +162,8 @@
|
||||||
import Badge from '@/components/Badge.vue'
|
import Badge from '@/components/Badge.vue'
|
||||||
import EditBooksDialog from '@/components/EditBooksDialog.vue'
|
import EditBooksDialog from '@/components/EditBooksDialog.vue'
|
||||||
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
|
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
|
||||||
|
import EmptyState from '@/components/EmptyState.vue'
|
||||||
|
import FilterMenuButton from '@/components/FilterMenuButton.vue'
|
||||||
import ItemBrowser from '@/components/ItemBrowser.vue'
|
import ItemBrowser from '@/components/ItemBrowser.vue'
|
||||||
import ItemCard from '@/components/ItemCard.vue'
|
import ItemCard from '@/components/ItemCard.vue'
|
||||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||||
|
|
@ -153,6 +171,7 @@ import SortMenuButton from '@/components/SortMenuButton.vue'
|
||||||
import ToolbarSticky from '@/components/ToolbarSticky.vue'
|
import ToolbarSticky from '@/components/ToolbarSticky.vue'
|
||||||
import { parseQuerySort } from '@/functions/query-params'
|
import { parseQuerySort } from '@/functions/query-params'
|
||||||
import { seriesThumbnailUrl } from '@/functions/urls'
|
import { seriesThumbnailUrl } from '@/functions/urls'
|
||||||
|
import { ReadStatus } from '@/types/enum-books'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
const cookiePageSize = 'pagesize'
|
const cookiePageSize = 'pagesize'
|
||||||
|
|
@ -162,12 +181,14 @@ export default Vue.extend({
|
||||||
components: {
|
components: {
|
||||||
ToolbarSticky,
|
ToolbarSticky,
|
||||||
SortMenuButton,
|
SortMenuButton,
|
||||||
|
FilterMenuButton,
|
||||||
Badge,
|
Badge,
|
||||||
EditSeriesDialog,
|
EditSeriesDialog,
|
||||||
EditBooksDialog,
|
EditBooksDialog,
|
||||||
ItemBrowser,
|
ItemBrowser,
|
||||||
PageSizeSelect,
|
PageSizeSelect,
|
||||||
ItemCard,
|
ItemCard,
|
||||||
|
EmptyState,
|
||||||
},
|
},
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -185,8 +206,11 @@ export default Vue.extend({
|
||||||
}] as SortOption[],
|
}] as SortOption[],
|
||||||
sortActive: {} as SortActive,
|
sortActive: {} as SortActive,
|
||||||
sortDefault: { key: 'metadata.numberSort', order: 'asc' } as SortActive,
|
sortDefault: { key: 'metadata.numberSort', order: 'asc' } as SortActive,
|
||||||
|
filterOptions: [{ name: 'READ STATUS', values: ReadStatus }],
|
||||||
|
filters: [[]] as any[],
|
||||||
dialogEdit: false,
|
dialogEdit: false,
|
||||||
sortUnwatch: null as any,
|
sortUnwatch: null as any,
|
||||||
|
filterUnwatch: null as any,
|
||||||
pageUnwatch: null as any,
|
pageUnwatch: null as any,
|
||||||
pageSizeUnwatch: null as any,
|
pageSizeUnwatch: null as any,
|
||||||
selected: [],
|
selected: [],
|
||||||
|
|
@ -253,6 +277,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
// restore from query param
|
// restore from query param
|
||||||
this.sortActive = this.parseQuerySortOrDefault(this.$route.query.sort)
|
this.sortActive = this.parseQuerySortOrDefault(this.$route.query.sort)
|
||||||
|
this.filters.splice(0, 1, this.parseQueryFilterStatus(this.$route.query.readStatus))
|
||||||
if (this.$route.query.page) this.page = Number(this.$route.query.page)
|
if (this.$route.query.page) this.page = Number(this.$route.query.page)
|
||||||
if (this.$route.query.pageSize) this.pageSize = Number(this.$route.query.pageSize)
|
if (this.$route.query.pageSize) this.pageSize = Number(this.$route.query.pageSize)
|
||||||
|
|
||||||
|
|
@ -266,6 +291,7 @@ export default Vue.extend({
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
|
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
|
||||||
|
this.filters.splice(0, 1, this.parseQueryFilterStatus(to.query.readStatus))
|
||||||
this.page = 1
|
this.page = 1
|
||||||
this.totalPages = 1
|
this.totalPages = 1
|
||||||
this.totalElements = null
|
this.totalElements = null
|
||||||
|
|
@ -281,6 +307,7 @@ export default Vue.extend({
|
||||||
methods: {
|
methods: {
|
||||||
setWatches () {
|
setWatches () {
|
||||||
this.sortUnwatch = this.$watch('sortActive', this.updateRouteAndReload)
|
this.sortUnwatch = this.$watch('sortActive', this.updateRouteAndReload)
|
||||||
|
this.filterUnwatch = this.$watch('filters', this.updateRouteAndReload)
|
||||||
this.pageSizeUnwatch = this.$watch('pageSize', (val) => {
|
this.pageSizeUnwatch = this.$watch('pageSize', (val) => {
|
||||||
this.$cookies.set(cookiePageSize, val, Infinity)
|
this.$cookies.set(cookiePageSize, val, Infinity)
|
||||||
this.updateRouteAndReload()
|
this.updateRouteAndReload()
|
||||||
|
|
@ -293,6 +320,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
unsetWatches () {
|
unsetWatches () {
|
||||||
this.sortUnwatch()
|
this.sortUnwatch()
|
||||||
|
this.filterUnwatch()
|
||||||
this.pageUnwatch()
|
this.pageUnwatch()
|
||||||
this.pageSizeUnwatch()
|
this.pageSizeUnwatch()
|
||||||
},
|
},
|
||||||
|
|
@ -314,6 +342,9 @@ export default Vue.extend({
|
||||||
parseQuerySortOrDefault (querySort: any): SortActive {
|
parseQuerySortOrDefault (querySort: any): SortActive {
|
||||||
return parseQuerySort(querySort, this.sortOptions) || this.$_.clone(this.sortDefault)
|
return parseQuerySort(querySort, this.sortOptions) || this.$_.clone(this.sortDefault)
|
||||||
},
|
},
|
||||||
|
parseQueryFilterStatus (queryStatus: any): string[] {
|
||||||
|
return queryStatus ? queryStatus.toString().split(',').filter((x: string) => Object.keys(ReadStatus).includes(x)) : []
|
||||||
|
},
|
||||||
updateRoute (index?: string) {
|
updateRoute (index?: string) {
|
||||||
this.$router.replace({
|
this.$router.replace({
|
||||||
name: this.$route.name,
|
name: this.$route.name,
|
||||||
|
|
@ -322,6 +353,7 @@ export default Vue.extend({
|
||||||
page: `${this.page}`,
|
page: `${this.page}`,
|
||||||
pageSize: `${this.pageSize}`,
|
pageSize: `${this.pageSize}`,
|
||||||
sort: `${this.sortActive.key},${this.sortActive.order}`,
|
sort: `${this.sortActive.key},${this.sortActive.order}`,
|
||||||
|
readStatus: `${this.filters[0]}`,
|
||||||
},
|
},
|
||||||
}).catch(_ => {
|
}).catch(_ => {
|
||||||
})
|
})
|
||||||
|
|
@ -335,7 +367,7 @@ export default Vue.extend({
|
||||||
if (sort) {
|
if (sort) {
|
||||||
pageRequest.sort = [`${sort.key},${sort.order}`]
|
pageRequest.sort = [`${sort.key},${sort.order}`]
|
||||||
}
|
}
|
||||||
const booksPage = await this.$komgaSeries.getBooks(seriesId, pageRequest)
|
const booksPage = await this.$komgaSeries.getBooks(seriesId, pageRequest, this.filters[0])
|
||||||
|
|
||||||
this.totalPages = booksPage.totalPages
|
this.totalPages = booksPage.totalPages
|
||||||
this.totalElements = booksPage.totalElements
|
this.totalElements = booksPage.totalElements
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
>
|
>
|
||||||
</empty-state>
|
</empty-state>
|
||||||
|
|
||||||
<horizontal-scroller v-if="inProgressBooks.length !== 0">
|
<horizontal-scroller v-if="inProgressBooks.length !== 0" class="my-4">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<div class="title">Keep Reading</div>
|
<div class="title">Keep Reading</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -29,9 +29,7 @@
|
||||||
</template>
|
</template>
|
||||||
</horizontal-scroller>
|
</horizontal-scroller>
|
||||||
|
|
||||||
<br>
|
<horizontal-scroller v-if="newSeries.length !== 0" class="my-4">
|
||||||
|
|
||||||
<horizontal-scroller v-if="newSeries.length !== 0">
|
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<div class="title">Recently Added Series</div>
|
<div class="title">Recently Added Series</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -43,9 +41,7 @@
|
||||||
</template>
|
</template>
|
||||||
</horizontal-scroller>
|
</horizontal-scroller>
|
||||||
|
|
||||||
<br>
|
<horizontal-scroller v-if="updatedSeries.length !== 0" class="my-4">
|
||||||
|
|
||||||
<horizontal-scroller v-if="updatedSeries.length !== 0">
|
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<div class="title">Recently Updated Series</div>
|
<div class="title">Recently Updated Series</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -57,9 +53,7 @@
|
||||||
</template>
|
</template>
|
||||||
</horizontal-scroller>
|
</horizontal-scroller>
|
||||||
|
|
||||||
<br>
|
<horizontal-scroller v-if="latestBooks.length !== 0" class="my-4">
|
||||||
|
|
||||||
<horizontal-scroller v-if="latestBooks.length !== 0">
|
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<div class="title">Recently Added Books</div>
|
<div class="title">Recently Added Books</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue