2021-10-17 20:20:00 -05:00
|
|
|
<template>
|
2023-03-06 10:58:08 -06:00
|
|
|
<div id="comic-reader" class="w-full h-full relative">
|
2023-06-17 17:34:08 -05:00
|
|
|
<modals-modal v-model="showInfoMenu" height="90%">
|
|
|
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click.stop="showInfoMenu = false">
|
|
|
|
<div class="w-full overflow-x-hidden overflow-y-auto bg-secondary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
|
|
|
<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>
|
2021-10-17 20:20:00 -05:00
|
|
|
</div>
|
2023-06-17 17:34:08 -05:00
|
|
|
</modals-modal>
|
2021-10-17 20:20:00 -05:00
|
|
|
|
|
|
|
<div class="overflow-hidden m-auto comicwrapper relative">
|
|
|
|
<div class="h-full flex justify-center">
|
|
|
|
<img v-if="mainImg" :src="mainImg" class="object-contain comicimg" />
|
|
|
|
</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>
|
2023-06-11 11:12:52 -05:00
|
|
|
|
2023-06-19 12:37:44 -05:00
|
|
|
<div class="fixed left-0 h-8 w-full bg-primary px-4 flex items-center text-white/80" :style="{ bottom: isPlayerOpen ? '120px' : '0px' }">
|
2023-06-17 17:34:08 -05:00
|
|
|
<div class="flex-grow" />
|
|
|
|
<p class="text-xs">{{ page }} / {{ numPages }}</p>
|
|
|
|
</div>
|
|
|
|
|
2023-06-11 11:12:52 -05:00
|
|
|
<modals-dialog v-model="showPageMenu" :items="pageItems" :selected="page" :width="360" item-padding-y="8px" @action="setPage" />
|
2021-10-17 20:20:00 -05:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
import Path from 'path'
|
|
|
|
import { Archive } from 'libarchive.js/main.js'
|
2022-11-05 15:13:52 -05:00
|
|
|
import { CompressedFile } from 'libarchive.js/src/compressed-file'
|
2021-10-17 20:20:00 -05:00
|
|
|
|
|
|
|
Archive.init({
|
|
|
|
workerUrl: '/libarchive/worker-bundle.js'
|
|
|
|
})
|
|
|
|
|
|
|
|
export default {
|
|
|
|
props: {
|
2023-06-04 11:13:05 -05:00
|
|
|
url: String,
|
|
|
|
libraryItem: {
|
|
|
|
type: Object,
|
|
|
|
default: () => {}
|
2023-06-11 11:12:52 -05:00
|
|
|
},
|
|
|
|
isLocal: Boolean,
|
|
|
|
keepProgress: Boolean
|
2021-10-17 20:20:00 -05:00
|
|
|
},
|
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
loading: false,
|
|
|
|
pages: null,
|
|
|
|
filesObject: null,
|
|
|
|
mainImg: null,
|
|
|
|
page: 0,
|
|
|
|
numPages: 0,
|
|
|
|
showPageMenu: false,
|
|
|
|
showInfoMenu: false,
|
|
|
|
loadTimeout: null,
|
|
|
|
loadedFirstPage: false,
|
2023-03-06 10:58:08 -06:00
|
|
|
comicMetadata: null,
|
2023-06-11 11:12:52 -05:00
|
|
|
pageMenuWidth: 256
|
2021-10-17 20:20:00 -05:00
|
|
|
}
|
|
|
|
},
|
|
|
|
watch: {
|
|
|
|
url: {
|
|
|
|
immediate: true,
|
|
|
|
handler() {
|
|
|
|
this.extract()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
computed: {
|
2023-06-04 11:13:05 -05:00
|
|
|
userToken() {
|
|
|
|
return this.$store.getters['user/getToken']
|
|
|
|
},
|
2023-06-11 11:12:52 -05:00
|
|
|
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
|
|
|
|
},
|
2021-10-17 20:20:00 -05:00
|
|
|
comicMetadataKeys() {
|
|
|
|
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
|
|
|
},
|
|
|
|
canGoNext() {
|
2023-06-11 11:12:52 -05:00
|
|
|
return this.page < this.numPages
|
2021-10-17 20:20:00 -05:00
|
|
|
},
|
|
|
|
canGoPrev() {
|
2023-06-11 11:12:52 -05:00
|
|
|
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++
|
|
|
|
}
|
|
|
|
})
|
2023-06-17 17:34:08 -05:00
|
|
|
},
|
2023-06-19 12:37:44 -05:00
|
|
|
isPlayerOpen() {
|
|
|
|
return this.$store.getters['getIsPlayerOpen']
|
2021-10-17 20:20:00 -05:00
|
|
|
}
|
|
|
|
},
|
|
|
|
methods: {
|
2023-06-11 11:12:52 -05:00
|
|
|
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) {
|
2023-09-15 17:35:44 -05:00
|
|
|
this.$nativeHttp.patch(`/api/me/progress/${this.serverLibraryItemId}`, payload).catch((error) => {
|
2023-06-11 11:12:52 -05:00
|
|
|
console.error('ComicReader.updateProgress failed:', error)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
2023-03-06 10:58:08 -06:00
|
|
|
clickShowInfoMenu() {
|
|
|
|
this.showInfoMenu = !this.showInfoMenu
|
|
|
|
this.showPageMenu = false
|
|
|
|
},
|
|
|
|
clickShowPageMenu() {
|
2023-06-17 17:34:08 -05:00
|
|
|
if (!this.numPages) return
|
2023-03-06 10:58:08 -06:00
|
|
|
this.showPageMenu = !this.showPageMenu
|
|
|
|
this.showInfoMenu = false
|
|
|
|
},
|
2021-10-17 20:20:00 -05:00
|
|
|
next() {
|
|
|
|
if (!this.canGoNext) return
|
|
|
|
this.setPage(this.page + 1)
|
|
|
|
},
|
|
|
|
prev() {
|
|
|
|
if (!this.canGoPrev) return
|
|
|
|
this.setPage(this.page - 1)
|
|
|
|
},
|
2023-06-11 11:12:52 -05:00
|
|
|
setPage(page) {
|
|
|
|
if (page <= 0 || page > this.numPages) {
|
2021-10-17 20:20:00 -05:00
|
|
|
return
|
|
|
|
}
|
2023-03-06 10:58:08 -06:00
|
|
|
this.showPageMenu = false
|
2023-06-11 11:12:52 -05:00
|
|
|
const filename = this.pages[page - 1]
|
|
|
|
this.page = page
|
|
|
|
|
|
|
|
this.updateProgress()
|
2021-10-17 20:20:00 -05:00
|
|
|
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
|
|
|
|
|
2023-06-19 17:42:15 -05:00
|
|
|
const buff = await this.$axios.$get(this.url, {
|
2023-06-04 11:13:05 -05:00
|
|
|
responseType: 'blob',
|
|
|
|
headers: {
|
|
|
|
Authorization: `Bearer ${this.userToken}`
|
|
|
|
}
|
2021-10-17 20:20:00 -05:00
|
|
|
})
|
2023-06-19 17:42:15 -05:00
|
|
|
|
2021-10-17 20:20:00 -05:00
|
|
|
const archive = await Archive.open(buff)
|
2022-11-05 15:13:52 -05:00
|
|
|
const originalFilesObject = await archive.getFilesObject()
|
|
|
|
// to support images in subfolders we need to flatten the object
|
|
|
|
// ref: https://github.com/advplyr/audiobookshelf/issues/811
|
|
|
|
this.filesObject = this.flattenFilesObject(originalFilesObject)
|
|
|
|
console.log('Extracted files object', this.filesObject)
|
2021-10-17 20:20:00 -05:00
|
|
|
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
|
|
|
|
|
2023-06-11 11:12:52 -05:00
|
|
|
// 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()
|
|
|
|
|
2021-10-17 20:20:00 -05:00
|
|
|
if (this.pages.length) {
|
|
|
|
this.loading = false
|
2023-06-11 11:12:52 -05:00
|
|
|
|
|
|
|
const startPage = this.savedPage > 0 && this.savedPage <= this.numPages ? this.savedPage : 1
|
|
|
|
await this.setPage(startPage)
|
2021-10-17 20:20:00 -05:00
|
|
|
this.loadedFirstPage = true
|
2023-06-17 17:34:08 -05:00
|
|
|
|
|
|
|
this.$emit('loaded', {
|
|
|
|
hasMetadata: this.comicMetadata
|
|
|
|
})
|
2021-10-17 20:20:00 -05:00
|
|
|
} else {
|
|
|
|
this.$toast.error('Unable to extract pages')
|
|
|
|
this.loading = false
|
|
|
|
}
|
|
|
|
},
|
2022-11-05 15:13:52 -05:00
|
|
|
flattenFilesObject(filesObject) {
|
|
|
|
const flattenObject = (obj, prefix = '') => {
|
|
|
|
var _obj = {}
|
|
|
|
for (const key in obj) {
|
|
|
|
const newKey = prefix ? prefix + '/' + key : key
|
|
|
|
if (obj[key] instanceof CompressedFile) {
|
|
|
|
_obj[newKey] = obj[key]
|
|
|
|
} else if (!key.startsWith('_') && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
|
|
|
|
_obj = {
|
|
|
|
..._obj,
|
|
|
|
...flattenObject(obj[key], newKey)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
_obj[newKey] = obj[key]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return _obj
|
|
|
|
}
|
|
|
|
return flattenObject(filesObject)
|
|
|
|
},
|
2021-10-17 20:20:00 -05:00
|
|
|
async extractXmlFile(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))
|
2023-08-29 16:06:20 -05:00
|
|
|
var numbersinpath = basename.match(/\d+/g)
|
|
|
|
if (!numbersinpath?.length) {
|
2021-10-17 20:20:00 -05:00
|
|
|
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
|
|
|
|
}
|
|
|
|
},
|
|
|
|
mounted() {},
|
|
|
|
beforeDestroy() {}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
#comic-reader {
|
2023-03-06 10:58:08 -06:00
|
|
|
height: calc(100% - 36px);
|
|
|
|
max-height: calc(100% - 36px);
|
|
|
|
padding-top: 36px;
|
|
|
|
}
|
|
|
|
.reader-player-open #comic-reader {
|
|
|
|
height: calc(100% - 156px);
|
|
|
|
max-height: calc(100% - 156px);
|
|
|
|
padding-top: 36px;
|
2021-10-17 20:20:00 -05:00
|
|
|
}
|
2023-03-06 10:58:08 -06:00
|
|
|
|
2021-10-17 20:20:00 -05:00
|
|
|
.comicimg {
|
|
|
|
height: 100%;
|
|
|
|
margin: auto;
|
|
|
|
}
|
|
|
|
.comicwrapper {
|
|
|
|
width: 100vw;
|
|
|
|
height: 100%;
|
|
|
|
}
|
|
|
|
</style>
|