mirror of
https://github.com/gotson/komga.git
synced 2025-12-30 12:22:41 +01:00
better version of the web reader (closes #28)
This commit is contained in:
parent
34551633ee
commit
b1770ac68f
3 changed files with 300 additions and 36 deletions
18
komga-webui/package-lock.json
generated
18
komga-webui/package-lock.json
generated
|
|
@ -10746,6 +10746,11 @@
|
|||
"merge-stream": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"jquery": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
|
||||
"integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
|
||||
},
|
||||
"js-beautify": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.2.tgz",
|
||||
|
|
@ -14190,6 +14195,11 @@
|
|||
"integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
|
||||
"dev": true
|
||||
},
|
||||
"slick-carousel": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/slick-carousel/-/slick-carousel-1.8.1.tgz",
|
||||
"integrity": "sha512-XB9Ftrf2EEKfzoQXt3Nitrt/IPbT+f1fgqBdoxO3W/+JYvtEOW6EgxnWfr9GH6nmULv7Y2tPmEX3koxThVmebA=="
|
||||
},
|
||||
"snapdragon": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz",
|
||||
|
|
@ -15852,6 +15862,14 @@
|
|||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.3.tgz",
|
||||
"integrity": "sha512-8iSa4mGNXBjyuSZFCCO4fiKfvzqk+mhL0lnKuGcQtO1eoj8nq3CmbEG8FwK5QqoqwDgsjsf1GDuisDX4cdb/aQ=="
|
||||
},
|
||||
"vue-slick": {
|
||||
"version": "1.1.15",
|
||||
"resolved": "https://registry.npmjs.org/vue-slick/-/vue-slick-1.1.15.tgz",
|
||||
"integrity": "sha512-FnoeFMmxj+3Y1iE1HjNsB0SPpcTxJ8y2FIgndwMP6WkQph3FkPQLdqz24MwH4RszCXXH8reoANV6DMIC07jEbQ==",
|
||||
"requires": {
|
||||
"slick-carousel": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"vue-style-loader": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@
|
|||
"dependencies": {
|
||||
"axios": "^0.19.0",
|
||||
"core-js": "^2.6.11",
|
||||
"jquery": "^3.4.1",
|
||||
"lodash": "^4.17.15",
|
||||
"qs": "^6.9.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-line-clamp": "^1.3.2",
|
||||
"vue-router": "^3.0.3",
|
||||
"vue-slick": "^1.1.15",
|
||||
"vuelidate": "^0.7.4",
|
||||
"vuetify": "^2.1.15",
|
||||
"vuex": "^3.1.2",
|
||||
|
|
|
|||
|
|
@ -1,33 +1,186 @@
|
|||
<template>
|
||||
<div v-if="pages.length > 0">
|
||||
<v-btn icon @click="closeBook">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="prev" :disabled="!canPrev">
|
||||
<v-icon>mdi-chevron-left</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn icon @click="next" :disabled="!canNext">
|
||||
<v-icon>mdi-chevron-right</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<transition name="slide-x-transition">
|
||||
<v-img :src="getPageUrl(currentPage)"
|
||||
:key="getPageUrl(currentPage)"
|
||||
contain
|
||||
:max-height="$vuetify.breakpoint.height"
|
||||
<!-- Carousel -->
|
||||
<slick ref="slick"
|
||||
:options="slickOptions"
|
||||
@afterChange="slickAfterChange"
|
||||
>
|
||||
<!-- Carousel: pages -->
|
||||
<v-img v-for="p in pages"
|
||||
:key="p.number"
|
||||
:data-lazy="getPageUrl(p)"
|
||||
:src="getPageUrl(p)"
|
||||
:max-height="maxHeight"
|
||||
:max-width="$vuetify.breakpoint.width"
|
||||
/>
|
||||
</transition>
|
||||
:contain="true"
|
||||
>
|
||||
<!-- clickable zone: previous page -->
|
||||
<div @click="prev"
|
||||
class="left-quarter full-height"
|
||||
style="z-index: 1; position: absolute"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: menu -->
|
||||
<div @click="showMenu = true"
|
||||
class="center-half full-height"
|
||||
style="z-index: 1; position: absolute"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: next page -->
|
||||
<div @click="next"
|
||||
class="right-quarter full-height"
|
||||
style="z-index: 1; position: absolute"
|
||||
/>
|
||||
</v-img>
|
||||
</slick>
|
||||
|
||||
<!-- Menu -->
|
||||
<v-overlay :value="showMenu"
|
||||
opacity=".8"
|
||||
>
|
||||
<!-- Menu: left zone with arrow -->
|
||||
<div class="fixed-position full-height left-quarter"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
>
|
||||
<v-icon size="8em">
|
||||
mdi-chevron-left
|
||||
</v-icon>
|
||||
</div>
|
||||
|
||||
<!-- Menu: central zone -->
|
||||
<div class="dashed-x fixed-position center-half full-height"
|
||||
@click.self="showMenu = false"
|
||||
>
|
||||
<v-btn icon
|
||||
@click="showMenu = false"
|
||||
absolute
|
||||
top
|
||||
right
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-container fluid
|
||||
class="pa-6 pt-12"
|
||||
style="border-bottom: 4px dashed"
|
||||
>
|
||||
<!-- Menu: number of pages -->
|
||||
<v-row>
|
||||
<v-col class="text-center title">
|
||||
Page {{ currentPage }} of {{ book.metadata.pagesCount }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Menu: progress bar -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-progress-linear :value="progress"
|
||||
height="20"
|
||||
background-color="white"
|
||||
color="secondary"
|
||||
rounded
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Menu: page slider -->
|
||||
<v-row align="baseline">
|
||||
<v-col cols="11">
|
||||
<v-slider
|
||||
v-model="goToPage"
|
||||
class="align-center"
|
||||
:max="book.metadata.pagesCount"
|
||||
min="1"
|
||||
hide-details
|
||||
@change="goTo"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-btn icon @click="goToFirst">
|
||||
<v-icon>mdi-arrow-collapse-left</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
<v-btn icon @click="goToLast">
|
||||
<v-icon>mdi-arrow-collapse-right</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-slider>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="goToPage"
|
||||
hide-details
|
||||
single-line
|
||||
type="number"
|
||||
@change="goTo"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Menu: buttons Close and Fit -->
|
||||
<v-row>
|
||||
<v-col class="text-center">
|
||||
<v-btn @click="closeBook"
|
||||
color="primary"
|
||||
>
|
||||
Close book
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col class="text-center">
|
||||
<v-btn-toggle v-model="fitButtons" dense mandatory>
|
||||
<v-btn @click="fitHeight = false" color="primary">
|
||||
Fit to width
|
||||
</v-btn>
|
||||
|
||||
<v-btn @click="fitHeight = true" color="primary">
|
||||
Fit to height
|
||||
</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Menu: keyboard shortcuts -->
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<div><kbd>←</kbd> / <kbd>⇞</kbd></div>
|
||||
<div><kbd>→</kbd> / <kbd>⇟</kbd></div>
|
||||
<div><kbd>space</kbd></div>
|
||||
<div><kbd>m</kbd></div>
|
||||
<div><kbd>esc</kbd></div>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<div>Previous page</div>
|
||||
<div>Next page</div>
|
||||
<div>Scroll down</div>
|
||||
<div>Show / hide menu</div>
|
||||
<div>Close book</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
|
||||
<!-- Menu: right zone with arrow -->
|
||||
<div class="fixed-position full-height right-quarter"
|
||||
style="display: flex; align-items: center; justify-content: center"
|
||||
>
|
||||
<v-icon size="8em">
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</div>
|
||||
|
||||
</v-overlay>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import Slick from 'vue-slick'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BookReader',
|
||||
components: { Slick },
|
||||
data: () => {
|
||||
return {
|
||||
baseURL: process.env.VUE_APP_KOMGA_API_URL ? process.env.VUE_APP_KOMGA_API_URL : window.location.origin,
|
||||
|
|
@ -35,12 +188,26 @@ export default Vue.extend({
|
|||
pages: [] as PageDto[],
|
||||
supportedMediaTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
|
||||
convertTo: 'png',
|
||||
currentPage: 1
|
||||
currentPage: 1,
|
||||
goToPage: 1,
|
||||
showMenu: false,
|
||||
fitButtons: 1,
|
||||
fitHeight: true,
|
||||
slickOptions: {
|
||||
infinite: false,
|
||||
arrows: false,
|
||||
variableWidth: false,
|
||||
adaptiveHeight: false,
|
||||
initialSlide: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
this.book = await this.$komgaBooks.getBook(this.bookId)
|
||||
this.pages = await this.$komgaBooks.getBookPages(this.bookId)
|
||||
async mounted () {
|
||||
window.addEventListener('keydown', this.keyPressed)
|
||||
this.setup(this.bookId, Number(this.$route.query.page))
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('keydown', this.keyPressed)
|
||||
},
|
||||
props: {
|
||||
bookId: {
|
||||
|
|
@ -50,14 +217,14 @@ export default Vue.extend({
|
|||
},
|
||||
async beforeRouteUpdate (to, from, next) {
|
||||
if (to.params.bookId !== from.params.bookId) {
|
||||
this.book = await this.$komgaBooks.getBook(Number(to.params.bookId))
|
||||
this.setup(Number(to.params.bookId), Number(to.query.page))
|
||||
}
|
||||
next()
|
||||
},
|
||||
watch: {
|
||||
currentPage (val) {
|
||||
this.updateRoute()
|
||||
this.preloadNext()
|
||||
this.goToPage = val
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -66,11 +233,45 @@ export default Vue.extend({
|
|||
},
|
||||
canNext (): boolean {
|
||||
return this.currentPage < this.book.metadata.pagesCount
|
||||
},
|
||||
progress (): number {
|
||||
return this.currentPage / this.book.metadata.pagesCount * 100
|
||||
},
|
||||
maxHeight (): number | undefined {
|
||||
return this.fitHeight ? this.$vuetify.breakpoint.height - 7 : undefined
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPageUrl (pageNum: number): string {
|
||||
const page = this.pages[pageNum - 1]
|
||||
keyPressed (e: KeyboardEvent) {
|
||||
switch (e.key) {
|
||||
case 'PageUp':
|
||||
case 'ArrowRight':
|
||||
this.next()
|
||||
break
|
||||
case 'PageDown':
|
||||
case 'ArrowLeft':
|
||||
this.prev()
|
||||
break
|
||||
case 'm':
|
||||
this.showMenu = !this.showMenu
|
||||
break
|
||||
case 'Escape':
|
||||
this.closeBook()
|
||||
break
|
||||
}
|
||||
},
|
||||
async setup (bookId: number, page: number) {
|
||||
this.book = await this.$komgaBooks.getBook(bookId)
|
||||
this.pages = await this.$komgaBooks.getBookPages(bookId)
|
||||
if (page >= 1 && page <= this.book.metadata.pagesCount) {
|
||||
this.currentPage = page
|
||||
this.slickOptions.initialSlide = page - 1
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
this.updateRoute()
|
||||
}
|
||||
},
|
||||
getPageUrl (page: PageDto): string {
|
||||
let url = `${this.baseURL}/api/v1/books/${this.bookId}/pages/${page.number}`
|
||||
if (!this.supportedMediaTypes.includes(page.mediaType)) {
|
||||
url += `?convert=${this.convertTo}`
|
||||
|
|
@ -79,34 +280,77 @@ export default Vue.extend({
|
|||
},
|
||||
prev () {
|
||||
if (this.canPrev) {
|
||||
this.currentPage -= 1
|
||||
(this.$refs.slick as any).prev()
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
},
|
||||
next () {
|
||||
if (this.canNext) {
|
||||
this.currentPage += 1
|
||||
(this.$refs.slick as any).next()
|
||||
window.scrollTo(0, 0)
|
||||
} else {
|
||||
this.showMenu = true
|
||||
}
|
||||
},
|
||||
preloadNext () {
|
||||
if (this.canNext) {
|
||||
const img = new Image()
|
||||
img.src = this.getPageUrl(this.currentPage + 1)
|
||||
}
|
||||
goTo (page: number) {
|
||||
(this.$refs.slick as any).$el.slick.slickGoTo(page - 1, true)
|
||||
},
|
||||
goToFirst () {
|
||||
this.goToPage = 1
|
||||
this.goTo(this.goToPage)
|
||||
},
|
||||
goToLast () {
|
||||
this.goToPage = this.book.metadata.pagesCount
|
||||
this.goTo(this.goToPage)
|
||||
},
|
||||
updateRoute () {
|
||||
this.$router.replace({
|
||||
name: this.$route.name,
|
||||
params: { bookId: this.$route.params.bookId },
|
||||
query: { page: this.currentPage.toString() }
|
||||
query: {
|
||||
page: this.currentPage.toString()
|
||||
}
|
||||
})
|
||||
},
|
||||
closeBook () {
|
||||
this.$router.back()
|
||||
this.$router.push({ name: 'browse-book', params: { bookId: this.bookId.toString() } })
|
||||
},
|
||||
slickAfterChange (event: any, slick: any, currentSlide: any) {
|
||||
this.currentPage = currentSlide + 1
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import "../../node_modules/slick-carousel/slick/slick.css";
|
||||
|
||||
.fixed-position {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
top: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left-quarter {
|
||||
left: 0;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.right-quarter {
|
||||
right: 0;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.center-half {
|
||||
left: 20%;
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.dashed-x {
|
||||
border-left: 4px dashed;
|
||||
border-right: 4px dashed;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue