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:
Gauthier Roebroeck 2020-08-06 10:11:23 +08:00
parent 969382988d
commit 44c814a5ba
17 changed files with 997 additions and 610 deletions

View file

@ -22,7 +22,7 @@ export default Vue.extend({
name: 'LibraryNavigation',
props: {
libraryId: {
type: Number,
type: String,
required: true,
},
},

View file

@ -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

View file

@ -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"

View 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>

View file

@ -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)

View file

@ -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>

View 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>

View 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>

View 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',
}

View file

@ -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)
}

View 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'),
]

View 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', '↓'),
]

View 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'),
]

View file

@ -0,0 +1,6 @@
export enum ScaleType {
SCREEN = 'screen',
WIDTH = 'width',
HEIGHT = 'height',
ORIGINAL = 'original'
}

View file

@ -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,

View 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
}
}

View file

@ -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>