mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-28 14:44:30 +02:00
Add:Progress for comics #738
This commit is contained in:
parent
c8b5cefeb5
commit
543ac209e4
7 changed files with 236 additions and 51 deletions
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="300" height="100%">
|
<modals-modal v-model="show" :width="width" height="100%">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div v-if="title" class="absolute top-8 left-4 z-40" style="max-width: 80%">
|
<div v-if="title" class="absolute top-8 left-4 z-40" style="max-width: 80%">
|
||||||
<p class="text-white text-xl truncate">{{ title }}</p>
|
<p class="text-white text-xl truncate">{{ title }}</p>
|
||||||
|
@ -9,9 +9,9 @@
|
||||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-2" style="max-height: 75%" @click.stop>
|
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-2" style="max-height: 75%" @click.stop>
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<template v-for="item in itemsToShow">
|
||||||
<slot :name="item.value" :item="item" :selected="item.value === selected">
|
<slot :name="item.value" :item="item" :selected="item.value === selected">
|
||||||
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" role="option" @click="clickedOption(item.value)">
|
<li :key="item.value" :ref="`item-${item.value}`" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" :style="{ paddingTop: itemPaddingY, paddingBottom: itemPaddingY }" role="option" @click="clickedOption(item.value)">
|
||||||
<div class="relative flex items-center px-3">
|
<div class="relative flex items-center px-3">
|
||||||
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-white text-opacity-80">{{ item.icon }}</span>
|
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-white text-opacity-80">{{ item.icon }}</span>
|
||||||
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
||||||
|
@ -34,11 +34,27 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
selected: String // optional
|
selected: [String, Number], // optional
|
||||||
|
itemPaddingY: {
|
||||||
|
type: String,
|
||||||
|
default: '16px'
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 300
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) this.$nextTick(this.init)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
get() {
|
get() {
|
||||||
|
@ -47,11 +63,32 @@ export default {
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
itemsToShow() {
|
||||||
|
return this.items.map((i) => {
|
||||||
|
if (typeof i === 'string') {
|
||||||
|
return {
|
||||||
|
text: i,
|
||||||
|
value: i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
clickedOption(action) {
|
clickedOption(action) {
|
||||||
this.$emit('action', action)
|
this.$emit('action', action)
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
if (this.selected && this.$refs[`item-${this.selected}`]?.[0]) {
|
||||||
|
// Set scroll position so that selected item is in the center
|
||||||
|
const containerOffset = this.$refs.container.offsetTop + this.$refs.container.clientHeight / 2
|
||||||
|
const scrollAmount = this.$refs[`item-${this.selected}`][0].offsetTop - containerOffset
|
||||||
|
this.$refs.container.scrollTo({
|
||||||
|
top: scrollAmount
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="comic-reader" class="w-full h-full relative">
|
<div id="comic-reader" class="w-full h-full relative">
|
||||||
<div v-show="showPageMenu" v-click-outside="clickOutsideObj" class="pagemenu absolute top-12 right-16 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52">
|
<div v-show="showInfoMenu" v-click-outside="clickedOutside" class="pagemenu absolute top-8 left-0 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-full" @click.stop.prevent>
|
||||||
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click.stop="setPage(index)">
|
|
||||||
<p class="text-sm truncate">{{ file }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-show="showInfoMenu" v-click-outside="clickedOutsideInfoMenu" class="pagemenu absolute top-12 right-0 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-full" @click.stop.prevent>
|
|
||||||
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
||||||
<p class="text-xs">
|
<p class="text-xs">
|
||||||
<strong>{{ key }}</strong>
|
<strong>{{ key }}</strong>
|
||||||
|
@ -14,14 +9,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="comicMetadata" class="absolute top-0 right-3 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
|
<div v-if="comicMetadata" id="btn-metadata" class="absolute top-0 left-14 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-8 h-7 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
|
||||||
<span class="material-icons text-lg">more</span>
|
<span class="material-icons text-base pointer-events-none">more</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @mouseup.prevent @click.stop.prevent="clickShowPageMenu">
|
<div v-if="numPages" id="btn-pages" class="absolute top-0 left-3 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-8 h-7 flex items-center justify-center text-center z-20" @mousedown.prevent @mouseup.prevent @click.stop.prevent="clickShowPageMenu">
|
||||||
<span class="material-icons text-lg">menu</span>
|
<span class="material-icons text-base pointer-events-none">menu</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-0 left-3 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20" @click.stop>
|
<div v-if="numPages" class="absolute top-0 right-3 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-7 flex items-center text-center z-20" @click.stop>
|
||||||
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
|
<p class="text-sm">{{ page }} / {{ numPages }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden m-auto comicwrapper relative">
|
<div class="overflow-hidden m-auto comicwrapper relative">
|
||||||
|
@ -33,6 +28,8 @@
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<modals-dialog v-model="showPageMenu" :items="pageItems" :selected="page" :width="360" item-padding-y="8px" @action="setPage" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -51,7 +48,9 @@ export default {
|
||||||
libraryItem: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
},
|
||||||
|
isLocal: Boolean,
|
||||||
|
keepProgress: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -66,11 +65,7 @@ export default {
|
||||||
loadTimeout: null,
|
loadTimeout: null,
|
||||||
loadedFirstPage: false,
|
loadedFirstPage: false,
|
||||||
comicMetadata: null,
|
comicMetadata: null,
|
||||||
clickOutsideObj: {
|
pageMenuWidth: 256
|
||||||
handler: this.clickedOutside,
|
|
||||||
events: ['mousedown'],
|
|
||||||
isActive: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -85,17 +80,112 @@ export default {
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem?.id
|
||||||
|
},
|
||||||
|
localLibraryItem() {
|
||||||
|
if (this.isLocal) return this.libraryItem
|
||||||
|
return this.libraryItem.localLibraryItem || null
|
||||||
|
},
|
||||||
|
localLibraryItemId() {
|
||||||
|
return this.localLibraryItem?.id
|
||||||
|
},
|
||||||
|
serverLibraryItemId() {
|
||||||
|
if (!this.isLocal) return this.libraryItem.id
|
||||||
|
// Check if local library item is connected to the current server
|
||||||
|
if (!this.libraryItem.serverAddress || !this.libraryItem.libraryItemId) return null
|
||||||
|
if (this.$store.getters['user/getServerAddress'] === this.libraryItem.serverAddress) {
|
||||||
|
return this.libraryItem.libraryItemId
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
comicMetadataKeys() {
|
comicMetadataKeys() {
|
||||||
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
||||||
},
|
},
|
||||||
canGoNext() {
|
canGoNext() {
|
||||||
return this.page < this.numPages - 1
|
return this.page < this.numPages
|
||||||
},
|
},
|
||||||
canGoPrev() {
|
canGoPrev() {
|
||||||
return this.page > 0
|
return this.page > 1
|
||||||
|
},
|
||||||
|
userItemProgress() {
|
||||||
|
if (this.isLocal) return this.localItemProgress
|
||||||
|
return this.serverItemProgress
|
||||||
|
},
|
||||||
|
localItemProgress() {
|
||||||
|
return this.$store.getters['globals/getLocalMediaProgressById'](this.localLibraryItemId)
|
||||||
|
},
|
||||||
|
serverItemProgress() {
|
||||||
|
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId)
|
||||||
|
},
|
||||||
|
savedPage() {
|
||||||
|
if (!this.keepProgress) return 0
|
||||||
|
|
||||||
|
// Validate ebookLocation is a number
|
||||||
|
if (!this.userItemProgress?.ebookLocation || isNaN(this.userItemProgress.ebookLocation)) return 0
|
||||||
|
return Number(this.userItemProgress.ebookLocation)
|
||||||
|
},
|
||||||
|
selectedCleanedPage() {
|
||||||
|
return this.cleanedPageNames[this.page - 1]
|
||||||
|
},
|
||||||
|
cleanedPageNames() {
|
||||||
|
return (
|
||||||
|
this.pages?.map((p) => {
|
||||||
|
if (p.length > 40) {
|
||||||
|
let firstHalf = p.slice(0, 18)
|
||||||
|
let lastHalf = p.slice(p.length - 17)
|
||||||
|
return `${firstHalf} ... ${lastHalf}`
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}) || []
|
||||||
|
)
|
||||||
|
},
|
||||||
|
pageItems() {
|
||||||
|
let index = 1
|
||||||
|
return this.cleanedPageNames.map((p) => {
|
||||||
|
return {
|
||||||
|
text: p,
|
||||||
|
value: index++
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async updateProgress() {
|
||||||
|
if (!this.keepProgress) return
|
||||||
|
|
||||||
|
if (!this.numPages) {
|
||||||
|
console.error('Num pages not loaded')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.savedPage === this.page) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
ebookLocation: String(this.page),
|
||||||
|
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local item
|
||||||
|
if (this.localLibraryItemId) {
|
||||||
|
const localPayload = {
|
||||||
|
localLibraryItemId: this.localLibraryItemId,
|
||||||
|
...payload
|
||||||
|
}
|
||||||
|
const localResponse = await this.$db.updateLocalEbookProgress(localPayload)
|
||||||
|
if (localResponse.localMediaProgress) {
|
||||||
|
this.$store.commit('globals/updateLocalMediaProgress', localResponse.localMediaProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update server item
|
||||||
|
if (this.serverLibraryItemId) {
|
||||||
|
this.$axios.$patch(`/api/me/progress/${this.serverLibraryItemId}`, payload).catch((error) => {
|
||||||
|
console.error('ComicReader.updateProgress failed:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
clickShowInfoMenu() {
|
clickShowInfoMenu() {
|
||||||
this.showInfoMenu = !this.showInfoMenu
|
this.showInfoMenu = !this.showInfoMenu
|
||||||
this.showPageMenu = false
|
this.showPageMenu = false
|
||||||
|
@ -104,11 +194,10 @@ export default {
|
||||||
this.showPageMenu = !this.showPageMenu
|
this.showPageMenu = !this.showPageMenu
|
||||||
this.showInfoMenu = false
|
this.showInfoMenu = false
|
||||||
},
|
},
|
||||||
clickedOutside() {
|
clickedOutside(e) {
|
||||||
this.showPageMenu = false
|
if (e.target?.id == 'btn-metadata') return
|
||||||
},
|
|
||||||
clickedOutsideInfoMenu() {
|
if (this.showInfoMenu) this.showInfoMenu = false
|
||||||
this.showInfoMenu = false
|
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
if (!this.canGoNext) return
|
if (!this.canGoNext) return
|
||||||
|
@ -118,13 +207,15 @@ export default {
|
||||||
if (!this.canGoPrev) return
|
if (!this.canGoPrev) return
|
||||||
this.setPage(this.page - 1)
|
this.setPage(this.page - 1)
|
||||||
},
|
},
|
||||||
setPage(index) {
|
setPage(page) {
|
||||||
if (index < 0 || index > this.numPages - 1) {
|
if (page <= 0 || page > this.numPages) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.showPageMenu = false
|
this.showPageMenu = false
|
||||||
const filename = this.pages[index]
|
const filename = this.pages[page - 1]
|
||||||
this.page = index
|
this.page = page
|
||||||
|
|
||||||
|
this.updateProgress()
|
||||||
return this.extractFile(filename)
|
return this.extractFile(filename)
|
||||||
},
|
},
|
||||||
setLoadTimeout() {
|
setLoadTimeout() {
|
||||||
|
@ -154,7 +245,6 @@ export default {
|
||||||
},
|
},
|
||||||
async extract() {
|
async extract() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
console.log('Extracting', this.url)
|
|
||||||
|
|
||||||
var buff = await this.$axios.$get(this.url, {
|
var buff = await this.$axios.$get(this.url, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
@ -176,9 +266,28 @@ export default {
|
||||||
|
|
||||||
this.numPages = this.pages.length
|
this.numPages = this.pages.length
|
||||||
|
|
||||||
|
// Calculate page menu size
|
||||||
|
const largestFilename = this.cleanedPageNames
|
||||||
|
.map((p) => p)
|
||||||
|
.sort((a, b) => a.length - b.length)
|
||||||
|
.pop()
|
||||||
|
const pEl = document.createElement('p')
|
||||||
|
pEl.innerText = largestFilename
|
||||||
|
pEl.style.fontSize = '0.875rem'
|
||||||
|
pEl.style.opacity = 0
|
||||||
|
pEl.style.position = 'absolute'
|
||||||
|
document.body.appendChild(pEl)
|
||||||
|
const textWidth = pEl.getBoundingClientRect()?.width
|
||||||
|
if (textWidth) {
|
||||||
|
this.pageMenuWidth = textWidth + (16 + 5 + 2 + 5)
|
||||||
|
}
|
||||||
|
pEl.remove()
|
||||||
|
|
||||||
if (this.pages.length) {
|
if (this.pages.length) {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
await this.setPage(0)
|
|
||||||
|
const startPage = this.savedPage > 0 && this.savedPage <= this.numPages ? this.savedPage : 1
|
||||||
|
await this.setPage(startPage)
|
||||||
this.loadedFirstPage = true
|
this.loadedFirstPage = true
|
||||||
} else {
|
} else {
|
||||||
this.$toast.error('Unable to extract pages')
|
this.$toast.error('Unable to extract pages')
|
||||||
|
@ -206,7 +315,6 @@ export default {
|
||||||
return flattenObject(filesObject)
|
return flattenObject(filesObject)
|
||||||
},
|
},
|
||||||
async extractXmlFile(filename) {
|
async extractXmlFile(filename) {
|
||||||
console.log('extracting xml filename', filename)
|
|
||||||
try {
|
try {
|
||||||
var file = await this.filesObject[filename].extract()
|
var file = await this.filesObject[filename].extract()
|
||||||
var reader = new FileReader()
|
var reader = new FileReader()
|
||||||
|
@ -272,7 +380,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagemenu {
|
.pagemenu {
|
||||||
max-height: calc(100% - 80px);
|
max-height: calc(100% - 20px);
|
||||||
}
|
}
|
||||||
.comicimg {
|
.comicimg {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -21,7 +21,8 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
isLocal: Boolean
|
isLocal: Boolean,
|
||||||
|
keepProgress: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -83,6 +84,13 @@ export default {
|
||||||
},
|
},
|
||||||
localStorageLocationsKey() {
|
localStorageLocationsKey() {
|
||||||
return `ebookLocations-${this.libraryItemId}`
|
return `ebookLocations-${this.libraryItemId}`
|
||||||
|
},
|
||||||
|
savedEbookLocation() {
|
||||||
|
if (!this.keepProgress) return null
|
||||||
|
if (!this.userItemProgress?.ebookLocation) return null
|
||||||
|
// Validate ebookLocation is an epubcfi
|
||||||
|
if (!String(this.userItemProgress.ebookLocation).startsWith('epubcfi')) return null
|
||||||
|
return this.userItemProgress.ebookLocation
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -107,6 +115,8 @@ export default {
|
||||||
* @param {string} payload.ebookProgress - eBook Progress Percentage
|
* @param {string} payload.ebookProgress - eBook Progress Percentage
|
||||||
*/
|
*/
|
||||||
async updateProgress(payload) {
|
async updateProgress(payload) {
|
||||||
|
if (!this.keepProgress) return
|
||||||
|
|
||||||
// Update local item
|
// Update local item
|
||||||
if (this.localLibraryItemId) {
|
if (this.localLibraryItemId) {
|
||||||
const localPayload = {
|
const localPayload = {
|
||||||
|
@ -213,7 +223,7 @@ export default {
|
||||||
},
|
},
|
||||||
/** @param {string} location - CFI of the new location */
|
/** @param {string} location - CFI of the new location */
|
||||||
relocated(location) {
|
relocated(location) {
|
||||||
if (this.userItemProgress?.ebookLocation === location.start.cfi) {
|
if (this.savedEbookLocation === location.start.cfi) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,10 +265,10 @@ export default {
|
||||||
})
|
})
|
||||||
|
|
||||||
// load saved progress
|
// load saved progress
|
||||||
reader.rendition.display(this.userItemProgress?.ebookLocation || reader.book.locations.start)
|
reader.rendition.display(this.savedEbookLocation || reader.book.locations.start)
|
||||||
|
|
||||||
// load style
|
// load style
|
||||||
reader.rendition.themes.default({ '*': { color: '#fff!important', 'background-color': 'rgb(35 35 35)!important' } })
|
reader.rendition.themes.default({ '*': { color: '#fff!important', 'background-color': 'rgb(35 35 35)!important' }, a: { color: '#fff!important' } })
|
||||||
|
|
||||||
reader.book.ready.then(() => {
|
reader.book.ready.then(() => {
|
||||||
// set up event listeners
|
// set up event listeners
|
||||||
|
|
|
@ -39,7 +39,8 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
isLocal: Boolean
|
isLocal: Boolean,
|
||||||
|
keepProgress: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -92,7 +93,11 @@ export default {
|
||||||
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId)
|
return this.$store.getters['user/getUserMediaProgress'](this.serverLibraryItemId)
|
||||||
},
|
},
|
||||||
savedPage() {
|
savedPage() {
|
||||||
return Number(this.userItemProgress?.ebookLocation || 0)
|
if (!this.keepProgress) return 0
|
||||||
|
|
||||||
|
// Validate ebookLocation is a number
|
||||||
|
if (!this.userItemProgress?.ebookLocation || isNaN(this.userItemProgress.ebookLocation)) return 0
|
||||||
|
return Number(this.userItemProgress.ebookLocation)
|
||||||
},
|
},
|
||||||
pdfDocInitParams() {
|
pdfDocInitParams() {
|
||||||
return {
|
return {
|
||||||
|
@ -105,10 +110,13 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async updateProgress() {
|
async updateProgress() {
|
||||||
|
if (!this.keepProgress) return
|
||||||
|
|
||||||
if (!this.numPages) {
|
if (!this.numPages) {
|
||||||
console.error('Num pages not loaded')
|
console.error('Num pages not loaded')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
ebookLocation: String(this.page),
|
ebookLocation: String(this.page),
|
||||||
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
|
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<span class="material-icons text-xl text-white" @click.stop="show = false">close</span>
|
<span class="material-icons text-xl text-white" @click.stop="show = false">close</span>
|
||||||
</div>
|
</div>
|
||||||
<component v-if="readerComponentName" ref="readerComponent" :is="readerComponentName" :url="ebookUrl" :library-item="selectedLibraryItem" :is-local="isLocal" />
|
<component v-if="readerComponentName" ref="readerComponent" :is="readerComponentName" :url="ebookUrl" :library-item="selectedLibraryItem" :is-local="isLocal" :keep-progress="keepProgress" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -62,10 +62,19 @@ export default {
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
ebookFile() {
|
ebookFile() {
|
||||||
return this.media?.ebookFile || null
|
// ebook file id is passed when reading a supplementary ebook
|
||||||
|
if (this.ebookFileId) {
|
||||||
|
return this.selectedLibraryItem.libraryFiles.find((lf) => lf.ino === this.ebookFileId)
|
||||||
|
}
|
||||||
|
return this.media.ebookFile
|
||||||
},
|
},
|
||||||
ebookFormat() {
|
ebookFormat() {
|
||||||
return this.ebookFile?.ebookFormat || null
|
if (!this.ebookFile) return null
|
||||||
|
// Use file extension for supplementary ebook
|
||||||
|
if (!this.ebookFile.ebookFormat) {
|
||||||
|
return this.ebookFile.metadata.ext.toLowerCase().slice(1)
|
||||||
|
}
|
||||||
|
return this.ebookFile.ebookFormat
|
||||||
},
|
},
|
||||||
ebookType() {
|
ebookType() {
|
||||||
if (this.isMobi) return 'mobi'
|
if (this.isMobi) return 'mobi'
|
||||||
|
@ -98,10 +107,20 @@ export default {
|
||||||
return Capacitor.convertFileSrc(this.localContentUrl)
|
return Capacitor.convertFileSrc(this.localContentUrl)
|
||||||
}
|
}
|
||||||
const serverAddress = this.$store.getters['user/getServerAddress']
|
const serverAddress = this.$store.getters['user/getServerAddress']
|
||||||
|
|
||||||
|
if (this.ebookFileId) {
|
||||||
|
return `${serverAddress}/api/items/${this.selectedLibraryItem.id}/ebook/${this.ebookFileId}`
|
||||||
|
}
|
||||||
return `${serverAddress}/api/items/${this.selectedLibraryItem.id}/ebook`
|
return `${serverAddress}/api/items/${this.selectedLibraryItem.id}/ebook`
|
||||||
},
|
},
|
||||||
playerLibraryItemId() {
|
playerLibraryItemId() {
|
||||||
return this.$store.state.playerLibraryItemId
|
return this.$store.state.playerLibraryItemId
|
||||||
|
},
|
||||||
|
keepProgress() {
|
||||||
|
return this.$store.state.ereaderKeepProgress
|
||||||
|
},
|
||||||
|
ebookFileId() {
|
||||||
|
return this.$store.state.ereaderFileId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -130,11 +149,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.touchendX < this.touchstartX) {
|
if (this.touchendX < this.touchstartX) {
|
||||||
console.log('swiped left')
|
|
||||||
this.next()
|
this.next()
|
||||||
}
|
}
|
||||||
if (this.touchendX > this.touchstartX) {
|
if (this.touchendX > this.touchstartX) {
|
||||||
console.log('swiped right')
|
|
||||||
this.prev()
|
this.prev()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -483,9 +483,9 @@ export default {
|
||||||
readBook() {
|
readBook() {
|
||||||
if (this.localLibraryItem?.media?.ebookFile) {
|
if (this.localLibraryItem?.media?.ebookFile) {
|
||||||
// Has local ebook file
|
// Has local ebook file
|
||||||
this.$store.commit('openReader', this.localLibraryItem)
|
this.$store.commit('showReader', { libraryItem: this.localLibraryItem, keepProgress: true })
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit('openReader', this.libraryItem)
|
this.$store.commit('showReader', { libraryItem: this.libraryItem, keepProgress: true })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
playAtTimestamp(seconds) {
|
playAtTimestamp(seconds) {
|
||||||
|
|
|
@ -20,6 +20,8 @@ export const state = () => ({
|
||||||
hasStoragePermission: false,
|
hasStoragePermission: false,
|
||||||
selectedLibraryItem: null,
|
selectedLibraryItem: null,
|
||||||
showReader: false,
|
showReader: false,
|
||||||
|
ereaderKeepProgress: false,
|
||||||
|
ereaderFileId: null,
|
||||||
showSideDrawer: false,
|
showSideDrawer: false,
|
||||||
isNetworkListenerInit: false,
|
isNetworkListenerInit: false,
|
||||||
serverSettings: null,
|
serverSettings: null,
|
||||||
|
@ -145,8 +147,11 @@ export const mutations = {
|
||||||
setIsNetworkUnmetered(state, val) {
|
setIsNetworkUnmetered(state, val) {
|
||||||
state.isNetworkUnmetered = val
|
state.isNetworkUnmetered = val
|
||||||
},
|
},
|
||||||
openReader(state, libraryItem) {
|
showReader(state, { libraryItem, keepProgress, fileId }) {
|
||||||
state.selectedLibraryItem = libraryItem
|
state.selectedLibraryItem = libraryItem
|
||||||
|
state.ereaderKeepProgress = keepProgress
|
||||||
|
state.ereaderFileId = fileId
|
||||||
|
|
||||||
state.showReader = true
|
state.showReader = true
|
||||||
},
|
},
|
||||||
setShowReader(state, val) {
|
setShowReader(state, val) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue