mirror of
https://github.com/gotson/komga.git
synced 2025-12-24 09:24:36 +01:00
add browsing for all libraries
add browsing by series add badge with number of books on series card
This commit is contained in:
parent
b687f7d161
commit
cc6d581b4e
12 changed files with 280 additions and 15 deletions
5
komga-webui/package-lock.json
generated
5
komga-webui/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
94
komga-webui/src/components/BrowseSeries.vue
Normal file
94
komga-webui/src/components/BrowseSeries.vue
Normal 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>
|
||||
74
komga-webui/src/components/CardBook.vue
Normal file
74
komga-webui/src/components/CardBook.vue
Normal 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>
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) })
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue