Fix icon size issue, clean up reader interface, extract metadata from comics #113

This commit is contained in:
advplyr 2021-10-17 18:05:43 -05:00
parent 03e39640be
commit e3425acd75
15 changed files with 524 additions and 485 deletions

View File

@ -11,7 +11,6 @@
font-weight: normal;
font-style: normal;
line-height: 1;
font-size: 1.5rem;
letter-spacing: normal;
text-transform: none;
display: inline-block;
@ -21,12 +20,7 @@
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
.material-icons.text-icon {
font-size: 1.15rem;
}
.material-icons.text-base {
font-size: 1rem;
}
@font-face {
font-family: 'Gentium Book Basic';

View File

@ -1,361 +0,0 @@
<template>
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-50 bg-white text-black">
<div class="absolute top-4 right-4 z-10">
<span class="material-icons cursor-pointer text-4xl" @click="show = false">close</span>
</div>
<!-- <div v-if="chapters.length" class="absolute top-0 left-0 w-52">
<select v-model="selectedChapter" class="w-52" @change="changedChapter">
<option v-for="chapter in chapters" :key="chapter.href" :value="chapter.href">{{ chapter.label }}</option>
</select>
</div> -->
<div class="absolute top-4 left-4 font-book">
<h1 class="text-2xl mb-1">{{ title || abTitle }}</h1>
<p v-if="author || abAuthor">by {{ author || abAuthor }}</p>
</div>
<!-- EPUB -->
<div v-if="ebookType === 'epub'" class="h-full flex items-center">
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden">
<span v-show="hasPrev" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageLeft">chevron_left</span>
</div>
<div id="frame" class="w-full" style="height: 650px">
<div id="viewer" class="spreads"></div>
<div class="px-16 flex justify-center" style="height: 50px">
<p class="px-4">{{ progress }}%</p>
</div>
</div>
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden">
<span v-show="hasNext" class="material-icons text-black text-opacity-30 hover:text-opacity-80 cursor-pointer text-8xl" @mousedown.prevent @click="pageRight">chevron_right</span>
</div>
</div>
<!-- MOBI/AZW3 -->
<div v-else-if="ebookType === 'mobi'" class="h-full max-h-full w-full">
<div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-12 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20">
<iframe title="html-viewer" width="100%"> Loading </iframe>
</div>
</div>
<!-- PDF -->
<div v-else-if="ebookType === 'pdf'" class="h-full flex items-center">
<app-pdf-reader :src="ebookUrl" />
</div>
<!-- COMIC -->
<div v-else-if="ebookType === 'comic'" class="h-full flex items-center">
<app-comic-reader :src="ebookUrl" @close="show = false" />
</div>
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
</div>
</template>
<script>
import ePub from 'epubjs'
import MobiParser from '@/assets/ebooks/mobi.js'
import HtmlParser from '@/assets/ebooks/htmlParser.js'
import defaultCss from '@/assets/ebooks/basic.js'
export default {
data() {
return {
scale: 1,
book: null,
rendition: null,
chapters: [],
title: '',
author: '',
progress: 0,
hasNext: true,
hasPrev: false,
ebookType: '',
ebookUrl: ''
}
},
watch: {
show(newVal) {
if (newVal) {
this.init()
} else {
this.close()
}
}
},
computed: {
show: {
get() {
return this.$store.state.showEReader
},
set(val) {
this.$store.commit('setShowEReader', val)
}
},
abTitle() {
return this.selectedAudiobook.book.title
},
abAuthor() {
return this.selectedAudiobook.book.author
},
selectedAudiobook() {
return this.$store.state.selectedAudiobook
},
libraryId() {
return this.selectedAudiobook.libraryId
},
folderId() {
return this.selectedAudiobook.folderId
},
ebooks() {
return this.selectedAudiobook.ebooks || []
},
epubEbook() {
return this.ebooks.find((eb) => eb.ext === '.epub')
},
mobiEbook() {
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
},
pdfEbook() {
return this.ebooks.find((eb) => eb.ext === '.pdf')
},
comicEbook() {
return this.ebooks.find((eb) => eb.ext === '.cbz' || eb.ext === '.cbr')
},
userToken() {
return this.$store.getters['user/getToken']
},
selectedAudiobookFile() {
return this.$store.state.selectedAudiobookFile
}
},
methods: {
getEbookUrl(path) {
return `/ebook/${this.libraryId}/${this.folderId}/${path}`
},
changedChapter() {
if (this.rendition) {
this.rendition.display(this.selectedChapter)
}
},
pageLeft() {
if (this.rendition) {
this.rendition.prev()
}
},
pageRight() {
if (this.rendition) {
this.rendition.next()
}
},
keyUp(e) {
if (!this.rendition) {
console.error('No rendition')
return
}
if ((e.keyCode || e.which) == 37) {
this.rendition.prev()
} else if ((e.keyCode || e.which) == 39) {
this.rendition.next()
} else if ((e.keyCode || e.which) == 27) {
this.show = false
}
},
registerListeners() {
document.addEventListener('keyup', this.keyUp)
},
unregisterListeners() {
document.removeEventListener('keyup', this.keyUp)
},
init() {
if (this.selectedAudiobookFile) {
this.ebookUrl = this.getEbookUrl(this.selectedAudiobookFile.path)
if (this.selectedAudiobookFile.ext === '.pdf') {
this.ebookType = 'pdf'
} else if (this.selectedAudiobookFile.ext === '.mobi' || this.selectedAudiobookFile.ext === '.azw3') {
this.ebookType = 'mobi'
this.initMobi()
} else if (this.selectedAudiobookFile.ext === '.epub') {
this.ebookType = 'epub'
this.initEpub()
} else if (this.selectedAudiobookFile.ext === '.cbr' || this.selectedAudiobookFile.ext === '.cbz') {
this.ebookType = 'comic'
}
} else if (this.epubEbook) {
this.ebookType = 'epub'
this.ebookUrl = this.getEbookUrl(this.epubEbook.path)
this.initEpub()
} else if (this.mobiEbook) {
this.ebookType = 'mobi'
this.ebookUrl = this.getEbookUrl(this.mobiEbook.path)
this.initMobi()
} else if (this.pdfEbook) {
this.ebookType = 'pdf'
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
} else if (this.comicEbook) {
this.ebookType = 'comic'
this.ebookUrl = this.getEbookUrl(this.comicEbook.path)
}
},
addHtmlCss() {
let iframe = document.getElementsByTagName('iframe')[0]
if (!iframe) return
let doc = iframe.contentDocument
if (!doc) return
let style = doc.createElement('style')
style.id = 'default-style'
style.textContent = defaultCss
doc.head.appendChild(style)
},
handleIFrameHeight(iFrame) {
const isElement = (obj) => !!(obj && obj.nodeType === 1)
var body = iFrame.contentWindow.document.body,
html = iFrame.contentWindow.document.documentElement
iFrame.height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight) * 2
setTimeout(() => {
let lastchild = body.lastElementChild
let lastEle = body.lastChild
let itemAs = body.querySelectorAll('a')
let itemPs = body.querySelectorAll('p')
let lastItemA = itemAs[itemAs.length - 1]
let lastItemP = itemPs[itemPs.length - 1]
let lastItem
if (isElement(lastItemA) && isElement(lastItemP)) {
if (lastItemA.clientHeight + lastItemA.offsetTop > lastItemP.clientHeight + lastItemP.offsetTop) {
lastItem = lastItemA
} else {
lastItem = lastItemP
}
}
if (!lastchild && !lastItem && !lastEle) return
if (lastEle.nodeType === 3 && !lastchild && !lastItem) return
let nodeHeight = 0
if (lastEle.nodeType === 3 && document.createRange) {
let range = document.createRange()
range.selectNodeContents(lastEle)
if (range.getBoundingClientRect) {
let rect = range.getBoundingClientRect()
if (rect) {
nodeHeight = rect.bottom - rect.top
}
}
}
var lastChildHeight = isElement(lastchild) ? lastchild.clientHeight + lastchild.offsetTop : 0
var lastEleHeight = isElement(lastEle) ? lastEle.clientHeight + lastEle.offsetTop : 0
var lastItemHeight = isElement(lastItem) ? lastItem.clientHeight + lastItem.offsetTop : 0
iFrame.height = Math.max(lastChildHeight, lastEleHeight, lastItemHeight) + 100 + nodeHeight
}, 500)
},
async initMobi() {
// Fetch mobi file as blob
var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob'
})
var reader = new FileReader()
reader.onload = async (event) => {
var file_content = event.target.result
let mobiFile = new MobiParser(file_content)
let content = await mobiFile.render()
let htmlParser = new HtmlParser(new DOMParser().parseFromString(content.outerHTML, 'text/html'))
var anchoredDoc = htmlParser.getAnchoredDoc()
let iFrame = document.getElementsByTagName('iframe')[0]
iFrame.contentDocument.body.innerHTML = anchoredDoc.documentElement.outerHTML
// Add css
let style = iFrame.contentDocument.createElement('style')
style.id = 'default-style'
style.textContent = defaultCss
iFrame.contentDocument.head.appendChild(style)
this.handleIFrameHeight(iFrame)
}
reader.readAsArrayBuffer(buff)
},
initEpub() {
this.registerListeners()
// var book = ePub(this.url, {
// requestHeaders: {
// Authorization: `Bearer ${this.userToken}`
// }
// })
var book = ePub(this.ebookUrl)
this.book = book
this.rendition = book.renderTo('viewer', {
width: window.innerWidth - 200,
height: 600,
ignoreClass: 'annotator-hl',
manager: 'continuous',
spread: 'always'
})
var displayed = this.rendition.display()
book.ready
.then(() => {
console.log('Book ready')
return book.locations.generate(1600)
})
.then((locations) => {
// console.log('Loaded locations', locations)
// Wait for book to be rendered to get current page
displayed.then(() => {
// Get the current CFI
var currentLocation = this.rendition.currentLocation()
if (!currentLocation.start) {
console.error('No Start', currentLocation)
} else {
var currentPage = book.locations.percentageFromCfi(currentLocation.start.cfi)
// console.log('current page', currentPage)
}
})
})
book.loaded.navigation.then((toc) => {
var _chapters = []
toc.forEach((chapter) => {
_chapters.push(chapter)
})
this.chapters = _chapters
})
book.loaded.metadata.then((metadata) => {
this.author = metadata.creator
this.title = metadata.title
})
this.rendition.on('keyup', this.keyUp)
this.rendition.on('relocated', (location) => {
var percent = book.locations.percentageFromCfi(location.start.cfi)
var percentage = Math.floor(percent * 100)
this.progress = percentage
this.hasNext = !location.atEnd
this.hasPrev = !location.atStart
})
},
close() {
if (this.ebookType === 'epub') {
this.unregisterListeners()
}
}
},
mounted() {
if (this.show) this.init()
},
beforeDestroy() {
this.close()
}
}
</script>
<style>
/* @import url(@/assets/calibre/basic.css); */
.ebook-viewer {
height: calc(100% - 96px);
}
</style>

View File

@ -1,29 +1,42 @@
<template>
<div class="w-full">
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-20 rounded-md overflow-y-auto bg-white shadow-lg z-20 border border-gray-400">
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-gray-200 px-2 py-1" @click="setPage(index)">
<p class="text-sm">{{ file }}</p>
<div class="w-full h-full">
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-20 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52">
<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="setPage(index)">
<p class="text-sm truncate">{{ file }}</p>
</div>
</div>
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-10 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-96">
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
<p class="text-xs">
<strong>{{ key }}</strong
>: {{ comicMetadata[key] }}
</p>
</div>
</div>
<div class="absolute top-0 right-40 border-b border-l border-r border-gray-400 hover:bg-gray-200 cursor-pointer rounded-b-md bg-gray-50 w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
<span class="material-icons">menu</span>
<div v-if="comicMetadata" class="absolute top-0 right-52 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="showInfoMenu = !showInfoMenu">
<span class="material-icons text-xl">more</span>
</div>
<div class="absolute top-0 right-20 border-b border-l border-r border-gray-400 rounded-b-md bg-gray-50 px-2 h-9 flex items-center text-center">
<div class="absolute top-0 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" style="right: 156px" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
<span class="material-icons text-xl">menu</span>
</div>
<div class="absolute top-0 right-20 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">
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
</div>
<div class="overflow-hidden m-auto comicwrapper relative">
<div class="flex items-center justify-center">
<div class="px-12">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-black" :class="!canGoPrev ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goPrevPage" @mousedown.prevent>arrow_back_ios</span>
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
</div>
</div>
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2 ml-auto">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
</div>
</div>
<div class="h-full flex justify-center">
<img v-if="mainImg" :src="mainImg" class="object-contain comicimg" />
<div class="px-12">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-black" :class="!canGoNext ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goNextPage" @mousedown.prevent>arrow_forward_ios</span>
</div>
</div>
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
@ -44,11 +57,10 @@ import { Archive } from 'libarchive.js/main.js'
Archive.init({
workerUrl: '/libarchive/worker-bundle.js'
})
// Archive.init()
export default {
props: {
src: String
url: String
},
data() {
return {
@ -59,19 +71,24 @@ export default {
page: 0,
numPages: 0,
showPageMenu: false,
showInfoMenu: false,
loadTimeout: null,
loadedFirstPage: false
loadedFirstPage: false,
comicMetadata: null
}
},
watch: {
src: {
url: {
immediate: true,
handler(newVal) {
handler() {
this.extract()
}
}
},
computed: {
comicMetadataKeys() {
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
},
canGoNext() {
return this.page < this.numPages - 1
},
@ -82,12 +99,13 @@ export default {
methods: {
clickOutside() {
if (this.showPageMenu) this.showPageMenu = false
if (this.showInfoMenu) this.showInfoMenu = false
},
goNextPage() {
next() {
if (!this.canGoNext) return
this.setPage(this.page + 1)
},
goPrevPage() {
prev() {
if (!this.canGoPrev) return
this.setPage(this.page - 1)
},
@ -126,9 +144,9 @@ export default {
},
async extract() {
this.loading = true
console.log('Extracting', this.src)
console.log('Extracting', this.url)
var buff = await this.$axios.$get(this.src, {
var buff = await this.$axios.$get(this.url, {
responseType: 'blob'
})
const archive = await Archive.open(buff)
@ -136,6 +154,9 @@ export default {
var filenames = Object.keys(this.filesObject)
this.parseFilenames(filenames)
var xmlFile = filenames.find((f) => (Path.extname(f) || '').toLowerCase() === '.xml')
if (xmlFile) await this.extractXmlFile(xmlFile)
this.numPages = this.pages.length
if (this.pages.length) {
@ -147,6 +168,23 @@ export default {
this.loading = false
}
},
async extractXmlFile(filename) {
console.log('extracting xml filename', filename)
try {
var file = await this.filesObject[filename].extract()
var reader = new FileReader()
reader.onload = (e) => {
this.comicMetadata = this.$xmlToJson(e.target.result)
console.log('Metadata', this.comicMetadata)
}
reader.onerror = (e) => {
console.error(e)
}
reader.readAsText(file)
} catch (error) {
console.error(error)
}
},
parseImageFilename(filename) {
var basename = Path.basename(filename, Path.extname(filename))
var numbersinpath = basename.match(/\d{1,4}/g)
@ -177,30 +215,10 @@ export default {
orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename))
this.pages = orderedImages
},
keyUp(e) {
if ((e.keyCode || e.which) == 37) {
this.goPrevPage()
} else if ((e.keyCode || e.which) == 39) {
this.goNextPage()
} else if ((e.keyCode || e.which) == 27) {
this.unregisterListeners()
this.$emit('close')
}
},
registerListeners() {
document.addEventListener('keyup', this.keyUp)
},
unregisterListeners() {
document.removeEventListener('keyup', this.keyUp)
}
},
mounted() {
this.registerListeners()
},
beforeDestroy() {
this.unregisterListeners()
}
mounted() {},
beforeDestroy() {}
}
</script>
@ -213,7 +231,8 @@ export default {
margin: auto;
}
.comicwrapper {
width: calc(100vw - 300px);
width: 100vw;
height: calc(100vh - 40px);
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,129 @@
<template>
<div class="h-full w-full">
<div class="h-full flex items-center">
<div style="width: 100px; max-width: 100px" class="h-full flex items-center overflow-x-hidden justify-center">
<span v-show="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
</div>
<div id="frame" class="w-full" style="height: 650px">
<div id="viewer" class="border border-gray-100 bg-white shadow-md"></div>
<div class="py-4 flex justify-center" style="height: 50px">
<p>{{ progress }}%</p>
</div>
</div>
<div style="width: 100px; max-width: 100px" class="h-full flex items-center justify-center overflow-x-hidden">
<span v-show="hasNext" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="next">chevron_right</span>
</div>
</div>
</div>
</template>
<script>
import ePub from 'epubjs'
export default {
props: {
url: String
},
data() {
return {
book: null,
rendition: null,
chapters: [],
title: '',
author: '',
progress: 0,
hasNext: true,
hasPrev: false
}
},
computed: {},
methods: {
changedChapter() {
if (this.rendition) {
this.rendition.display(this.selectedChapter)
}
},
prev() {
if (this.rendition) {
this.rendition.prev()
}
},
next() {
if (this.rendition) {
this.rendition.next()
}
},
keyUp() {
if ((e.keyCode || e.which) == 37) {
this.prev()
} else if ((e.keyCode || e.which) == 39) {
this.next()
}
},
initEpub() {
// var book = ePub(this.url, {
// requestHeaders: {
// Authorization: `Bearer ${this.userToken}`
// }
// })
var book = ePub(this.url)
this.book = book
this.rendition = book.renderTo('viewer', {
width: window.innerWidth - 200,
height: 600,
ignoreClass: 'annotator-hl',
manager: 'continuous',
spread: 'always'
})
var displayed = this.rendition.display()
book.ready
.then(() => {
console.log('Book ready')
return book.locations.generate(1600)
})
.then((locations) => {
// console.log('Loaded locations', locations)
// Wait for book to be rendered to get current page
displayed.then(() => {
// Get the current CFI
var currentLocation = this.rendition.currentLocation()
if (!currentLocation.start) {
console.error('No Start', currentLocation)
} else {
var currentPage = book.locations.percentageFromCfi(currentLocation.start.cfi)
// console.log('current page', currentPage)
}
})
})
book.loaded.navigation.then((toc) => {
var _chapters = []
toc.forEach((chapter) => {
_chapters.push(chapter)
})
this.chapters = _chapters
})
book.loaded.metadata.then((metadata) => {
this.author = metadata.creator
this.title = metadata.title
})
this.rendition.on('keyup', this.keyUp)
this.rendition.on('relocated', (location) => {
var percent = book.locations.percentageFromCfi(location.start.cfi)
this.progress = Math.floor(percent * 100)
this.hasNext = !location.atEnd
this.hasPrev = !location.atStart
})
}
},
mounted() {
this.initEpub()
}
}
</script>

View File

@ -0,0 +1,118 @@
<template>
<div class="w-full h-full">
<div class="h-full max-h-full w-full">
<div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-12 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20 shadow-md bg-white">
<iframe title="html-viewer" width="100%"> Loading </iframe>
</div>
</div>
</div>
</template>
<script>
import MobiParser from '@/assets/ebooks/mobi.js'
import HtmlParser from '@/assets/ebooks/htmlParser.js'
import defaultCss from '@/assets/ebooks/basic.js'
export default {
props: {
url: String
},
data() {
return {}
},
computed: {},
methods: {
addHtmlCss() {
let iframe = document.getElementsByTagName('iframe')[0]
if (!iframe) return
let doc = iframe.contentDocument
if (!doc) return
let style = doc.createElement('style')
style.id = 'default-style'
style.textContent = defaultCss
doc.head.appendChild(style)
},
handleIFrameHeight(iFrame) {
const isElement = (obj) => !!(obj && obj.nodeType === 1)
var body = iFrame.contentWindow.document.body,
html = iFrame.contentWindow.document.documentElement
iFrame.height = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight) * 2
setTimeout(() => {
let lastchild = body.lastElementChild
let lastEle = body.lastChild
let itemAs = body.querySelectorAll('a')
let itemPs = body.querySelectorAll('p')
let lastItemA = itemAs[itemAs.length - 1]
let lastItemP = itemPs[itemPs.length - 1]
let lastItem
if (isElement(lastItemA) && isElement(lastItemP)) {
if (lastItemA.clientHeight + lastItemA.offsetTop > lastItemP.clientHeight + lastItemP.offsetTop) {
lastItem = lastItemA
} else {
lastItem = lastItemP
}
}
if (!lastchild && !lastItem && !lastEle) return
if (lastEle.nodeType === 3 && !lastchild && !lastItem) return
let nodeHeight = 0
if (lastEle.nodeType === 3 && document.createRange) {
let range = document.createRange()
range.selectNodeContents(lastEle)
if (range.getBoundingClientRect) {
let rect = range.getBoundingClientRect()
if (rect) {
nodeHeight = rect.bottom - rect.top
}
}
}
var lastChildHeight = isElement(lastchild) ? lastchild.clientHeight + lastchild.offsetTop : 0
var lastEleHeight = isElement(lastEle) ? lastEle.clientHeight + lastEle.offsetTop : 0
var lastItemHeight = isElement(lastItem) ? lastItem.clientHeight + lastItem.offsetTop : 0
iFrame.height = Math.max(lastChildHeight, lastEleHeight, lastItemHeight) + 100 + nodeHeight
}, 500)
},
async initMobi() {
// Fetch mobi file as blob
var buff = await this.$axios.$get(this.url, {
responseType: 'blob'
})
var reader = new FileReader()
reader.onload = async (event) => {
var file_content = event.target.result
let mobiFile = new MobiParser(file_content)
let content = await mobiFile.render()
let htmlParser = new HtmlParser(new DOMParser().parseFromString(content.outerHTML, 'text/html'))
var anchoredDoc = htmlParser.getAnchoredDoc()
let iFrame = document.getElementsByTagName('iframe')[0]
iFrame.contentDocument.body.innerHTML = anchoredDoc.documentElement.outerHTML
// Add css
let style = iFrame.contentDocument.createElement('style')
style.id = 'default-style'
style.textContent = defaultCss
iFrame.contentDocument.head.appendChild(style)
this.handleIFrameHeight(iFrame)
}
reader.readAsArrayBuffer(buff)
}
},
mounted() {
this.initMobi()
}
}
</script>
<style>
.ebook-viewer {
height: calc(100% - 96px);
}
</style>

View File

@ -1,23 +1,31 @@
<template>
<div class="w-full pt-20">
<div class="w-full h-full pt-20 relative">
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2">
<span class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
</div>
</div>
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2 ml-auto">
<span class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
</div>
</div>
<div class="absolute top-0 right-20 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">
<p class="font-mono">{{ page }} / {{ numPages }}</p>
</div>
<div :style="{ height: pdfHeight + 'px' }" class="overflow-hidden m-auto">
<div class="flex items-center justify-center">
<div class="px-12">
<span class="material-icons text-5xl text-black" :class="!canGoPrev ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goPrevPage" @mousedown.prevent>arrow_back_ios</span>
</div>
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="w-full h-full overflow-auto">
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="src" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event"></pdf>
</div>
<div class="px-12">
<span class="material-icons text-5xl text-black" :class="!canGoNext ? 'text-opacity-10' : 'cursor-pointer text-opacity-30 hover:text-opacity-90'" @click.stop.prevent="goNextPage" @mousedown.prevent>arrow_forward_ios</span>
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="url" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event"></pdf>
</div>
</div>
</div>
<div class="text-center py-2 text-lg">
<!-- <div class="text-center py-2 text-lg">
<p>{{ page }} / {{ numPages }}</p>
</div>
</div> -->
</div>
</template>
@ -29,7 +37,7 @@ export default {
pdf
},
props: {
src: String
url: String
},
data() {
return {
@ -57,11 +65,11 @@ export default {
numPagesLoaded(e) {
this.numPages = e
},
goPrevPage() {
prev() {
if (this.page <= 1) return
this.page--
},
goNextPage() {
next() {
if (this.page >= this.numPages) return
this.page++
},

View File

@ -0,0 +1,166 @@
<template>
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-50 bg-primary text-white">
<div class="absolute top-4 right-4 z-20">
<span class="material-icons cursor-pointer text-4xl" @click="show = false">close</span>
</div>
<div class="absolute top-4 left-4 font-book">
<h1 class="text-2xl mb-1">{{ abTitle }}</h1>
<p v-if="abAuthor">by {{ abAuthor }}</p>
</div>
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" />
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
</div>
</template>
<script>
export default {
data() {
return {
ebookType: '',
ebookUrl: ''
}
},
watch: {
show(newVal) {
if (newVal) {
this.init()
} else {
this.close()
}
}
},
computed: {
show: {
get() {
return this.$store.state.showEReader
},
set(val) {
this.$store.commit('setShowEReader', val)
}
},
componentName() {
if (this.ebookType === 'epub') return 'readers-epub-reader'
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
else if (this.ebookType === 'comic') return 'readers-comic-reader'
return null
},
abTitle() {
return this.selectedAudiobook.book.title
},
abAuthor() {
return this.selectedAudiobook.book.author
},
selectedAudiobook() {
return this.$store.state.selectedAudiobook
},
libraryId() {
return this.selectedAudiobook.libraryId
},
folderId() {
return this.selectedAudiobook.folderId
},
ebooks() {
return this.selectedAudiobook.ebooks || []
},
epubEbook() {
return this.ebooks.find((eb) => eb.ext === '.epub')
},
mobiEbook() {
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
},
pdfEbook() {
return this.ebooks.find((eb) => eb.ext === '.pdf')
},
comicEbook() {
return this.ebooks.find((eb) => eb.ext === '.cbz' || eb.ext === '.cbr')
},
userToken() {
return this.$store.getters['user/getToken']
},
selectedAudiobookFile() {
return this.$store.state.selectedAudiobookFile
}
},
methods: {
getEbookUrl(path) {
return `/ebook/${this.libraryId}/${this.folderId}/${path}`
},
keyUp(e) {
if (!this.$refs.readerComponent) {
return
}
if ((e.keyCode || e.which) == 37) {
if (this.$refs.readerComponent.prev) {
this.$refs.readerComponent.prev()
}
} else if ((e.keyCode || e.which) == 39) {
if (this.$refs.readerComponent.next) {
this.$refs.readerComponent.next()
}
} else if ((e.keyCode || e.which) == 27) {
this.show = false
}
},
registerListeners() {
document.addEventListener('keyup', this.keyUp)
},
unregisterListeners() {
document.removeEventListener('keyup', this.keyUp)
},
init() {
this.registerListeners()
if (this.selectedAudiobookFile) {
this.ebookUrl = this.getEbookUrl(this.selectedAudiobookFile.path)
if (this.selectedAudiobookFile.ext === '.pdf') {
this.ebookType = 'pdf'
} else if (this.selectedAudiobookFile.ext === '.mobi' || this.selectedAudiobookFile.ext === '.azw3') {
this.ebookType = 'mobi'
// this.initMobi()
} else if (this.selectedAudiobookFile.ext === '.epub') {
this.ebookType = 'epub'
// this.initEpub()
} else if (this.selectedAudiobookFile.ext === '.cbr' || this.selectedAudiobookFile.ext === '.cbz') {
this.ebookType = 'comic'
}
} else if (this.epubEbook) {
this.ebookType = 'epub'
this.ebookUrl = this.getEbookUrl(this.epubEbook.path)
// this.initEpub()
} else if (this.mobiEbook) {
this.ebookType = 'mobi'
this.ebookUrl = this.getEbookUrl(this.mobiEbook.path)
// this.initMobi()
} else if (this.pdfEbook) {
this.ebookType = 'pdf'
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
} else if (this.comicEbook) {
this.ebookType = 'comic'
this.ebookUrl = this.getEbookUrl(this.comicEbook.path)
}
},
close() {
if (this.ebookType === 'epub') {
this.unregisterListeners()
}
}
},
mounted() {
if (this.show) this.init()
},
beforeDestroy() {
this.close()
}
}
</script>
<style>
/* @import url(@/assets/calibre/basic.css); */
.ebook-viewer {
height: calc(100% - 96px);
}
</style>

View File

@ -7,7 +7,7 @@
<app-stream-container ref="streamContainer" />
<modals-libraries-modal />
<modals-edit-modal />
<app-reader />
<readers-reader />
<!-- <widgets-scan-alert /> -->
</div>
</template>

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.4.11",
"version": "1.4.12",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {

View File

@ -24,11 +24,6 @@
</p>
<nuxt-link v-if="series" :to="`/library/${libraryId}/bookshelf/series?series=${$encode(series)}`" class="hover:underline font-sans text-gray-300 text-lg leading-7 mb-4"> {{ seriesText }}</nuxt-link>
<!-- <div class="w-min">
<ui-tooltip :text="authorTooltipText" direction="bottom">
<span class="text-base text-gray-100 leading-8 whitespace-nowrap"><span class="text-white text-opacity-60">By:</span> {{ author }}</span>
</ui-tooltip>
</div> -->
<div v-if="narrator" class="flex py-0.5">
<div class="w-32">
<span class="text-white text-opacity-60 uppercase text-sm">Narrated By</span>
@ -81,10 +76,6 @@
<span class="material-icons text-2xl">warning_amber</span>
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
</div>
<div v-show="showEpubAlert" class="bg-error p-4 rounded-xl flex items-center mt-2">
<span class="material-icons text-2xl">warning_amber</span>
<p class="ml-4">Book has valid ebook files, but the experimental e-reader currently only supports epub files.</p>
</div>
<div v-if="progressPercent > 0 && progressPercent < 1" class="px-4 py-2 mt-4 bg-primary text-sm font-semibold rounded-md text-gray-200 relative max-w-max" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
@ -152,8 +143,6 @@
</div>
</div>
</div>
<!-- <app-reader v-if="showExperimentalFeatures" v-model="showReader" :url="epubUrl" /> -->
</div>
</template>
@ -321,14 +310,10 @@ export default {
ebooks() {
return this.audiobook.ebooks
},
showEpubAlert() {
return this.ebooks.length && !this.numEbooks && !this.tracks.length
},
showExperimentalReadAlert() {
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
},
numEbooks() {
// Number of currently supported for reading ebooks, not all ebooks
return this.audiobook.numEbooks
},
userToken() {

View File

@ -1,5 +1,9 @@
export default function ({ $axios, store }) {
$axios.onRequest(config => {
if (!config.url) {
console.error('Axios request invalid config', config)
return
}
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
return
}

View File

@ -127,6 +127,19 @@ Vue.prototype.$sanitizeFilename = (input, replacement = '') => {
return sanitized
}
function xmlToJson(xml) {
const json = {};
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
const key = res[1] || res[3];
const value = res[2] && xmlToJson(res[2]);
json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null;
}
return json;
}
Vue.prototype.$xmlToJson = xmlToJson
const encode = (text) => encodeURIComponent(Buffer.from(text).toString('base64'))
Vue.prototype.$encode = encode
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.4.11",
"version": "1.4.12",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {

View File

@ -127,22 +127,6 @@ class Audiobook {
return this.otherFiles.filter(file => file.filetype === 'ebook')
}
get hasEpub() {
return this.ebooks.find(file => file.ext === '.epub')
}
get hasMobi() {
return this.ebooks.find(file => file.ext === '.mobi' || file.ext === '.azw3')
}
get hasPdf() {
return this.ebooks.find(file => file.ext === '.pdf')
}
get hasComic() {
return this.ebooks.find(file => file.ext === '.cbr' || file.ext === '.cbz')
}
get hasMissingIno() {
return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino)
}
@ -214,7 +198,7 @@ class Audiobook {
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
// numEbooks: this.ebooks.length,
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf || this.hasComic) ? 1 : 0,
numEbooks: this.ebooks.length,
numTracks: this.tracks.length,
chapters: this.chapters || [],
isMissing: !!this.isMissing,
@ -241,7 +225,7 @@ class Audiobook {
audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
ebooks: this.ebooks.map(ebook => ebook.toJSON()),
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf || this.hasComic) ? 1 : 0,
numEbooks: this.ebooks.length,
numTracks: this.tracks.length,
tags: this.tags,
book: this.bookToJSON(),

View File

@ -29,26 +29,6 @@ const levenshteinDistance = (str1, str2, caseSensitive = false) => {
}
module.exports.levenshteinDistance = levenshteinDistance
const cleanString = (str) => {
if (!str) return ''
// Now supporting all utf-8 characters, can remove this method in future
// replace accented characters: https://stackoverflow.com/a/49901740/7431543
// str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
// const availableChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
// const cleanChar = (char) => availableChars.indexOf(char) < 0 ? '?' : char
// var cleaned = ''
// for (let i = 0; i < str.length; i++) {
// cleaned += cleanChar(str[i])
// }
return cleaned.trim()
}
module.exports.cleanString = cleanString
module.exports.isObject = (val) => {
return val !== null && typeof val === 'object'
}