Support cbr and cbz comics and comic reader #109

This commit is contained in:
advplyr 2021-10-16 20:33:49 -05:00
parent 18c1d8f1a3
commit 88e2bac3f5
12 changed files with 276 additions and 20 deletions

View File

@ -0,0 +1,219 @@
<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>
</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>
<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">
<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>
<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">
<ui-loading-indicator />
</div>
</div>
<!-- <div v-show="loading" class="w-screen h-screen absolute top-0 left-0 bg-black bg-opacity-20 flex items-center justify-center">
<ui-loading-indicator />
</div> -->
</div>
</template>
<script>
import Path from 'path'
import { Archive } from 'libarchive.js/main.js'
Archive.init({
workerUrl: '/libarchive/worker-bundle.js'
})
// Archive.init()
export default {
props: {
src: String
},
data() {
return {
loading: false,
pages: null,
filesObject: null,
mainImg: null,
page: 0,
numPages: 0,
showPageMenu: false,
loadTimeout: null,
loadedFirstPage: false
}
},
watch: {
src: {
immediate: true,
handler(newVal) {
this.extract()
}
}
},
computed: {
canGoNext() {
return this.page < this.numPages - 1
},
canGoPrev() {
return this.page > 0
}
},
methods: {
clickOutside() {
if (this.showPageMenu) this.showPageMenu = false
},
goNextPage() {
if (!this.canGoNext) return
this.setPage(this.page + 1)
},
goPrevPage() {
if (!this.canGoPrev) return
this.setPage(this.page - 1)
},
setPage(index) {
if (index < 0 || index > this.numPages - 1) {
return
}
var filename = this.pages[index]
this.page = index
return this.extractFile(filename)
},
setLoadTimeout() {
this.loadTimeout = setTimeout(() => {
this.loading = true
}, 150)
},
extractFile(filename) {
return new Promise(async (resolve) => {
this.setLoadTimeout()
var file = await this.filesObject[filename].extract()
var reader = new FileReader()
reader.onload = (e) => {
this.mainImg = e.target.result
this.loading = false
resolve()
}
reader.onerror = (e) => {
console.error(e)
this.$toast.error('Read page file failed')
this.loading = false
resolve()
}
reader.readAsDataURL(file)
clearTimeout(this.loadTimeout)
})
},
async extract() {
this.loading = true
console.log('Extracting', this.src)
var buff = await this.$axios.$get(this.src, {
responseType: 'blob'
})
const archive = await Archive.open(buff)
this.filesObject = await archive.getFilesObject()
var filenames = Object.keys(this.filesObject)
this.parseFilenames(filenames)
this.numPages = this.pages.length
if (this.pages.length) {
this.loading = false
await this.setPage(0)
this.loadedFirstPage = true
} else {
this.$toast.error('Unable to extract pages')
this.loading = false
}
},
parseImageFilename(filename) {
var basename = Path.basename(filename, Path.extname(filename))
var numbersinpath = basename.match(/\d{1,4}/g)
if (!numbersinpath || !numbersinpath.length) {
return {
index: -1,
filename
}
} else {
return {
index: Number(numbersinpath[numbersinpath.length - 1]),
filename
}
}
},
parseFilenames(filenames) {
const acceptableImages = ['.jpeg', '.jpg', '.png']
var imageFiles = filenames.filter((f) => {
return acceptableImages.includes((Path.extname(f) || '').toLowerCase())
})
var imageFileObjs = imageFiles.map((img) => {
return this.parseImageFilename(img)
})
var imagesWithNum = imageFileObjs.filter((i) => i.index >= 0)
var orderedImages = imagesWithNum.sort((a, b) => a.index - b.index).map((i) => i.filename)
var noNumImages = imageFileObjs.filter((i) => i.index < 0)
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()
}
}
</script>
<style scoped>
.pagemenu {
max-height: calc(100vh - 60px);
}
.comicimg {
height: calc(100vh - 40px);
margin: auto;
}
.comicwrapper {
width: calc(100vw - 300px);
height: calc(100vh - 40px);
}
</style>

View File

@ -39,6 +39,10 @@
<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>
@ -111,6 +115,9 @@ export default {
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']
},
@ -158,7 +165,6 @@ export default {
document.removeEventListener('keyup', this.keyUp)
},
init() {
this.registerListeners()
if (this.selectedAudiobookFile) {
this.ebookUrl = this.getEbookUrl(this.selectedAudiobookFile.path)
if (this.selectedAudiobookFile.ext === '.pdf') {
@ -169,6 +175,8 @@ export default {
} 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'
@ -181,6 +189,9 @@ export default {
} 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() {
@ -266,6 +277,7 @@ export default {
reader.readAsArrayBuffer(buff)
},
initEpub() {
this.registerListeners()
// var book = ePub(this.url, {
// requestHeaders: {
// Authorization: `Bearer ${this.userToken}`
@ -327,14 +339,16 @@ export default {
})
},
close() {
this.unregisterListeners()
if (this.ebookType === 'epub') {
this.unregisterListeners()
}
}
},
mounted() {
if (this.show) this.init()
},
beforeDestroy() {
this.unregisterListeners()
this.close()
}
}
</script>

View File

@ -19,7 +19,7 @@ module.exports = {
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'AudioBookshelf',
title: 'Audiobookshelf',
htmlAttrs: {
lang: 'en'
},
@ -98,8 +98,7 @@ module.exports = {
},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {
},
build: {},
watchers: {
webpack: {
aggregateTimeout: 300,

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.4.8",
"version": "1.4.10",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -7385,6 +7385,11 @@
"launch-editor": "^2.2.1"
}
},
"libarchive.js": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/libarchive.js/-/libarchive.js-1.3.0.tgz",
"integrity": "sha512-EkQfRXt9DhWwj6BnEA2TNpOf4jTnzSTUPGgE+iFxcdNqjktY8GitbDeHnx8qZA0/IukNyyBUR3oQKRdYkO+HFg=="
},
"lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.4.9",
"version": "1.4.10",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
@ -19,6 +19,7 @@
"date-fns": "^2.25.0",
"epubjs": "^0.3.88",
"hls.js": "^1.0.7",
"libarchive.js": "^1.3.0",
"nuxt": "^2.15.7",
"nuxt-socket-io": "^1.1.18",
"vue-pdf": "^4.3.0",
@ -29,4 +30,4 @@
"@nuxtjs/tailwindcss": "^4.2.1",
"postcss": "^8.3.6"
}
}
}

View File

@ -104,7 +104,7 @@
{{ isMissing ? 'Missing' : 'Incomplete' }}
</ui-btn>
<ui-btn v-if="showExperimentalFeatures && (epubEbook || mobiEbook)" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
<ui-btn v-if="showExperimentalFeatures && numEbooks" color="info" :padding-x="4" small class="flex items-center h-9 mr-2" @click="openEbook">
<span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
Read
</ui-btn>
@ -322,16 +322,14 @@ export default {
return this.audiobook.ebooks
},
showEpubAlert() {
return this.ebooks.length && !this.epubEbook && !this.tracks.length
return this.ebooks.length && !this.numEbooks && !this.tracks.length
},
showExperimentalReadAlert() {
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
},
epubEbook() {
return this.ebooks.find((eb) => eb.ext === '.epub')
},
mobiEbook() {
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
numEbooks() {
// Number of currently supported for reading ebooks, not all ebooks
return this.audiobook.numEbooks
},
userToken() {
return this.$store.getters['user/getToken']

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

View File

@ -139,6 +139,10 @@ class Audiobook {
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)
}
@ -210,7 +214,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) ? 1 : 0, // Only supporting epubs in the reader currently
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf || this.hasComic) ? 1 : 0,
numTracks: this.tracks.length,
chapters: this.chapters || [],
isMissing: !!this.isMissing,
@ -237,7 +241,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) ? 1 : 0,
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf || this.hasComic) ? 1 : 0,
numTracks: this.tracks.length,
tags: this.tags,
book: this.bookToJSON(),

View File

@ -1,7 +1,7 @@
const globals = {
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4'],
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3']
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz']
}
module.exports = globals