mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-08 10:21:38 +00:00
Added vertical continuous scroll
Added a setting for the epub viewer to continuously scroll vertically, and a setting which allows setting the viewing width of the page (works for paginated too)
This commit is contained in:
parent
2116f60133
commit
d2b5d28a17
3 changed files with 91 additions and 30 deletions
|
|
@ -1,13 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="epub-reader" class="h-full w-full">
|
<div id="epub-reader" class="h-full w-full">
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<button type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100">
|
<button v-if="ereaderSettings.flow === 'paginated'" type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100">
|
||||||
<span v-if="hasPrev" class="material-symbols text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
<span v-if="hasPrev" class="material-symbols text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||||
</button>
|
</button>
|
||||||
<div id="frame" class="w-full" style="height: 80%">
|
|
||||||
<div id="viewer"></div>
|
<div id="frame" class="w-full" :class="isScrolled() ? 'absolute bottom-0 h-[90%]' : 'h-[80%]'">
|
||||||
|
<div id="viewer" class="flex justify-center" :class="{ 'h-full': isScrolled() }"></div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100">
|
|
||||||
|
<button v-if="ereaderSettings.flow === 'paginated'" type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100">
|
||||||
<span v-if="hasNext" class="material-symbols text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
<span v-if="hasNext" class="material-symbols text-6xl" @mousedown.prevent @click="next">chevron_right</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -47,7 +49,9 @@ export default {
|
||||||
fontScale: 100,
|
fontScale: 100,
|
||||||
lineSpacing: 115,
|
lineSpacing: 115,
|
||||||
spread: 'auto',
|
spread: 'auto',
|
||||||
textStroke: 0
|
textStroke: 0,
|
||||||
|
flow: 'paginated',
|
||||||
|
widthPercentage: 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -85,12 +89,12 @@ export default {
|
||||||
return `ebookLocations-${this.libraryItemId}`
|
return `ebookLocations-${this.libraryItemId}`
|
||||||
},
|
},
|
||||||
readerWidth() {
|
readerWidth() {
|
||||||
if (this.windowWidth < 640) return this.windowWidth
|
let width = Math.floor((this.windowWidth * this.ereaderSettings.widthPercentage) / 100)
|
||||||
return this.windowWidth - 200
|
|
||||||
|
return this.windowWidth < 640 || this.isScrolled() ? width : width - 200
|
||||||
},
|
},
|
||||||
readerHeight() {
|
readerHeight() {
|
||||||
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight
|
return this.windowHeight < 400 || !this.playerOpen ? this.windowHeight : this.windowHeight - 164
|
||||||
return this.windowHeight - 164
|
|
||||||
},
|
},
|
||||||
ebookUrl() {
|
ebookUrl() {
|
||||||
if (this.fileId) {
|
if (this.fileId) {
|
||||||
|
|
@ -103,21 +107,13 @@ export default {
|
||||||
const isDark = theme === 'dark'
|
const isDark = theme === 'dark'
|
||||||
const isSepia = theme === 'sepia'
|
const isSepia = theme === 'sepia'
|
||||||
|
|
||||||
const fontColor = isDark
|
const fontColor = isDark ? '#fff' : isSepia ? '#5b4636' : '#000'
|
||||||
? '#fff'
|
|
||||||
: isSepia
|
|
||||||
? '#5b4636'
|
|
||||||
: '#000'
|
|
||||||
|
|
||||||
const backgroundColor = isDark
|
const backgroundColor = isDark ? 'rgb(35 35 35)' : isSepia ? 'rgb(244, 236, 216)' : 'rgb(255, 255, 255)'
|
||||||
? 'rgb(35 35 35)'
|
|
||||||
: isSepia
|
|
||||||
? 'rgb(244, 236, 216)'
|
|
||||||
: 'rgb(255, 255, 255)'
|
|
||||||
|
|
||||||
const lineSpacing = this.ereaderSettings.lineSpacing / 100
|
const lineSpacing = this.ereaderSettings.lineSpacing / 100
|
||||||
const fontScale = this.ereaderSettings.fontScale / 100
|
const fontScale = this.ereaderSettings.fontScale / 100
|
||||||
const textStroke = this.ereaderSettings.textStroke / 100
|
const textStroke = this.ereaderSettings.textStroke / 100
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'*': {
|
'*': {
|
||||||
|
|
@ -144,6 +140,11 @@ export default {
|
||||||
this.rendition.themes.fontSize(`${fontScale}%`)
|
this.rendition.themes.fontSize(`${fontScale}%`)
|
||||||
this.rendition.themes.font(settings.font)
|
this.rendition.themes.font(settings.font)
|
||||||
this.rendition.spread(settings.spread || 'auto')
|
this.rendition.spread(settings.spread || 'auto')
|
||||||
|
this.rendition.flow(settings.flow || 'paginated')
|
||||||
|
this.resize()
|
||||||
|
},
|
||||||
|
isScrolled() {
|
||||||
|
return this.ereaderSettings.flow === 'scrolled-continuous'
|
||||||
},
|
},
|
||||||
prev() {
|
prev() {
|
||||||
if (!this.rendition?.manager) return
|
if (!this.rendition?.manager) return
|
||||||
|
|
@ -312,6 +313,31 @@ export default {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Adds a bottom spacer div to the last page's iframe content to allow extra scroll space.
|
||||||
|
* @param {object} view - The view object containing the iframe.
|
||||||
|
* @param {object} section - The current section object with an index.
|
||||||
|
*/
|
||||||
|
addBottomSpacerToLastPage(view, section) {
|
||||||
|
const isLastPage = section.index === this.book?.spine?.spineItems?.length - 1
|
||||||
|
const doc = view?.iframe?.contentDocument || view?.iframe?.contentWindow?.document
|
||||||
|
|
||||||
|
if (!isLastPage || !doc) return
|
||||||
|
|
||||||
|
const existingSpacer = doc.getElementById('bottom-spacer')
|
||||||
|
|
||||||
|
if (!this.isScrolled()) {
|
||||||
|
existingSpacer?.remove()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingSpacer) return
|
||||||
|
|
||||||
|
const newSpacer = doc.createElement('div')
|
||||||
|
newSpacer.id = 'bottom-spacer'
|
||||||
|
newSpacer.style.height = Math.floor(window.innerHeight / 2) + 'px'
|
||||||
|
doc.body.appendChild(newSpacer)
|
||||||
|
},
|
||||||
initEpub() {
|
initEpub() {
|
||||||
/** @type {EpubReader} */
|
/** @type {EpubReader} */
|
||||||
const reader = this
|
const reader = this
|
||||||
|
|
@ -338,20 +364,17 @@ export default {
|
||||||
|
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
reader.rendition = reader.book.renderTo('viewer', {
|
reader.rendition = reader.book.renderTo('viewer', {
|
||||||
width: this.readerWidth,
|
|
||||||
height: this.readerHeight * 0.8,
|
|
||||||
allowScriptedContent: this.allowScriptedContent,
|
allowScriptedContent: this.allowScriptedContent,
|
||||||
spread: 'auto',
|
|
||||||
snap: true,
|
snap: true,
|
||||||
manager: 'continuous',
|
manager: 'continuous'
|
||||||
flow: 'paginated'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// load saved progress
|
// load saved progress
|
||||||
reader.rendition.display(this.savedEbookLocation || reader.book.locations.start)
|
reader.rendition.display(this.savedEbookLocation || reader.book.locations.start)
|
||||||
|
|
||||||
reader.rendition.on('rendered', () => {
|
reader.rendition.on('rendered', (section, view) => {
|
||||||
this.applyTheme()
|
this.applyTheme()
|
||||||
|
this.addBottomSpacerToLastPage(view, section)
|
||||||
})
|
})
|
||||||
|
|
||||||
reader.book.ready
|
reader.book.ready
|
||||||
|
|
@ -453,7 +476,8 @@ export default {
|
||||||
resize() {
|
resize() {
|
||||||
this.windowWidth = window.innerWidth
|
this.windowWidth = window.innerWidth
|
||||||
this.windowHeight = window.innerHeight
|
this.windowHeight = window.innerHeight
|
||||||
this.rendition?.resize(this.readerWidth, this.readerHeight * 0.8)
|
|
||||||
|
this.rendition.resize(this.readerWidth, this.readerHeight * (this.isScrolled() ? 0.9 : 0.8))
|
||||||
},
|
},
|
||||||
applyTheme() {
|
applyTheme() {
|
||||||
if (!this.rendition) return
|
if (!this.rendition) return
|
||||||
|
|
@ -474,3 +498,10 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
.epub-container {
|
||||||
|
/* Fixes unexpected scroll jump issue with epubjs continuous scroll: see https://github.com/futurepress/epub.js/issues/1303 */
|
||||||
|
overflow-anchor: none !important;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,23 @@
|
||||||
</div>
|
</div>
|
||||||
<ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" @input="settingsUpdated" />
|
<ui-range-input v-model="ereaderSettings.textStroke" :min="0" :max="300" :step="5" @input="settingsUpdated" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-40">
|
||||||
|
<p class="text-lg">{{ $strings.LabelPageWidth }}:</p>
|
||||||
|
</div>
|
||||||
|
<ui-range-input v-model="ereaderSettings.widthPercentage" :min="10" :max="100" :step="5" @input="settingsUpdated" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-40">
|
||||||
|
<p class="text-lg">{{ $strings.LabelPageFlow }}:</p>
|
||||||
|
</div>
|
||||||
|
<ui-toggle-btns v-model="ereaderSettings.flow" :items="flowItems" @input="settingsUpdated" />
|
||||||
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-40">
|
<div class="w-40">
|
||||||
<p class="text-lg">{{ $strings.LabelLayout }}:</p>
|
<p class="text-lg">{{ $strings.LabelLayout }}:</p>
|
||||||
</div>
|
</div>
|
||||||
<ui-toggle-btns v-model="ereaderSettings.spread" :items="spreadItems" @input="settingsUpdated" />
|
<ui-toggle-btns :disabled="ereaderSettings.flow === 'scrolled-continuous'" v-model="ereaderSettings.spread" :items="spreadItems" @input="settingsUpdated" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
|
|
@ -143,7 +155,9 @@ export default {
|
||||||
lineSpacing: 115,
|
lineSpacing: 115,
|
||||||
fontBoldness: 100,
|
fontBoldness: 100,
|
||||||
spread: 'auto',
|
spread: 'auto',
|
||||||
textStroke: 0
|
textStroke: 0,
|
||||||
|
flow: 'paginated',
|
||||||
|
widthPercentage: 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -179,6 +193,18 @@ export default {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
flowItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelFlowPaginated,
|
||||||
|
value: 'paginated'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelFlowScrolled,
|
||||||
|
value: 'scrolled-continuous'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
themeItems() {
|
themeItems() {
|
||||||
return {
|
return {
|
||||||
theme: [
|
theme: [
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,8 @@
|
||||||
"LabelFilterByUser": "Filter by User",
|
"LabelFilterByUser": "Filter by User",
|
||||||
"LabelFindEpisodes": "Find Episodes",
|
"LabelFindEpisodes": "Find Episodes",
|
||||||
"LabelFinished": "Finished",
|
"LabelFinished": "Finished",
|
||||||
|
"LabelFlowPaginated": "Paginated",
|
||||||
|
"LabelFlowScrolled": "Scrolled",
|
||||||
"LabelFolder": "Folder",
|
"LabelFolder": "Folder",
|
||||||
"LabelFolders": "Folders",
|
"LabelFolders": "Folders",
|
||||||
"LabelFontBold": "Bold",
|
"LabelFontBold": "Bold",
|
||||||
|
|
@ -495,6 +497,8 @@
|
||||||
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
|
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
|
||||||
"LabelOpenRSSFeed": "Open RSS Feed",
|
"LabelOpenRSSFeed": "Open RSS Feed",
|
||||||
"LabelOverwrite": "Overwrite",
|
"LabelOverwrite": "Overwrite",
|
||||||
|
"LabelPageFlow": "Flow",
|
||||||
|
"LabelPageWidth": "Page Width",
|
||||||
"LabelPaginationPageXOfY": "Page {0} of {1}",
|
"LabelPaginationPageXOfY": "Page {0} of {1}",
|
||||||
"LabelPassword": "Password",
|
"LabelPassword": "Password",
|
||||||
"LabelPath": "Path",
|
"LabelPath": "Path",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue