<template> <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 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 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 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> <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' }) export default { props: { url: String }, data() { return { loading: false, pages: null, filesObject: null, mainImg: null, page: 0, numPages: 0, showPageMenu: false, showInfoMenu: false, loadTimeout: null, loadedFirstPage: false, comicMetadata: null } }, watch: { url: { immediate: true, handler() { this.extract() } } }, computed: { comicMetadataKeys() { return this.comicMetadata ? Object.keys(this.comicMetadata) : [] }, canGoNext() { return this.page < this.numPages - 1 }, canGoPrev() { return this.page > 0 } }, methods: { clickOutside() { if (this.showPageMenu) this.showPageMenu = false if (this.showInfoMenu) this.showInfoMenu = false }, next() { if (!this.canGoNext) return this.setPage(this.page + 1) }, prev() { 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.url) var buff = await this.$axios.$get(this.url, { responseType: 'blob' }) const archive = await Archive.open(buff) this.filesObject = await archive.getFilesObject() 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) { this.loading = false await this.setPage(0) this.loadedFirstPage = true } else { this.$toast.error('Unable to extract pages') 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) 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 } }, mounted() {}, beforeDestroy() {} } </script> <style scoped> .pagemenu { max-height: calc(100vh - 60px); } .comicimg { height: calc(100vh - 40px); margin: auto; } .comicwrapper { width: 100vw; height: calc(100vh - 40px); margin-top: 20px; } </style>