komga/komga-webui/src/views/BrowseLibraries.vue

434 lines
15 KiB
Vue

<template>
<div :style="$vuetify.breakpoint.name === 'xs' ? 'margin-bottom: 56px' : undefined">
<toolbar-sticky v-if="selectedSeries.length === 0">
<!-- Action menu -->
<library-actions-menu v-if="library"
:library="library"/>
<v-toolbar-title>
<span>{{ library ? library.name : $t('common.all_libraries') }}</span>
<v-chip label class="mx-4" v-if="totalElements">
<span style="font-size: 1.1rem">{{ totalElements }}</span>
</v-chip>
</v-toolbar-title>
<v-spacer/>
<page-size-select v-model="pageSize"/>
<v-btn icon @click="drawer = !drawer">
<v-icon :color="sortOrFilterActive ? 'secondary' : ''">mdi-filter-variant</v-icon>
</v-btn>
</toolbar-sticky>
<series-multi-select-bar
v-model="selectedSeries"
@unselect-all="selectedSeries = []"
@mark-read="markSelectedRead"
@mark-unread="markSelectedUnread"
@add-to-collection="addToCollection"
@edit="editMultipleSeries"
/>
<library-navigation :libraryId="libraryId"/>
<filter-drawer v-model="drawer">
<template v-slot:default>
<filter-list
:filters-options="filterOptionsList"
: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
:sort-default="sortDefault"
:sort-options="sortOptions"
:sort-active.sync="sortActive"
/>
</template>
</filter-drawer>
<v-container fluid>
<empty-state
v-if="totalPages === 0"
:title="$t('common.filter_no_matches')"
:sub-title="$t('common.use_filter_panel_to_change_filter')"
icon="mdi-book-multiple"
icon-color="secondary"
>
</empty-state>
<template v-else>
<v-pagination
v-if="totalPages > 1"
v-model="page"
:total-visible="paginationVisible"
:length="totalPages"
/>
<item-browser
:items="series"
:selected.sync="selectedSeries"
:edit-function="editSingleSeries"
/>
<v-pagination
v-if="totalPages > 1"
v-model="page"
:total-visible="paginationVisible"
:length="totalPages"
/>
</template>
</v-container>
</div>
</template>
<script lang="ts">
import SeriesMultiSelectBar from '@/components/bars/SeriesMultiSelectBar.vue'
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
import EmptyState from '@/components/EmptyState.vue'
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 {parseQueryFilter, parseQuerySort} from '@/functions/query-params'
import {ReadStatus} from '@/types/enum-books'
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'
import {LIBRARIES_ALL} from '@/types/library'
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 {SeriesDto} from "@/types/komga-series";
const cookiePageSize = 'pagesize'
export default Vue.extend({
name: 'BrowseLibraries',
components: {
LibraryActionsMenu,
EmptyState,
ToolbarSticky,
ItemBrowser,
PageSizeSelect,
LibraryNavigation,
SeriesMultiSelectBar,
FilterDrawer,
FilterPanels,
FilterList,
SortList,
},
data: function () {
return {
library: undefined as LibraryDto | undefined,
series: [] as SeriesDto[],
selectedSeries: [] as SeriesDto[],
page: 1,
pageSize: 20,
totalPages: 1,
totalElements: null as number | null,
sortActive: {} as SortActive,
sortDefault: {key: 'metadata.titleSort', order: 'asc'} as SortActive,
filters: {} as FiltersActive,
sortUnwatch: null as any,
filterUnwatch: null as any,
pageUnwatch: null as any,
pageSizeUnwatch: null as any,
drawer: false,
filterOptions: {
genre: [] as NameValue[],
tag: [] as NameValue[],
publisher: [] as NameValue[],
language: [] as NameValue[],
ageRating: [] as NameValue[],
releaseDate: [] as NameValue[],
},
}
},
props: {
libraryId: {
type: String,
default: LIBRARIES_ALL,
},
},
watch: {
selectedSeries(val: SeriesDto[]) {
val.forEach(s => {
const index = this.series.findIndex(x => x.id === s.id)
if (index !== -1) {
this.series.splice(index, 1, s)
}
})
},
},
created() {
this.$eventHub.$on(SERIES_CHANGED, this.reloadSeries)
this.$eventHub.$on(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$on(LIBRARY_CHANGED, this.reloadLibrary)
},
beforeDestroy() {
this.$eventHub.$off(SERIES_CHANGED, this.reloadSeries)
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
},
async mounted() {
if (this.$cookies.isKey(cookiePageSize)) {
this.pageSize = Number(this.$cookies.get(cookiePageSize))
}
// restore from query param
this.resetParams(this.$route)
if (this.$route.query.page) this.page = Number(this.$route.query.page)
if (this.$route.query.pageSize) this.pageSize = Number(this.$route.query.pageSize)
this.loadLibrary(this.libraryId)
this.setWatches()
},
beforeRouteUpdate(to, from, next) {
if (to.params.libraryId !== from.params.libraryId) {
this.unsetWatches()
// reset
this.resetParams(to)
this.page = 1
this.totalPages = 1
this.totalElements = null
this.series = []
this.filterOptions.genre = []
this.filterOptions.tag = []
this.filterOptions.publisher = []
this.filterOptions.language = []
this.filterOptions.ageRating = []
this.filterOptions.releaseDate = []
this.loadLibrary(to.params.libraryId)
this.setWatches()
}
next()
},
computed: {
sortOptions(): SortOption[] {
return [
{name: this.$t('sort.name').toString(), key: 'metadata.titleSort'},
{name: this.$t('sort.date_added').toString(), key: 'createdDate'},
{name: this.$t('sort.date_updated').toString(), key: 'lastModifiedDate'},
] as SortOption[]
},
filterOptionsList(): FiltersOptions {
return {
readStatus: {values: [{name: this.$t('filter.unread').toString(), value: ReadStatus.UNREAD}]},
} as FiltersOptions
},
filterOptionsPanel(): FiltersOptions {
return {
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},
publisher: {name: this.$t('filter.publisher').toString(), values: this.filterOptions.publisher},
language: {name: this.$t('filter.language').toString(), values: this.filterOptions.language},
ageRating: {
name: this.$t('filter.age_rating').toString(),
values: this.filterOptions.ageRating.map((x: NameValue) => ({
name: (x.value === 'None' ? this.$t('filter.age_rating_none').toString() : x.name),
value: x.value,
} as NameValue),
),
},
releaseDate: {name: this.$t('filter.release_date').toString(), values: this.filterOptions.releaseDate},
} as FiltersOptions
},
isAdmin(): boolean {
return this.$store.getters.meAdmin
},
paginationVisible(): number {
switch (this.$vuetify.breakpoint.name) {
case 'xs':
return 5
case 'sm':
case 'md':
return 10
case 'lg':
case 'xl':
default:
return 15
}
},
sortOrFilterActive(): boolean {
return sortOrFilterActive(this.sortActive, this.sortDefault, this.filters)
},
},
methods: {
cookieSort(libraryId: string): string {
return `library.sort.${libraryId}`
},
cookieFilter(libraryId: string): string {
return `library.filter.${libraryId}`
},
resetParams(route: any) {
this.sortActive = parseQuerySort(route.query.sort, this.sortOptions) ||
this.$cookies.get(this.cookieSort(route.params.libraryId)) ||
this.$_.clone(this.sortDefault)
if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating) {
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.filterOptions.genre.map(x => x.value))
this.filters.tag = parseQueryFilter(route.query.tag, this.filterOptions.tag.map(x => x.value))
this.filters.language = parseQueryFilter(route.query.language, this.filterOptions.language.map(x => x.value))
this.filters.ageRating = parseQueryFilter(route.query.ageRating, this.filterOptions.ageRating.map(x => x.value))
this.filters.releaseDate = parseQueryFilter(route.query.releaseDate, this.filterOptions.releaseDate.map(x => x.value))
} else {
this.filters = this.$cookies.get(this.cookieFilter(route.params.libraryId)) || {} as FiltersActive
}
},
libraryDeleted(event: EventLibraryDeleted) {
if (event.id === this.libraryId) {
this.$router.push({name: 'home'})
} else if (this.libraryId === LIBRARIES_ALL) {
this.loadLibrary(this.libraryId)
}
},
setWatches() {
this.sortUnwatch = this.$watch('sortActive', (val) => {
this.$cookies.set(this.cookieSort(this.libraryId), val, Infinity)
this.updateRouteAndReload()
})
this.filterUnwatch = this.$watch('filters', (val) => {
this.$cookies.set(this.cookieFilter(this.libraryId), val, Infinity)
this.updateRouteAndReload()
})
this.pageSizeUnwatch = this.$watch('pageSize', (val) => {
this.$cookies.set(cookiePageSize, val, Infinity)
this.updateRouteAndReload()
})
this.pageUnwatch = this.$watch('page', (val) => {
this.updateRoute()
this.loadPage(this.libraryId, val, this.sortActive)
})
},
unsetWatches() {
this.sortUnwatch()
this.filterUnwatch()
this.pageUnwatch()
this.pageSizeUnwatch()
},
updateRouteAndReload() {
this.unsetWatches()
this.page = 1
this.updateRoute()
this.loadPage(this.libraryId, this.page, this.sortActive)
this.setWatches()
},
reloadSeries(event: EventSeriesChanged) {
if (this.libraryId === LIBRARIES_ALL || event.libraryId === this.libraryId) {
this.loadPage(this.libraryId, this.page, this.sortActive)
}
},
reloadLibrary(event: EventLibraryChanged) {
if (this.libraryId === LIBRARIES_ALL || event.id === this.libraryId) {
this.loadLibrary(this.libraryId)
}
},
async loadLibrary(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined
this.filterOptions.genre = toNameValue(await this.$komgaReferential.getGenres(requestLibraryId))
this.filterOptions.tag = toNameValue(await this.$komgaReferential.getTags(requestLibraryId))
this.filterOptions.publisher = toNameValue(await this.$komgaReferential.getPublishers(requestLibraryId))
this.filterOptions.language = (await this.$komgaReferential.getLanguages(requestLibraryId))
this.filterOptions.ageRating = toNameValue(await this.$komgaReferential.getAgeRatings(requestLibraryId))
this.filterOptions.releaseDate = toNameValue(await this.$komgaReferential.getSeriesReleaseDates(requestLibraryId))
await this.loadPage(libraryId, this.page, this.sortActive)
},
updateRoute() {
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}`,
},
} as Location
mergeFilterParams(this.filters, loc.query)
this.$router.replace(loc).catch((_: any) => {
})
},
async loadPage(libraryId: string, page: number, sort: SortActive) {
this.selectedSeries = []
const pageRequest = {
page: page - 1,
size: this.pageSize,
} as PageRequest
if (sort) {
pageRequest.sort = [`${sort.key},${sort.order}`]
}
const requestLibraryId = libraryId !== LIBRARIES_ALL ? libraryId : undefined
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)
this.totalPages = seriesPage.totalPages
this.totalElements = seriesPage.totalElements
this.series = seriesPage.content
},
getLibraryLazy(libraryId: any): LibraryDto | undefined {
if (libraryId !== 0) {
return this.$store.getters.getLibraryById(libraryId)
} else {
return undefined
}
},
async markSelectedRead() {
await Promise.all(this.selectedSeries.map(s =>
this.$komgaSeries.markAsRead(s.id),
))
this.selectedSeries = await Promise.all(this.selectedSeries.map(s =>
this.$komgaSeries.getOneSeries(s.id),
))
},
async markSelectedUnread() {
await Promise.all(this.selectedSeries.map(s =>
this.$komgaSeries.markAsUnread(s.id),
))
this.selectedSeries = await Promise.all(this.selectedSeries.map(s =>
this.$komgaSeries.getOneSeries(s.id),
))
},
addToCollection() {
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries)
},
editSingleSeries(series: SeriesDto) {
this.$store.dispatch('dialogUpdateSeries', series)
},
editMultipleSeries() {
this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
},
},
})
</script>
<style scoped>
</style>