add browsing for all libraries

add browsing by series
add badge with number of books on series card
This commit is contained in:
Gauthier Roebroeck 2019-11-19 18:04:30 +08:00
parent b687f7d161
commit cc6d581b4e
12 changed files with 280 additions and 15 deletions

View file

@ -15834,6 +15834,11 @@
"vue-template-es2015-compiler": "^1.6.0"
}
},
"vue-line-clamp": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/vue-line-clamp/-/vue-line-clamp-1.3.2.tgz",
"integrity": "sha512-SuZlRDBKsTlMOSLsJsceJPM7Sd2wcaKayyMEsf65b33chuK1zgTIU1RqlbAXSxO2R+FOQV5rYHLiAopdwVjroQ=="
},
"vue-loader": {
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.7.2.tgz",

View file

@ -15,6 +15,7 @@
"qs": "^6.9.1",
"vue": "^2.6.10",
"vue-infinite-loading": "^2.4.4",
"vue-line-clamp": "^1.3.2",
"vue-router": "^3.0.3",
"vuelidate": "^0.7.4",
"vuetify": "^2.1.10",

View file

@ -1,5 +1,7 @@
<template>
<div>
<div class="display-1">{{ libraryName }}</div>
<v-container fluid>
<v-row justify="start">
@ -7,7 +9,7 @@
:key="s.id"
justify-self="start"
:series="s"
class="ma-3"
class="ma-3 ml-2 mr-2"
></card-series>
</v-row>
@ -40,20 +42,25 @@ export default Vue.extend({
return {
series: [] as SeriesDto[],
seriesPage: {} as Page<SeriesDto>,
infiniteId: +new Date()
infiniteId: +new Date(),
libraryName: ''
}
},
props: {
libraryId: {
type: Number,
required: true
required: false
}
},
created () {
this.libraryName = this.getLibraryName()
},
watch: {
libraryId (val) {
this.series = []
this.seriesPage = {} as Page<SeriesDto>
this.infiniteId += 1
this.libraryName = this.getLibraryName()
}
},
mounted (): void {
@ -71,12 +78,19 @@ export default Vue.extend({
if (this.$_.get(this.seriesPage, 'last', false) !== true) {
const pageRequest = {
page: this.$_.get(this.seriesPage, 'number', -1) + 1,
size: 20
size: 50
} as PageRequest
this.seriesPage = await this.$komgaSeries.getSeries(this.libraryId, pageRequest)
this.series = this.series.concat(this.seriesPage.content)
}
},
getLibraryName (): string {
if (this.libraryId) {
return this.$store.getters.getLibraryById(this.libraryId).name
} else {
return 'All libraries'
}
}
}
})

View file

@ -0,0 +1,94 @@
<template>
<div>
<div class="display-1">{{ $_.get(series, 'name', '') }}</div>
<v-container fluid>
<v-row justify="start">
<card-book v-for="b in books"
:key="b.id"
justify-self="start"
:book="b"
class="ma-3 ml-2 mr-2"
></card-book>
</v-row>
</v-container>
<infinite-loading @infinite="infiniteHandler"
:identifier="infiniteId"
>
<div slot="spinner">
<v-progress-circular
indeterminate
color="primary"
></v-progress-circular>
</div>
<div slot="no-more"></div>
<div slot="no-results"></div>
</infinite-loading>
</div>
</template>
<script lang="ts">
import CardBook from '@/components/CardBook.vue'
import Vue from 'vue'
import InfiniteLoading from 'vue-infinite-loading'
export default Vue.extend({
name: 'BrowseSeries',
components: { CardBook, InfiniteLoading },
data: () => {
return {
series: {} as SeriesDto,
books: [] as BookDto[],
booksPage: {} as Page<BookDto>,
infiniteId: +new Date()
}
},
async created () {
this.series = await this.$komgaSeries.getOneSeries(this.seriesId)
},
props: {
seriesId: {
type: Number,
required: true
}
},
watch: {
async seriesId (val) {
this.series = await this.$komgaSeries.getOneSeries(this.seriesId)
this.books = []
this.booksPage = {} as Page<BookDto>
this.infiniteId += 1
}
},
mounted (): void {
},
methods: {
async infiniteHandler ($state: any) {
await this.loadNextPage()
if (this.booksPage.last) {
$state.complete()
} else {
$state.loaded()
}
},
async loadNextPage () {
if (this.$_.get(this.booksPage, 'last', false) !== true) {
const pageRequest = {
page: this.$_.get(this.booksPage, 'number', -1) + 1,
size: 20
} as PageRequest
this.booksPage = await this.$komgaSeries.getBooks(this.seriesId, pageRequest)
this.books = this.books.concat(this.booksPage.content)
}
}
}
})
</script>
<style scoped>
</style>

View file

@ -0,0 +1,74 @@
<template>
<v-card :width="width"
>
<v-img
:src="getThumbnailUrl()"
lazy-src="../assets/cover.svg"
aspect-ratio="0.7071"
>
<span class="white--text pa-1 px-2 subtitle-2"
style="background: darkorange; position: absolute; right: 0"
>
{{ getFormat() }}
</span>
</v-img>
<v-card-subtitle class="pa-2 pb-1 text--primary"
v-line-clamp="2"
>
{{ book.name }}
</v-card-subtitle>
<v-card-text class="px-2"
>
<div>{{ book.size }}</div>
<div v-if="book.metadata.pagesCount === 1">{{ book.metadata.pagesCount }} page</div>
<div v-else>{{ book.metadata.pagesCount }} pages</div>
</v-card-text>
</v-card>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'CardBook',
data: () => {
return {
baseURL: process.env.VUE_APP_KOMGA_API_URL ? process.env.VUE_APP_KOMGA_API_URL : window.location.origin
}
},
props: {
book: {
type: Object as () => BookDto,
required: true
},
width: {
type: [String, Number],
required: false,
default: 150
}
},
methods: {
getThumbnailUrl () {
return `${this.baseURL}/api/v1/series/${this.book.seriesId}/books/${this.book.id}/thumbnail`
},
getFormat () {
switch (this.book.metadata.mediaType) {
case 'application/x-rar-compressed':
return 'CBR'
case 'application/zip':
return 'CBZ'
case 'application/pdf':
return 'PDF'
default:
return 'UNKNOWN'
}
}
}
})
</script>
<style scoped>
</style>

View file

@ -1,19 +1,32 @@
<template>
<v-card :width="width"
:to="{name:'browse-series', params: {seriesId: series.id}}"
>
<v-img
:src="getThumbnailUrl()"
lazy-src="../assets/cover.svg"
aspect-ratio="0.7071"
>
<span class="white--text pa-1 px-2 subtitle-2"
style="background: darkorange; position: absolute; right: 0"
>
{{ series.booksCount }}
</span>
</v-img>
<v-card-subtitle class="text-truncate d-inline-block pa-2 pb-1"
:style="{'max-width': width-16 + 'px'}"
<v-card-subtitle class="pa-2 pb-1 text--primary"
v-line-clamp="2"
>
{{ series.name }}
</v-card-subtitle>
<v-card-text class="px-2"
>
<span v-if="series.booksCount === 1">{{ series.booksCount }} book</span>
<span v-else>{{ series.booksCount }} books</span>
</v-card-text>
</v-card>
</template>
@ -35,7 +48,7 @@ export default Vue.extend({
width: {
type: [String, Number],
required: false,
default: 125
default: 150
}
},
methods: {

View file

@ -1,5 +1,7 @@
import _, { LoDashStatic } from 'lodash'
import Vue from 'vue'
// @ts-ignore
import * as lineClamp from 'vue-line-clamp'
import Vuelidate from 'vuelidate'
import { sync } from 'vuex-router-sync'
import App from './App.vue'
@ -13,6 +15,7 @@ import router from './router'
import store from './store'
Vue.use(Vuelidate)
Vue.use(lineClamp)
Vue.use(httpPlugin)
Vue.use(komgaFileSystem, { http: Vue.prototype.$http })

View file

@ -7,7 +7,12 @@ let service: KomgaLibrariesService
const vuexModule: Module<any, any> = {
state: {
libraries: {} as LibraryDto
libraries: [] as LibraryDto[]
},
getters: {
getLibraryById: (state) => (id: number) => {
return state.libraries.find((l: any) => l.id === id)
}
},
mutations: {
setLibraries (state, libraries) {

View file

@ -56,10 +56,16 @@ export default new Router({
component: () => import(/* webpackChunkName: "account" */ './components/AccountSettings.vue')
},
{
path: '/library/:libraryId',
name: 'browse-library',
path: '/libraries/:libraryId?',
name: 'browse-libraries',
component: () => import(/* webpackChunkName: "browse" */ './components/BrowseLibraries.vue'),
props: (route) => ({ libraryId: Number(route.params.libraryId) })
},
{
path: '/series/:seriesId',
name: 'browse-series',
component: () => import(/* webpackChunkName: "browse" */ './components/BrowseSeries.vue'),
props: (route) => ({ seriesId: Number(route.params.seriesId) })
}
]
},

View file

@ -13,12 +13,44 @@ export default class KomgaSeriesService {
async getSeries (libraryId?: number, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
try {
const params = { ...pageRequest } as any
if (libraryId) {
params.library_id = libraryId
}
return (await this.http.get(API_SERIES, {
params: { library_id: libraryId, ...pageRequest },
params: params,
paramsSerializer: params => qs.stringify(params, { indices: false })
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve libraries'
let msg = 'An error occurred while trying to retrieve series'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async getOneSeries (seriesId: number): Promise<SeriesDto> {
try {
return (await this.http.get(`${API_SERIES}/${seriesId}`)).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve series'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
async getBooks (seriesId: number, pageRequest?: PageRequest): Promise<Page<BookDto>> {
try {
const data = (await this.http.get(`${API_SERIES}/${seriesId}/books`)).data
data.content.forEach(function (b: any) {
b.seriesId = seriesId
})
return data
} catch (e) {
let msg = 'An error occurred while trying to retrieve books'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}

View file

@ -2,5 +2,23 @@ interface SeriesDto {
id: number,
name: string,
url: string,
lastModified: string
lastModified: string,
booksCount: number
}
interface BookDto {
id: number,
name: string,
url: string,
lastModified: string,
sizeBytes: number,
size: string,
metadata: BookMetadataDto,
seriesId?: number
}
interface BookMetadataDto {
status: string,
mediaType: string,
pagesCount: number
}

View file

@ -40,7 +40,7 @@
</v-list-item-content>
</v-list-item>
<v-list-item>
<v-list-item :to="{name:'browse-libraries'}" exact>
<v-list-item-icon>
<v-icon>mdi-book-multiple</v-icon>
</v-list-item-icon>
@ -57,7 +57,7 @@
<v-list-item v-for="(l, index) in libraries"
:key="index"
dense
:to="{name:'browse-library', params: {libraryId: l.id}}"
:to="{name:'browse-libraries', params: {libraryId: l.id}}"
>
<v-list-item-icon>
</v-list-item-icon>