mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02:00
parent
75b72164fe
commit
5867db77f5
8 changed files with 286 additions and 241 deletions
|
|
@ -1,20 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<v-item-group multiple v-model="selectedItems">
|
<v-item-group multiple v-model="selectedItems">
|
||||||
<v-row justify="start" ref="content" v-resize="onResize" v-if="hasItems">
|
<v-row justify="start" ref="content" v-resize="onResize" v-if="hasItems">
|
||||||
<v-skeleton-loader v-for="(item, index) in items"
|
<v-item
|
||||||
|
v-for="(item, index) in items"
|
||||||
:key="index"
|
:key="index"
|
||||||
:width="itemWidth"
|
class="my-3 mx-2"
|
||||||
:height="itemHeight"
|
v-slot:default="{ toggle, active }" :value="$_.get(item, 'id', 0)"
|
||||||
justify-self="start"
|
|
||||||
:loading="item === null"
|
|
||||||
type="card, text"
|
|
||||||
class="ma-3 mx-2"
|
|
||||||
:data-index="index"
|
|
||||||
v-intersect="onElementIntersect"
|
|
||||||
|
|
||||||
>
|
>
|
||||||
<v-item v-slot:default="{ toggle, active }" :value="$_.get(item, 'id', 0)">
|
<slot name="item"
|
||||||
<slot name="item" v-bind:data="{ toggle, active, item, index, itemWidth, preselect: shouldPreselect(), editItem }">
|
v-bind:data="{ toggle, active, item, index, itemWidth, preselect: shouldPreselect(), editItem }">
|
||||||
<item-card
|
<item-card
|
||||||
:item="item"
|
:item="item"
|
||||||
:width="itemWidth"
|
:width="itemWidth"
|
||||||
|
|
@ -25,7 +19,6 @@
|
||||||
></item-card>
|
></item-card>
|
||||||
</slot>
|
</slot>
|
||||||
</v-item>
|
</v-item>
|
||||||
</v-skeleton-loader>
|
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-row v-else justify="center">
|
<v-row v-else justify="center">
|
||||||
<slot name="empty"></slot>
|
<slot name="empty"></slot>
|
||||||
|
|
@ -34,13 +27,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from 'vue'
|
|
||||||
import { computeCardWidth } from '@/functions/grid-utilities'
|
|
||||||
import ItemCard from '@/components/ItemCard.vue'
|
import ItemCard from '@/components/ItemCard.vue'
|
||||||
import mixins from 'vue-typed-mixins'
|
import { computeCardWidth } from '@/functions/grid-utilities'
|
||||||
import VisibleElements from '@/mixins/VisibleElements'
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default mixins(VisibleElements).extend({
|
export default Vue.extend({
|
||||||
name: 'ItemBrowser',
|
name: 'ItemBrowser',
|
||||||
components: { ItemCard },
|
components: { ItemCard },
|
||||||
props: {
|
props: {
|
||||||
|
|
@ -66,12 +57,6 @@ export default mixins(VisibleElements).extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
series: {
|
|
||||||
handler () {
|
|
||||||
this.visibleElements = []
|
|
||||||
},
|
|
||||||
immediate: true,
|
|
||||||
},
|
|
||||||
selectedItems: {
|
selectedItems: {
|
||||||
handler () {
|
handler () {
|
||||||
this.$emit('update:selected', this.selectedItems)
|
this.$emit('update:selected', this.selectedItems)
|
||||||
|
|
|
||||||
57
komga-webui/src/components/PageSizeSelect.vue
Normal file
57
komga-webui/src/components/PageSizeSelect.vue
Normal 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>
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -5,13 +5,19 @@ import store from './store'
|
||||||
|
|
||||||
Vue.use(Router)
|
Vue.use(Router)
|
||||||
|
|
||||||
const lStore = store
|
const lStore = store as any
|
||||||
|
|
||||||
const adminGuard = (to: any, from: any, next: any) => {
|
const adminGuard = (to: any, from: any, next: any) => {
|
||||||
if (!lStore.getters.meAdmin) next({ name: 'home' })
|
if (!lStore.getters.meAdmin) next({ name: 'home' })
|
||||||
else next()
|
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({
|
const router = new Router({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
base: urls.base,
|
base: urls.base,
|
||||||
|
|
@ -36,6 +42,7 @@ const router = new Router({
|
||||||
{
|
{
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
name: 'dashboard',
|
name: 'dashboard',
|
||||||
|
beforeEnter: noLibraryGuard,
|
||||||
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue'),
|
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -71,13 +78,14 @@ const router = new Router({
|
||||||
component: () => import(/* webpackChunkName: "account" */ './views/AccountSettings.vue'),
|
component: () => import(/* webpackChunkName: "account" */ './views/AccountSettings.vue'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/libraries/:libraryId/:index?',
|
path: '/libraries/:libraryId',
|
||||||
name: 'browse-libraries',
|
name: 'browse-libraries',
|
||||||
|
beforeEnter: noLibraryGuard,
|
||||||
component: () => import(/* webpackChunkName: "browse-libraries" */ './views/BrowseLibraries.vue'),
|
component: () => import(/* webpackChunkName: "browse-libraries" */ './views/BrowseLibraries.vue'),
|
||||||
props: (route) => ({ libraryId: Number(route.params.libraryId) }),
|
props: (route) => ({ libraryId: Number(route.params.libraryId) }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/series/:seriesId/:index?',
|
path: '/series/:seriesId',
|
||||||
name: 'browse-series',
|
name: 'browse-series',
|
||||||
component: () => import(/* webpackChunkName: "browse-series" */ './views/BrowseSeries.vue'),
|
component: () => import(/* webpackChunkName: "browse-series" */ './views/BrowseSeries.vue'),
|
||||||
props: (route) => ({ seriesId: Number(route.params.seriesId) }),
|
props: (route) => ({ seriesId: Number(route.params.seriesId) }),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
export enum LoadState {
|
|
||||||
Loaded,
|
|
||||||
NotLoaded,
|
|
||||||
Loading
|
|
||||||
}
|
|
||||||
|
|
@ -42,6 +42,9 @@
|
||||||
:sort-options="sortOptions"
|
:sort-options="sortOptions"
|
||||||
:sort-active.sync="sortActive"
|
:sort-active.sync="sortActive"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<page-size-select v-model="pageSize"/>
|
||||||
|
|
||||||
</toolbar-sticky>
|
</toolbar-sticky>
|
||||||
|
|
||||||
<v-scroll-y-transition hide-on-leave>
|
<v-scroll-y-transition hide-on-leave>
|
||||||
|
|
@ -69,57 +72,72 @@
|
||||||
:series.sync="editSeriesSingle"
|
:series.sync="editSeriesSingle"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<item-browser :items="series"
|
<v-container>
|
||||||
:selected.sync="selected"
|
<empty-state
|
||||||
:edit-function="this.singleEdit"
|
v-if="totalPages === 0"
|
||||||
class="px-6"
|
title="The active filter has no matches"
|
||||||
@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"
|
sub-title="Use the menu above to change the active filter"
|
||||||
icon="mdi-book-multiple"
|
icon="mdi-book-multiple"
|
||||||
icon-color="secondary"
|
icon-color="secondary"
|
||||||
>
|
>
|
||||||
<v-btn @click="filterStatus = []">Clear filter</v-btn>
|
<v-btn @click="filterStatus = []">Clear filter</v-btn>
|
||||||
</empty-state>
|
</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>
|
||||||
<template #empty v-else>
|
</v-container>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Badge from '@/components/Badge.vue'
|
import Badge from '@/components/Badge.vue'
|
||||||
import EmptyState from '@/components/EmptyState.vue'
|
|
||||||
import EditSeriesDialog from '@/components/EditSeriesDialog.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 LibraryActionsMenu from '@/components/LibraryActionsMenu.vue'
|
||||||
|
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||||
import SortMenuButton from '@/components/SortMenuButton.vue'
|
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 { LoadState } from '@/types/common'
|
|
||||||
import { SeriesStatus } from '@/types/enum-series'
|
import { SeriesStatus } from '@/types/enum-series'
|
||||||
import ItemBrowser from '@/components/ItemBrowser.vue'
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
const cookiePageSize = 'pagesize'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'BrowseLibraries',
|
name: 'BrowseLibraries',
|
||||||
components: { LibraryActionsMenu, EmptyState, ToolbarSticky, SortMenuButton, Badge, EditSeriesDialog, ItemBrowser },
|
components: {
|
||||||
|
LibraryActionsMenu,
|
||||||
|
EmptyState,
|
||||||
|
ToolbarSticky,
|
||||||
|
SortMenuButton,
|
||||||
|
Badge,
|
||||||
|
EditSeriesDialog,
|
||||||
|
ItemBrowser,
|
||||||
|
PageSizeSelect,
|
||||||
|
},
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
library: undefined as LibraryDto | undefined,
|
library: undefined as LibraryDto | undefined,
|
||||||
series: [] as SeriesDto[],
|
series: [] as SeriesDto[],
|
||||||
selectedSeries: [] as SeriesDto[],
|
selectedSeries: [] as SeriesDto[],
|
||||||
editSeriesSingle: {} as SeriesDto,
|
editSeriesSingle: {} as SeriesDto,
|
||||||
pagesState: [] as LoadState[],
|
page: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
totalPages: 1,
|
||||||
totalElements: null as number | null,
|
totalElements: null as number | null,
|
||||||
sortOptions: [
|
sortOptions: [
|
||||||
{ name: 'Name', key: 'metadata.titleSort' },
|
{ name: 'Name', key: 'metadata.titleSort' },
|
||||||
|
|
@ -132,6 +150,8 @@ export default Vue.extend({
|
||||||
SeriesStatus,
|
SeriesStatus,
|
||||||
sortUnwatch: null as any,
|
sortUnwatch: null as any,
|
||||||
filterUnwatch: null as any,
|
filterUnwatch: null as any,
|
||||||
|
pageUnwatch: null as any,
|
||||||
|
pageSizeUnwatch: null as any,
|
||||||
selected: [],
|
selected: [],
|
||||||
dialogEdit: false,
|
dialogEdit: false,
|
||||||
dialogEditSingle: false,
|
dialogEditSingle: false,
|
||||||
|
|
@ -163,24 +183,18 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async created () {
|
|
||||||
this.library = await this.getLibraryLazy(this.libraryId)
|
|
||||||
},
|
|
||||||
mounted () {
|
mounted () {
|
||||||
// fill series skeletons if an index is provided, so scroll position can be restored
|
if (this.$cookies.isKey(cookiePageSize)) {
|
||||||
if (this.$route.params.index) {
|
this.pageSize = Number(this.$cookies.get(cookiePageSize))
|
||||||
this.series = Array(Number(this.$route.params.index)).fill(null)
|
|
||||||
} else { // else fill one page of skeletons
|
|
||||||
this.series = Array(this.pageSize).fill(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore sort from query param
|
// restore from query param
|
||||||
this.sortActive = this.parseQuerySortOrDefault(this.$route.query.sort)
|
this.sortActive = this.parseQuerySortOrDefault(this.$route.query.sort)
|
||||||
|
|
||||||
// restore filter status from query params
|
|
||||||
this.filterStatus = this.parseQueryFilterStatus(this.$route.query.status)
|
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()
|
this.setWatches()
|
||||||
},
|
},
|
||||||
|
|
@ -188,16 +202,14 @@ export default Vue.extend({
|
||||||
if (to.params.libraryId !== from.params.libraryId) {
|
if (to.params.libraryId !== from.params.libraryId) {
|
||||||
this.unsetWatches()
|
this.unsetWatches()
|
||||||
|
|
||||||
this.library = this.getLibraryLazy(Number(to.params.libraryId))
|
// reset
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
|
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
|
||||||
this.filterStatus = this.parseQueryFilterStatus(to.query.status)
|
this.page = 1
|
||||||
this.reloadData(Number(to.params.libraryId), this.series.length)
|
this.totalPages = 1
|
||||||
|
this.totalElements = null
|
||||||
|
this.series = []
|
||||||
|
|
||||||
|
this.loadLibrary(Number(to.params.libraryId))
|
||||||
|
|
||||||
this.setWatches()
|
this.setWatches()
|
||||||
}
|
}
|
||||||
|
|
@ -208,34 +220,54 @@ export default Vue.extend({
|
||||||
isAdmin (): boolean {
|
isAdmin (): boolean {
|
||||||
return this.$store.getters.meAdmin
|
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
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setWatches () {
|
setWatches () {
|
||||||
this.sortUnwatch = this.$watch('sortActive', this.updateRouteAndReload)
|
this.sortUnwatch = this.$watch('sortActive', this.updateRouteAndReload)
|
||||||
this.filterUnwatch = this.$watch('filterStatus', 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 () {
|
unsetWatches () {
|
||||||
this.sortUnwatch()
|
this.sortUnwatch()
|
||||||
this.filterUnwatch()
|
this.filterUnwatch()
|
||||||
|
this.pageUnwatch()
|
||||||
|
this.pageSizeUnwatch()
|
||||||
},
|
},
|
||||||
updateRouteAndReload () {
|
updateRouteAndReload () {
|
||||||
|
this.unsetWatches()
|
||||||
|
|
||||||
|
this.selected = []
|
||||||
|
this.page = 1
|
||||||
|
|
||||||
this.updateRoute()
|
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 {
|
parseQuerySortOrDefault (querySort: any): SortActive {
|
||||||
return parseQuerySort(querySort, this.sortOptions) || this.$_.clone(this.sortDefault)
|
return parseQuerySort(querySort, this.sortOptions) || this.$_.clone(this.sortDefault)
|
||||||
|
|
@ -243,52 +275,38 @@ export default Vue.extend({
|
||||||
parseQueryFilterStatus (queryStatus: any): string[] {
|
parseQueryFilterStatus (queryStatus: any): string[] {
|
||||||
return queryStatus ? queryStatus.toString().split(',').filter((x: string) => Object.keys(SeriesStatus).includes(x)) : []
|
return queryStatus ? queryStatus.toString().split(',').filter((x: string) => Object.keys(SeriesStatus).includes(x)) : []
|
||||||
},
|
},
|
||||||
reloadData (libraryId: number, countItem?: number) {
|
updateRoute () {
|
||||||
this.totalElements = null
|
|
||||||
this.pagesState = []
|
|
||||||
this.series = Array(countItem || this.pageSize).fill(null)
|
|
||||||
this.loadInitialData(libraryId)
|
|
||||||
},
|
|
||||||
updateRoute (index?: string) {
|
|
||||||
this.$router.replace({
|
this.$router.replace({
|
||||||
name: this.$route.name,
|
name: this.$route.name,
|
||||||
params: { libraryId: this.$route.params.libraryId, index: index || this.$route.params.index },
|
params: { libraryId: this.$route.params.libraryId },
|
||||||
query: {
|
query: {
|
||||||
|
page: `${this.page}`,
|
||||||
|
pageSize: `${this.pageSize}`,
|
||||||
sort: `${this.sortActive.key},${this.sortActive.order}`,
|
sort: `${this.sortActive.key},${this.sortActive.order}`,
|
||||||
status: `${this.filterStatus}`,
|
status: `${this.filterStatus}`,
|
||||||
},
|
},
|
||||||
}).catch(_ => {
|
}).catch(_ => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async loadInitialData (libraryId: number, pageToLoad: number = 0) {
|
async loadPage (libraryId: number, page: number, sort: SortActive) {
|
||||||
this.processPage(await this.loadPage(pageToLoad, libraryId))
|
|
||||||
},
|
|
||||||
async loadPage (page: number, libraryId: number): Promise<Page<SeriesDto>> {
|
|
||||||
this.pagesState[page] = LoadState.Loading
|
|
||||||
const pageRequest = {
|
const pageRequest = {
|
||||||
page: page,
|
page: page - 1,
|
||||||
size: this.pageSize,
|
size: this.pageSize,
|
||||||
} as PageRequest
|
} as PageRequest
|
||||||
|
|
||||||
if (this.sortActive != null) {
|
if (sort) {
|
||||||
pageRequest.sort = [`${this.sortActive.key},${this.sortActive.order}`]
|
pageRequest.sort = [`${sort.key},${sort.order}`]
|
||||||
}
|
}
|
||||||
|
|
||||||
let requestLibraryId
|
let requestLibraryId
|
||||||
if (libraryId !== 0) {
|
if (libraryId !== 0) {
|
||||||
requestLibraryId = libraryId
|
requestLibraryId = libraryId
|
||||||
}
|
}
|
||||||
return this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filterStatus)
|
const seriesPage = await this.$komgaSeries.getSeries(requestLibraryId, pageRequest, undefined, this.filterStatus)
|
||||||
},
|
|
||||||
processPage (page: Page<SeriesDto>) {
|
this.totalPages = seriesPage.totalPages
|
||||||
if (this.totalElements === null) {
|
this.totalElements = seriesPage.totalElements
|
||||||
// initialize page data
|
this.series = seriesPage.content
|
||||||
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
|
|
||||||
},
|
},
|
||||||
getLibraryLazy (libraryId: any): LibraryDto | undefined {
|
getLibraryLazy (libraryId: any): LibraryDto | undefined {
|
||||||
if (libraryId !== 0) {
|
if (libraryId !== 0) {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@
|
||||||
:sort-options="sortOptions"
|
:sort-options="sortOptions"
|
||||||
:sort-active.sync="sortActive"
|
:sort-active.sync="sortActive"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<page-size-select v-model="pageSize"/>
|
||||||
|
|
||||||
</toolbar-sticky>
|
</toolbar-sticky>
|
||||||
|
|
||||||
<v-scroll-y-transition hide-on-leave>
|
<v-scroll-y-transition hide-on-leave>
|
||||||
|
|
@ -104,11 +107,18 @@
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<v-divider class="my-4"/>
|
<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>
|
</v-container>
|
||||||
<edit-series-dialog v-model="dialogEdit"
|
|
||||||
:series.sync="series"/>
|
<edit-series-dialog v-model="dialogEdit" :series.sync="series"/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -117,24 +127,27 @@ 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 ItemBrowser from '@/components/ItemBrowser.vue'
|
import ItemBrowser from '@/components/ItemBrowser.vue'
|
||||||
|
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||||
import SortMenuButton from '@/components/SortMenuButton.vue'
|
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 { LoadState } from '@/types/common'
|
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
const cookiePageSize = 'pagesize'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'BrowseSeries',
|
name: 'BrowseSeries',
|
||||||
components: { ToolbarSticky, SortMenuButton, Badge, EditSeriesDialog, EditBooksDialog, ItemBrowser },
|
components: { ToolbarSticky, SortMenuButton, Badge, EditSeriesDialog, EditBooksDialog, ItemBrowser, PageSizeSelect },
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
series: {} as SeriesDto,
|
series: {} as SeriesDto,
|
||||||
books: [] as BookDto[],
|
books: [] as BookDto[],
|
||||||
selectedBooks: [] as BookDto[],
|
selectedBooks: [] as BookDto[],
|
||||||
editBookSingle: {} as BookDto,
|
editBookSingle: {} as BookDto,
|
||||||
pagesState: [] as LoadState[],
|
page: 1,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
|
totalPages: 1,
|
||||||
totalElements: null as number | null,
|
totalElements: null as number | null,
|
||||||
sortOptions: [{ name: 'Number', key: 'metadata.numberSort' }, { name: 'Date added', key: 'createdDate' }, {
|
sortOptions: [{ name: 'Number', key: 'metadata.numberSort' }, { name: 'Date added', key: 'createdDate' }, {
|
||||||
name: 'File size',
|
name: 'File size',
|
||||||
|
|
@ -144,6 +157,8 @@ export default Vue.extend({
|
||||||
sortDefault: { key: 'metadata.numberSort', order: 'asc' } as SortActive,
|
sortDefault: { key: 'metadata.numberSort', order: 'asc' } as SortActive,
|
||||||
dialogEdit: false,
|
dialogEdit: false,
|
||||||
sortUnwatch: null as any,
|
sortUnwatch: null as any,
|
||||||
|
pageUnwatch: null as any,
|
||||||
|
pageSizeUnwatch: null as any,
|
||||||
selected: [],
|
selected: [],
|
||||||
dialogEditBooks: false,
|
dialogEditBooks: false,
|
||||||
dialogEditBookSingle: false,
|
dialogEditBookSingle: false,
|
||||||
|
|
@ -153,12 +168,22 @@ export default Vue.extend({
|
||||||
isAdmin (): boolean {
|
isAdmin (): boolean {
|
||||||
return this.$store.getters.meAdmin
|
return this.$store.getters.meAdmin
|
||||||
},
|
},
|
||||||
sortCustom (): boolean {
|
|
||||||
return this.sortActive.key !== this.sortDefault.key || this.sortActive.order !== this.sortDefault.order
|
|
||||||
},
|
|
||||||
thumbnailUrl (): string {
|
thumbnailUrl (): string {
|
||||||
return seriesThumbnailUrl(this.seriesId)
|
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: {
|
props: {
|
||||||
seriesId: {
|
seriesId: {
|
||||||
|
|
@ -191,39 +216,32 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async created () {
|
|
||||||
this.loadSeries()
|
|
||||||
},
|
|
||||||
mounted () {
|
mounted () {
|
||||||
// fill books skeletons if an index is provided, so scroll position can be restored
|
if (this.$cookies.isKey(cookiePageSize)) {
|
||||||
if (this.$route.params.index) {
|
this.pageSize = Number(this.$cookies.get(cookiePageSize))
|
||||||
this.books = Array(Number(this.$route.params.index)).fill(null)
|
|
||||||
} else { // else fill one page of skeletons
|
|
||||||
this.books = Array(this.pageSize).fill(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// restore sort from query param
|
// restore from query param
|
||||||
this.sortActive = this.parseQuerySortOrDefault(this.$route.query.sort)
|
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.setWatches()
|
||||||
this.loadSeries()
|
|
||||||
},
|
},
|
||||||
async beforeRouteUpdate (to, from, next) {
|
async beforeRouteUpdate (to, from, next) {
|
||||||
if (to.params.seriesId !== from.params.seriesId) {
|
if (to.params.seriesId !== from.params.seriesId) {
|
||||||
this.unsetWatches()
|
this.unsetWatches()
|
||||||
|
|
||||||
this.series = await this.$komgaSeries.getOneSeries(Number(to.params.seriesId))
|
// reset
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sortActive = this.parseQuerySortOrDefault(to.query.sort)
|
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()
|
this.setWatches()
|
||||||
}
|
}
|
||||||
|
|
@ -231,33 +249,37 @@ export default Vue.extend({
|
||||||
next()
|
next()
|
||||||
},
|
},
|
||||||
methods: {
|
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 () {
|
setWatches () {
|
||||||
this.sortUnwatch = this.$watch('sortActive', this.updateRouteAndReload)
|
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 () {
|
unsetWatches () {
|
||||||
this.sortUnwatch()
|
this.sortUnwatch()
|
||||||
|
this.pageUnwatch()
|
||||||
|
this.pageSizeUnwatch()
|
||||||
},
|
},
|
||||||
updateRouteAndReload () {
|
updateRouteAndReload () {
|
||||||
|
this.unsetWatches()
|
||||||
|
|
||||||
|
this.selected = []
|
||||||
|
this.page = 1
|
||||||
|
|
||||||
this.updateRoute()
|
this.updateRoute()
|
||||||
this.reloadData(this.seriesId)
|
this.loadPage(this.seriesId, this.page, this.sortActive)
|
||||||
|
|
||||||
|
this.setWatches()
|
||||||
},
|
},
|
||||||
async loadSeries () {
|
async loadSeries (seriesId: number) {
|
||||||
this.series = await this.$komgaSeries.getOneSeries(this.seriesId)
|
this.series = await this.$komgaSeries.getOneSeries(seriesId)
|
||||||
|
await this.loadPage(seriesId, this.page, this.sortActive)
|
||||||
},
|
},
|
||||||
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)
|
||||||
|
|
@ -265,43 +287,29 @@ export default Vue.extend({
|
||||||
updateRoute (index?: string) {
|
updateRoute (index?: string) {
|
||||||
this.$router.replace({
|
this.$router.replace({
|
||||||
name: this.$route.name,
|
name: this.$route.name,
|
||||||
params: { seriesId: this.$route.params.seriesId, index: index || this.$route.params.index },
|
params: { seriesId: this.$route.params.seriesId },
|
||||||
query: {
|
query: {
|
||||||
|
page: `${this.page}`,
|
||||||
|
pageSize: `${this.pageSize}`,
|
||||||
sort: `${this.sortActive.key},${this.sortActive.order}`,
|
sort: `${this.sortActive.key},${this.sortActive.order}`,
|
||||||
},
|
},
|
||||||
}).catch(_ => {
|
}).catch(_ => {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
reloadData (seriesId: number, countItem?: number) {
|
async loadPage (seriesId: number, page: number, sort: SortActive) {
|
||||||
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
|
|
||||||
const pageRequest = {
|
const pageRequest = {
|
||||||
page: page,
|
page: page - 1,
|
||||||
size: this.pageSize,
|
size: this.pageSize,
|
||||||
} as PageRequest
|
} as PageRequest
|
||||||
|
|
||||||
if (this.sortActive != null) {
|
if (sort) {
|
||||||
pageRequest.sort = [`${this.sortActive.key},${this.sortActive.order}`]
|
pageRequest.sort = [`${sort.key},${sort.order}`]
|
||||||
}
|
}
|
||||||
return this.$komgaSeries.getBooks(seriesId, pageRequest)
|
const booksPage = await this.$komgaSeries.getBooks(seriesId, pageRequest)
|
||||||
},
|
|
||||||
processPage (page: Page<BookDto>) {
|
this.totalPages = booksPage.totalPages
|
||||||
if (this.totalElements === null) {
|
this.totalElements = booksPage.totalElements
|
||||||
// initialize page data
|
this.books = booksPage.content
|
||||||
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
|
|
||||||
},
|
},
|
||||||
analyze () {
|
analyze () {
|
||||||
this.$komgaSeries.analyzeSeries(this.series)
|
this.$komgaSeries.analyzeSeries(this.series)
|
||||||
|
|
|
||||||
|
|
@ -75,11 +75,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 EditBooksDialog from '@/components/EditBooksDialog.vue'
|
||||||
|
import EditSeriesDialog from '@/components/EditSeriesDialog.vue'
|
||||||
|
import HorizontalScroller from '@/components/HorizontalScroller.vue'
|
||||||
import ItemCard from '@/components/ItemCard.vue'
|
import ItemCard from '@/components/ItemCard.vue'
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
|
|
@ -98,13 +98,9 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
if (this.$store.state.komgaLibraries.libraries.length === 0) {
|
|
||||||
this.$router.push({ name: 'welcome' })
|
|
||||||
} else {
|
|
||||||
this.loadNewSeries()
|
this.loadNewSeries()
|
||||||
this.loadUpdatedSeries()
|
this.loadUpdatedSeries()
|
||||||
this.loadLatestBooks()
|
this.loadLatestBooks()
|
||||||
}
|
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
editSeriesSingle (val: SeriesDto) {
|
editSeriesSingle (val: SeriesDto) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue