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"> <div v-else-if="ebookType === 'pdf'" class="h-full flex items-center">
<app-pdf-reader :src="ebookUrl" /> <app-pdf-reader :src="ebookUrl" />
</div> </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 class="absolute bottom-2 left-2">{{ ebookType }}</div>
</div> </div>
@ -111,6 +115,9 @@ export default {
pdfEbook() { pdfEbook() {
return this.ebooks.find((eb) => eb.ext === '.pdf') return this.ebooks.find((eb) => eb.ext === '.pdf')
}, },
comicEbook() {
return this.ebooks.find((eb) => eb.ext === '.cbz' || eb.ext === '.cbr')
},
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.$store.getters['user/getToken']
}, },
@ -158,7 +165,6 @@ export default {
document.removeEventListener('keyup', this.keyUp) document.removeEventListener('keyup', this.keyUp)
}, },
init() { init() {
this.registerListeners()
if (this.selectedAudiobookFile) { if (this.selectedAudiobookFile) {
this.ebookUrl = this.getEbookUrl(this.selectedAudiobookFile.path) this.ebookUrl = this.getEbookUrl(this.selectedAudiobookFile.path)
if (this.selectedAudiobookFile.ext === '.pdf') { if (this.selectedAudiobookFile.ext === '.pdf') {
@ -169,6 +175,8 @@ export default {
} else if (this.selectedAudiobookFile.ext === '.epub') { } else if (this.selectedAudiobookFile.ext === '.epub') {
this.ebookType = 'epub' this.ebookType = 'epub'
this.initEpub() this.initEpub()
} else if (this.selectedAudiobookFile.ext === '.cbr' || this.selectedAudiobookFile.ext === '.cbz') {
this.ebookType = 'comic'
} }
} else if (this.epubEbook) { } else if (this.epubEbook) {
this.ebookType = 'epub' this.ebookType = 'epub'
@ -181,6 +189,9 @@ export default {
} else if (this.pdfEbook) { } else if (this.pdfEbook) {
this.ebookType = 'pdf' this.ebookType = 'pdf'
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path) this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
} else if (this.comicEbook) {
this.ebookType = 'comic'
this.ebookUrl = this.getEbookUrl(this.comicEbook.path)
} }
}, },
addHtmlCss() { addHtmlCss() {
@ -266,6 +277,7 @@ export default {
reader.readAsArrayBuffer(buff) reader.readAsArrayBuffer(buff)
}, },
initEpub() { initEpub() {
this.registerListeners()
// var book = ePub(this.url, { // var book = ePub(this.url, {
// requestHeaders: { // requestHeaders: {
// Authorization: `Bearer ${this.userToken}` // Authorization: `Bearer ${this.userToken}`
@ -327,14 +339,16 @@ export default {
}) })
}, },
close() { close() {
this.unregisterListeners() if (this.ebookType === 'epub') {
this.unregisterListeners()
}
} }
}, },
mounted() { mounted() {
if (this.show) this.init() if (this.show) this.init()
}, },
beforeDestroy() { beforeDestroy() {
this.unregisterListeners() this.close()
} }
} }
</script> </script>

View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "1.4.8", "version": "1.4.10",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -7385,6 +7385,11 @@
"launch-editor": "^2.2.1" "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": { "lie": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",

View File

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

View File

@ -104,7 +104,7 @@
{{ isMissing ? 'Missing' : 'Incomplete' }} {{ isMissing ? 'Missing' : 'Incomplete' }}
</ui-btn> </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> <span class="material-icons -ml-2 pr-2 text-white">auto_stories</span>
Read Read
</ui-btn> </ui-btn>
@ -322,16 +322,14 @@ export default {
return this.audiobook.ebooks return this.audiobook.ebooks
}, },
showEpubAlert() { showEpubAlert() {
return this.ebooks.length && !this.epubEbook && !this.tracks.length return this.ebooks.length && !this.numEbooks && !this.tracks.length
}, },
showExperimentalReadAlert() { showExperimentalReadAlert() {
return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures return !this.tracks.length && this.ebooks.length && !this.showExperimentalFeatures
}, },
epubEbook() { numEbooks() {
return this.ebooks.find((eb) => eb.ext === '.epub') // Number of currently supported for reading ebooks, not all ebooks
}, return this.audiobook.numEbooks
mobiEbook() {
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
}, },
userToken() { userToken() {
return this.$store.getters['user/getToken'] 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", "name": "audiobookshelf",
"version": "1.4.9", "version": "1.4.10",
"description": "Self-hosted audiobook server for managing and playing audiobooks", "description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -139,6 +139,10 @@ class Audiobook {
return this.ebooks.find(file => file.ext === '.pdf') return this.ebooks.find(file => file.ext === '.pdf')
} }
get hasComic() {
return this.ebooks.find(file => file.ext === '.cbr' || file.ext === '.cbz')
}
get hasMissingIno() { get hasMissingIno() {
return !this.ino || this._audioFiles.find(abf => !abf.ino) || this._otherFiles.find(f => !f.ino) || this._tracks.find(t => !t.ino) 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, hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0,
// numEbooks: this.ebooks.length, // numEbooks: this.ebooks.length,
ebooks: this.ebooks.map(ebook => ebook.toJSON()), 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, numTracks: this.tracks.length,
chapters: this.chapters || [], chapters: this.chapters || [],
isMissing: !!this.isMissing, isMissing: !!this.isMissing,
@ -237,7 +241,7 @@ class Audiobook {
audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()), audioFiles: this._audioFiles.map(audioFile => audioFile.toJSON()),
otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()), otherFiles: this._otherFiles.map(otherFile => otherFile.toJSON()),
ebooks: this.ebooks.map(ebook => ebook.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, numTracks: this.tracks.length,
tags: this.tags, tags: this.tags,
book: this.bookToJSON(), book: this.bookToJSON(),

View File

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