mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-03-05 00:18:30 +01:00
change color of book read icon #105, basic .pdf reader #107, fix: cover path updating properly #102, step forward/backward from book edit modal #100, add all files tab to edit modal #99, select input auto submit on blur #98
This commit is contained in:
parent
315592efe5
commit
03963aa9a1
@ -23,7 +23,7 @@
|
|||||||
<template v-for="entity in shelf">
|
<template v-for="entity in shelf">
|
||||||
<cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" />
|
<cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" />
|
||||||
<!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> -->
|
<!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> -->
|
||||||
<cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" />
|
<cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||||
@ -138,6 +138,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
editBook(audiobook) {
|
||||||
|
var bookIds = this.entities.map((e) => e.id)
|
||||||
|
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||||
|
this.$store.commit('showEditModal', audiobook)
|
||||||
|
},
|
||||||
clickGroup(group) {
|
clickGroup(group) {
|
||||||
this.$emit('update:selectedSeries', group.name)
|
this.$emit('update:selectedSeries', group.name)
|
||||||
},
|
},
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
|
<div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
|
||||||
<div class="flex items-center -mb-2">
|
<div class="flex items-center -mb-2">
|
||||||
<template v-for="entity in shelf.books">
|
<template v-for="entity in shelf.books">
|
||||||
<cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" />
|
<cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" @edit="editBook" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,6 +53,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
editBook(audiobook) {
|
||||||
|
var bookIds = this.shelf.books.map((e) => e.id)
|
||||||
|
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||||
|
this.$store.commit('showEditModal', audiobook)
|
||||||
|
},
|
||||||
scrolled() {
|
scrolled() {
|
||||||
clearTimeout(this.scrollTimer)
|
clearTimeout(this.scrollTimer)
|
||||||
this.scrollTimer = setTimeout(() => {
|
this.scrollTimer = setTimeout(() => {
|
||||||
|
74
client/components/app/PdfReader.vue
Normal file
74
client/components/app/PdfReader.vue
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full pt-20">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center py-2 text-lg">
|
||||||
|
<p>{{ page }} / {{ numPages }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import pdf from 'vue-pdf'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
pdf
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
src: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rotate: 0,
|
||||||
|
loadedRatio: 0,
|
||||||
|
page: 1,
|
||||||
|
numPages: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pdfWidth() {
|
||||||
|
return this.pdfHeight * 0.6667
|
||||||
|
},
|
||||||
|
pdfHeight() {
|
||||||
|
return window.innerHeight - 120
|
||||||
|
},
|
||||||
|
canGoNext() {
|
||||||
|
return this.page < this.numPages
|
||||||
|
},
|
||||||
|
canGoPrev() {
|
||||||
|
return this.page > 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
numPagesLoaded(e) {
|
||||||
|
this.numPages = e
|
||||||
|
},
|
||||||
|
goPrevPage() {
|
||||||
|
if (this.page <= 1) return
|
||||||
|
this.page--
|
||||||
|
},
|
||||||
|
goNextPage() {
|
||||||
|
if (this.page >= this.numPages) return
|
||||||
|
this.page++
|
||||||
|
},
|
||||||
|
error(err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- EPUB -->
|
<!-- EPUB -->
|
||||||
<div v-if="epubEbook" class="h-full flex items-center">
|
<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">
|
<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>
|
<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>
|
||||||
@ -30,11 +30,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- MOBI/AZW3 -->
|
<!-- MOBI/AZW3 -->
|
||||||
<div v-else class="h-full max-h-full w-full">
|
<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">
|
<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>
|
<iframe title="html-viewer" width="100%"> Loading </iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- PDF -->
|
||||||
|
<div v-else-if="ebookType === 'pdf'" class="h-full flex items-center">
|
||||||
|
<app-pdf-reader :src="ebookUrl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -55,7 +61,9 @@ export default {
|
|||||||
author: '',
|
author: '',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
hasNext: true,
|
hasNext: true,
|
||||||
hasPrev: false
|
hasPrev: false,
|
||||||
|
ebookType: '',
|
||||||
|
ebookUrl: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -97,28 +105,23 @@ export default {
|
|||||||
epubEbook() {
|
epubEbook() {
|
||||||
return this.ebooks.find((eb) => eb.ext === '.epub')
|
return this.ebooks.find((eb) => eb.ext === '.epub')
|
||||||
},
|
},
|
||||||
epubPath() {
|
|
||||||
return this.epubEbook ? this.epubEbook.path : null
|
|
||||||
},
|
|
||||||
mobiEbook() {
|
mobiEbook() {
|
||||||
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
|
return this.ebooks.find((eb) => eb.ext === '.mobi' || eb.ext === '.azw3')
|
||||||
},
|
},
|
||||||
mobiPath() {
|
pdfEbook() {
|
||||||
return this.mobiEbook ? this.mobiEbook.path : null
|
return this.ebooks.find((eb) => eb.ext === '.pdf')
|
||||||
},
|
|
||||||
mobiUrl() {
|
|
||||||
if (!this.mobiPath) return null
|
|
||||||
return `/ebook/${this.libraryId}/${this.folderId}/${this.mobiPath}`
|
|
||||||
},
|
|
||||||
url() {
|
|
||||||
if (!this.epubPath) return null
|
|
||||||
return `/ebook/${this.libraryId}/${this.folderId}/${this.epubPath}`
|
|
||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
selectedAudiobookFile() {
|
||||||
|
return this.$store.state.selectedAudiobookFile
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getEbookUrl(path) {
|
||||||
|
return `/ebook/${this.libraryId}/${this.folderId}/${path}`
|
||||||
|
},
|
||||||
changedChapter() {
|
changedChapter() {
|
||||||
if (this.rendition) {
|
if (this.rendition) {
|
||||||
this.rendition.display(this.selectedChapter)
|
this.rendition.display(this.selectedChapter)
|
||||||
@ -156,11 +159,28 @@ export default {
|
|||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.registerListeners()
|
this.registerListeners()
|
||||||
|
if (this.selectedAudiobookFile) {
|
||||||
if (this.epubEbook) {
|
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.epubEbook) {
|
||||||
|
this.ebookType = 'epub'
|
||||||
|
this.ebookUrl = this.getEbookUrl(this.epubEbook.path)
|
||||||
this.initEpub()
|
this.initEpub()
|
||||||
} else if (this.mobiEbook) {
|
} else if (this.mobiEbook) {
|
||||||
|
this.ebookType = 'mobi'
|
||||||
|
this.ebookUrl = this.getEbookUrl(this.mobiEbook.path)
|
||||||
this.initMobi()
|
this.initMobi()
|
||||||
|
} else if (this.pdfEbook) {
|
||||||
|
this.ebookType = 'pdf'
|
||||||
|
this.ebookUrl = this.getEbookUrl(this.pdfEbook.path)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
addHtmlCss() {
|
addHtmlCss() {
|
||||||
@ -219,7 +239,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async initMobi() {
|
async initMobi() {
|
||||||
// Fetch mobi file as blob
|
// Fetch mobi file as blob
|
||||||
var buff = await this.$axios.$get(this.mobiUrl, {
|
var buff = await this.$axios.$get(this.ebookUrl, {
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
})
|
})
|
||||||
var reader = new FileReader()
|
var reader = new FileReader()
|
||||||
@ -251,7 +271,7 @@ export default {
|
|||||||
// Authorization: `Bearer ${this.userToken}`
|
// Authorization: `Bearer ${this.userToken}`
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
var book = ePub(this.url)
|
var book = ePub(this.ebookUrl)
|
||||||
this.book = book
|
this.book = book
|
||||||
|
|
||||||
this.rendition = book.renderTo('viewer', {
|
this.rendition = book.renderTo('viewer', {
|
||||||
|
@ -228,7 +228,8 @@ export default {
|
|||||||
this.$root.socket.emit('open_stream', this.audiobookId)
|
this.$root.socket.emit('open_stream', this.audiobookId)
|
||||||
},
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
this.$store.commit('showEditModal', this.audiobook)
|
// this.$store.commit('showEditModal', this.audiobook)
|
||||||
|
this.$emit('edit', this.audiobook)
|
||||||
},
|
},
|
||||||
clickCard(e) {
|
clickCard(e) {
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
|
@ -10,6 +10,14 @@
|
|||||||
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-show="canGoPrev" class="absolute -left-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
|
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goPrevBook" @mousedown.prevent>arrow_back_ios</div>
|
||||||
|
</div>
|
||||||
|
<div v-show="canGoNext" class="absolute -right-24 top-0 bottom-0 h-full pointer-events-none flex items-center px-6">
|
||||||
|
<div class="material-icons text-5xl text-white text-opacity-50 hover:text-opacity-90 cursor-pointer pointer-events-auto" @click.stop.prevent="goNextBook" @mousedown.prevent>arrow_forward_ios</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
|
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
|
||||||
<keep-alive>
|
<keep-alive>
|
||||||
<component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" />
|
<component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" />
|
||||||
@ -51,6 +59,11 @@ export default {
|
|||||||
title: 'Chapters',
|
title: 'Chapters',
|
||||||
component: 'modals-edit-tabs-chapters'
|
component: 'modals-edit-tabs-chapters'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'files',
|
||||||
|
title: 'Files',
|
||||||
|
component: 'modals-edit-tabs-files'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'download',
|
id: 'download',
|
||||||
title: 'Download',
|
title: 'Download',
|
||||||
@ -68,6 +81,7 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!availableTabIds.includes(this.selectedTab)) {
|
if (!availableTabIds.includes(this.selectedTab)) {
|
||||||
this.selectedTab = availableTabIds[0]
|
this.selectedTab = availableTabIds[0]
|
||||||
}
|
}
|
||||||
@ -137,9 +151,44 @@ export default {
|
|||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book.title || 'No Title'
|
return this.book.title || 'No Title'
|
||||||
|
},
|
||||||
|
bookshelfBookIds() {
|
||||||
|
return this.$store.state.bookshelfBookIds || []
|
||||||
|
},
|
||||||
|
currentBookshelfIndex() {
|
||||||
|
if (!this.bookshelfBookIds.length) return 0
|
||||||
|
return this.bookshelfBookIds.findIndex((bid) => bid === this.selectedAudiobookId)
|
||||||
|
},
|
||||||
|
canGoPrev() {
|
||||||
|
return this.bookshelfBookIds.length && this.currentBookshelfIndex > 0
|
||||||
|
},
|
||||||
|
canGoNext() {
|
||||||
|
return this.bookshelfBookIds.length && this.currentBookshelfIndex < this.bookshelfBookIds.length - 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
goPrevBook() {
|
||||||
|
if (this.currentBookshelfIndex - 1 < 0) return
|
||||||
|
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
||||||
|
var prevBook = this.$store.getters['audiobooks/getAudiobook'](prevBookId)
|
||||||
|
if (prevBook) {
|
||||||
|
this.$store.commit('showEditModalOnTab', { audiobook: prevBook, tab: this.selectedTab })
|
||||||
|
this.$nextTick(this.init)
|
||||||
|
} else {
|
||||||
|
console.error('Book not found', prevBookId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goNextBook() {
|
||||||
|
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length) return
|
||||||
|
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
||||||
|
var nextBook = this.$store.getters['audiobooks/getAudiobook'](nextBookId)
|
||||||
|
if (nextBook) {
|
||||||
|
this.$store.commit('showEditModalOnTab', { audiobook: nextBook, tab: this.selectedTab })
|
||||||
|
this.$nextTick(this.init)
|
||||||
|
} else {
|
||||||
|
console.error('Book not found', nextBookId)
|
||||||
|
}
|
||||||
|
},
|
||||||
selectTab(tab) {
|
selectTab(tab) {
|
||||||
this.selectedTab = tab
|
this.selectedTab = tab
|
||||||
},
|
},
|
||||||
@ -155,9 +204,12 @@ export default {
|
|||||||
},
|
},
|
||||||
async fetchFull() {
|
async fetchFull() {
|
||||||
try {
|
try {
|
||||||
|
this.processing = true
|
||||||
this.audiobook = await this.$axios.$get(`/api/audiobook/${this.selectedAudiobookId}`)
|
this.audiobook = await this.$axios.$get(`/api/audiobook/${this.selectedAudiobookId}`)
|
||||||
|
this.processing = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch audiobook', this.selectedAudiobookId, error)
|
console.error('Failed to fetch audiobook', this.selectedAudiobookId, error)
|
||||||
|
this.processing = false
|
||||||
this.show = false
|
this.show = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
||||||
<template v-for="cover in localCovers">
|
<template v-for="cover in localCovers">
|
||||||
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover.localPath)">
|
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
||||||
<div class="h-24 bg-primary" style="width: 60px">
|
<div class="h-24 bg-primary" style="width: 60px">
|
||||||
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
|
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
@ -265,8 +265,24 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
this.hasSearched = true
|
this.hasSearched = true
|
||||||
},
|
},
|
||||||
setCover(cover) {
|
setCover(coverFile) {
|
||||||
this.updateCover(cover)
|
this.isProcessing = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/audiobook/${this.audiobook.id}/coverfile`, coverFile)
|
||||||
|
.then((data) => {
|
||||||
|
console.log('response data', data)
|
||||||
|
if (data && typeof data === 'string') {
|
||||||
|
this.$toast.success(data)
|
||||||
|
}
|
||||||
|
this.isProcessing = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update', error)
|
||||||
|
if (error.response && error.response.data) {
|
||||||
|
this.$toast.error(error.response.data)
|
||||||
|
}
|
||||||
|
this.isProcessing = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,8 +214,6 @@ export default {
|
|||||||
this.details.volumeNumber = this.book.volumeNumber
|
this.details.volumeNumber = this.book.volumeNumber
|
||||||
this.details.publishYear = this.book.publishYear
|
this.details.publishYear = this.book.publishYear
|
||||||
|
|
||||||
console.log('INIT', this.details)
|
|
||||||
|
|
||||||
this.newTags = this.audiobook.tags || []
|
this.newTags = this.audiobook.tags || []
|
||||||
},
|
},
|
||||||
resetProgress() {
|
resetProgress() {
|
||||||
|
21
client/components/modals/edit-tabs/Files.vue
Normal file
21
client/components/modals/edit-tabs/Files.vue
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
||||||
|
<tables-all-files-table :audiobook="audiobook" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
audiobook: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -53,7 +53,6 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
tracks: null,
|
tracks: null,
|
||||||
audioFiles: null,
|
|
||||||
showFullPath: false
|
showFullPath: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -104,7 +103,6 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
this.audioFiles = this.audiobook.audioFiles
|
|
||||||
this.tracks = this.audiobook.tracks
|
this.tracks = this.audiobook.tracks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
109
client/components/tables/AllFilesTable.vue
Normal file
109
client/components/tables/AllFilesTable.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full my-2">
|
||||||
|
<div class="w-full bg-primary px-6 py-2 flex items-center cursor-pointer">
|
||||||
|
<p class="pr-4">Files</p>
|
||||||
|
<span class="bg-black-400 rounded-xl py-1 px-2 text-sm font-mono">{{ allFiles.length }}</span>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<ui-btn small :color="showFullPath ? 'gray-600' : 'primary'" @click.stop="showFullPath = !showFullPath">Full Path</ui-btn>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<table class="text-sm tracksTable">
|
||||||
|
<tr class="font-book">
|
||||||
|
<th class="text-left px-4">Path</th>
|
||||||
|
<th class="text-left px-4 w-24">Filetype</th>
|
||||||
|
<th v-if="userCanDownload" class="text-center w-20">Download</th>
|
||||||
|
</tr>
|
||||||
|
<template v-for="file in allFiles">
|
||||||
|
<tr :key="file.path">
|
||||||
|
<td class="font-book pl-2">
|
||||||
|
{{ showFullPath ? file.fullPath : file.path }}
|
||||||
|
</td>
|
||||||
|
<td class="text-xs">
|
||||||
|
<p>{{ file.filetype }}</p>
|
||||||
|
</td>
|
||||||
|
<td v-if="userCanDownload" class="text-center">
|
||||||
|
<a :href="`/s/book/${audiobookId}/${file.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
audiobook: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showFullPath: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
audiobookId() {
|
||||||
|
return this.audiobook.id
|
||||||
|
},
|
||||||
|
audiobookPath() {
|
||||||
|
return this.audiobook.path
|
||||||
|
},
|
||||||
|
userCanDownload() {
|
||||||
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
|
},
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.audiobook.isMissing
|
||||||
|
},
|
||||||
|
showDownload() {
|
||||||
|
return this.userCanDownload && !this.isMissing
|
||||||
|
},
|
||||||
|
otherFiles() {
|
||||||
|
return this.audiobook.otherFiles || []
|
||||||
|
},
|
||||||
|
audioFiles() {
|
||||||
|
return this.audiobook.audioFiles || []
|
||||||
|
},
|
||||||
|
audioFilesCleaned() {
|
||||||
|
return this.audioFiles.map((af) => {
|
||||||
|
return {
|
||||||
|
path: af.path,
|
||||||
|
fullPath: af.fullPath,
|
||||||
|
relativePath: this.getRelativePath(af.path),
|
||||||
|
filetype: 'audio'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
otherFilesCleaned() {
|
||||||
|
return this.otherFiles.map((af) => {
|
||||||
|
return {
|
||||||
|
path: af.path,
|
||||||
|
fullPath: af.fullPath,
|
||||||
|
relativePath: this.getRelativePath(af.path),
|
||||||
|
filetype: af.filetype
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
allFiles() {
|
||||||
|
return this.audioFilesCleaned.concat(this.otherFilesCleaned)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getRelativePath(path) {
|
||||||
|
var filePath = path.replace(/\\/g, '/')
|
||||||
|
var audiobookPath = this.audiobookPath.replace(/\\/g, '/')
|
||||||
|
return filePath
|
||||||
|
.replace(audiobookPath + '/', '')
|
||||||
|
.replace(/%/g, '%25')
|
||||||
|
.replace(/#/g, '%23')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -20,7 +20,7 @@
|
|||||||
<tr class="font-book">
|
<tr class="font-book">
|
||||||
<th class="text-left px-4">Path</th>
|
<th class="text-left px-4">Path</th>
|
||||||
<th class="text-left px-4 w-24">Filetype</th>
|
<th class="text-left px-4 w-24">Filetype</th>
|
||||||
<th v-if="userCanDownload" class="text-center w-20">Download</th>
|
<th v-if="userCanDownload && !isMissing" class="text-center w-20">Download</th>
|
||||||
</tr>
|
</tr>
|
||||||
<template v-for="file in otherFilesCleaned">
|
<template v-for="file in otherFilesCleaned">
|
||||||
<tr :key="file.path">
|
<tr :key="file.path">
|
||||||
@ -28,9 +28,12 @@
|
|||||||
{{ showFullPath ? file.fullPath : file.path }}
|
{{ showFullPath ? file.fullPath : file.path }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-xs">
|
<td class="text-xs">
|
||||||
<p>{{ file.filetype }}</p>
|
<div class="flex items-center">
|
||||||
|
<span v-if="file.filetype === 'ebook'" class="material-icons text-base mr-1 cursor-pointer text-white text-opacity-60 hover:text-opacity-100" @click="readEbookClick(file)">auto_stories </span>
|
||||||
|
<p>{{ file.filetype }}</p>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td v-if="userCanDownload" class="text-center">
|
<td v-if="userCanDownload && !isMissing" class="text-center">
|
||||||
<a :href="`/s/book/${audiobookId}/${file.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
<a :href="`/s/book/${audiobookId}/${file.relativePath}?token=${userToken}`" download><span class="material-icons icon-text">download</span></a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -83,11 +86,17 @@ export default {
|
|||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
|
isMissing() {
|
||||||
|
return this.audiobook.isMissing
|
||||||
|
},
|
||||||
userCanDownload() {
|
userCanDownload() {
|
||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
readEbookClick(file) {
|
||||||
|
this.$store.commit('showEReaderForFile', { audiobook: this.audiobook, file })
|
||||||
|
},
|
||||||
clickBar() {
|
clickBar() {
|
||||||
this.showFiles = !this.showFiles
|
this.showFiles = !this.showFiles
|
||||||
}
|
}
|
||||||
|
@ -116,9 +116,6 @@ export default {
|
|||||||
this.textInput = null
|
this.textInput = null
|
||||||
this.currentSearch = null
|
this.currentSearch = null
|
||||||
this.input = item
|
this.input = item
|
||||||
|
|
||||||
// this.input = this.textInput ? this.textInput.trim() : null
|
|
||||||
console.log('Clicked option', item)
|
|
||||||
if (this.$refs.input) this.$refs.input.blur()
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -127,6 +127,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isFocused = false
|
this.isFocused = false
|
||||||
|
if (this.textInput) this.submitForm()
|
||||||
}, 50)
|
}, 50)
|
||||||
},
|
},
|
||||||
focus() {
|
focus() {
|
||||||
@ -145,6 +146,7 @@ export default {
|
|||||||
var newSelected = null
|
var newSelected = null
|
||||||
if (this.selected.includes(itemValue)) {
|
if (this.selected.includes(itemValue)) {
|
||||||
newSelected = this.selected.filter((s) => s !== itemValue)
|
newSelected = this.selected.filter((s) => s !== itemValue)
|
||||||
|
this.$emit('removedItem', itemValue)
|
||||||
} else {
|
} else {
|
||||||
newSelected = this.selected.concat([itemValue])
|
newSelected = this.selected.concat([itemValue])
|
||||||
}
|
}
|
||||||
@ -164,6 +166,7 @@ export default {
|
|||||||
removeItem(item) {
|
removeItem(item) {
|
||||||
var remaining = this.selected.filter((i) => i !== item)
|
var remaining = this.selected.filter((i) => i !== item)
|
||||||
this.$emit('input', remaining)
|
this.$emit('input', remaining)
|
||||||
|
this.$emit('removedItem', item)
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalcMenuPos()
|
this.recalcMenuPos()
|
||||||
})
|
})
|
||||||
@ -171,6 +174,7 @@ export default {
|
|||||||
insertNewItem(item) {
|
insertNewItem(item) {
|
||||||
this.selected.push(item)
|
this.selected.push(item)
|
||||||
this.$emit('input', this.selected)
|
this.$emit('input', this.selected)
|
||||||
|
this.$emit('newItem', item)
|
||||||
this.textInput = null
|
this.textInput = null
|
||||||
this.currentSearch = null
|
this.currentSearch = null
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="icon-btn rounded-md bg-primary border border-gray-600 flex items-center justify-center h-9 w-9 relative" @click="clickBtn">
|
<button class="icon-btn rounded-md bg-primary border border-gray-600 flex items-center justify-center h-9 w-9 relative" @click="clickBtn">
|
||||||
<div class="w-5 h-5 text-white relative">
|
<div class="w-5 h-5 text-white relative">
|
||||||
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
||||||
</svg>
|
</svg>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
81
client/package-lock.json
generated
81
client/package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.4.6",
|
"version": "1.4.8",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -3415,6 +3415,11 @@
|
|||||||
"@babel/helper-define-polyfill-provider": "^0.2.2"
|
"@babel/helper-define-polyfill-provider": "^0.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"babel-plugin-syntax-dynamic-import": {
|
||||||
|
"version": "6.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-dynamic-import/-/babel-plugin-syntax-dynamic-import-6.18.0.tgz",
|
||||||
|
"integrity": "sha1-jWomIpyDdFqZgqRBBRVyyqF5sdo="
|
||||||
|
},
|
||||||
"backo2": {
|
"backo2": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
|
||||||
@ -8494,6 +8499,11 @@
|
|||||||
"sha.js": "^2.4.8"
|
"sha.js": "^2.4.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pdfjs-dist": {
|
||||||
|
"version": "2.6.347",
|
||||||
|
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.6.347.tgz",
|
||||||
|
"integrity": "sha512-QC+h7hG2su9v/nU1wEI3SnpPIrqJODL7GTDFvR74ANKGq1AFJW16PH8VWnhpiTi9YcLSFV9xLeWSgq+ckHLdVQ=="
|
||||||
|
},
|
||||||
"picomatch": {
|
"picomatch": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
|
||||||
@ -11239,6 +11249,37 @@
|
|||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||||
},
|
},
|
||||||
|
"raw-loader": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==",
|
||||||
|
"requires": {
|
||||||
|
"loader-utils": "^2.0.0",
|
||||||
|
"schema-utils": "^3.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"loader-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||||
|
"requires": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema-utils": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
||||||
|
"requires": {
|
||||||
|
"@types/json-schema": "^7.0.8",
|
||||||
|
"ajv": "^6.12.5",
|
||||||
|
"ajv-keywords": "^3.5.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"rc9": {
|
"rc9": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/rc9/-/rc9-1.2.0.tgz",
|
||||||
@ -13314,6 +13355,24 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
||||||
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
||||||
},
|
},
|
||||||
|
"vue-pdf": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zd3lJj6CbtrawgaaDDciTDjkJMUKiLWtbEmBg5CvFn9Noe9oAO/GNy/fc5c59qGuFCJ14ibIV1baw4S07e5bSQ==",
|
||||||
|
"requires": {
|
||||||
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
|
"loader-utils": "^1.4.0",
|
||||||
|
"pdfjs-dist": "2.6.347",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
|
"vue-resize-sensor": "^2.0.0",
|
||||||
|
"worker-loader": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vue-resize-sensor": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-W+y2EAI/BxS4Vlcca9scQv8ifeBFck56DRtSwWJ2H4Cw1GLNUYxiZxUHHkuzuI5JPW/cYtL1bPO5xPyEXx4LmQ=="
|
||||||
|
},
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.2.tgz",
|
||||||
@ -14181,6 +14240,26 @@
|
|||||||
"errno": "~0.1.7"
|
"errno": "~0.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"worker-loader": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/worker-loader/-/worker-loader-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==",
|
||||||
|
"requires": {
|
||||||
|
"loader-utils": "^1.0.0",
|
||||||
|
"schema-utils": "^0.4.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"schema-utils": {
|
||||||
|
"version": "0.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
|
||||||
|
"integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==",
|
||||||
|
"requires": {
|
||||||
|
"ajv": "^6.1.0",
|
||||||
|
"ajv-keywords": "^3.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"wrap-ansi": {
|
"wrap-ansi": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.4.8",
|
"version": "1.4.9",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -21,6 +21,7 @@
|
|||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
"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-toastification": "^1.7.11",
|
"vue-toastification": "^1.7.11",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^2.24.3"
|
||||||
},
|
},
|
||||||
|
@ -411,6 +411,7 @@ export default {
|
|||||||
this.$root.socket.emit('open_stream', this.audiobook.id)
|
this.$root.socket.emit('open_stream', this.audiobook.id)
|
||||||
},
|
},
|
||||||
editClick() {
|
editClick() {
|
||||||
|
this.$store.commit('setBookshelfBookIds', [])
|
||||||
this.$store.commit('showEditModal', this.audiobook)
|
this.$store.commit('showEditModal', this.audiobook)
|
||||||
},
|
},
|
||||||
lookupMetadata(index) {
|
lookupMetadata(index) {
|
||||||
|
@ -33,10 +33,10 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-1/2 px-1">
|
||||||
<ui-multi-select v-model="audiobook.book.genres" label="Genres" :items="genres" />
|
<ui-multi-select v-model="audiobook.book.genres" label="Genres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-multi-select v-model="audiobook.tags" label="Tags" :items="tags" />
|
<ui-multi-select v-model="audiobook.tags" label="Tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -76,7 +76,9 @@ export default {
|
|||||||
isProcessing: false,
|
isProcessing: false,
|
||||||
audiobookCopies: [],
|
audiobookCopies: [],
|
||||||
isScrollable: false,
|
isScrollable: false,
|
||||||
newSeriesItems: []
|
newSeriesItems: [],
|
||||||
|
newTagItems: [],
|
||||||
|
newGenreItems: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -86,9 +88,15 @@ export default {
|
|||||||
genres() {
|
genres() {
|
||||||
return this.$store.state.audiobooks.genres
|
return this.$store.state.audiobooks.genres
|
||||||
},
|
},
|
||||||
|
genreItems() {
|
||||||
|
return this.genres.concat(this.newGenreItems)
|
||||||
|
},
|
||||||
tags() {
|
tags() {
|
||||||
return this.$store.state.audiobooks.tags
|
return this.$store.state.audiobooks.tags
|
||||||
},
|
},
|
||||||
|
tagItems() {
|
||||||
|
return this.tags.concat(this.newTagItems)
|
||||||
|
},
|
||||||
series() {
|
series() {
|
||||||
return this.$store.state.audiobooks.series
|
return this.$store.state.audiobooks.series
|
||||||
},
|
},
|
||||||
@ -100,9 +108,42 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
newTagItem(item) {
|
||||||
|
if (item && !this.newTagItems.includes(item)) {
|
||||||
|
this.newTagItems.push(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removedTagItem(item) {
|
||||||
|
// If newly added, remove if not used on any other audiobooks
|
||||||
|
if (item && this.newTagItems.includes(item)) {
|
||||||
|
var usedByOtherAb = this.audiobookCopies.find((ab) => {
|
||||||
|
return ab.tags && ab.tags.includes(item)
|
||||||
|
})
|
||||||
|
if (!usedByOtherAb) {
|
||||||
|
this.newTagItems = this.newTagItems.filter((t) => t !== item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
newGenreItem(item) {
|
||||||
|
if (item && !this.newGenreItems.includes(item)) {
|
||||||
|
this.newGenreItems.push(item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removedGenreItem(item) {
|
||||||
|
// If newly added, remove if not used on any other audiobooks
|
||||||
|
if (item && this.newGenreItems.includes(item)) {
|
||||||
|
var usedByOtherAb = this.audiobookCopies.find((ab) => {
|
||||||
|
return ab.book.genres && ab.book.genres.includes(item)
|
||||||
|
})
|
||||||
|
if (!usedByOtherAb) {
|
||||||
|
this.newGenreItems = this.newGenreItems.filter((t) => t !== item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
newSeriesItem(item) {
|
newSeriesItem(item) {
|
||||||
if (!item) return
|
if (item && !this.newSeriesItems.includes(item)) {
|
||||||
this.newSeriesItems.push(item)
|
this.newSeriesItems.push(item)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
seriesChanged() {
|
seriesChanged() {
|
||||||
this.newSeriesItems = this.newSeriesItems.filter((item) => {
|
this.newSeriesItems = this.newSeriesItems.filter((item) => {
|
||||||
|
@ -21,9 +21,7 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
asyncData({ redirect, store }) {
|
asyncData({ redirect, store }) {
|
||||||
var currentLibraryId = store.state.libraries.currentLibraryId
|
redirect(`/library/${store.state.libraries.currentLibraryId}`)
|
||||||
console.log('Redir', currentLibraryId)
|
|
||||||
redirect(`/library/${currentLibraryId}`)
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
@ -31,6 +31,7 @@ export default {
|
|||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
store.commit('audiobooks/setSearchResults', searchResults)
|
store.commit('audiobooks/setSearchResults', searchResults)
|
||||||
|
if (searchResults.length) searchResults.forEach((ab) => store.commit('audiobooks/addUpdate', ab))
|
||||||
}
|
}
|
||||||
var selectedSeries = query.series ? app.$decode(query.series) : null
|
var selectedSeries = query.series ? app.$decode(query.series) : null
|
||||||
store.commit('audiobooks/setSelectedSeries', selectedSeries)
|
store.commit('audiobooks/setSelectedSeries', selectedSeries)
|
||||||
|
@ -9,6 +9,7 @@ export const state = () => ({
|
|||||||
showEditModal: false,
|
showEditModal: false,
|
||||||
showEReader: false,
|
showEReader: false,
|
||||||
selectedAudiobook: null,
|
selectedAudiobook: null,
|
||||||
|
selectedAudiobookFile: null,
|
||||||
playOnLoad: false,
|
playOnLoad: false,
|
||||||
developerMode: false,
|
developerMode: false,
|
||||||
selectedAudiobooks: [],
|
selectedAudiobooks: [],
|
||||||
@ -16,7 +17,8 @@ export const state = () => ({
|
|||||||
previousPath: '/',
|
previousPath: '/',
|
||||||
routeHistory: [],
|
routeHistory: [],
|
||||||
showExperimentalFeatures: false,
|
showExperimentalFeatures: false,
|
||||||
backups: []
|
backups: [],
|
||||||
|
bookshelfBookIds: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@ -66,6 +68,9 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setBookshelfBookIds(state, val) {
|
||||||
|
state.bookshelfBookIds = val || []
|
||||||
|
},
|
||||||
setRouteHistory(state, val) {
|
setRouteHistory(state, val) {
|
||||||
state.routeHistory = val
|
state.routeHistory = val
|
||||||
},
|
},
|
||||||
@ -113,7 +118,15 @@ export const mutations = {
|
|||||||
state.showEditModal = val
|
state.showEditModal = val
|
||||||
},
|
},
|
||||||
showEReader(state, audiobook) {
|
showEReader(state, audiobook) {
|
||||||
|
state.selectedAudiobookFile = null
|
||||||
state.selectedAudiobook = audiobook
|
state.selectedAudiobook = audiobook
|
||||||
|
|
||||||
|
state.showEReader = true
|
||||||
|
},
|
||||||
|
showEReaderForFile(state, { audiobook, file }) {
|
||||||
|
state.selectedAudiobookFile = file
|
||||||
|
state.selectedAudiobook = audiobook
|
||||||
|
|
||||||
state.showEReader = true
|
state.showEReader = true
|
||||||
},
|
},
|
||||||
setShowEReader(state, val) {
|
setShowEReader(state, val) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.4.8",
|
"version": "1.4.9",
|
||||||
"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": {
|
||||||
|
@ -47,6 +47,7 @@ class ApiController {
|
|||||||
this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this))
|
this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this))
|
||||||
this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this))
|
this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this))
|
||||||
this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this))
|
this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this))
|
||||||
|
this.router.patch('/audiobook/:id/coverfile', this.updateAudiobookCoverFromFile.bind(this))
|
||||||
this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this))
|
this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this))
|
||||||
|
|
||||||
this.router.patch('/match/:id', this.match.bind(this))
|
this.router.patch('/match/:id', this.match.bind(this))
|
||||||
@ -445,6 +446,26 @@ class ApiController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateAudiobookCoverFromFile(req, res) {
|
||||||
|
if (!req.user.canUpdate) {
|
||||||
|
Logger.warn('User attempted to update without permission', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
|
||||||
|
if (!audiobook) return res.sendStatus(404)
|
||||||
|
|
||||||
|
var coverFile = req.body
|
||||||
|
var updated = await audiobook.setCoverFromFile(coverFile)
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
await this.db.updateAudiobook(audiobook)
|
||||||
|
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) res.status(200).send('Cover updated successfully')
|
||||||
|
else res.status(200).send('No update was made to cover')
|
||||||
|
}
|
||||||
|
|
||||||
async updateAudiobook(req, res) {
|
async updateAudiobook(req, res) {
|
||||||
if (!req.user.canUpdate) {
|
if (!req.user.canUpdate) {
|
||||||
Logger.warn('User attempted to update without permission', req.user)
|
Logger.warn('User attempted to update without permission', req.user)
|
||||||
|
@ -241,10 +241,13 @@ class DownloadManager {
|
|||||||
|
|
||||||
if (shouldIncludeCover) {
|
if (shouldIncludeCover) {
|
||||||
var _cover = audiobook.book.coverFullPath
|
var _cover = audiobook.book.coverFullPath
|
||||||
|
|
||||||
|
// Supporting old local file prefix
|
||||||
if (!_cover && audiobook.book.cover && audiobook.book.cover.startsWith(Path.sep + 'local')) {
|
if (!_cover && audiobook.book.cover && audiobook.book.cover.startsWith(Path.sep + 'local')) {
|
||||||
_cover = Path.join(this.AudiobookPath, _cover.replace(Path.sep + 'local', ''))
|
_cover = Path.join(this.AudiobookPath, _cover.replace(Path.sep + 'local', ''))
|
||||||
Logger.debug('Local cover url', _cover)
|
Logger.debug('Local cover url', _cover)
|
||||||
}
|
}
|
||||||
|
|
||||||
ffmpegInputs.push({
|
ffmpegInputs.push({
|
||||||
input: _cover,
|
input: _cover,
|
||||||
options: ['-f image2pipe']
|
options: ['-f image2pipe']
|
||||||
|
@ -128,11 +128,15 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get hasEpub() {
|
get hasEpub() {
|
||||||
return this.otherFiles.find(file => file.ext === '.epub')
|
return this.ebooks.find(file => file.ext === '.epub')
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasMobi() {
|
get hasMobi() {
|
||||||
return this.otherFiles.find(file => file.ext === '.mobi' || file.ext === '.azw3')
|
return this.ebooks.find(file => file.ext === '.mobi' || file.ext === '.azw3')
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasPdf() {
|
||||||
|
return this.ebooks.find(file => file.ext === '.pdf')
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasMissingIno() {
|
get hasMissingIno() {
|
||||||
@ -206,7 +210,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) ? 1 : 0, // Only supporting epubs in the reader currently
|
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf) ? 1 : 0, // Only supporting epubs in the reader currently
|
||||||
numTracks: this.tracks.length,
|
numTracks: this.tracks.length,
|
||||||
chapters: this.chapters || [],
|
chapters: this.chapters || [],
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
@ -233,7 +237,8 @@ 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 ? 1 : 0,
|
numEbooks: (this.hasEpub || this.hasMobi || this.hasPdf) ? 1 : 0,
|
||||||
|
numTracks: this.tracks.length,
|
||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
book: this.bookToJSON(),
|
book: this.bookToJSON(),
|
||||||
tracks: this.tracksToJSON(),
|
tracks: this.tracksToJSON(),
|
||||||
@ -363,6 +368,19 @@ class Audiobook {
|
|||||||
this.book.setData(data)
|
this.book.setData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCoverFromFile(file) {
|
||||||
|
if (!file || !file.fullPath || !file.path) {
|
||||||
|
Logger.error(`[Audiobook] "${this.title}" Invalid file for setCoverFromFile`, file)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var updateBookPayload = {}
|
||||||
|
updateBookPayload.coverFullPath = Path.normalize(file.fullPath)
|
||||||
|
// Set ab local static path from file relative path
|
||||||
|
var relImagePath = file.path.replace(this.path, '')
|
||||||
|
updateBookPayload.cover = Path.normalize(Path.join(`/s/book/${this.id}`, relImagePath))
|
||||||
|
return this.book.update(updateBookPayload)
|
||||||
|
}
|
||||||
|
|
||||||
addTrack(trackData) {
|
addTrack(trackData) {
|
||||||
var track = new AudioTrack()
|
var track = new AudioTrack()
|
||||||
track.setData(trackData)
|
track.setData(trackData)
|
||||||
|
@ -132,12 +132,18 @@ class Book {
|
|||||||
update(payload) {
|
update(payload) {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
|
||||||
|
// Normalize cover paths if passed
|
||||||
if (payload.cover) {
|
if (payload.cover) {
|
||||||
// If updating to local cover then normalize path
|
|
||||||
if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) {
|
if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) {
|
||||||
payload.cover = Path.normalize(payload.cover)
|
payload.cover = Path.normalize(payload.cover)
|
||||||
if (payload.coverFullPath) payload.coverFullPath = Path.normalize(payload.coverFullPath)
|
if (payload.coverFullPath) payload.coverFullPath = Path.normalize(payload.coverFullPath)
|
||||||
|
else {
|
||||||
|
Logger.warn(`[Book] "${this.title}" updating book cover to "${payload.cover}" but no full path was passed`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else if (payload.coverFullPath) {
|
||||||
|
Logger.warn(`[Book] "${this.title}" updating book full cover path to "${payload.coverFullPath}" but no relative path was passed`)
|
||||||
|
payload.coverFullPath = Path.normalize(payload.coverFullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
|
Loading…
Reference in New Issue
Block a user