+
You're at the beginning
of the book.
Click or press previous again
to move to the previous book.
@@ -250,17 +234,19 @@
-
+
{{ notification.message }}
-
+
+
+
@@ -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({
},
})
-