mirror of
https://github.com/gotson/komga.git
synced 2025-12-22 00:13:30 +01:00
feat(webreader): webtoon reader, fit to screen
shortcut help changed from menu to dialog shortcut help is now context aware closes #81, closes #145
This commit is contained in:
parent
969382988d
commit
44c814a5ba
17 changed files with 997 additions and 610 deletions
|
|
@ -22,7 +22,7 @@ export default Vue.extend({
|
|||
name: 'LibraryNavigation',
|
||||
props: {
|
||||
libraryId: {
|
||||
type: Number,
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<v-row justify-md="center" justify-sm="start">
|
||||
<v-col cols="5" md="4" class="text-left" align-self="center">
|
||||
<v-label class=""> {{ label }} </v-label>
|
||||
<v-col cols="5" class="text-left" align-self="center">
|
||||
<span class="">{{ label }}</span>
|
||||
</v-col>
|
||||
<v-col cols="7" md="4" >
|
||||
<v-col cols="7">
|
||||
<v-select
|
||||
filled
|
||||
dense
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<v-row justify-md="center" justify-sm="start">
|
||||
<v-col cols="5" md="3" align-self="center">
|
||||
<v-label> {{ label }} </v-label>
|
||||
<v-col cols="5" align-self="center">
|
||||
<span>{{ label }}</span>
|
||||
</v-col>
|
||||
<v-col cols="4" md="3" align-self="center" class="text-right">
|
||||
<v-label> {{ status }} </v-label>
|
||||
<v-col cols="4" align-self="center" class="text-right">
|
||||
<span>{{ status }}</span>
|
||||
</v-col>
|
||||
<v-col cols="3" md="2" align-self="center">
|
||||
<v-switch v-model="input"
|
||||
<v-col cols="3" align-self="center" class="pa-0">
|
||||
<v-switch v-model="input" dense
|
||||
@input="updateInput"
|
||||
@change="updateInput"
|
||||
class="float-right"
|
||||
|
|
|
|||
81
komga-webui/src/components/dialogs/ShortcutHelpDialog.vue
Normal file
81
komga-webui/src/components/dialogs/ShortcutHelpDialog.vue
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<template>
|
||||
<v-dialog
|
||||
offset-y :max-height="$vuetify.breakpoint.height"
|
||||
v-model="model"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-btn icon absolute top right @click="model = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<template v-for="(category, i) in Object.keys(shortcuts)">
|
||||
<v-col :key="i" cols="12" md="4">
|
||||
<div class="text-center text-h6">
|
||||
{{ category }}
|
||||
</div>
|
||||
<v-simple-table>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Key</th>
|
||||
<th class="text-left">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(s, j) in shortcuts[category]"
|
||||
:key="j"
|
||||
>
|
||||
<td>
|
||||
<kbd style="font-size: 1.2em" class="text-truncate">
|
||||
{{ s.display }}
|
||||
</kbd>
|
||||
</td>
|
||||
<td>{{ s.description }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ShortcutHelpDialog',
|
||||
data: () => {
|
||||
return {
|
||||
model: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
shortcuts: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
model (val) {
|
||||
this.$emit('input', val)
|
||||
},
|
||||
value (val) {
|
||||
this.model = val
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -91,7 +91,7 @@ export default Vue.extend({
|
|||
this.$emit('input', this.input)
|
||||
},
|
||||
goTo (page: number) {
|
||||
this.$emit('goToPage', page)
|
||||
this.$emit('go', page)
|
||||
},
|
||||
getThumbnailUrl (page: number): string {
|
||||
return bookPageThumbnailUrl(this.bookId, page)
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
<template>
|
||||
<v-menu offset-y :max-height="$vuetify.breakpoint.height">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
icon
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-information</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-row no-gutters>
|
||||
<template v-for="(item,i) in shortcutHelp.items()">
|
||||
<v-col :key="i" cols="12" md="4">
|
||||
<div class="text-center">
|
||||
{{ item.key }}
|
||||
</div>
|
||||
<v-simple-table>
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Key</th>
|
||||
<th class="text-left">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(s,i) in item.value" :key="i">
|
||||
<td>
|
||||
<kbd style="height: 24px;" class="text-truncate">
|
||||
<v-icon class="white--text text-capitalize" style="font-size: 1rem;">
|
||||
{{ s.key }}
|
||||
</v-icon>
|
||||
</kbd>
|
||||
</td>
|
||||
<td>{{ s.desc }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { shortcutHelp } from '@/functions/shortcuts'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ShortcutHelpMenu',
|
||||
data: () => {
|
||||
return {
|
||||
shortcutHelp: shortcutHelp,
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
159
komga-webui/src/components/readers/ContinuousReader.vue
Normal file
159
komga-webui/src/components/readers/ContinuousReader.vue
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
<template>
|
||||
<div>
|
||||
<div :class="`d-flex flex-column px-0 mx-0` "
|
||||
v-scroll="onScroll"
|
||||
>
|
||||
<img v-for="(page, i) in pages"
|
||||
:key="`page${i}`"
|
||||
:alt="`Page ${page.number}`"
|
||||
:src="shouldLoad(i) ? page.url : undefined"
|
||||
:height="page.height / (page.width / $vuetify.breakpoint.width)"
|
||||
:width="$vuetify.breakpoint.width"
|
||||
:id="`page${page.number}`"
|
||||
v-intersect.once="onIntersect"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- clickable zone: top -->
|
||||
<div @click="prev()"
|
||||
class="top-quarter"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: bottom -->
|
||||
<div @click="next()"
|
||||
class="bottom-quarter"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: menu -->
|
||||
<div @click="centerClick()"
|
||||
class="center-vertical"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ContinuousReader',
|
||||
data: () => {
|
||||
return {
|
||||
offsetTop: 0,
|
||||
totalHeight: 1000,
|
||||
currentPage: 1,
|
||||
seen: [] as boolean[],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
pages: {
|
||||
type: Array as () => PageDtoWithUrl[],
|
||||
required: true,
|
||||
},
|
||||
animations: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
pages: {
|
||||
handler (val) {
|
||||
this.seen = new Array(val.length).fill(false)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
page: {
|
||||
handler (val) {
|
||||
if (val != this.currentPage) {
|
||||
this.$vuetify.goTo(`#page${val}`, {
|
||||
duration: 0,
|
||||
})
|
||||
}
|
||||
},
|
||||
immediate: false,
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
if (this.page != this.currentPage) {
|
||||
this.$vuetify.goTo(`#page${this.page}`, {
|
||||
duration: 0,
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canPrev (): boolean {
|
||||
return this.offsetTop > 0
|
||||
},
|
||||
canNext (): boolean {
|
||||
return this.offsetTop + this.$vuetify.breakpoint.height < this.totalHeight
|
||||
},
|
||||
goToOptions (): object | undefined {
|
||||
if (this.animations) return undefined
|
||||
return { duration: 0 }
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onScroll (e: any) {
|
||||
this.offsetTop = e.target.scrollingElement.scrollTop
|
||||
this.totalHeight = e.target.scrollingElement.scrollHeight
|
||||
},
|
||||
onIntersect (entries: any) {
|
||||
if (entries[0].isIntersecting) {
|
||||
this.currentPage = parseInt(entries[0].target.id.replace('page', ''))
|
||||
this.seen.splice(this.currentPage - 1, 1, true)
|
||||
this.$emit('update:page', this.currentPage)
|
||||
}
|
||||
},
|
||||
shouldLoad (page: number): boolean {
|
||||
return this.seen[page] || Math.abs((this.currentPage - 1) - page) <= 2
|
||||
},
|
||||
centerClick () {
|
||||
this.$emit('menu')
|
||||
},
|
||||
prev () {
|
||||
if (this.canPrev) {
|
||||
const step = this.$vuetify.breakpoint.height * 0.95
|
||||
this.$vuetify.goTo(this.offsetTop - step, this.goToOptions)
|
||||
} else {
|
||||
this.$emit('jump-previous')
|
||||
}
|
||||
},
|
||||
next () {
|
||||
if (this.canNext) {
|
||||
const step = this.$vuetify.breakpoint.height * 0.95
|
||||
this.$vuetify.goTo(this.offsetTop + step, this.goToOptions)
|
||||
} else {
|
||||
this.$emit('jump-next')
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.top-quarter {
|
||||
top: 0;
|
||||
height: 25vh;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.bottom-quarter {
|
||||
top: 75vh;
|
||||
height: 25vh;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.center-vertical {
|
||||
top: 25vh;
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
</style>
|
||||
361
komga-webui/src/components/readers/PagedReader.vue
Normal file
361
komga-webui/src/components/readers/PagedReader.vue
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
<template>
|
||||
<div
|
||||
v-touch="{
|
||||
left: () => {if(swipe) {turnRight()}},
|
||||
right: () => {if(swipe) {turnLeft()}},
|
||||
up: () => {if(swipe) {verticalNext()}},
|
||||
down: () => {if(swipe) {verticalPrev()}}
|
||||
}"
|
||||
>
|
||||
<v-carousel v-model="carouselPage"
|
||||
:show-arrows="false"
|
||||
:continuous="false"
|
||||
:reverse="flipDirection"
|
||||
:vertical="vertical"
|
||||
hide-delimiters
|
||||
touchless
|
||||
height="100%"
|
||||
>
|
||||
<!-- Carousel: pages -->
|
||||
<v-carousel-item v-for="(spread, i) in spreads"
|
||||
:key="`spread${i}`"
|
||||
:eager="eagerLoad(i)"
|
||||
class="full-height"
|
||||
:transition="animations ? undefined : false"
|
||||
:reverse-transition="animations ? undefined : false"
|
||||
>
|
||||
<div class="full-height d-flex flex-column justify-center">
|
||||
<div :class="`d-flex flex-row${flipDirection ? '-reverse' : ''} justify-center px-0 mx-0`">
|
||||
<img v-for="(page, j) in spread"
|
||||
:alt="`Page ${page.number}`"
|
||||
:key="`spread${i}-${j}`"
|
||||
:src="page.url"
|
||||
:class="imgClass(spread)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
|
||||
<!-- clickable zone: left -->
|
||||
<div v-if="!vertical"
|
||||
@click="turnLeft()"
|
||||
class="left-quarter"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: right -->
|
||||
<div v-if="!vertical"
|
||||
@click="turnRight()"
|
||||
class="right-quarter"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: top -->
|
||||
<div v-if="vertical"
|
||||
@click="verticalPrev()"
|
||||
class="top-quarter"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: bottom -->
|
||||
<div v-if="vertical"
|
||||
@click="verticalNext()"
|
||||
class="bottom-quarter"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: menu -->
|
||||
<div @click="centerClick()"
|
||||
:class="`${vertical ? 'center-vertical' : 'center-horizontal'}`"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { isPageLandscape } from '@/functions/page'
|
||||
import Vue from 'vue'
|
||||
import { ReadingDirection } from '@/types/enum-books'
|
||||
import { ScaleType } from '@/types/enum-reader'
|
||||
import { shortcutsLTR, shortcutsRTL, shortcutsVertical } from '@/functions/shortcuts/paged-reader'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'PagedReader',
|
||||
data: () => {
|
||||
return {
|
||||
carouselPage: 0,
|
||||
spreads: [] as PageDtoWithUrl[][],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
pages: {
|
||||
type: Array as () => PageDtoWithUrl[],
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
doublePages: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
animations: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
swipe: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
readingDirection: {
|
||||
type: String as () => ReadingDirection,
|
||||
required: true,
|
||||
},
|
||||
scale: {
|
||||
type: String as () => ScaleType,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
pages: {
|
||||
handler () {
|
||||
this.spreads = this.buildSpreads()
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
currentPage (val) {
|
||||
this.$emit('update:page', val)
|
||||
},
|
||||
page (val) {
|
||||
this.carouselPage = this.toSpreadIndex(val)
|
||||
},
|
||||
doublePages: {
|
||||
handler () {
|
||||
const current = this.page
|
||||
this.spreads = this.buildSpreads()
|
||||
this.carouselPage = this.toSpreadIndex(current)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('keydown', this.keyPressed)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('keydown', this.keyPressed)
|
||||
},
|
||||
computed: {
|
||||
shortcuts (): any {
|
||||
const shortcuts = []
|
||||
switch (this.readingDirection) {
|
||||
case ReadingDirection.LEFT_TO_RIGHT:
|
||||
shortcuts.push(...shortcutsLTR)
|
||||
break
|
||||
case ReadingDirection.RIGHT_TO_LEFT:
|
||||
shortcuts.push(...shortcutsRTL)
|
||||
break
|
||||
case ReadingDirection.VERTICAL:
|
||||
shortcuts.push(...shortcutsVertical)
|
||||
break
|
||||
}
|
||||
return this.$_.keyBy(shortcuts, x => x.key)
|
||||
},
|
||||
flipDirection (): boolean {
|
||||
return this.readingDirection === ReadingDirection.RIGHT_TO_LEFT
|
||||
},
|
||||
vertical (): boolean {
|
||||
return this.readingDirection === ReadingDirection.VERTICAL
|
||||
},
|
||||
currentSlide (): number {
|
||||
return this.carouselPage + 1
|
||||
},
|
||||
currentPage (): number {
|
||||
if (this.carouselPage >= 0 && this.carouselPage < this.spreads.length && this.spreads.length > 0) {
|
||||
return this.spreads[this.carouselPage][0].number
|
||||
}
|
||||
return 1
|
||||
},
|
||||
slidesCount (): number {
|
||||
return this.spreads.length
|
||||
},
|
||||
canPrev (): boolean {
|
||||
return this.currentSlide > 1
|
||||
},
|
||||
canNext (): boolean {
|
||||
return this.currentSlide < this.slidesCount
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
keyPressed (e: KeyboardEvent) {
|
||||
this.shortcuts[e.key]?.execute(this)
|
||||
},
|
||||
buildSpreads (): PageDtoWithUrl[][] {
|
||||
if (this.pages.length === 0) return []
|
||||
if (this.doublePages) {
|
||||
const spreads = []
|
||||
spreads.push([this.pages[0]])
|
||||
const pages = this.$_.drop(this.$_.dropRight(this.pages)) as PageDtoWithUrl[]
|
||||
while (pages.length > 0) {
|
||||
const p = pages.shift() as PageDtoWithUrl
|
||||
if (isPageLandscape(p)) {
|
||||
spreads.push([p])
|
||||
} else {
|
||||
if (pages.length > 0) {
|
||||
const p2 = pages.shift() as PageDtoWithUrl
|
||||
if (isPageLandscape(p2)) {
|
||||
spreads.push([p])
|
||||
spreads.push([p2])
|
||||
} else {
|
||||
spreads.push([p, p2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
spreads.push([this.pages[this.pages.length - 1]])
|
||||
return spreads
|
||||
} else {
|
||||
return this.pages.map(p => [p])
|
||||
}
|
||||
},
|
||||
imgClass (spread: PageDtoWithUrl[]): string {
|
||||
const double = spread.length > 1
|
||||
switch (this.scale) {
|
||||
case ScaleType.WIDTH:
|
||||
return double ? 'img-double-fit-width' : 'img-fit-width'
|
||||
case ScaleType.HEIGHT:
|
||||
return 'img-fit-height'
|
||||
case ScaleType.SCREEN:
|
||||
return double ? 'img-double-fit-screen' : 'img-fit-screen'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
},
|
||||
eagerLoad (spreadIndex: number): boolean {
|
||||
return Math.abs(this.carouselPage - spreadIndex) <= 2
|
||||
},
|
||||
centerClick () {
|
||||
this.$emit('menu')
|
||||
},
|
||||
turnRight () {
|
||||
if (!this.vertical)
|
||||
this.flipDirection ? this.prev() : this.next()
|
||||
},
|
||||
turnLeft () {
|
||||
if (!this.vertical)
|
||||
this.flipDirection ? this.next() : this.prev()
|
||||
},
|
||||
verticalPrev () {
|
||||
if (this.vertical) this.prev()
|
||||
},
|
||||
verticalNext () {
|
||||
if (this.vertical) this.next()
|
||||
},
|
||||
prev () {
|
||||
if (this.canPrev) {
|
||||
this.carouselPage--
|
||||
window.scrollTo(0, 0)
|
||||
} else {
|
||||
this.$emit('jump-previous')
|
||||
}
|
||||
},
|
||||
next () {
|
||||
if (this.canNext) {
|
||||
this.carouselPage++
|
||||
window.scrollTo(0, 0)
|
||||
} else {
|
||||
this.$emit('jump-next')
|
||||
}
|
||||
},
|
||||
toSpreadIndex (i: number): number {
|
||||
if (this.spreads.length > 0) {
|
||||
if (this.doublePages) {
|
||||
for (let j = 0; j < this.spreads.length; j++) {
|
||||
for (let k = 0; k < this.spreads[j].length; k++) {
|
||||
if (this.spreads[j][k].number === i) {
|
||||
return j
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return i - 1
|
||||
}
|
||||
}
|
||||
return i - 1
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<style scoped>
|
||||
.full-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.left-quarter {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.right-quarter {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.top-quarter {
|
||||
top: 0;
|
||||
height: 25%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.bottom-quarter {
|
||||
bottom: 0;
|
||||
height: 25%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.center-horizontal {
|
||||
top: 0;
|
||||
left: 25%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.center-vertical {
|
||||
top: 25%;
|
||||
height: 50%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.img-fit-width {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.img-double-fit-width {
|
||||
max-width: 50vw;
|
||||
}
|
||||
|
||||
.img-fit-height {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.img-fit-screen {
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.img-double-fit-screen {
|
||||
max-width: 50vw;
|
||||
max-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
16
komga-webui/src/functions/reader.ts
Normal file
16
komga-webui/src/functions/reader.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { ScaleType } from '@/types/enum-reader'
|
||||
import { ReadingDirection } from '@/types/enum-books'
|
||||
|
||||
export const ScaleTypeText = {
|
||||
[ScaleType.SCREEN]: 'Fit screen',
|
||||
[ScaleType.HEIGHT]: 'Fit height',
|
||||
[ScaleType.WIDTH]: 'Fit width',
|
||||
[ScaleType.ORIGINAL]: 'Original',
|
||||
}
|
||||
|
||||
export const ReadingDirectionText = {
|
||||
[ReadingDirection.LEFT_TO_RIGHT]: 'Left to right',
|
||||
[ReadingDirection.RIGHT_TO_LEFT]: 'Right to left',
|
||||
[ReadingDirection.VERTICAL]: 'Vertical',
|
||||
[ReadingDirection.WEBTOON]: 'Webtoon',
|
||||
}
|
||||
|
|
@ -1,185 +0,0 @@
|
|||
import { ReadingDirection } from '@/types/enum-books'
|
||||
|
||||
interface Map<V> {
|
||||
[key: string]: V
|
||||
}
|
||||
class MultiMap<V> {
|
||||
dict: Map<V[]> = {}
|
||||
|
||||
add (key:string, value: V) {
|
||||
this.dict[key] = (this.dict[key]?.concat([value])) || [value]
|
||||
}
|
||||
|
||||
get (key: string): V[] {
|
||||
return this.dict[key]
|
||||
}
|
||||
|
||||
items () {
|
||||
return Object.keys(this.dict).map((k) => ({ key: k, value: this.dict[k] }))
|
||||
}
|
||||
}
|
||||
|
||||
type Action = (ctx: any) => void
|
||||
|
||||
class Shortcut {
|
||||
name: string
|
||||
category: string
|
||||
description: string
|
||||
action: Action
|
||||
|
||||
keys: string[]
|
||||
|
||||
constructor (name: string, category: string, description: string, action: Action, keys: string[]) {
|
||||
this.name = name
|
||||
this.category = category
|
||||
this.description = description
|
||||
this.action = action
|
||||
this.keys = keys
|
||||
}
|
||||
|
||||
execute (ctx: any): boolean {
|
||||
this.action(ctx)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const KEY_DISPLAY = {
|
||||
'ArrowRight': 'mdi-arrow-right',
|
||||
'ArrowLeft': 'mdi-arrow-left',
|
||||
'PageUp': 'PgUp',
|
||||
'PageDown': 'PgDn',
|
||||
'ArrowUp': 'mdi-arrow-up',
|
||||
'ArrowDown': 'mdi-arrow-down',
|
||||
'Escape': 'Esc',
|
||||
} as Map<string>
|
||||
|
||||
const SHORTCUTS: Shortcut[] = []
|
||||
|
||||
function shortcut (name: string, category: string, description: string, action: Action, ...keys: string[]) {
|
||||
SHORTCUTS.push(new Shortcut(name, category, description, action, keys))
|
||||
}
|
||||
|
||||
enum ShortcutCategory {
|
||||
READER_NAVIGATION = 'Reader Navigation',
|
||||
READER_SETTINGS = 'Reader Settings',
|
||||
MENUS = 'Menus'
|
||||
}
|
||||
|
||||
// Reader Navigation
|
||||
shortcut('seekForward', ShortcutCategory.READER_NAVIGATION, 'Next Page',
|
||||
(ctx: any) => {
|
||||
ctx.flipDirection ? ctx.prev() : ctx.next()
|
||||
}, 'PageUp', 'ArrowRight')
|
||||
|
||||
shortcut('seekBackward', ShortcutCategory.READER_NAVIGATION, 'Prev Page',
|
||||
(ctx: any) => {
|
||||
ctx.flipDirection ? ctx.next() : ctx.prev()
|
||||
}, 'PageDown', 'ArrowLeft')
|
||||
|
||||
shortcut('seekUp', ShortcutCategory.READER_NAVIGATION, 'Prev Page (Vertical)',
|
||||
(ctx: any) => {
|
||||
if (ctx.vertical) {
|
||||
ctx.prev()
|
||||
}
|
||||
}
|
||||
, 'ArrowUp')
|
||||
|
||||
shortcut('seekDown', ShortcutCategory.READER_NAVIGATION, 'Next Page (Vertical)',
|
||||
(ctx: any) => {
|
||||
if (ctx.vertical) {
|
||||
ctx.next()
|
||||
}
|
||||
}
|
||||
, 'ArrowDown')
|
||||
|
||||
shortcut('seekBegin', ShortcutCategory.READER_NAVIGATION, 'Goto First Page',
|
||||
(ctx: any) => {
|
||||
ctx.goToFirst()
|
||||
}
|
||||
, 'Home')
|
||||
|
||||
shortcut('seekEnd', ShortcutCategory.READER_NAVIGATION, 'Goto Last Page',
|
||||
(ctx: any) => {
|
||||
ctx.goToLast()
|
||||
}
|
||||
, 'End')
|
||||
|
||||
// Reader Settings
|
||||
|
||||
shortcut('directionLTR', ShortcutCategory.READER_SETTINGS, 'Direction: Left to Right',
|
||||
(ctx: any) => ctx.changeReadingDir(ReadingDirection.LEFT_TO_RIGHT)
|
||||
, 'l')
|
||||
|
||||
shortcut('directionRTL', ShortcutCategory.READER_SETTINGS, 'Direction: Right to Left',
|
||||
(ctx: any) => ctx.changeReadingDir(ReadingDirection.RIGHT_TO_LEFT)
|
||||
, 'r')
|
||||
|
||||
shortcut('directionVRT', ShortcutCategory.READER_SETTINGS, 'Direction: Vertical',
|
||||
(ctx: any) => ctx.changeReadingDir(ReadingDirection.VERTICAL),
|
||||
'v')
|
||||
|
||||
shortcut('toggleDoublePage', ShortcutCategory.READER_SETTINGS, 'Toggle Double Page',
|
||||
(ctx: any) => ctx.toggleDoublePages()
|
||||
, 'd')
|
||||
|
||||
shortcut('cycleScale', ShortcutCategory.READER_SETTINGS, 'Cycle Scale',
|
||||
(ctx: any) => ctx.cycleScale()
|
||||
, 'c')
|
||||
|
||||
// Menus
|
||||
|
||||
shortcut('toggleToolbar', ShortcutCategory.MENUS, 'Toggle Toolbar',
|
||||
(ctx: any) => {
|
||||
ctx.toolbar = !ctx.toolbar
|
||||
},
|
||||
'm')
|
||||
|
||||
shortcut('toggleMenu', ShortcutCategory.MENUS, 'Toggle Settings Menu',
|
||||
(ctx: any) => {
|
||||
ctx.menu = !ctx.menu
|
||||
},
|
||||
's')
|
||||
|
||||
shortcut('toggleExplorer', ShortcutCategory.MENUS, 'Toggle Explorer',
|
||||
(ctx: any) => {
|
||||
ctx.showThumbnailsExplorer = !ctx.showThumbnailsExplorer
|
||||
}, 't')
|
||||
|
||||
shortcut('escape', ShortcutCategory.MENUS, 'Close',
|
||||
(ctx: any) => {
|
||||
if (ctx.showThumbnailsExplorer) {
|
||||
ctx.showThumbnailsExplorer = false
|
||||
return
|
||||
}
|
||||
if (ctx.menu) {
|
||||
ctx.menu = false
|
||||
return
|
||||
}
|
||||
if (ctx.toolbar) {
|
||||
ctx.toolbar = false
|
||||
return
|
||||
}
|
||||
ctx.closeBook()
|
||||
}, 'Escape')
|
||||
|
||||
// Make sure all shortcuts are registered before this is called
|
||||
export const shortcutHelp = new MultiMap<object>()
|
||||
const keyMapping = {} as Map<Shortcut>
|
||||
|
||||
function setupShortcuts () {
|
||||
for (const s of SHORTCUTS) {
|
||||
for (const key of s.keys) {
|
||||
keyMapping[key] = s
|
||||
shortcutHelp.add(s.category, {
|
||||
key: KEY_DISPLAY[key] || key,
|
||||
desc: s.description,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
setupShortcuts()
|
||||
|
||||
export function executeShortcut (ctx: any, e: KeyboardEvent): boolean {
|
||||
const k: string = e.key
|
||||
return keyMapping[k]?.execute(ctx)
|
||||
}
|
||||
41
komga-webui/src/functions/shortcuts/bookreader.ts
Normal file
41
komga-webui/src/functions/shortcuts/bookreader.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { Shortcut } from '@/types/shortcuts'
|
||||
import { ReadingDirection } from '@/types/enum-books'
|
||||
|
||||
export const shortcutsSettings = [
|
||||
new Shortcut('Left to Right',
|
||||
(ctx: any) => ctx.changeReadingDir(ReadingDirection.LEFT_TO_RIGHT)
|
||||
, 'l'),
|
||||
new Shortcut('Right to Left',
|
||||
(ctx: any) => ctx.changeReadingDir(ReadingDirection.RIGHT_TO_LEFT)
|
||||
, 'r'),
|
||||
new Shortcut('Vertical',
|
||||
(ctx: any) => ctx.changeReadingDir(ReadingDirection.VERTICAL)
|
||||
, 'v'),
|
||||
new Shortcut('Webtoon',
|
||||
(ctx: any) => ctx.changeReadingDir(ReadingDirection.WEBTOON)
|
||||
, 'w'),
|
||||
new Shortcut('Toggle double pages',
|
||||
(ctx: any) => ctx.toggleDoublePages()
|
||||
, 'd'),
|
||||
new Shortcut('Cycle scale',
|
||||
(ctx: any) => ctx.cycleScale()
|
||||
, 'c'),
|
||||
]
|
||||
|
||||
export const shortcutsMenus = [
|
||||
new Shortcut('Show/hide toolbars',
|
||||
(ctx: any) => ctx.toggleToolbars()
|
||||
, 'm'),
|
||||
new Shortcut('Show/hide settings menu',
|
||||
(ctx: any) => ctx.toggleSettings()
|
||||
, 's'),
|
||||
new Shortcut('Show/hide thumbnails explorer',
|
||||
(ctx: any) => ctx.toggleExplorer()
|
||||
, 't'),
|
||||
new Shortcut('Show/hide help',
|
||||
(ctx: any) => ctx.toggleHelp()
|
||||
, 'h'),
|
||||
new Shortcut('Close',
|
||||
(ctx: any) => ctx.closeDialog()
|
||||
, 'Escape'),
|
||||
]
|
||||
34
komga-webui/src/functions/shortcuts/paged-reader.ts
Normal file
34
komga-webui/src/functions/shortcuts/paged-reader.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Shortcut } from '@/types/shortcuts'
|
||||
|
||||
export const shortcutsLTR = [
|
||||
new Shortcut('Previous page',
|
||||
(ctx: any) => {
|
||||
ctx.turnLeft()
|
||||
}, 'ArrowLeft', '←'),
|
||||
new Shortcut('Next page',
|
||||
(ctx: any) => {
|
||||
ctx.turnRight()
|
||||
}, 'ArrowRight', '→'),
|
||||
]
|
||||
|
||||
export const shortcutsRTL = [
|
||||
new Shortcut('Previous page',
|
||||
(ctx: any) => {
|
||||
ctx.turnRight()
|
||||
}, 'ArrowRight', '→'),
|
||||
new Shortcut('Next page',
|
||||
(ctx: any) => {
|
||||
ctx.turnLeft()
|
||||
}, 'ArrowLeft', '←'),
|
||||
]
|
||||
|
||||
export const shortcutsVertical = [
|
||||
new Shortcut('Previous page',
|
||||
(ctx: any) => {
|
||||
ctx.verticalPrev()
|
||||
}, 'ArrowUp', '↑'),
|
||||
new Shortcut('Next page',
|
||||
(ctx: any) => {
|
||||
ctx.verticalNext()
|
||||
}, 'ArrowDown', '↓'),
|
||||
]
|
||||
12
komga-webui/src/functions/shortcuts/reader.ts
Normal file
12
komga-webui/src/functions/shortcuts/reader.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Shortcut } from '@/types/shortcuts'
|
||||
|
||||
export const shortcutsAll = [
|
||||
new Shortcut('First page',
|
||||
(ctx: any) => {
|
||||
ctx.goToFirst()
|
||||
}, 'Home'),
|
||||
new Shortcut('Last page',
|
||||
(ctx: any) => {
|
||||
ctx.goToLast()
|
||||
}, 'End'),
|
||||
]
|
||||
6
komga-webui/src/types/enum-reader.ts
Normal file
6
komga-webui/src/types/enum-reader.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export enum ScaleType {
|
||||
SCREEN = 'screen',
|
||||
WIDTH = 'width',
|
||||
HEIGHT = 'height',
|
||||
ORIGINAL = 'original'
|
||||
}
|
||||
|
|
@ -28,6 +28,15 @@ interface PageDto {
|
|||
height?: number,
|
||||
}
|
||||
|
||||
interface PageDtoWithUrl {
|
||||
number: number,
|
||||
fileName: string,
|
||||
mediaType: string,
|
||||
width?: number,
|
||||
height?: number,
|
||||
url: string,
|
||||
}
|
||||
|
||||
interface BookMetadataDto {
|
||||
created: string,
|
||||
lastModified: string,
|
||||
|
|
|
|||
21
komga-webui/src/types/shortcuts.ts
Normal file
21
komga-webui/src/types/shortcuts.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
type Action = (ctx: any) => void
|
||||
|
||||
export class Shortcut {
|
||||
description: string
|
||||
action: Action
|
||||
key: string
|
||||
display: string
|
||||
|
||||
|
||||
constructor (description: string, action: Action, key: string, display: string = key) {
|
||||
this.description = description
|
||||
this.action = action
|
||||
this.key = key
|
||||
this.display = display
|
||||
}
|
||||
|
||||
execute (ctx: any): boolean {
|
||||
this.action(ctx)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,13 @@
|
|||
<template>
|
||||
<v-container class="ma-0 pa-0 full-height" fluid v-if="pages.length > 0"
|
||||
:style="`width: 100%; background-color: ${backgroundColor}`"
|
||||
v-touch="{
|
||||
left: () => {if(swipe) {turnRight()}},
|
||||
right: () => {if(swipe) {turnLeft()}},
|
||||
up: () => {if(swipe) {verticalNext()}},
|
||||
down: () => {if(swipe) {verticalPrev()}}
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<v-slide-y-transition>
|
||||
<!-- Top Toolbar-->
|
||||
<v-toolbar
|
||||
dense elevation="1"
|
||||
v-if="toolbar"
|
||||
v-if="showToolbars"
|
||||
class="settings full-width"
|
||||
style="position: fixed; top: 0"
|
||||
>
|
||||
|
|
@ -25,21 +19,28 @@
|
|||
</v-btn>
|
||||
<v-toolbar-title> {{ bookTitle }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<shortcut-help-menu/>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
@click="showThumbnailsExplorer = !showThumbnailsExplorer"
|
||||
@click="showHelp = !showHelp">
|
||||
<v-icon>mdi-help-circle</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
@click="showExplorer = !showExplorer"
|
||||
>
|
||||
<v-icon>mdi-view-grid</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
@click="menu = !menu"
|
||||
@click="showSettings = !showSettings"
|
||||
>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
</v-slide-y-transition>
|
||||
|
||||
<v-slide-y-reverse-transition>
|
||||
<!-- Bottom Toolbar-->
|
||||
<v-toolbar
|
||||
|
|
@ -48,7 +49,7 @@
|
|||
class="settings full-width"
|
||||
style="position: fixed; bottom: 0"
|
||||
horizontal
|
||||
v-if="toolbar"
|
||||
v-if="showToolbars"
|
||||
>
|
||||
<v-row justify="center">
|
||||
<!-- Menu: page slider -->
|
||||
|
|
@ -66,7 +67,7 @@
|
|||
<v-icon @click="previousBook" class="">mdi-undo</v-icon>
|
||||
<v-icon @click="goToFirst" class="mx-2">mdi-skip-previous</v-icon>
|
||||
<v-label>
|
||||
{{ currentPage }}
|
||||
{{ page }}
|
||||
</v-label>
|
||||
</template>
|
||||
<template v-slot:append>
|
||||
|
|
@ -84,119 +85,102 @@
|
|||
</v-slide-y-reverse-transition>
|
||||
</div>
|
||||
|
||||
<!-- clickable zone: left -->
|
||||
<div @click="turnLeft()"
|
||||
class="left-quarter full-height top"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: menu -->
|
||||
<div @click="toolbar = !toolbar"
|
||||
class="center-half full-height top"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<!-- clickable zone: right -->
|
||||
<div @click="turnRight()"
|
||||
class="right-quarter full-height top"
|
||||
style="z-index: 1;"
|
||||
/>
|
||||
|
||||
<div class="full-height">
|
||||
<!-- Carousel -->
|
||||
<v-carousel v-model="carouselPage"
|
||||
:show-arrows="false"
|
||||
:continuous="false"
|
||||
:reverse="flipDirection"
|
||||
:vertical="vertical"
|
||||
hide-delimiters
|
||||
touchless
|
||||
height="100%"
|
||||
>
|
||||
<!-- Carousel: pages -->
|
||||
<v-carousel-item v-for="(spread, i) in spreads"
|
||||
:key="`spread${i}`"
|
||||
:eager="eagerLoad(i)"
|
||||
class="full-height"
|
||||
:transition="animations ? undefined : false"
|
||||
:reverse-transition="animations ? undefined : false"
|
||||
>
|
||||
<div class="full-height d-flex flex-column justify-center">
|
||||
<div :class="`d-flex flex-row${flipDirection ? '-reverse' : ''} justify-center px-0 mx-0` ">
|
||||
<img v-for="(page, j) in spread"
|
||||
:key="`spread${i}-${j}`"
|
||||
:src="getPageUrl(page.number)"
|
||||
:height="maxHeight"
|
||||
:width="maxWidth(i)"
|
||||
:style="imgStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-carousel-item>
|
||||
</v-carousel>
|
||||
<continuous-reader
|
||||
v-if="continuousReader"
|
||||
:pages="pages"
|
||||
:page.sync="page"
|
||||
:animations="animations"
|
||||
@menu="toggleToolbars()"
|
||||
@jump-previous="jumpToPrevious()"
|
||||
@jump-next="jumpToNext()"
|
||||
></continuous-reader>
|
||||
|
||||
<paged-reader
|
||||
v-else
|
||||
:pages="pages"
|
||||
:page.sync="page"
|
||||
:reading-direction="readingDirection"
|
||||
:double-pages="doublePages"
|
||||
:scale="scale"
|
||||
:animations="animations"
|
||||
:swipe="swipe"
|
||||
@menu="toggleToolbars()"
|
||||
@jump-previous="jumpToPrevious()"
|
||||
@jump-next="jumpToNext()"
|
||||
></paged-reader>
|
||||
</div>
|
||||
|
||||
<thumbnail-explorer-dialog
|
||||
v-model="showThumbnailsExplorer"
|
||||
v-model="showExplorer"
|
||||
:bookId="bookId"
|
||||
@goToPage="goTo"
|
||||
@go="goTo"
|
||||
:pagesCount="pagesCount"
|
||||
></thumbnail-explorer-dialog>
|
||||
|
||||
<v-bottom-sheet
|
||||
v-model="menu"
|
||||
v-model="showSettings"
|
||||
:close-on-content-click="false"
|
||||
:width="$vuetify.breakpoint.width * ($vuetify.breakpoint.smAndUp ? 0.5 : 1)"
|
||||
max-width="500"
|
||||
@keydown.esc.stop=""
|
||||
scrollable
|
||||
>
|
||||
<v-container fluid class="pa-0">
|
||||
<v-card>
|
||||
<v-toolbar dark color="primary">
|
||||
<v-btn icon dark @click="menu = false">
|
||||
<v-btn icon dark @click="showSettings = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>Reader Settings</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<v-list class="full-height full-width">
|
||||
<v-list-item>
|
||||
<settings-switch v-model="doublePages" label="Page Layout"
|
||||
:status="`${ doublePages ? 'Double Pages' : 'Single Page'}`"></settings-switch>
|
||||
</v-list-item>
|
||||
<v-card-text class="pa-0">
|
||||
<v-list class="full-height full-width">
|
||||
<v-subheader class="font-weight-black text-h6">General</v-subheader>
|
||||
<v-list-item>
|
||||
<settings-select
|
||||
:items="readingDirs"
|
||||
v-model="readingDirection"
|
||||
label="Reading mode"
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<settings-switch v-model="animations" label="Page Transitions"></settings-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<settings-switch v-model="animations" label="Animate page transitions"></settings-switch>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<settings-switch v-model="swipe" label="Swipe navigation"></settings-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<settings-switch v-model="swipe" label="Gestures"></settings-switch>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<settings-select
|
||||
:items="backgroundColors"
|
||||
v-model="backgroundColor"
|
||||
label="Background color"
|
||||
>
|
||||
</settings-select>
|
||||
</v-list-item>
|
||||
<v-subheader class="font-weight-black text-h6">Display</v-subheader>
|
||||
<v-list-item>
|
||||
<settings-select
|
||||
:items="backgroundColors"
|
||||
v-model="backgroundColor"
|
||||
label="Background color"
|
||||
>
|
||||
</settings-select>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<settings-select
|
||||
:items="readingDirs"
|
||||
v-model="readingDirection"
|
||||
label="Reading Direction"
|
||||
/>
|
||||
</v-list-item>
|
||||
<div v-if="!continuousReader">
|
||||
<v-subheader class="font-weight-black text-h6">Paged</v-subheader>
|
||||
<v-list-item>
|
||||
<settings-select
|
||||
:items="scaleTypes"
|
||||
v-model="scale"
|
||||
label="Scale type"
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<settings-select
|
||||
:items="imageFits"
|
||||
v-model="imageFit"
|
||||
label="Scaling"
|
||||
/>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-container>
|
||||
<v-list-item>
|
||||
<settings-switch v-model="doublePages" label="Double pages"></settings-switch>
|
||||
</v-list-item>
|
||||
</div>
|
||||
|
||||
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-bottom-sheet>
|
||||
<v-snackbar
|
||||
v-model="jumpToPreviousBook"
|
||||
|
|
@ -207,7 +191,7 @@
|
|||
multi-line
|
||||
class="mt-12"
|
||||
>
|
||||
<div class="title pa-6">
|
||||
<div class="text-h6 pa-6">
|
||||
<p>You're at the beginning<br/>of the book.</p>
|
||||
<p v-if="!$_.isEmpty(siblingPrevious)">Click or press previous again<br/>to move to the previous book.</p>
|
||||
</div>
|
||||
|
|
@ -250,17 +234,19 @@
|
|||
|
||||
<v-snackbar
|
||||
v-model="notification.enabled"
|
||||
class="mb-12"
|
||||
color="rgba(0, 0, 0, 0.8)"
|
||||
multi-line
|
||||
vertical
|
||||
centered
|
||||
:timeout="notification.timeout"
|
||||
>
|
||||
<div class="title pa-2">
|
||||
<span class="text-h6">
|
||||
{{ notification.message }}
|
||||
</div>
|
||||
</span>
|
||||
</v-snackbar>
|
||||
|
||||
<shortcut-help-dialog
|
||||
v-model="showHelp"
|
||||
:shortcuts="shortcutsHelp"
|
||||
/>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
|
|
@ -268,15 +254,20 @@
|
|||
import SettingsSelect from '@/components/SettingsSelect.vue'
|
||||
import SettingsSwitch from '@/components/SettingsSwitch.vue'
|
||||
import ThumbnailExplorerDialog from '@/components/dialogs/ThumbnailExplorerDialog.vue'
|
||||
import ShortcutHelpMenu from '@/components/menus/ShortcutHelpMenu.vue'
|
||||
import ShortcutHelpDialog from '@/components/dialogs/ShortcutHelpDialog.vue'
|
||||
import { getBookTitleCompact } from '@/functions/book-title'
|
||||
import { checkWebpFeature } from '@/functions/check-webp'
|
||||
import { bookPageUrl } from '@/functions/urls'
|
||||
import { ReadingDirection } from '@/types/enum-books'
|
||||
import { executeShortcut } from '@/functions/shortcuts'
|
||||
import Vue from 'vue'
|
||||
import { isPageLandscape } from '@/functions/page'
|
||||
import { Location } from 'vue-router'
|
||||
import PagedReader from '@/components/readers/PagedReader.vue'
|
||||
import ContinuousReader from '@/components/readers/ContinuousReader.vue'
|
||||
import { ScaleType } from '@/types/enum-reader'
|
||||
import { ReadingDirectionText, ScaleTypeText } from '@/functions/reader'
|
||||
import { shortcutsLTR, shortcutsRTL, shortcutsVertical } from '@/functions/shortcuts/paged-reader'
|
||||
import { shortcutsMenus, shortcutsSettings } from '@/functions/shortcuts/bookreader'
|
||||
import { shortcutsAll } from '@/functions/shortcuts/reader'
|
||||
|
||||
const cookieFit = 'webreader.fit'
|
||||
const cookieReadingDirection = 'webreader.readingDirection'
|
||||
|
|
@ -285,18 +276,18 @@ const cookieSwipe = 'webreader.swipe'
|
|||
const cookieAnimations = 'webreader.animations'
|
||||
const cookieBackground = 'webreader.background'
|
||||
|
||||
enum ImageFit {
|
||||
WIDTH = 'width',
|
||||
HEIGHT = 'height',
|
||||
ORIGINAL = 'original'
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BookReader',
|
||||
components: { SettingsSwitch, SettingsSelect, ThumbnailExplorerDialog, ShortcutHelpMenu },
|
||||
components: {
|
||||
ContinuousReader,
|
||||
PagedReader,
|
||||
SettingsSwitch,
|
||||
SettingsSelect,
|
||||
ThumbnailExplorerDialog,
|
||||
ShortcutHelpDialog,
|
||||
},
|
||||
data: () => {
|
||||
return {
|
||||
ImageFit,
|
||||
book: {} as BookDto,
|
||||
series: {} as SeriesDto,
|
||||
siblingPrevious: {} as BookDto,
|
||||
|
|
@ -305,40 +296,37 @@ export default Vue.extend({
|
|||
jumpToPreviousBook: false,
|
||||
jumpConfirmationDelay: 3000,
|
||||
snackReadingDirection: false,
|
||||
pages: [] as PageDto[],
|
||||
pages: [] as PageDtoWithUrl[],
|
||||
page: 1,
|
||||
supportedMediaTypes: ['image/jpeg', 'image/png', 'image/gif'],
|
||||
convertTo: 'jpeg',
|
||||
carouselPage: 0,
|
||||
showThumbnailsExplorer: false,
|
||||
toolbar: false,
|
||||
menu: false,
|
||||
dialogGoto: false,
|
||||
showExplorer: false,
|
||||
showToolbars: false,
|
||||
showSettings: false,
|
||||
showHelp: false,
|
||||
goToPage: 1,
|
||||
settings: {
|
||||
doublePages: false,
|
||||
swipe: true,
|
||||
fit: ImageFit.HEIGHT,
|
||||
readingDirection: ReadingDirection.LEFT_TO_RIGHT,
|
||||
animations: true,
|
||||
scale: ScaleType.SCREEN,
|
||||
readingDirection: ReadingDirection.LEFT_TO_RIGHT,
|
||||
backgroundColor: 'black',
|
||||
imageFits: Object.values(ImageFit),
|
||||
readingDirs: Object.values(ReadingDirection),
|
||||
},
|
||||
shortcuts: {} as any,
|
||||
notification: {
|
||||
enabled: false,
|
||||
message: '',
|
||||
timeout: 4000,
|
||||
},
|
||||
readingDirs: [
|
||||
{ text: 'Left to right', value: ReadingDirection.LEFT_TO_RIGHT },
|
||||
{ text: 'Right to left', value: ReadingDirection.RIGHT_TO_LEFT },
|
||||
{ text: 'Vertical', value: ReadingDirection.VERTICAL },
|
||||
],
|
||||
imageFits: [
|
||||
{ text: 'Fit to height', value: ImageFit.HEIGHT },
|
||||
{ text: 'Fit to width', value: ImageFit.WIDTH },
|
||||
{ text: 'Original', value: ImageFit.ORIGINAL },
|
||||
],
|
||||
readingDirs: Object.values(ReadingDirection).map(x => ({
|
||||
text: ReadingDirectionText[x],
|
||||
value: x,
|
||||
})),
|
||||
scaleTypes: Object.values(ScaleType).map(x => ({
|
||||
text: ScaleTypeText[x],
|
||||
value: x,
|
||||
})),
|
||||
backgroundColors: [
|
||||
{ text: 'White', value: 'white' },
|
||||
{ text: 'Black', value: 'black' },
|
||||
|
|
@ -351,10 +339,10 @@ export default Vue.extend({
|
|||
this.supportedMediaTypes.push('image/webp')
|
||||
}
|
||||
})
|
||||
this.shortcuts = this.$_.keyBy([...shortcutsSettings, ...shortcutsMenus, ...shortcutsAll], x => x.key)
|
||||
window.addEventListener('keydown', this.keyPressed)
|
||||
},
|
||||
async mounted () {
|
||||
window.addEventListener('keydown', this.keyPressed)
|
||||
|
||||
this.loadFromCookie(cookieReadingDirection, (v) => {
|
||||
this.readingDirection = v
|
||||
})
|
||||
|
|
@ -368,14 +356,10 @@ export default Vue.extend({
|
|||
this.swipe = (v === 'true')
|
||||
})
|
||||
this.loadFromCookie(cookieFit, (v) => {
|
||||
if (v) {
|
||||
this.imageFit = v
|
||||
}
|
||||
this.scale = v
|
||||
})
|
||||
this.loadFromCookie(cookieBackground, (v) => {
|
||||
if (v) {
|
||||
this.backgroundColor = v
|
||||
}
|
||||
this.backgroundColor = v
|
||||
})
|
||||
|
||||
this.setup(this.bookId, Number(this.$route.query.page))
|
||||
|
|
@ -397,7 +381,7 @@ export default Vue.extend({
|
|||
next()
|
||||
},
|
||||
watch: {
|
||||
currentPage (val) {
|
||||
page (val) {
|
||||
this.updateRoute()
|
||||
this.goToPage = val
|
||||
this.markProgress(val)
|
||||
|
|
@ -410,32 +394,11 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
currentSlide (): number {
|
||||
return this.carouselPage + 1
|
||||
},
|
||||
currentPage (): number {
|
||||
if (this.carouselPage >= 0 && this.carouselPage < this.spreads.length && this.spreads.length > 0) {
|
||||
return this.spreads[this.carouselPage][0].number
|
||||
}
|
||||
return 1
|
||||
},
|
||||
canPrev (): boolean {
|
||||
return this.currentSlide > 1
|
||||
},
|
||||
canNext (): boolean {
|
||||
return this.currentSlide < this.slidesCount
|
||||
continuousReader (): boolean {
|
||||
return this.readingDirection === ReadingDirection.WEBTOON
|
||||
},
|
||||
progress (): number {
|
||||
return this.currentPage / this.pagesCount * 100
|
||||
},
|
||||
maxHeight (): number | null {
|
||||
return this.imageFit === ImageFit.HEIGHT ? this.$vuetify.breakpoint.height : null
|
||||
},
|
||||
imgStyle (): string {
|
||||
return this.imageFit === ImageFit.WIDTH ? 'height:intrinsic' : ''
|
||||
},
|
||||
slidesCount (): number {
|
||||
return this.spreads.length
|
||||
return this.page / this.pagesCount * 100
|
||||
},
|
||||
pagesCount (): number {
|
||||
return this.pages.length
|
||||
|
|
@ -443,6 +406,30 @@ export default Vue.extend({
|
|||
bookTitle (): string {
|
||||
return getBookTitleCompact(this.book.metadata.title, this.series.metadata.title)
|
||||
},
|
||||
readingDirectionText (): string {
|
||||
return ReadingDirectionText[this.readingDirection]
|
||||
},
|
||||
shortcutsHelp (): object {
|
||||
let nav = []
|
||||
switch (this.readingDirection) {
|
||||
case ReadingDirection.LEFT_TO_RIGHT:
|
||||
nav.push(...shortcutsLTR, ...shortcutsAll)
|
||||
break
|
||||
case ReadingDirection.RIGHT_TO_LEFT:
|
||||
nav.push(...shortcutsRTL, ...shortcutsAll)
|
||||
break
|
||||
case ReadingDirection.VERTICAL:
|
||||
nav.push(...shortcutsVertical, ...shortcutsAll)
|
||||
break
|
||||
default:
|
||||
nav.push(...shortcutsAll)
|
||||
}
|
||||
return {
|
||||
'Reader Navigation': nav,
|
||||
'Settings': shortcutsSettings,
|
||||
'Menus': shortcutsMenus,
|
||||
}
|
||||
},
|
||||
|
||||
animations: {
|
||||
get: function (): boolean {
|
||||
|
|
@ -453,31 +440,15 @@ export default Vue.extend({
|
|||
this.$cookies.set(cookieAnimations, animations, Infinity)
|
||||
},
|
||||
},
|
||||
readingDirection: {
|
||||
get: function (): ReadingDirection {
|
||||
return this.settings.readingDirection
|
||||
scale: {
|
||||
get: function (): ScaleType {
|
||||
return this.settings.scale
|
||||
},
|
||||
set: function (readingDirection: ReadingDirection): void {
|
||||
this.settings.readingDirection = readingDirection
|
||||
this.$cookies.set(cookieReadingDirection, readingDirection, Infinity)
|
||||
},
|
||||
},
|
||||
readingDirectionText (): string {
|
||||
return this.readingDirs.find(x => x.value === this.readingDirection)?.text ?? ''
|
||||
},
|
||||
flipDirection (): boolean {
|
||||
return this.readingDirection === ReadingDirection.RIGHT_TO_LEFT
|
||||
},
|
||||
vertical (): boolean {
|
||||
return this.readingDirection === ReadingDirection.VERTICAL
|
||||
},
|
||||
imageFit: {
|
||||
get: function (): ImageFit {
|
||||
return this.settings.fit
|
||||
},
|
||||
set: function (fit: ImageFit): void {
|
||||
this.settings.fit = fit
|
||||
this.$cookies.set(cookieFit, fit, Infinity)
|
||||
set: function (scale: ScaleType): void {
|
||||
if (Object.values(ScaleType).includes(scale)) {
|
||||
this.settings.scale = scale
|
||||
this.$cookies.set(cookieFit, scale, Infinity)
|
||||
}
|
||||
},
|
||||
},
|
||||
backgroundColor: {
|
||||
|
|
@ -485,8 +456,21 @@ export default Vue.extend({
|
|||
return this.settings.backgroundColor
|
||||
},
|
||||
set: function (color: string): void {
|
||||
this.settings.backgroundColor = color
|
||||
this.$cookies.set(cookieBackground, color, Infinity)
|
||||
if (this.backgroundColors.map(x => x.value).includes(color)) {
|
||||
this.settings.backgroundColor = color
|
||||
this.$cookies.set(cookieBackground, color, Infinity)
|
||||
}
|
||||
},
|
||||
},
|
||||
readingDirection: {
|
||||
get: function (): ReadingDirection {
|
||||
return this.settings.readingDirection
|
||||
},
|
||||
set: function (readingDirection: ReadingDirection): void {
|
||||
if (Object.values(ReadingDirection).includes(readingDirection)) {
|
||||
this.settings.readingDirection = readingDirection
|
||||
this.$cookies.set(cookieReadingDirection, readingDirection, Infinity)
|
||||
}
|
||||
},
|
||||
},
|
||||
doublePages: {
|
||||
|
|
@ -494,9 +478,7 @@ export default Vue.extend({
|
|||
return this.settings.doublePages
|
||||
},
|
||||
set: function (doublePages: boolean): void {
|
||||
const current = this.currentPage
|
||||
this.settings.doublePages = doublePages
|
||||
this.goTo(current)
|
||||
this.$cookies.set(cookieDoublePages, doublePages, Infinity)
|
||||
},
|
||||
},
|
||||
|
|
@ -509,42 +491,17 @@ export default Vue.extend({
|
|||
this.$cookies.set(cookieSwipe, swipe, Infinity)
|
||||
},
|
||||
},
|
||||
spreads (): PageDto[][] {
|
||||
if (this.pages.length === 0) return []
|
||||
if (this.doublePages) {
|
||||
const spreads = []
|
||||
spreads.push([this.pages[0]])
|
||||
const pages = this.$_.drop(this.$_.dropRight(this.pages)) as PageDto[]
|
||||
while (pages.length > 0) {
|
||||
const p = pages.shift() as PageDto
|
||||
if (isPageLandscape(p)) {
|
||||
spreads.push([p])
|
||||
} else {
|
||||
if (pages.length > 0) {
|
||||
const p2 = pages.shift() as PageDto
|
||||
if (isPageLandscape(p2)) {
|
||||
spreads.push([p])
|
||||
spreads.push([p2])
|
||||
} else {
|
||||
spreads.push([p, p2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
spreads.push([this.pages[this.pages.length - 1]])
|
||||
return spreads
|
||||
} else {
|
||||
return this.pages.map(p => [p])
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
keyPressed (e: KeyboardEvent) {
|
||||
executeShortcut(this, e)
|
||||
this.shortcuts[e.key]?.execute(this)
|
||||
},
|
||||
async setup (bookId: string, page: number) {
|
||||
this.book = await this.$komgaBooks.getBook(bookId)
|
||||
this.pages = await this.$komgaBooks.getBookPages(bookId)
|
||||
const pageDtos = (await this.$komgaBooks.getBookPages(bookId))
|
||||
pageDtos.forEach((p: any) => p['url'] = this.getPageUrl(p))
|
||||
this.pages = pageDtos as PageDtoWithUrl[]
|
||||
|
||||
if (page >= 1 && page <= this.pagesCount) {
|
||||
this.goTo(page)
|
||||
} else if (this.book.readProgress?.completed === false) {
|
||||
|
|
@ -554,16 +511,10 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
// set non-persistent reading direction if exists in metadata
|
||||
switch (this.book.metadata.readingDirection) {
|
||||
case ReadingDirection.LEFT_TO_RIGHT:
|
||||
case ReadingDirection.RIGHT_TO_LEFT:
|
||||
case ReadingDirection.VERTICAL:
|
||||
if (this.readingDirection !== this.book.metadata.readingDirection) {
|
||||
// bypass setter so cookies aren't set
|
||||
this.settings.readingDirection = this.book.metadata.readingDirection
|
||||
this.snackReadingDirection = true
|
||||
}
|
||||
break
|
||||
if (this.book.metadata.readingDirection in ReadingDirection && this.readingDirection !== this.book.metadata.readingDirection) {
|
||||
// bypass setter so cookies aren't set
|
||||
this.settings.readingDirection = this.book.metadata.readingDirection as ReadingDirection
|
||||
this.snackReadingDirection = true
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -577,49 +528,31 @@ export default Vue.extend({
|
|||
this.siblingPrevious = {} as BookDto
|
||||
}
|
||||
},
|
||||
getPageUrl (page: number): string {
|
||||
if (!this.supportedMediaTypes.includes(this.pages[page - 1].mediaType)) {
|
||||
return bookPageUrl(this.bookId, page, this.convertTo)
|
||||
getPageUrl (page: PageDto): string {
|
||||
if (!this.supportedMediaTypes.includes(page.mediaType)) {
|
||||
return bookPageUrl(this.bookId, page.number, this.convertTo)
|
||||
} else {
|
||||
return bookPageUrl(this.bookId, page)
|
||||
return bookPageUrl(this.bookId, page.number)
|
||||
}
|
||||
},
|
||||
turnRight () {
|
||||
if (this.vertical) return
|
||||
return this.flipDirection ? this.prev() : this.next()
|
||||
},
|
||||
turnLeft () {
|
||||
if (this.vertical) return
|
||||
return this.flipDirection ? this.next() : this.prev()
|
||||
},
|
||||
verticalPrev () {
|
||||
if (this.vertical) this.prev()
|
||||
},
|
||||
verticalNext () {
|
||||
if (this.vertical) this.next()
|
||||
},
|
||||
prev () {
|
||||
if (this.canPrev) {
|
||||
this.carouselPage--
|
||||
window.scrollTo(0, 0)
|
||||
jumpToPrevious () {
|
||||
if (this.jumpToPreviousBook) {
|
||||
this.previousBook()
|
||||
} else {
|
||||
if (this.jumpToPreviousBook) {
|
||||
this.previousBook()
|
||||
} else {
|
||||
this.jumpToPreviousBook = true
|
||||
}
|
||||
this.jumpToPreviousBook = true
|
||||
}
|
||||
},
|
||||
next () {
|
||||
if (this.canNext) {
|
||||
this.carouselPage++
|
||||
window.scrollTo(0, 0)
|
||||
jumpToNext () {
|
||||
if (this.jumpToNextBook) {
|
||||
this.nextBook()
|
||||
} else {
|
||||
if (this.jumpToNextBook) {
|
||||
this.nextBook()
|
||||
} else {
|
||||
this.jumpToNextBook = true
|
||||
}
|
||||
this.jumpToNextBook = true
|
||||
}
|
||||
},
|
||||
previousBook () {
|
||||
if (!this.$_.isEmpty(this.siblingPrevious)) {
|
||||
this.jumpToPreviousBook = false
|
||||
this.$router.push({ name: 'read-book', params: { bookId: this.siblingPrevious.id.toString() } })
|
||||
}
|
||||
},
|
||||
nextBook () {
|
||||
|
|
@ -630,14 +563,8 @@ export default Vue.extend({
|
|||
this.$router.push({ name: 'read-book', params: { bookId: this.siblingNext.id.toString() } })
|
||||
}
|
||||
},
|
||||
previousBook () {
|
||||
if (!this.$_.isEmpty(this.siblingPrevious)) {
|
||||
this.jumpToPreviousBook = false
|
||||
this.$router.push({ name: 'read-book', params: { bookId: this.siblingPrevious.id.toString() } })
|
||||
}
|
||||
},
|
||||
goTo (page: number) {
|
||||
this.carouselPage = this.toSpreadIndex(page)
|
||||
this.page = page
|
||||
},
|
||||
goToFirst () {
|
||||
this.goTo(1)
|
||||
|
|
@ -650,46 +577,13 @@ export default Vue.extend({
|
|||
name: this.$route.name,
|
||||
params: { bookId: this.$route.params.bookId },
|
||||
query: {
|
||||
page: this.currentPage.toString(),
|
||||
page: this.page.toString(),
|
||||
},
|
||||
} as Location)
|
||||
},
|
||||
closeBook () {
|
||||
this.$router.push({ name: 'browse-book', params: { bookId: this.bookId.toString() } })
|
||||
},
|
||||
toSinglePages (i: number): number {
|
||||
if (i === 1) return 1
|
||||
if (i === this.slidesCount) return this.pagesCount
|
||||
return (i - 1) * 2
|
||||
},
|
||||
toSpreadIndex (i: number): number {
|
||||
if (this.spreads.length > 0) {
|
||||
if (this.doublePages) {
|
||||
for (let j = 0; j < this.spreads.length; j++) {
|
||||
for (let k = 0; k < this.spreads[j].length; k++) {
|
||||
if (this.spreads[j][k].number === i) {
|
||||
return j
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return i - 1
|
||||
}
|
||||
}
|
||||
return i - 1
|
||||
},
|
||||
eagerLoad (spreadIndex: number): boolean {
|
||||
return Math.abs(this.carouselPage - spreadIndex) <= 2
|
||||
},
|
||||
maxWidth (spreadIndex: number): number | null {
|
||||
if (this.imageFit !== ImageFit.WIDTH) {
|
||||
return null
|
||||
}
|
||||
if (this.doublePages && this.spreads[spreadIndex].length === 2) {
|
||||
return this.$vuetify.breakpoint.width / 2
|
||||
}
|
||||
return this.$vuetify.breakpoint.width
|
||||
},
|
||||
loadFromCookie (cookieKey: string, setter: (value: any) => void): void {
|
||||
if (this.$cookies.isKey(cookieKey)) {
|
||||
setter(this.$cookies.get(cookieKey))
|
||||
|
|
@ -697,24 +591,51 @@ export default Vue.extend({
|
|||
},
|
||||
changeReadingDir (dir: ReadingDirection) {
|
||||
this.readingDirection = dir
|
||||
const i = this.settings.readingDirs.indexOf(this.readingDirection)
|
||||
const text = this.readingDirs[i].text
|
||||
const text = ReadingDirectionText[this.readingDirection]
|
||||
this.sendNotification(`Changing Reading Direction to: ${text}`)
|
||||
},
|
||||
cycleScale () {
|
||||
const fit: ImageFit = this.settings.fit
|
||||
const i = (this.settings.imageFits.indexOf(fit) + 1) % (this.settings.imageFits.length)
|
||||
this.imageFit = this.settings.imageFits[i]
|
||||
const text = this.imageFits[i].text
|
||||
// The text here only works cause this.imageFits has the same index structure as the ImageFit enum
|
||||
if (this.continuousReader) return
|
||||
const enumValues = Object.values(ScaleType)
|
||||
const i = (enumValues.indexOf(this.settings.scale) + 1) % (enumValues.length)
|
||||
this.scale = enumValues[i]
|
||||
const text = ScaleTypeText[this.scale]
|
||||
this.sendNotification(`Cycling Scale: ${text}`)
|
||||
},
|
||||
toggleDoublePages () {
|
||||
if (this.continuousReader) return
|
||||
this.doublePages = !this.doublePages
|
||||
this.sendNotification(`${this.doublePages ? 'Enabled' : 'Disabled'} Double Pages`)
|
||||
},
|
||||
toggleToolbars () {
|
||||
this.showToolbars = !this.showToolbars
|
||||
},
|
||||
toggleExplorer () {
|
||||
this.showExplorer = !this.showExplorer
|
||||
},
|
||||
toggleSettings () {
|
||||
this.showSettings = !this.showSettings
|
||||
},
|
||||
toggleHelp () {
|
||||
this.showHelp = !this.showHelp
|
||||
},
|
||||
closeDialog () {
|
||||
if (this.showExplorer) {
|
||||
this.showExplorer = false
|
||||
return
|
||||
}
|
||||
if (this.showSettings) {
|
||||
this.showSettings = false
|
||||
return
|
||||
}
|
||||
if (this.showToolbars) {
|
||||
this.showToolbars = false
|
||||
return
|
||||
}
|
||||
this.closeBook()
|
||||
},
|
||||
sendNotification (message: string, timeout: number = 4000) {
|
||||
this.notification.timeout = 4000
|
||||
this.notification.timeout = timeout
|
||||
this.notification.message = message
|
||||
this.notification.enabled = true
|
||||
},
|
||||
|
|
@ -724,17 +645,11 @@ export default Vue.extend({
|
|||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings {
|
||||
/*position: absolute;*/
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.full-height {
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -742,22 +657,4 @@ export default Vue.extend({
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.left-quarter {
|
||||
left: 0;
|
||||
width: 20%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.right-quarter {
|
||||
right: 0;
|
||||
width: 20%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.center-half {
|
||||
left: 20%;
|
||||
width: 60%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Reference in a new issue