mirror of
https://github.com/gotson/komga.git
synced 2026-05-09 05:10:19 +02:00
parent
917012b7c5
commit
7f0ab5fde3
6 changed files with 566 additions and 326 deletions
75
komga-webui/src/components/SettingsSelect.vue
Normal file
75
komga-webui/src/components/SettingsSelect.vue
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<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>
|
||||||
|
<v-col cols="7" md="4" >
|
||||||
|
<v-select
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
solo
|
||||||
|
:items="items"
|
||||||
|
v-model="input"
|
||||||
|
hide-selected
|
||||||
|
@input="updateInput"
|
||||||
|
@change="updateInput"
|
||||||
|
hide-details="true"
|
||||||
|
>
|
||||||
|
<template v-slot:item="data">
|
||||||
|
<slot name="item" v-bind="data"></slot>
|
||||||
|
</template>
|
||||||
|
<template v-slot:selection="data">
|
||||||
|
<slot name="selection" v-bind="data"></slot>
|
||||||
|
</template>
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'SettingsSelect',
|
||||||
|
props: {
|
||||||
|
items: null as any,
|
||||||
|
label: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
display: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
input: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler (after) {
|
||||||
|
this.input = after
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateInput () {
|
||||||
|
this.$emit('input', this.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.v-text-field__details, div.v-input__control {
|
||||||
|
min-height: 0 !important;
|
||||||
|
}
|
||||||
|
.v-text-field__details {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
55
komga-webui/src/components/SettingsSwitch.vue
Normal file
55
komga-webui/src/components/SettingsSwitch.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<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>
|
||||||
|
<v-col cols="4" md="3" align-self="center" class="text-right">
|
||||||
|
<v-label> {{ status }} </v-label>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3" md="2" align-self="center">
|
||||||
|
<v-switch v-model="input"
|
||||||
|
@input="updateInput"
|
||||||
|
@change="updateInput"
|
||||||
|
class="float-right"
|
||||||
|
>
|
||||||
|
</v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'SettingsSwitch',
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
input: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler (after) {
|
||||||
|
this.input = after
|
||||||
|
},
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateInput () {
|
||||||
|
this.$emit('input', this.input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
100
komga-webui/src/components/ThumbnailExplorerDialog.vue
Normal file
100
komga-webui/src/components/ThumbnailExplorerDialog.vue
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="input" scrollable>
|
||||||
|
<v-card :max-height="$vuetify.breakpoint.height * .9" dark>
|
||||||
|
<v-card-title>
|
||||||
|
<v-pagination
|
||||||
|
v-model="page"
|
||||||
|
:total-visible="perPage"
|
||||||
|
:length="Math.ceil(thumbnails.length/perPage)"
|
||||||
|
></v-pagination>
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-container fluid>
|
||||||
|
<v-row class="mb-2 align-center justify-space-around">
|
||||||
|
|
||||||
|
<div v-for="(url, i) in visibleThumbnails"
|
||||||
|
:key="url"
|
||||||
|
style="min-height: 220px; max-width: 140px"
|
||||||
|
class="d-flex flex-column justify-center"
|
||||||
|
>
|
||||||
|
<v-img
|
||||||
|
:src="url"
|
||||||
|
lazy-src="../assets/cover.svg"
|
||||||
|
aspect-ratio="0.7071"
|
||||||
|
:contain="true"
|
||||||
|
max-height="200"
|
||||||
|
max-width="140"
|
||||||
|
class="ma-2"
|
||||||
|
@click="input = false; goTo(((page - 1 ) * perPage + i + 1))"
|
||||||
|
style="cursor: pointer"
|
||||||
|
/>
|
||||||
|
<div class="white--text text-center font-weight-bold">{{ (page - 1 ) * perPage + i + 1 }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue'
|
||||||
|
import { bookPageThumbnailUrl } from '@/functions/urls'
|
||||||
|
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'ThumbnailExplorerDialog',
|
||||||
|
props: {
|
||||||
|
pagesCount: {
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
bookId: {
|
||||||
|
type: Number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: () => {
|
||||||
|
return {
|
||||||
|
input: '',
|
||||||
|
page: 1,
|
||||||
|
perPage: 8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value (val) {
|
||||||
|
this.input = val
|
||||||
|
},
|
||||||
|
input (val) {
|
||||||
|
!val && this.$emit('input', false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
thumbnails (): string[] {
|
||||||
|
let thumbnails = []
|
||||||
|
for (let p = 1; p <= this.pagesCount; p++) {
|
||||||
|
thumbnails.push(this.getThumbnailUrl(p))
|
||||||
|
}
|
||||||
|
return thumbnails
|
||||||
|
},
|
||||||
|
visibleThumbnails (): String[] {
|
||||||
|
let a : number = (this.page - 1) * this.perPage
|
||||||
|
let b : number = this.page * this.perPage
|
||||||
|
return this.thumbnails.slice(a, b)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateInput () {
|
||||||
|
this.$emit('input', this.input)
|
||||||
|
},
|
||||||
|
goTo (page: number) {
|
||||||
|
this.$emit('goToPage', page)
|
||||||
|
},
|
||||||
|
getThumbnailUrl (page: number): string {
|
||||||
|
return bookPageThumbnailUrl(this.bookId, page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -3,12 +3,19 @@ import 'typeface-roboto/index.css'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Vuetify from 'vuetify/lib'
|
import Vuetify from 'vuetify/lib'
|
||||||
|
|
||||||
Vue.use(Vuetify)
|
import { Touch } from 'vuetify/lib/directives'
|
||||||
|
|
||||||
|
Vue.use(Vuetify, {
|
||||||
|
directives: {
|
||||||
|
Touch
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export default new Vuetify({
|
export default new Vuetify({
|
||||||
icons: {
|
icons: {
|
||||||
iconfont: 'mdi'
|
iconfont: 'mdi'
|
||||||
},
|
},
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
options: {
|
options: {
|
||||||
customProperties: true
|
customProperties: true
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ export enum ImageFit {
|
||||||
ORIGINAL = 'original'
|
ORIGINAL = 'original'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ReadingDirection {
|
||||||
|
LeftToRight = 'ltl',
|
||||||
|
RightToLeft = 'rtl'
|
||||||
|
}
|
||||||
|
|
||||||
export enum MediaStatus {
|
export enum MediaStatus {
|
||||||
READY = 'READY',
|
READY = 'READY',
|
||||||
UNKNOWN = 'UNKNOWN',
|
UNKNOWN = 'UNKNOWN',
|
||||||
|
|
|
||||||
|
|
@ -1,284 +1,223 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="pages.length > 0" style="background: black; width: 100%; height: 100%">
|
<v-container class="ma-0 pa-0 full-height" fluid v-if="pages.length > 0" style="width: 100%;"
|
||||||
<!-- clickable zone: left -->
|
v-touch="{
|
||||||
<div @click="rtl ? next() : prev()"
|
left: () => turnLeft(),
|
||||||
class="left-quarter full-height"
|
right: () => turnRight(),
|
||||||
style="z-index: 1; position: absolute"
|
}"
|
||||||
/>
|
>
|
||||||
|
<div>
|
||||||
<!-- clickable zone: menu -->
|
<v-slide-y-transition>
|
||||||
<div @click="showMenu = true"
|
<v-toolbar
|
||||||
class="center-half full-height"
|
dense elevation="1"
|
||||||
style="z-index: 1; position: absolute"
|
v-if="toolbar"
|
||||||
/>
|
class="settings full-width"
|
||||||
|
style="position: fixed; top: 0"
|
||||||
<!-- clickable zone: right -->
|
>
|
||||||
<div @click="rtl ? prev() : next()"
|
<v-btn
|
||||||
class="right-quarter full-height"
|
icon
|
||||||
style="z-index: 1; position: absolute"
|
@click="closeBook"
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Carousel -->
|
|
||||||
<v-carousel v-model="carouselPage"
|
|
||||||
:show-arrows="false"
|
|
||||||
hide-delimiters
|
|
||||||
:continuous="false"
|
|
||||||
height="auto"
|
|
||||||
touchless
|
|
||||||
:reverse="rtl"
|
|
||||||
>
|
|
||||||
<!-- Carousel: pages -->
|
|
||||||
<v-carousel-item v-for="p in slidesRange"
|
|
||||||
:key="doublePages ? `db${p}` : `sp${p}`"
|
|
||||||
:eager="eagerLoad(p)"
|
|
||||||
>
|
|
||||||
<div :class="`d-flex flex-row${rtl ? '-reverse' : ''} justify-center`">
|
|
||||||
<img :src="getPageUrl(p)"
|
|
||||||
:height="maxHeight"
|
|
||||||
:width="maxWidth(p)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<img v-if="doublePages && p !== 1 && p !== pagesCount && p+1 !== pagesCount"
|
|
||||||
:src="getPageUrl(p+1)"
|
|
||||||
:height="maxHeight"
|
|
||||||
:width="maxWidth(p+1)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</v-carousel-item>
|
|
||||||
</v-carousel>
|
|
||||||
|
|
||||||
<!-- Menu -->
|
|
||||||
<v-overlay :value="showMenu"
|
|
||||||
opacity=".8"
|
|
||||||
>
|
|
||||||
<!-- Menu: left zone with arrow -->
|
|
||||||
<div class="fixed-position full-height left-quarter"
|
|
||||||
style="display: flex; align-items: center; justify-content: center"
|
|
||||||
>
|
|
||||||
<v-icon size="8em">
|
|
||||||
mdi-chevron-left
|
|
||||||
</v-icon>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Menu: central zone -->
|
|
||||||
<div class="dashed-x fixed-position center-half full-height"
|
|
||||||
@click.self="showMenu = false"
|
|
||||||
>
|
|
||||||
<div style="position: absolute; top: 1em; left: 1em">
|
|
||||||
<v-btn @click="closeBook"
|
|
||||||
color="primary"
|
|
||||||
>
|
>
|
||||||
Close book
|
<v-icon>mdi-arrow-left</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-toolbar-title> {{ this.bookTitle }}</v-toolbar-title>
|
||||||
<v-btn @click="showMenu = false; showThumbnailsExplorer = true"
|
<v-spacer></v-spacer>
|
||||||
color="primary"
|
<v-btn
|
||||||
class="ml-2"
|
icon
|
||||||
|
@click="showThumbnailsExplorer = !showThumbnailsExplorer"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-view-grid</v-icon>
|
<v-icon>mdi-view-grid</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
<v-btn
|
||||||
|
icon
|
||||||
<v-btn icon
|
@click="menu = !menu"
|
||||||
@click="showMenu = false"
|
>
|
||||||
absolute
|
<v-icon>mdi-settings</v-icon>
|
||||||
top
|
</v-btn>
|
||||||
right
|
</v-toolbar>
|
||||||
|
</v-slide-y-transition>
|
||||||
|
<v-slide-y-reverse-transition>
|
||||||
|
<v-toolbar
|
||||||
|
dense
|
||||||
|
elevation="1"
|
||||||
|
class="settings full-width"
|
||||||
|
style="position: fixed; bottom: 0"
|
||||||
|
horizontal
|
||||||
|
v-if="toolbar"
|
||||||
>
|
>
|
||||||
<v-icon>mdi-close</v-icon>
|
<v-row justify="center">
|
||||||
</v-btn>
|
<!-- Menu: page slider -->
|
||||||
|
<v-col>
|
||||||
<v-container fluid
|
|
||||||
class="pa-6 pt-12"
|
|
||||||
style="border-bottom: 4px dashed"
|
|
||||||
>
|
|
||||||
<!-- Menu: book title -->
|
|
||||||
<v-row>
|
|
||||||
<v-col class="text-center title">
|
|
||||||
{{ bookTitle }}
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Menu: number of pages -->
|
|
||||||
<v-row>
|
|
||||||
<v-col class="text-center title">
|
|
||||||
Page {{ currentPage }} of {{ pagesCount }}
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Menu: progress bar -->
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-progress-linear :value="progress"
|
|
||||||
height="20"
|
|
||||||
background-color="white"
|
|
||||||
color="secondary"
|
|
||||||
rounded
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Menu: go to page -->
|
|
||||||
<v-row align="baseline" justify="center">
|
|
||||||
<v-col cols="auto">
|
|
||||||
Go to page
|
|
||||||
</v-col>
|
|
||||||
<v-col cols="auto">
|
|
||||||
<v-text-field
|
|
||||||
v-model="goToPage"
|
|
||||||
hide-details
|
|
||||||
single-line
|
|
||||||
type="number"
|
|
||||||
@change="goTo"
|
|
||||||
style="width: 4em"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Menu: page slider -->
|
|
||||||
<v-row align="baseline">
|
|
||||||
<v-col cols="12">
|
|
||||||
<v-slider
|
<v-slider
|
||||||
|
hide-details
|
||||||
|
thumb-label
|
||||||
|
@change="goTo"
|
||||||
v-model="goToPage"
|
v-model="goToPage"
|
||||||
class="align-center"
|
class="align-center"
|
||||||
:max="pagesCount"
|
|
||||||
min="1"
|
min="1"
|
||||||
hide-details
|
:max="pagesCount"
|
||||||
@change="goTo"
|
|
||||||
>
|
>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-btn icon @click="goToFirst">
|
<v-icon @click="goToFirst" class="mx-2">mdi-arrow-collapse-left</v-icon>
|
||||||
<v-icon>mdi-arrow-collapse-left</v-icon>
|
<v-label>
|
||||||
</v-btn>
|
{{ currentPage }}
|
||||||
|
</v-label>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-btn icon @click="goToLast">
|
<v-label>
|
||||||
<v-icon>mdi-arrow-collapse-right</v-icon>
|
{{ pagesCount }}
|
||||||
</v-btn>
|
</v-label>
|
||||||
|
<v-icon @click="goToLast" class="mx-2">mdi-arrow-collapse-right</v-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-slider>
|
</v-slider>
|
||||||
|
<!-- <v-dialog v-model="dialogGoto" persistent max-width="290">-->
|
||||||
|
<!-- <template v-slot:activator="{ on }">-->
|
||||||
|
<!-- <v-icon large class="ma-auto" v-on="on">mdi-arrow-right-bold-circle</v-icon>-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- <v-card >-->
|
||||||
|
<!-- <v-card-text class="d-flex flex-row">-->
|
||||||
|
<!-- <v-text-field-->
|
||||||
|
<!-- v-model="goToPage"-->
|
||||||
|
<!-- hide-details-->
|
||||||
|
<!-- single-line-->
|
||||||
|
<!-- type="number"-->
|
||||||
|
<!-- autofocus-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </v-card-text>-->
|
||||||
|
|
||||||
|
<!-- <v-card-actions>-->
|
||||||
|
<!-- <v-spacer></v-spacer>-->
|
||||||
|
<!-- <v-btn color="darken-1" text @click="dialogGoto = false">Close</v-btn>-->
|
||||||
|
<!-- <v-btn color="green darken-1" text @click="goTo(goToPage)">Go To</v-btn>-->
|
||||||
|
<!-- </v-card-actions>-->
|
||||||
|
<!-- </v-card>-->
|
||||||
|
<!-- </v-dialog>-->
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
<!-- Menu: fit buttons -->
|
</v-toolbar>
|
||||||
<v-row justify="center">
|
</v-slide-y-reverse-transition>
|
||||||
<v-col cols="auto">
|
</div>
|
||||||
<v-btn-toggle v-model="fitButtons" dense mandatory active-class="primary" class="flex-column flex-md-row">
|
|
||||||
<v-btn @click="setFit(ImageFit.WIDTH)">
|
|
||||||
Fit to width
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn @click="setFit(ImageFit.HEIGHT)">
|
<!-- clickable zone: left -->
|
||||||
Fit to height
|
<div @click="turnLeft()"
|
||||||
</v-btn>
|
class="left-quarter full-height top"
|
||||||
|
style="z-index: 1;"
|
||||||
|
/>
|
||||||
|
|
||||||
<v-btn @click="setFit(ImageFit.ORIGINAL)">
|
<!-- clickable zone: menu -->
|
||||||
Original
|
<div @click="toolbar = !toolbar"
|
||||||
</v-btn>
|
class="center-half full-height top"
|
||||||
</v-btn-toggle>
|
style="z-index: 1;"
|
||||||
</v-col>
|
/>
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Menu: RTL buttons -->
|
<!-- clickable zone: right -->
|
||||||
<v-row justify="center">
|
<div @click="turnRight()"
|
||||||
<v-col cols="auto">
|
class="right-quarter full-height top"
|
||||||
<v-btn-toggle v-model="rtlButtons" dense mandatory active-class="primary" class="flex-column flex-md-row">
|
style="z-index: 1;"
|
||||||
<v-btn @click="setRtl(false)">
|
/>
|
||||||
Left to right
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn @click="setRtl(true)">
|
<div class="full-height">
|
||||||
Right to left
|
<!-- Carousel -->
|
||||||
</v-btn>
|
<v-carousel v-model="carouselPage"
|
||||||
</v-btn-toggle>
|
:show-arrows="false"
|
||||||
</v-col>
|
:continuous="false"
|
||||||
</v-row>
|
:reverse="flipDirection"
|
||||||
|
hide-delimiters
|
||||||
<!-- Menu: double pages buttons -->
|
touchless
|
||||||
<v-row justify="center">
|
height="100%"
|
||||||
<v-col cols="auto">
|
|
||||||
<v-btn-toggle v-model="doublePagesButtons" dense mandatory active-class="primary" class="flex-column flex-md-row">
|
|
||||||
<v-btn @click="setDoublePages(false)">
|
|
||||||
Single page
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<v-btn @click="setDoublePages(true)">
|
|
||||||
Double pages
|
|
||||||
</v-btn>
|
|
||||||
</v-btn-toggle>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
|
|
||||||
<!-- Menu: keyboard shortcuts -->
|
|
||||||
<v-row>
|
|
||||||
<v-col cols="auto">
|
|
||||||
<div><kbd>←</kbd> / <kbd>⇞</kbd></div>
|
|
||||||
<div><kbd>→</kbd> / <kbd>⇟</kbd></div>
|
|
||||||
<div><kbd>home</kbd></div>
|
|
||||||
<div><kbd>end</kbd></div>
|
|
||||||
<div><kbd>space</kbd></div>
|
|
||||||
<div><kbd>m</kbd></div>
|
|
||||||
<div><kbd>t</kbd></div>
|
|
||||||
<div><kbd>esc</kbd></div>
|
|
||||||
</v-col>
|
|
||||||
<v-col>
|
|
||||||
<div v-if="!rtl">Previous page</div>
|
|
||||||
<div v-else>Next page</div>
|
|
||||||
<div v-if="!rtl">Next page</div>
|
|
||||||
<div v-else>Previous page</div>
|
|
||||||
<div>First page</div>
|
|
||||||
<div>Last page</div>
|
|
||||||
<div>Scroll down</div>
|
|
||||||
<div>Show / hide menu</div>
|
|
||||||
<div>Show / hide thumbnails</div>
|
|
||||||
<div>Close book</div>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</v-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Menu: right zone with arrow -->
|
|
||||||
<div class="fixed-position full-height right-quarter"
|
|
||||||
style="display: flex; align-items: center; justify-content: center"
|
|
||||||
>
|
>
|
||||||
<v-icon size="8em">
|
<!-- Carousel: pages -->
|
||||||
mdi-chevron-right
|
<v-carousel-item v-for="p in slidesRange"
|
||||||
</v-icon>
|
:key="doublePages ? `db${p}` : `sp${p}`"
|
||||||
</div>
|
:eager="eagerLoad(p)"
|
||||||
|
class="full-height"
|
||||||
|
:transition="animations ? undefined : false"
|
||||||
|
:reverse-transition="animations ? undefined : false"
|
||||||
|
>
|
||||||
|
<div class="full-height d-flex flex-column justify-center reader-background">
|
||||||
|
<div :class="`d-flex flex-row${flipDirection ? '-reverse' : ''} justify-center px-0 mx-0` " >
|
||||||
|
<img :src="getPageUrl(p)"
|
||||||
|
:height="maxHeight"
|
||||||
|
:width="maxWidth(p)"
|
||||||
|
/>
|
||||||
|
<img v-if="doublePages && p !== 1 && p !== pagesCount && p+1 !== pagesCount"
|
||||||
|
:src="getPageUrl(p+1)"
|
||||||
|
:height="maxHeight"
|
||||||
|
:width="maxWidth(p+1)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-carousel-item>
|
||||||
|
</v-carousel>
|
||||||
|
</div>
|
||||||
|
|
||||||
</v-overlay>
|
<thumbnail-explorer-dialog
|
||||||
|
v-model="showThumbnailsExplorer"
|
||||||
|
:bookId="bookId"
|
||||||
|
@goToPage="goTo"
|
||||||
|
:pagesCount="pagesCount"
|
||||||
|
></thumbnail-explorer-dialog>
|
||||||
|
|
||||||
<v-dialog v-model="showThumbnailsExplorer" scrollable>
|
<v-dialog
|
||||||
<v-card :max-height="$vuetify.breakpoint.height * .9"
|
v-model="menu"
|
||||||
dark
|
:close-on-content-click="false"
|
||||||
>
|
transition="dialog-bottom-transition"
|
||||||
<v-card-text>
|
:width="$vuetify.breakpoint.width * ($vuetify.breakpoint.smAndUp ? 0.5 : 1)"
|
||||||
<v-container fluid>
|
>
|
||||||
<v-row>
|
<v-container fluid class="pa-0">
|
||||||
<div v-for="p in pagesCount"
|
<v-toolbar dark color="primary">
|
||||||
:key="p"
|
<v-btn icon dark @click="menu = false">
|
||||||
style="min-height: 220px; max-width: 140px"
|
<v-icon>mdi-close</v-icon>
|
||||||
class="mb-2"
|
</v-btn>
|
||||||
>
|
<v-toolbar-title>Reader Settings</v-toolbar-title>
|
||||||
<v-img
|
</v-toolbar>
|
||||||
:src="getThumbnailUrl(p)"
|
|
||||||
lazy-src="../assets/cover.svg"
|
<v-list class="full-height full-width">
|
||||||
aspect-ratio="0.7071"
|
<v-list-item>
|
||||||
:contain="true"
|
<settings-switch v-model="doublePages" label="Page Layout" :status="`${ doublePages ? 'Double Pages' : 'Single Page'}`"></settings-switch>
|
||||||
max-height="200"
|
</v-list-item>
|
||||||
max-width="140"
|
<v-list-item>
|
||||||
class="ma-2"
|
<settings-switch v-model="animations" label="Page Transitions"></settings-switch>
|
||||||
@click="showThumbnailsExplorer = false; goTo(p)"
|
</v-list-item>
|
||||||
style="cursor: pointer"
|
<v-list-item>
|
||||||
/>
|
<settings-select
|
||||||
<div class="white--text text-center font-weight-bold">{{p}}</div>
|
:items="settings.readingDirections"
|
||||||
</div>
|
v-model="readingDirection"
|
||||||
</v-row>
|
label="Reading Direction"
|
||||||
</v-container>
|
>
|
||||||
</v-card-text>
|
<template v-slot:item="data" >
|
||||||
</v-card>
|
<div class="text-capitalize">
|
||||||
|
{{ readingDirectionDisplay(data.item) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:selection="data">
|
||||||
|
<div class="text-capitalize">
|
||||||
|
{{ readingDirectionDisplay(data.item) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</settings-select>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<settings-select
|
||||||
|
:items="settings.imageFits"
|
||||||
|
v-model="imageFit"
|
||||||
|
label="Scaling"
|
||||||
|
>
|
||||||
|
<template v-slot:item="data">
|
||||||
|
<div class="text-capitalize">
|
||||||
|
{{ imageFitDisplay(data.item) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:selection="data">
|
||||||
|
<div class="text-capitalize">
|
||||||
|
{{ imageFitDisplay(data.item) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</settings-select>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-container>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
<v-snackbar
|
<v-snackbar
|
||||||
v-model="jumpToPreviousBook"
|
v-model="jumpToPreviousBook"
|
||||||
:timeout="jumpConfirmationDelay"
|
:timeout="jumpConfirmationDelay"
|
||||||
|
|
@ -309,23 +248,38 @@
|
||||||
<p v-else>Click or press next again<br/>to exit the reader.</p>
|
<p v-else>Click or press next again<br/>to exit the reader.</p>
|
||||||
</div>
|
</div>
|
||||||
</v-snackbar>
|
</v-snackbar>
|
||||||
|
</v-container>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import SettingsSwitch from '@/components/SettingsSwitch.vue'
|
||||||
|
import SettingsSelect from '@/components/SettingsSelect.vue'
|
||||||
|
import ThumbnailExplorerDialog from '@/components/ThumbnailExplorerDialog.vue'
|
||||||
|
|
||||||
import { checkWebpFeature } from '@/functions/check-webp'
|
import { checkWebpFeature } from '@/functions/check-webp'
|
||||||
import { bookPageThumbnailUrl, bookPageUrl } from '@/functions/urls'
|
import { bookPageUrl } from '@/functions/urls'
|
||||||
import { ImageFit } from '@/types/common'
|
import { ImageFit, ReadingDirection } from '@/types/common'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import { getBookTitleCompact } from '@/functions/book-title'
|
import { getBookTitleCompact } from '@/functions/book-title'
|
||||||
|
|
||||||
const cookieFit = 'webreader.fit'
|
const cookieFit = 'webreader.fit'
|
||||||
const cookieRtl = 'webreader.rtl'
|
const cookieReadingDirection = 'webreader.readingDirection'
|
||||||
const cookieDoublePages = 'webreader.doublePages'
|
const cookieDoublePages = 'webreader.doublePages'
|
||||||
|
const cookieAnimations = 'webreader.animations'
|
||||||
|
|
||||||
|
const fitDisplay = {
|
||||||
|
[ImageFit.HEIGHT]: 'fit to height',
|
||||||
|
[ImageFit.WIDTH]: 'fit to width',
|
||||||
|
[ImageFit.ORIGINAL]: 'original'
|
||||||
|
}
|
||||||
|
const dirDisplay = {
|
||||||
|
[ReadingDirection.RightToLeft]: 'right to left',
|
||||||
|
[ReadingDirection.LeftToRight]: 'left to right'
|
||||||
|
}
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: 'BookReader',
|
name: 'BookReader',
|
||||||
|
components: { SettingsSwitch, SettingsSelect, ThumbnailExplorerDialog },
|
||||||
data: () => {
|
data: () => {
|
||||||
return {
|
return {
|
||||||
ImageFit,
|
ImageFit,
|
||||||
|
|
@ -340,15 +294,19 @@ export default Vue.extend({
|
||||||
supportedMediaTypes: ['image/jpeg', 'image/png', 'image/gif'],
|
supportedMediaTypes: ['image/jpeg', 'image/png', 'image/gif'],
|
||||||
convertTo: 'jpeg',
|
convertTo: 'jpeg',
|
||||||
carouselPage: 0,
|
carouselPage: 0,
|
||||||
|
showThumbnailsExplorer: false,
|
||||||
|
toolbar: false,
|
||||||
|
menu: false,
|
||||||
|
dialogGoto: false,
|
||||||
goToPage: 1,
|
goToPage: 1,
|
||||||
showMenu: false,
|
settings: {
|
||||||
fitButtons: 1,
|
doublePages: false,
|
||||||
fit: ImageFit.HEIGHT,
|
imageFits: Object.values(ImageFit),
|
||||||
rtlButtons: 0,
|
fit: ImageFit.HEIGHT,
|
||||||
rtl: false,
|
readingDirections: Object.values(ReadingDirection),
|
||||||
doublePages: false,
|
readingDirection: ReadingDirection.RightToLeft,
|
||||||
doublePagesButtons: 0,
|
animations: true
|
||||||
showThumbnailsExplorer: false
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
|
|
@ -362,20 +320,10 @@ export default Vue.extend({
|
||||||
window.addEventListener('keydown', this.keyPressed)
|
window.addEventListener('keydown', this.keyPressed)
|
||||||
this.setup(this.bookId, Number(this.$route.query.page))
|
this.setup(this.bookId, Number(this.$route.query.page))
|
||||||
|
|
||||||
// restore options for RTL, fit, and double pages
|
this.loadFromCookie(cookieReadingDirection, (v) => { this.readingDirection = v })
|
||||||
if (this.$cookies.isKey(cookieRtl)) {
|
this.loadFromCookie(cookieAnimations, (v) => { this.animations = (v === 'true') })
|
||||||
if (this.$cookies.get(cookieRtl) === 'true') {
|
this.loadFromCookie(cookieDoublePages, (v) => { this.doublePages = (v === 'true') })
|
||||||
this.setRtl(true)
|
this.loadFromCookie(cookieFit, (v) => { if (v) { this.imageFit = v } })
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.$cookies.isKey(cookieFit)) {
|
|
||||||
this.setFit(this.$cookies.get(cookieFit))
|
|
||||||
}
|
|
||||||
if (this.$cookies.isKey(cookieDoublePages)) {
|
|
||||||
if (this.$cookies.get(cookieDoublePages) === 'true') {
|
|
||||||
this.setDoublePages(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
window.removeEventListener('keydown', this.keyPressed)
|
window.removeEventListener('keydown', this.keyPressed)
|
||||||
|
|
@ -421,8 +369,8 @@ export default Vue.extend({
|
||||||
progress (): number {
|
progress (): number {
|
||||||
return this.currentPage / this.pagesCount * 100
|
return this.currentPage / this.pagesCount * 100
|
||||||
},
|
},
|
||||||
maxHeight (): number | string {
|
maxHeight (): number | null {
|
||||||
return this.fit === ImageFit.HEIGHT ? this.$vuetify.breakpoint.height : 'auto'
|
return this.imageFit === ImageFit.HEIGHT ? this.$vuetify.breakpoint.height : null
|
||||||
},
|
},
|
||||||
slidesRange (): number[] {
|
slidesRange (): number[] {
|
||||||
if (!this.doublePages) {
|
if (!this.doublePages) {
|
||||||
|
|
@ -442,18 +390,69 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
bookTitle (): string {
|
bookTitle (): string {
|
||||||
return getBookTitleCompact(this.book.name, this.series.name)
|
return getBookTitleCompact(this.book.name, this.series.name)
|
||||||
|
},
|
||||||
|
|
||||||
|
animations: {
|
||||||
|
get: function (): boolean {
|
||||||
|
return this.settings.animations
|
||||||
|
},
|
||||||
|
set: function (animations: boolean): void {
|
||||||
|
this.settings.animations = animations
|
||||||
|
this.$cookies.set(cookieAnimations, animations, Infinity)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
readingDirection: {
|
||||||
|
get: function (): ReadingDirection {
|
||||||
|
return this.settings.readingDirection
|
||||||
|
},
|
||||||
|
set: function (readingDirection: ReadingDirection): void {
|
||||||
|
this.settings.readingDirection = readingDirection
|
||||||
|
this.$cookies.set(cookieReadingDirection, readingDirection, Infinity)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flipDirection (): boolean {
|
||||||
|
switch (this.readingDirection) {
|
||||||
|
case ReadingDirection.LeftToRight:
|
||||||
|
return false
|
||||||
|
case ReadingDirection.RightToLeft:
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageFit: {
|
||||||
|
get: function (): ImageFit {
|
||||||
|
return this.settings.fit
|
||||||
|
},
|
||||||
|
set: function (fit: ImageFit): void {
|
||||||
|
this.settings.fit = fit
|
||||||
|
this.$cookies.set(cookieFit, fit, Infinity)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
doublePages: {
|
||||||
|
get: function (): boolean {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
swipe (direction: string) {
|
||||||
|
alert(direction)
|
||||||
|
},
|
||||||
keyPressed (e: KeyboardEvent) {
|
keyPressed (e: KeyboardEvent) {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'PageUp':
|
case 'PageUp':
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
this.rtl ? this.prev() : this.next()
|
this.flipDirection ? this.prev() : this.next()
|
||||||
break
|
break
|
||||||
case 'PageDown':
|
case 'PageDown':
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
this.rtl ? this.next() : this.prev()
|
this.flipDirection ? this.next() : this.prev()
|
||||||
break
|
break
|
||||||
case 'Home':
|
case 'Home':
|
||||||
this.goToFirst()
|
this.goToFirst()
|
||||||
|
|
@ -462,7 +461,7 @@ export default Vue.extend({
|
||||||
this.goToLast()
|
this.goToLast()
|
||||||
break
|
break
|
||||||
case 'm':
|
case 'm':
|
||||||
this.showMenu = !this.showMenu
|
this.toolbar = !this.toolbar
|
||||||
break
|
break
|
||||||
case 't':
|
case 't':
|
||||||
this.showThumbnailsExplorer = !this.showThumbnailsExplorer
|
this.showThumbnailsExplorer = !this.showThumbnailsExplorer
|
||||||
|
|
@ -499,8 +498,11 @@ export default Vue.extend({
|
||||||
return bookPageUrl(this.bookId, page)
|
return bookPageUrl(this.bookId, page)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getThumbnailUrl (page: number): string {
|
turnRight () {
|
||||||
return bookPageThumbnailUrl(this.bookId, page)
|
return this.flipDirection ? this.prev() : this.next()
|
||||||
|
},
|
||||||
|
turnLeft () {
|
||||||
|
return this.flipDirection ? this.next() : this.prev()
|
||||||
},
|
},
|
||||||
prev () {
|
prev () {
|
||||||
if (this.canPrev) {
|
if (this.canPrev) {
|
||||||
|
|
@ -555,33 +557,6 @@ export default Vue.extend({
|
||||||
closeBook () {
|
closeBook () {
|
||||||
this.$router.push({ name: 'browse-book', params: { bookId: this.bookId.toString() } })
|
this.$router.push({ name: 'browse-book', params: { bookId: this.bookId.toString() } })
|
||||||
},
|
},
|
||||||
setRtl (rtl: boolean) {
|
|
||||||
this.rtl = rtl
|
|
||||||
this.rtlButtons = rtl ? 1 : 0
|
|
||||||
this.$cookies.set(cookieRtl, rtl, Infinity)
|
|
||||||
},
|
|
||||||
setFit (fit: ImageFit) {
|
|
||||||
this.fit = fit
|
|
||||||
switch (fit) {
|
|
||||||
case ImageFit.WIDTH:
|
|
||||||
this.fitButtons = 0
|
|
||||||
break
|
|
||||||
case ImageFit.HEIGHT:
|
|
||||||
this.fitButtons = 1
|
|
||||||
break
|
|
||||||
case ImageFit.ORIGINAL:
|
|
||||||
this.fitButtons = 2
|
|
||||||
break
|
|
||||||
}
|
|
||||||
this.$cookies.set(cookieFit, fit, Infinity)
|
|
||||||
},
|
|
||||||
setDoublePages (doublePages: boolean) {
|
|
||||||
const current = this.currentPage
|
|
||||||
this.doublePages = doublePages
|
|
||||||
this.goTo(current)
|
|
||||||
this.doublePagesButtons = doublePages ? 1 : 0
|
|
||||||
this.$cookies.set(cookieDoublePages, doublePages, Infinity)
|
|
||||||
},
|
|
||||||
toSinglePages (i: number): number {
|
toSinglePages (i: number): number {
|
||||||
if (i === 1) return 1
|
if (i === 1) return 1
|
||||||
if (i === this.slidesCount) return this.pagesCount
|
if (i === this.slidesCount) return this.pagesCount
|
||||||
|
|
@ -597,46 +572,69 @@ export default Vue.extend({
|
||||||
eagerLoad (p: number): boolean {
|
eagerLoad (p: number): boolean {
|
||||||
return Math.abs(this.currentPage - p) <= 2
|
return Math.abs(this.currentPage - p) <= 2
|
||||||
},
|
},
|
||||||
maxWidth (p: number): number | string {
|
maxWidth (p: number): number | null {
|
||||||
if (this.fit !== ImageFit.WIDTH) {
|
if (this.imageFit !== ImageFit.WIDTH) {
|
||||||
return 'auto'
|
return null
|
||||||
}
|
}
|
||||||
if (this.doublePages && p !== 1 && p !== this.pagesCount) {
|
if (this.doublePages && p !== 1 && p !== this.pagesCount) {
|
||||||
return this.$vuetify.breakpoint.width / 2
|
return this.$vuetify.breakpoint.width / 2
|
||||||
}
|
}
|
||||||
return this.$vuetify.breakpoint.width
|
return this.$vuetify.breakpoint.width
|
||||||
|
},
|
||||||
|
imageFitDisplay (fit: ImageFit): string {
|
||||||
|
return fitDisplay[fit]
|
||||||
|
},
|
||||||
|
readingDirectionDisplay (dir: ReadingDirection): string {
|
||||||
|
return dirDisplay[dir]
|
||||||
|
},
|
||||||
|
loadFromCookie (cookieKey: string, setter: (value: any) => void): void {
|
||||||
|
if (this.$cookies.isKey(cookieKey)) {
|
||||||
|
let value = this.$cookies.get(cookieKey)
|
||||||
|
setter(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fixed-position {
|
|
||||||
position: fixed;
|
.reader-background {
|
||||||
|
background-color: white; /* TODO add a setting for this, some books might not be white */
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings {
|
||||||
|
/*position: absolute;*/
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-height {
|
.full-height {
|
||||||
top: 0;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full-width {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.left-quarter {
|
.left-quarter {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 20%;
|
width: 20%;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-quarter {
|
.right-quarter {
|
||||||
right: 0;
|
right: 0;
|
||||||
width: 20%;
|
width: 20%;
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
.center-half {
|
.center-half {
|
||||||
left: 20%;
|
left: 20%;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
}
|
position: absolute;
|
||||||
|
|
||||||
.dashed-x {
|
|
||||||
border-left: 4px dashed;
|
|
||||||
border-right: 4px dashed;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue