fix(webui): use pagination for browsing screens

closes #91
This commit is contained in:
Gauthier Roebroeck 2020-06-03 15:43:03 +08:00
parent 75b72164fe
commit 5867db77f5
8 changed files with 286 additions and 241 deletions

View file

@ -1,31 +1,24 @@
<template>
<v-item-group multiple v-model="selectedItems">
<v-row justify="start" ref="content" v-resize="onResize" v-if="hasItems">
<v-skeleton-loader v-for="(item, index) in items"
:key="index"
:width="itemWidth"
:height="itemHeight"
justify-self="start"
:loading="item === null"
type="card, text"
class="ma-3 mx-2"
:data-index="index"
v-intersect="onElementIntersect"
<v-item
v-for="(item, index) in items"
:key="index"
class="my-3 mx-2"
v-slot:default="{ toggle, active }" :value="$_.get(item, 'id', 0)"
>
<v-item v-slot:default="{ toggle, active }" :value="$_.get(item, 'id', 0)">
<slot name="item" v-bind:data="{ toggle, active, item, index, itemWidth, preselect: shouldPreselect(), editItem }">
<item-card
:item="item"
:width="itemWidth"
:selected="active"
:preselect="shouldPreselect()"
:onEdit="editItem"
:onSelected="toggle"
></item-card>
</slot>
</v-item>
</v-skeleton-loader>
<slot name="item"
v-bind:data="{ toggle, active, item, index, itemWidth, preselect: shouldPreselect(), editItem }">
<item-card
:item="item"
:width="itemWidth"
:selected="active"
:preselect="shouldPreselect()"
:onEdit="editItem"
:onSelected="toggle"
></item-card>
</slot>
</v-item>
</v-row>
<v-row v-else justify="center">
<slot name="empty"></slot>
@ -34,13 +27,11 @@
</template>
<script lang="ts">
import Vue from 'vue'
import { computeCardWidth } from '@/functions/grid-utilities'
import ItemCard from '@/components/ItemCard.vue'
import mixins from 'vue-typed-mixins'
import VisibleElements from '@/mixins/VisibleElements'
import { computeCardWidth } from '@/functions/grid-utilities'
import Vue from 'vue'
export default mixins(VisibleElements).extend({
export default Vue.extend({
name: 'ItemBrowser',
components: { ItemCard },
props: {
@ -66,12 +57,6 @@ export default mixins(VisibleElements).extend({
}
},
watch: {
series: {
handler () {
this.visibleElements = []
},
immediate: true,
},
selectedItems: {
handler () {
this.$emit('update:selected', this.selectedItems)

View file

@ -0,0 +1,57 @@
<template>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-btn icon v-on="on">
<v-icon>mdi-view-grid-plus</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item-group v-model="selection">
<v-list-item v-for="(item, index) in items"
:key="index"
@click="setPageSize(item)"
>
<v-list-item-title>{{ item }}</v-list-item-title>
</v-list-item>
</v-list-item-group>
</v-list>
</v-menu>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'PageSizeSelect',
data: () => {
return {
selection: 0,
}
},
props: {
items: {
type: Array,
default: () => [20, 50, 100, 200, 500],
},
value: {
type: Number,
required: true,
},
},
watch: {
value (val) {
this.selection = this.items.findIndex(x => x === val)
},
},
methods: {
setPageSize (size: number) {
this.$emit('input', size)
},
},
})
</script>
<style scoped>
</style>

View file

@ -1,22 +0,0 @@
import Vue from 'vue'
const visibleElements = Vue.extend({
data: () => {
return {
visibleElements: [] as number[],
}
},
methods: {
async onElementIntersect (entries: any, observer: any, isIntersecting: boolean) {
const elementIndex = Number(entries[0].target.dataset['index'])
if (isIntersecting) {
this.visibleElements.push(elementIndex)
} else {
this.$_.pull(this.visibleElements, elementIndex)
}
this.$emit('update', this.visibleElements)
},
},
})
export default visibleElements

View file

@ -5,13 +5,19 @@ import store from './store'
Vue.use(Router)
const lStore = store
const lStore = store as any
const adminGuard = (to: any, from: any, next: any) => {
if (!lStore.getters.meAdmin) next({ name: 'home' })
else next()
}
const noLibraryGuard = (to: any, from: any, next: any) => {
if (lStore.state.komgaLibraries.libraries.length === 0) {
next({ name: 'welcome' })
} else next()
}
const router = new Router({
mode: 'history',
base: urls.base,
@ -36,6 +42,7 @@ const router = new Router({
{
path: '/dashboard',
name: 'dashboard',
beforeEnter: noLibraryGuard,
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue'),
},
{
@ -71,13 +78,14 @@ const router = new Router({
component: () => import(/* webpackChunkName: "account" */ './views/AccountSettings.vue'),
},
{
path: '/libraries/:libraryId/:index?',
path: '/libraries/:libraryId',
name: 'browse-libraries',
beforeEnter: noLibraryGuard,
component: () => import(/* webpackChunkName: "browse-libraries" */ './views/BrowseLibraries.vue'),
props: (route) => ({ libraryId: Number(route.params.libraryId) }),
},
{
path: '/series/:seriesId/:index?',
path: '/series/:seriesId',
name: 'browse-series',
component: () => import(/* webpackChunkName: "browse-series" */ './views/BrowseSeries.vue'),
props: (route) => ({ seriesId: Number(route.params.seriesId) }),

View file

@ -1,5 +0,0 @@
export enum LoadState {
Loaded,
NotLoaded,
Loading
}

View file

@ -42,6 +42,9 @@
:sort-options="sortOptions"
:sort-active.sync="sortActive"
/>
<page-size-select v-model="pageSize"/>
</toolbar-sticky>
<v-scroll-y-transition hide-on-leave>
@ -69,57 +72,72 @@
:series.sync="editSeriesSingle"
/>
<item-browser :items="series"
:selected.sync="selected"
:edit-function="this.singleEdit"
class="px-6"
@update="updateVisible"
>
<template #empty v-if="filterStatus.length > 0">
<empty-state title="The active filter has no matches"
sub-title="Use the menu above to change the active filter"
icon="mdi-book-multiple"
icon-color="secondary"
>
<v-btn @click="filterStatus = []">Clear filter</v-btn>
</empty-state>
<v-container>
<empty-state
v-if="totalPages === 0"
title="The active filter has no matches"
sub-title="Use the menu above to change the active filter"
icon="mdi-book-multiple"
icon-color="secondary"
>
<v-btn @click="filterStatus = []">Clear filter</v-btn>
</empty-state>
<template v-else>
<v-pagination
v-model="page"
:total-visible="paginationVisible"
:length="totalPages"
/>
<item-browser
:items="series"
:selected.sync="selected"
:edit-function="this.singleEdit"
class="px-6"
/>
</template>
<template #empty v-else>
<empty-state title="There are no current libraries"
sub-title="Use the + button on the side bar to add a library"
icon="mdi-book-multiple"
icon-color="secondary"
>
</empty-state>
</template>
</item-browser>
</v-container>
</div>
</template>
<script lang="ts">
import Badge from '@/components/Badge.vue'
import EmptyState from '@/components/EmptyState.vue'
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
import EmptyState from '@/components/EmptyState.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
import SortMenuButton from '@/components/SortMenuButton.vue'
import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { parseQuerySort } from '@/functions/query-params'
import { LoadState } from '@/types/common'
import { SeriesStatus } from '@/types/enum-series'
import ItemBrowser from '@/components/ItemBrowser.vue'
import Vue from 'vue'
const cookiePageSize = 'pagesize'
export default Vue.extend({
name: 'BrowseLibraries',
components: { LibraryActionsMenu, EmptyState, ToolbarSticky, SortMenuButton, Badge, EditSeriesDialog, ItemBrowser },
components: {
LibraryActionsMenu,
EmptyState,
ToolbarSticky,
SortMenuButton,
Badge,
EditSeriesDialog,
ItemBrowser,
PageSizeSelect,
},
data: () => {
return {
library: undefined as LibraryDto | undefined,
series: [] as SeriesDto[],
selectedSeries: [] as SeriesDto[],
editSeriesSingle: {} as SeriesDto,
pagesState: [] as LoadState[],
page: 1,
pageSize: 20,
totalPages: 1,
totalElements: null as number | null,
sortOptions: [
{ name: 'Name', key: 'metadata.titleSort' },
@ -132,6 +150,8 @@ export default Vue.extend({
SeriesStatus,
sortUnwatch: null as any,
filterUnwatch: null as any,
pageUnwatch: null as any,
pageSizeUnwatch: null as any,
selected: [],
dialogEdit: false,
dialogEditSingle: false,
@ -163,24 +183,18 @@ export default Vue.extend({
}
},
},
async created () {
this.library = await this.getLibraryLazy(this.libraryId)
},
mounted () {
// fill series skeletons if an index is provided, so scroll position can be restored
if (this.$route.params.index) {
this.series = Array(Number(this.$route.params.index)).fill(null)
} else { // else fill one page of skeletons
this.series = Array(this.pageSize).fill(null)
if (this.$cookies.isKey(cookiePageSize)) {
this.pageSize = Number(this.$cookies.get(cookiePageSize))
}
// restore sort from query param
// restore from query param
this.sortActive = this.parseQuerySortOrDefault(this.$route.query.sort)
// restore filter status from query params
this.filterStatus = this.parseQueryFilterStatus(this.$route.query.status)
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.reloadData(Number(this.$route.params.libraryId), this.series.length)
this.loadLibrary(this.libraryId)
this.setWatches()
},
@ -188,16 +202,14 @@ export default Vue.extend({
if (to.params.libraryId !== from.params.libraryId) {
this.unsetWatches()
this.library = this.getLibraryLazy(Number(to.params.libraryId))
if (to.params.index) {
this.series = Array(Number(to.params.index)).fill(null)
} else { // else fill one page of skeletons
this.series = Array(this.pageSize).fill(null)
}
// reset
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
this.filterStatus = this.parseQueryFilterStatus(to.query.status)
this.reloadData(Number(to.params.libraryId), this.series.length)
this.page = 1
this.totalPages = 1
this.totalElements = null
this.series = []
this.loadLibrary(Number(to.params.libraryId))
this.setWatches()
}
@ -208,34 +220,54 @@ export default Vue.extend({
isAdmin (): boolean {
return this.$store.getters.meAdmin
},
},
methods: {
async updateVisible (val: []) {
for (const i of val) {
const pageNumber = Math.floor(i / this.pageSize)
if (this.pagesState[pageNumber] === undefined || this.pagesState[pageNumber] === LoadState.NotLoaded) {
this.processPage(await this.loadPage(pageNumber, this.libraryId))
}
}
const max = this.$_.max(val) as number | undefined
const index = (max === undefined ? 0 : max).toString()
if (this.$route.params.index !== index) {
this.updateRoute(index)
paginationVisible (): number {
switch (this.$vuetify.breakpoint.name) {
case 'xs':
return 5
case 'sm':
case 'md':
return 10
case 'lg':
case 'xl':
default:
return 15
}
},
},
methods: {
setWatches () {
this.sortUnwatch = this.$watch('sortActive', this.updateRouteAndReload)
this.filterUnwatch = this.$watch('filterStatus', 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.selected = []
this.page = 1
this.updateRoute()
this.reloadData(this.libraryId)
this.loadPage(this.libraryId, this.page, this.sortActive)
this.setWatches()
},
async loadLibrary (libraryId: number) {
this.library = this.getLibraryLazy(libraryId)
await this.loadPage(libraryId, this.page, this.sortActive)
},
parseQuerySortOrDefault (querySort: any): SortActive {
return parseQuerySort(querySort, this.sortOptions) || this.$_.clone(this.sortDefault)
@ -243,52 +275,38 @@ export default Vue.extend({
parseQueryFilterStatus (queryStatus: any): string[] {
return queryStatus ? queryStatus.toString().split(',').filter((x: string) => Object.keys(SeriesStatus).includes(x)) : []
},
reloadData (libraryId: number, countItem?: number) {
this.totalElements = null
this.pagesState = []
this.series = Array(countItem || this.pageSize).fill(null)
this.loadInitialData(libraryId)
},
updateRoute (index?: string) {
updateRoute () {
this.$router.replace({
name: this.$route.name,
params: { libraryId: this.$route.params.libraryId, index: index || this.$route.params.index },
params: { libraryId: this.$route.params.libraryId },
query: {
page: `${this.page}`,
pageSize: `${this.pageSize}`,
sort: `${this.sortActive.key},${this.sortActive.order}`,
status: `${this.filterStatus}`,
},
}).catch(_ => {
})
},
async loadInitialData (libraryId: number, pageToLoad: number = 0) {
this.processPage(await this.loadPage(pageToLoad, libraryId))
},
async loadPage (page: number, libraryId: number): Promise<Page<SeriesDto>> {
this.pagesState[page] = LoadState.Loading
async loadPage (libraryId: number, page: number, sort: SortActive) {
const pageRequest = {
page: page,
page: page - 1,
size: this.pageSize,
} as PageRequest
if (this.sortActive != null) {
pageRequest.sort = [`${this.sortActive.key},${this.sortActive.order}`]
if (sort) {
pageRequest.sort = [`${sort.key},${sort.order}`]
}
let requestLibraryId
if (libraryId !== 0) {
requestLibraryId = libraryId
}
return this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filterStatus)
},
processPage (page: Page<SeriesDto>) {
if (this.totalElements === null) {
// initialize page data
this.totalElements = page.totalElements
this.series = Array(this.totalElements).fill(null)
this.pagesState = Array(page.totalPages).fill(LoadState.NotLoaded)
}
this.series.splice(page.number * page.size, page.size, ...page.content)
this.pagesState[page.number] = LoadState.Loaded
const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filterStatus)
this.totalPages = seriesPage.totalPages
this.totalElements = seriesPage.totalElements
this.series = seriesPage.content
},
getLibraryLazy (libraryId: any): LibraryDto | undefined {
if (libraryId !== 0) {

View file

@ -42,6 +42,9 @@
:sort-options="sortOptions"
:sort-active.sync="sortActive"
/>
<page-size-select v-model="pageSize"/>
</toolbar-sticky>
<v-scroll-y-transition hide-on-leave>
@ -104,11 +107,18 @@
</v-row>
<v-divider class="my-4"/>
<item-browser :items="books" :selected.sync="selected" :edit-function="this.singleEdit" class="px-6"
@update="updateVisible"></item-browser>
<v-pagination
v-model="page"
:total-visible="paginationVisible"
:length="totalPages"
/>
<item-browser :items="books" :selected.sync="selected" :edit-function="this.singleEdit" class="px-6"/>
</v-container>
<edit-series-dialog v-model="dialogEdit"
:series.sync="series"/>
<edit-series-dialog v-model="dialogEdit" :series.sync="series"/>
</div>
</template>
@ -117,24 +127,27 @@ import Badge from '@/components/Badge.vue'
import EditBooksDialog from '@/components/EditBooksDialog.vue'
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
import SortMenuButton from '@/components/SortMenuButton.vue'
import ToolbarSticky from '@/components/ToolbarSticky.vue'
import { parseQuerySort } from '@/functions/query-params'
import { seriesThumbnailUrl } from '@/functions/urls'
import { LoadState } from '@/types/common'
import Vue from 'vue'
const cookiePageSize = 'pagesize'
export default Vue.extend({
name: 'BrowseSeries',
components: { ToolbarSticky, SortMenuButton, Badge, EditSeriesDialog, EditBooksDialog, ItemBrowser },
components: { ToolbarSticky, SortMenuButton, Badge, EditSeriesDialog, EditBooksDialog, ItemBrowser, PageSizeSelect },
data: () => {
return {
series: {} as SeriesDto,
books: [] as BookDto[],
selectedBooks: [] as BookDto[],
editBookSingle: {} as BookDto,
pagesState: [] as LoadState[],
page: 1,
pageSize: 20,
totalPages: 1,
totalElements: null as number | null,
sortOptions: [{ name: 'Number', key: 'metadata.numberSort' }, { name: 'Date added', key: 'createdDate' }, {
name: 'File size',
@ -144,6 +157,8 @@ export default Vue.extend({
sortDefault: { key: 'metadata.numberSort', order: 'asc' } as SortActive,
dialogEdit: false,
sortUnwatch: null as any,
pageUnwatch: null as any,
pageSizeUnwatch: null as any,
selected: [],
dialogEditBooks: false,
dialogEditBookSingle: false,
@ -153,12 +168,22 @@ export default Vue.extend({
isAdmin (): boolean {
return this.$store.getters.meAdmin
},
sortCustom (): boolean {
return this.sortActive.key !== this.sortDefault.key || this.sortActive.order !== this.sortDefault.order
},
thumbnailUrl (): string {
return seriesThumbnailUrl(this.seriesId)
},
paginationVisible (): number {
switch (this.$vuetify.breakpoint.name) {
case 'xs':
return 5
case 'sm':
case 'md':
return 10
case 'lg':
case 'xl':
default:
return 15
}
},
},
props: {
seriesId: {
@ -191,39 +216,32 @@ export default Vue.extend({
}
},
},
async created () {
this.loadSeries()
},
mounted () {
// fill books skeletons if an index is provided, so scroll position can be restored
if (this.$route.params.index) {
this.books = Array(Number(this.$route.params.index)).fill(null)
} else { // else fill one page of skeletons
this.books = Array(this.pageSize).fill(null)
if (this.$cookies.isKey(cookiePageSize)) {
this.pageSize = Number(this.$cookies.get(cookiePageSize))
}
// restore sort from query param
// restore from query param
this.sortActive = this.parseQuerySortOrDefault(this.$route.query.sort)
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.reloadData(Number(this.$route.params.seriesId), this.books.length)
this.loadSeries(this.seriesId)
this.setWatches()
this.loadSeries()
},
async beforeRouteUpdate (to, from, next) {
if (to.params.seriesId !== from.params.seriesId) {
this.unsetWatches()
this.series = await this.$komgaSeries.getOneSeries(Number(to.params.seriesId))
if (to.params.index) {
this.books = Array(Number(to.params.index)).fill(null)
} else { // else fill one page of skeletons
this.books = Array(this.pageSize).fill(null)
}
// reset
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
this.reloadData(Number(to.params.seriesId), this.books.length)
this.page = 1
this.totalPages = 1
this.totalElements = null
this.books = []
this.loadSeries(Number(to.params.seriesId))
this.setWatches()
}
@ -231,33 +249,37 @@ export default Vue.extend({
next()
},
methods: {
async updateVisible (val: []) {
for (const i of val) {
const pageNumber = Math.floor(i / this.pageSize)
if (this.pagesState[pageNumber] === undefined || this.pagesState[pageNumber] === LoadState.NotLoaded) {
this.processPage(await this.loadPage(pageNumber, this.seriesId))
}
}
const max = this.$_.max(val) as number | undefined
const index = (max === undefined ? 0 : max).toString()
if (this.$route.params.index !== index) {
this.updateRoute(index)
}
},
setWatches () {
this.sortUnwatch = this.$watch('sortActive', 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.seriesId, val, this.sortActive)
})
},
unsetWatches () {
this.sortUnwatch()
this.pageUnwatch()
this.pageSizeUnwatch()
},
updateRouteAndReload () {
this.unsetWatches()
this.selected = []
this.page = 1
this.updateRoute()
this.reloadData(this.seriesId)
this.loadPage(this.seriesId, this.page, this.sortActive)
this.setWatches()
},
async loadSeries () {
this.series = await this.$komgaSeries.getOneSeries(this.seriesId)
async loadSeries (seriesId: number) {
this.series = await this.$komgaSeries.getOneSeries(seriesId)
await this.loadPage(seriesId, this.page, this.sortActive)
},
parseQuerySortOrDefault (querySort: any): SortActive {
return parseQuerySort(querySort, this.sortOptions) || this.$_.clone(this.sortDefault)
@ -265,43 +287,29 @@ export default Vue.extend({
updateRoute (index?: string) {
this.$router.replace({
name: this.$route.name,
params: { seriesId: this.$route.params.seriesId, index: index || this.$route.params.index },
params: { seriesId: this.$route.params.seriesId },
query: {
page: `${this.page}`,
pageSize: `${this.pageSize}`,
sort: `${this.sortActive.key},${this.sortActive.order}`,
},
}).catch(_ => {
})
},
reloadData (seriesId: number, countItem?: number) {
this.totalElements = null
this.pagesState = []
this.books = Array(countItem || this.pageSize).fill(null)
this.loadInitialData(seriesId)
},
async loadInitialData (seriesId: number, pageToLoad: number = 0) {
this.processPage(await this.loadPage(pageToLoad, seriesId))
},
async loadPage (page: number, seriesId: number): Promise<Page<BookDto>> {
this.pagesState[page] = LoadState.Loading
async loadPage (seriesId: number, page: number, sort: SortActive) {
const pageRequest = {
page: page,
page: page - 1,
size: this.pageSize,
} as PageRequest
if (this.sortActive != null) {
pageRequest.sort = [`${this.sortActive.key},${this.sortActive.order}`]
if (sort) {
pageRequest.sort = [`${sort.key},${sort.order}`]
}
return this.$komgaSeries.getBooks(seriesId, pageRequest)
},
processPage (page: Page<BookDto>) {
if (this.totalElements === null) {
// initialize page data
this.totalElements = page.totalElements
this.books = Array(this.totalElements).fill(null)
this.pagesState = Array(page.totalPages).fill(LoadState.NotLoaded)
}
this.books.splice(page.number * page.size, page.size, ...page.content)
this.pagesState[page.number] = LoadState.Loaded
const booksPage = await this.$komgaSeries.getBooks(seriesId, pageRequest)
this.totalPages = booksPage.totalPages
this.totalElements = booksPage.totalElements
this.books = booksPage.content
},
analyze () {
this.$komgaSeries.analyzeSeries(this.series)

View file

@ -75,11 +75,11 @@
</template>
<script lang="ts">
import HorizontalScroller from '@/components/HorizontalScroller.vue'
import Vue from 'vue'
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
import EditBooksDialog from '@/components/EditBooksDialog.vue'
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
import HorizontalScroller from '@/components/HorizontalScroller.vue'
import ItemCard from '@/components/ItemCard.vue'
import Vue from 'vue'
export default Vue.extend({
name: 'Dashboard',
@ -98,13 +98,9 @@ export default Vue.extend({
}
},
mounted () {
if (this.$store.state.komgaLibraries.libraries.length === 0) {
this.$router.push({ name: 'welcome' })
} else {
this.loadNewSeries()
this.loadUpdatedSeries()
this.loadLatestBooks()
}
this.loadNewSeries()
this.loadUpdatedSeries()
this.loadLatestBooks()
},
watch: {
editSeriesSingle (val: SeriesDto) {