mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-24 04:35:59 +02:00
New data model updates for bookshelf, covers, cards
This commit is contained in:
parent
84bab5de1b
commit
03312390cb
16 changed files with 1094 additions and 454 deletions
|
@ -450,18 +450,6 @@ export default {
|
|||
this.$refs.audioPlayer.setPlaybackSpeed(this.playbackSpeed)
|
||||
}
|
||||
},
|
||||
streamUpdated(type, data) {
|
||||
if (type === 'download') {
|
||||
if (data) {
|
||||
this.download = { ...data }
|
||||
if (this.audioPlayerReady) {
|
||||
this.playDownload()
|
||||
}
|
||||
} else if (this.download) {
|
||||
this.cancelStream()
|
||||
}
|
||||
}
|
||||
},
|
||||
setListeners() {
|
||||
if (!this.$server.socket) {
|
||||
console.error('Invalid server socket not set')
|
||||
|
@ -481,6 +469,16 @@ export default {
|
|||
this.$refs.audioPlayer.terminateStream()
|
||||
}
|
||||
}
|
||||
},
|
||||
async playLibraryItem(libraryItemId) {
|
||||
var libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
||||
console.error('Failed to fetch full item', error)
|
||||
return null
|
||||
})
|
||||
if (!libraryItem) return
|
||||
this.$store.commit('setLibraryItemStream', libraryItem)
|
||||
|
||||
// TODO: Call load library item in native
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
@ -491,9 +489,9 @@ export default {
|
|||
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||
|
||||
this.setListeners()
|
||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$on('close_stream', this.closeStreamOnly)
|
||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||
this.$store.commit('setStreamListener', this.streamUpdated)
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
||||
|
@ -506,10 +504,9 @@ export default {
|
|||
this.$server.socket.off('stream_ready', this.streamReady)
|
||||
this.$server.socket.off('stream_reset', this.streamReset)
|
||||
}
|
||||
|
||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||
this.$eventBus.$off('close_stream', this.closeStreamOnly)
|
||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||
this.$store.commit('removeStreamListener')
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -144,19 +144,29 @@ export default {
|
|||
if (!this.initialized) {
|
||||
this.currentSFQueryString = this.buildSearchParams()
|
||||
}
|
||||
var entityPath = this.entityName === 'books' ? `books/all` : this.entityName
|
||||
// var entityPath = this.entityName === 'books' ? `books/all` : this.entityName
|
||||
// var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
// var queryString = `?${sfQueryString}&limit=${this.booksPerFetch}&page=${page}`
|
||||
|
||||
// if (this.entityName === 'series-books') {
|
||||
// entityPath = `series/${this.seriesId}`
|
||||
// queryString = ''
|
||||
// }
|
||||
|
||||
// var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${queryString}`).catch((error) => {
|
||||
// console.error('failed to fetch books', error)
|
||||
// return null
|
||||
// })
|
||||
|
||||
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
var queryString = `?${sfQueryString}&limit=${this.booksPerFetch}&page=${page}`
|
||||
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||
|
||||
if (this.entityName === 'series-books') {
|
||||
entityPath = `series/${this.seriesId}`
|
||||
queryString = ''
|
||||
}
|
||||
|
||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${queryString}`).catch((error) => {
|
||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||
console.error('failed to fetch books', error)
|
||||
return null
|
||||
})
|
||||
|
||||
this.isFetchingEntities = false
|
||||
if (this.pendingReset) {
|
||||
this.pendingReset = false
|
||||
|
@ -390,42 +400,42 @@ export default {
|
|||
this.resetEntities()
|
||||
}
|
||||
},
|
||||
audiobookAdded(audiobook) {
|
||||
console.log('Audiobook added', audiobook)
|
||||
// TODO: Check if audiobook would be on this shelf
|
||||
libraryItemAdded(libraryItem) {
|
||||
console.log('libraryItem added', libraryItem)
|
||||
// TODO: Check if item would be on this shelf
|
||||
this.resetEntities()
|
||||
},
|
||||
audiobookUpdated(audiobook) {
|
||||
console.log('Audiobook updated', audiobook)
|
||||
libraryItemUpdated(libraryItem) {
|
||||
console.log('Item updated', libraryItem)
|
||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === audiobook.id)
|
||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||
if (indexOf >= 0) {
|
||||
this.entities[indexOf] = audiobook
|
||||
this.entities[indexOf] = libraryItem
|
||||
if (this.entityComponentRefs[indexOf]) {
|
||||
this.entityComponentRefs[indexOf].setEntity(audiobook)
|
||||
this.entityComponentRefs[indexOf].setEntity(libraryItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
audiobookRemoved(audiobook) {
|
||||
libraryItemRemoved(libraryItem) {
|
||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === audiobook.id)
|
||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||
if (indexOf >= 0) {
|
||||
this.entities = this.entities.filter((ent) => ent.id !== audiobook.id)
|
||||
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
|
||||
this.totalEntities = this.entities.length
|
||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||
this.remountEntities()
|
||||
this.executeRebuild()
|
||||
}
|
||||
}
|
||||
},
|
||||
audiobooksAdded(audiobooks) {
|
||||
console.log('audiobooks added', audiobooks)
|
||||
// TODO: Check if audiobook would be on this shelf
|
||||
libraryItemsAdded(libraryItems) {
|
||||
console.log('items added', libraryItems)
|
||||
// TODO: Check if item would be on this shelf
|
||||
this.resetEntities()
|
||||
},
|
||||
audiobooksUpdated(audiobooks) {
|
||||
audiobooks.forEach((ab) => {
|
||||
this.audiobookUpdated(ab)
|
||||
libraryItemsUpdated(libraryItems) {
|
||||
libraryItems.forEach((ab) => {
|
||||
this.libraryItemUpdated(ab)
|
||||
})
|
||||
},
|
||||
initListeners() {
|
||||
|
@ -433,19 +443,17 @@ export default {
|
|||
if (bookshelf) {
|
||||
bookshelf.addEventListener('scroll', this.scroll)
|
||||
}
|
||||
// this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
// this.$eventBus.$on('bookshelf-select-all', this.selectAllEntities)
|
||||
// this.$eventBus.$on('bookshelf-keyword-filter', this.updateKeywordFilter)
|
||||
|
||||
this.$eventBus.$on('library-changed', this.libraryChanged)
|
||||
this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
|
||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
||||
|
||||
if (this.$server.socket) {
|
||||
this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
|
||||
this.$server.socket.on('audiobook_added', this.audiobookAdded)
|
||||
this.$server.socket.on('audiobook_removed', this.audiobookRemoved)
|
||||
this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
|
||||
this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
|
||||
this.$server.socket.on('item_updated', this.libraryItemUpdated)
|
||||
this.$server.socket.on('item_added', this.libraryItemAdded)
|
||||
this.$server.socket.on('item_removed', this.libraryItemRemoved)
|
||||
this.$server.socket.on('items_updated', this.libraryItemsUpdated)
|
||||
this.$server.socket.on('items_added', this.libraryItemsAdded)
|
||||
} else {
|
||||
console.error('Bookshelf - Socket not initialized')
|
||||
}
|
||||
|
@ -455,16 +463,17 @@ export default {
|
|||
if (bookshelf) {
|
||||
bookshelf.removeEventListener('scroll', this.scroll)
|
||||
}
|
||||
|
||||
this.$eventBus.$off('library-changed', this.libraryChanged)
|
||||
this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
|
||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
||||
|
||||
if (this.$server.socket) {
|
||||
this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
|
||||
this.$server.socket.off('audiobook_added', this.audiobookAdded)
|
||||
this.$server.socket.off('audiobook_removed', this.audiobookRemoved)
|
||||
this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated)
|
||||
this.$server.socket.off('audiobooks_added', this.audiobooksAdded)
|
||||
this.$server.socket.off('item_updated', this.libraryItemUpdated)
|
||||
this.$server.socket.off('item_added', this.libraryItemAdded)
|
||||
this.$server.socket.off('item_removed', this.libraryItemRemoved)
|
||||
this.$server.socket.off('items_updated', this.libraryItemsUpdated)
|
||||
this.$server.socket.off('items_added', this.libraryItemsAdded)
|
||||
} else {
|
||||
console.error('Bookshelf - Socket not initialized')
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="w-full relative">
|
||||
<div class="bookshelfRow flex items-end px-3 max-w-full overflow-x-auto" :style="{ height: shelfHeight + 'px' }">
|
||||
<template v-for="(entity, index) in entities">
|
||||
<cards-lazy-book-card v-if="type === 'books'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
||||
<cards-lazy-book-card v-if="type === 'book'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
||||
<cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
<template>
|
||||
<div ref="card" :id="`book-card-${index}`" :style="{ width: width + 'px', minWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @click="clickCard">
|
||||
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @click="clickCard">
|
||||
<!-- When cover image does not fill -->
|
||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<!-- Alternative bookshelf title/author/sort -->
|
||||
<div v-if="isAlternativeBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
|
||||
{{ displayTitle }}
|
||||
</p>
|
||||
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
|
||||
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div>
|
||||
|
||||
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||
<div v-show="audiobook && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
||||
</div>
|
||||
|
||||
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="hasCover ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||
|
||||
<!-- Placeholder Cover Title & Author -->
|
||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||
|
@ -23,25 +34,26 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Downloaded indicator icon -->
|
||||
<div v-if="hasDownload" class="absolute z-10" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }">
|
||||
<span class="material-icons text-success" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">download_done</span>
|
||||
</div>
|
||||
<!-- No progress shown for collapsed series in library -->
|
||||
<div v-if="!booksInSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||
|
||||
<div v-if="showError" :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||
<!-- Error widget -->
|
||||
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
||||
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
|
||||
<div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||
<!-- Volume number -->
|
||||
<div v-if="volumeNumber && showVolumeNumber && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
index: Number,
|
||||
|
@ -55,15 +67,18 @@ export default {
|
|||
},
|
||||
bookCoverAspectRatio: Number,
|
||||
showVolumeNumber: Boolean,
|
||||
bookshelfView: Number,
|
||||
bookMount: {
|
||||
// Book can be passed as prop or set with setEntity()
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
orderBy: String,
|
||||
filterBy: String,
|
||||
sortingIgnorePrefix: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false,
|
||||
isMoreMenuOpen: false,
|
||||
isProcessingReadUpdate: false,
|
||||
audiobook: null,
|
||||
imageReady: false,
|
||||
|
@ -73,87 +88,145 @@ export default {
|
|||
showCoverBg: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bookMount: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.audiobook = newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_audiobook() {
|
||||
showExperimentalFeatures() {
|
||||
return this.store.state.showExperimentalFeatures
|
||||
},
|
||||
_libraryItem() {
|
||||
return this.audiobook || {}
|
||||
},
|
||||
media() {
|
||||
return this._libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
placeholderUrl() {
|
||||
return '/book_placeholder.jpg'
|
||||
},
|
||||
hasDownload() {
|
||||
return !!this._audiobook.download
|
||||
},
|
||||
downloadedCover() {
|
||||
if (!this._audiobook.download) return null
|
||||
return this._audiobook.download.cover
|
||||
},
|
||||
bookCoverSrc() {
|
||||
if (this.downloadedCover) return this.downloadedCover
|
||||
return this.store.getters['audiobooks/getBookCoverSrc'](this._audiobook, this.placeholderUrl)
|
||||
return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
||||
},
|
||||
audiobookId() {
|
||||
return this._audiobook.id
|
||||
libraryItemId() {
|
||||
return this._libraryItem.id
|
||||
},
|
||||
series() {
|
||||
return this.mediaMetadata.series
|
||||
},
|
||||
libraryId() {
|
||||
return this._libraryItem.libraryId
|
||||
},
|
||||
hasEbook() {
|
||||
return this._audiobook.numEbooks
|
||||
if (!this.media.ebooks) return 0
|
||||
return this.media.ebooks.length
|
||||
},
|
||||
hasTracks() {
|
||||
return this._audiobook.numTracks
|
||||
hasAudiobook() {
|
||||
if (!this.media.audiobooks) return 0
|
||||
return this.media.audiobooks.length
|
||||
},
|
||||
book() {
|
||||
return this._audiobook.book || {}
|
||||
processingBatch() {
|
||||
return this.store.state.processingBatch
|
||||
},
|
||||
booksInSeries() {
|
||||
// Only added to audiobook object when collapseSeries is enabled
|
||||
return this._libraryItem.booksInSeries
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.book.cover
|
||||
return !!this.media.coverPath
|
||||
},
|
||||
squareAspectRatio() {
|
||||
return this.bookCoverAspectRatio === 1
|
||||
},
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.squareAspectRatio ? 160 : 100
|
||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||
return this.width / baseSize
|
||||
},
|
||||
title() {
|
||||
return this.book.title || ''
|
||||
return this.mediaMetadata.title || ''
|
||||
},
|
||||
playIconFontSize() {
|
||||
return Math.max(2, 3 * this.sizeMultiplier)
|
||||
},
|
||||
authors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
author() {
|
||||
return this.book.author
|
||||
},
|
||||
authorFL() {
|
||||
return this.book.authorFL || this.author
|
||||
return this.authors.map((au) => au.name).join(', ')
|
||||
},
|
||||
authorLF() {
|
||||
return this.book.authorLF || this.author
|
||||
return this.authors
|
||||
.map((au) => {
|
||||
var parts = au.name.split(' ')
|
||||
if (parts.length === 1) return parts[0]
|
||||
return `${parts[1]}, ${parts[0]}`
|
||||
})
|
||||
.join(', ')
|
||||
},
|
||||
volumeNumber() {
|
||||
return this.book.volumeNumber || null
|
||||
return this.mediaMetadata.volumeNumber || null
|
||||
},
|
||||
displayTitle() {
|
||||
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
|
||||
return this.title.substr(4) + ', The'
|
||||
}
|
||||
return this.title
|
||||
},
|
||||
displayAuthor() {
|
||||
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
|
||||
return this.author
|
||||
},
|
||||
displaySortLine() {
|
||||
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
|
||||
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
|
||||
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
|
||||
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
|
||||
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
|
||||
return null
|
||||
},
|
||||
userProgress() {
|
||||
return this.store.getters['user/getUserAudiobook'](this.audiobookId)
|
||||
return this.store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
||||
},
|
||||
userProgressPercent() {
|
||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||
},
|
||||
userIsRead() {
|
||||
return this.userProgress ? !!this.userProgress.isRead : false
|
||||
itemIsFinished() {
|
||||
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||
},
|
||||
showError() {
|
||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||
},
|
||||
isStreaming() {
|
||||
return this.store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
||||
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
|
||||
},
|
||||
showReadButton() {
|
||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||
},
|
||||
showPlayButton() {
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasAudiobook && !this.isStreaming
|
||||
},
|
||||
showSmallEBookIcon() {
|
||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||
},
|
||||
isMissing() {
|
||||
return this._audiobook.isMissing
|
||||
return this._libraryItem.isMissing
|
||||
},
|
||||
isInvalid() {
|
||||
return this._audiobook.isInvalid
|
||||
return this._libraryItem.isInvalid
|
||||
},
|
||||
hasMissingParts() {
|
||||
return this._audiobook.hasMissingParts
|
||||
return this._libraryItem.hasMissingParts
|
||||
},
|
||||
hasInvalidParts() {
|
||||
return this._audiobook.hasInvalidParts
|
||||
return this._libraryItem.hasInvalidParts
|
||||
},
|
||||
errorText() {
|
||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||
|
@ -168,6 +241,15 @@ export default {
|
|||
}
|
||||
return txt || 'Unknown Error'
|
||||
},
|
||||
overlayWrapperClasslist() {
|
||||
var classes = []
|
||||
if (this.isSelectionMode) classes.push('bg-opacity-60')
|
||||
else classes.push('bg-opacity-40')
|
||||
if (this.selected) {
|
||||
classes.push('border-2 border-yellow-400')
|
||||
}
|
||||
return classes
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
|
@ -206,11 +288,21 @@ export default {
|
|||
return this.title
|
||||
},
|
||||
authorCleaned() {
|
||||
if (!this.authorFL) return ''
|
||||
if (this.authorFL.length > 30) {
|
||||
return this.authorFL.slice(0, 27) + '...'
|
||||
if (!this.author) return ''
|
||||
if (this.author.length > 30) {
|
||||
return this.author.slice(0, 27) + '...'
|
||||
}
|
||||
return this.authorFL
|
||||
return this.author
|
||||
},
|
||||
isAlternativeBookshelfView() {
|
||||
return false
|
||||
// var constants = this.$constants || this.$nuxt.$constants
|
||||
// return this.bookshelfView === constants.BookshelfView.TITLES
|
||||
},
|
||||
titleDisplayBottomOffset() {
|
||||
if (!this.isAlternativeBookshelfView) return 0
|
||||
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
||||
return 4.25 * this.sizeMultiplier
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -218,8 +310,8 @@ export default {
|
|||
this.isSelectionMode = val
|
||||
if (!val) this.selected = false
|
||||
},
|
||||
setEntity(audiobook) {
|
||||
this.audiobook = audiobook
|
||||
setEntity(libraryItem) {
|
||||
this.audiobook = libraryItem
|
||||
},
|
||||
clickCard(e) {
|
||||
if (this.isSelectionMode) {
|
||||
|
@ -228,13 +320,85 @@ export default {
|
|||
this.selectBtnClick()
|
||||
} else {
|
||||
var router = this.$router || this.$nuxt.$router
|
||||
if (router) router.push(`/audiobook/${this.audiobookId}`)
|
||||
if (router) {
|
||||
if (this.booksInSeries) router.push(`/library/${this.libraryId}/series/${this.$encode(this.series)}`)
|
||||
else router.push(`/item/${this.libraryItemId}`)
|
||||
}
|
||||
}
|
||||
},
|
||||
editClick() {
|
||||
this.$emit('edit', this.audiobook)
|
||||
},
|
||||
toggleFinished() {
|
||||
var updatePayload = {
|
||||
isFinished: !this.itemIsFinished
|
||||
}
|
||||
this.isProcessingReadUpdate = true
|
||||
var toast = this.$toast || this.$nuxt.$toast
|
||||
var axios = this.$axios || this.$nuxt.$axios
|
||||
axios
|
||||
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.isProcessingReadUpdate = false
|
||||
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
|
||||
})
|
||||
},
|
||||
rescan() {
|
||||
this.rescanning = true
|
||||
this.$axios
|
||||
.$get(`/api/items/${this.libraryItemId}/scan`)
|
||||
.then((data) => {
|
||||
this.rescanning = false
|
||||
var result = data.result
|
||||
if (!result) {
|
||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
||||
} else if (result === 'UPDATED') {
|
||||
this.$toast.success(`Re-Scan complete item was updated`)
|
||||
} else if (result === 'UPTODATE') {
|
||||
this.$toast.success(`Re-Scan complete item was up to date`)
|
||||
} else if (result === 'REMOVED') {
|
||||
this.$toast.error(`Re-Scan complete item was removed`)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to scan library item', error)
|
||||
this.$toast.error('Failed to scan library item')
|
||||
this.rescanning = false
|
||||
})
|
||||
},
|
||||
showEditModalTracks() {
|
||||
// More menu func
|
||||
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'tracks' })
|
||||
},
|
||||
showEditModalMatch() {
|
||||
// More menu func
|
||||
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'match' })
|
||||
},
|
||||
showEditModalDownload() {
|
||||
// More menu func
|
||||
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'download' })
|
||||
},
|
||||
openCollections() {
|
||||
this.store.commit('setSelectedLibraryItem', this.audiobook)
|
||||
this.store.commit('globals/setShowUserCollectionsModal', true)
|
||||
},
|
||||
clickReadEBook() {
|
||||
this.store.commit('showEReader', this.audiobook)
|
||||
},
|
||||
selectBtnClick() {
|
||||
if (this.processingBatch) return
|
||||
this.selected = !this.selected
|
||||
this.$emit('select', this.audiobook)
|
||||
},
|
||||
play() {
|
||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||
eventBus.$emit('play-item', this.libraryItemId)
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
this.$destroy()
|
||||
|
|
|
@ -5,20 +5,11 @@
|
|||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<img v-if="audiobook" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||
<div v-show="loading && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||
<img v-if="libraryItem" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||
<div class="absolute top-2 right-2">
|
||||
<div class="la-ball-spin-clockwise la-sm">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<widgets-loading-spinner />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -44,11 +35,10 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
authorOverride: String,
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120
|
||||
|
@ -76,12 +66,15 @@ export default {
|
|||
height() {
|
||||
return this.width * this.bookCoverAspectRatio
|
||||
},
|
||||
book() {
|
||||
if (!this.audiobook) return {}
|
||||
return this.audiobook.book || {}
|
||||
media() {
|
||||
if (!this.libraryItem) return {}
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
title() {
|
||||
return this.book.title || 'No Title'
|
||||
return this.mediaMetadata.title || 'No Title'
|
||||
},
|
||||
titleCleaned() {
|
||||
if (this.title.length > 60) {
|
||||
|
@ -89,9 +82,11 @@ export default {
|
|||
}
|
||||
return this.title
|
||||
},
|
||||
authors() {
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
author() {
|
||||
if (this.authorOverride) return this.authorOverride
|
||||
return this.book.author || 'Unknown'
|
||||
return this.authors.map((au) => au.name).join(', ')
|
||||
},
|
||||
authorCleaned() {
|
||||
if (this.author.length > 30) {
|
||||
|
@ -104,15 +99,15 @@ export default {
|
|||
},
|
||||
fullCoverUrl() {
|
||||
if (this.downloadCover) return this.downloadCover
|
||||
if (!this.audiobook) return null
|
||||
if (!this.libraryItem) return null
|
||||
var store = this.$store || this.$nuxt.$store
|
||||
return store.getters['audiobooks/getBookCoverSrc'](this.audiobook, this.placeholderUrl)
|
||||
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
|
||||
},
|
||||
cover() {
|
||||
return this.book.cover || this.placeholderUrl
|
||||
return this.media.coverPath || this.placeholderUrl
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.book.cover
|
||||
return !!this.media.coverPath
|
||||
},
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||
|
@ -140,7 +135,6 @@ export default {
|
|||
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
|
||||
}
|
||||
},
|
||||
hideCoverBg() {},
|
||||
imageLoaded() {
|
||||
this.loading = false
|
||||
this.$nextTick(() => {
|
||||
|
@ -170,214 +164,3 @@ export default {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*!
|
||||
* Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
|
||||
* Copyright 2015 Daniel Cardoso <@DanielCardoso>
|
||||
* Licensed under MIT
|
||||
*/
|
||||
.la-ball-spin-clockwise,
|
||||
.la-ball-spin-clockwise > div {
|
||||
position: relative;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.la-ball-spin-clockwise {
|
||||
display: block;
|
||||
font-size: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-dark {
|
||||
color: #262626;
|
||||
}
|
||||
.la-ball-spin-clockwise > div {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
background-color: currentColor;
|
||||
border: 0 solid currentColor;
|
||||
}
|
||||
.la-ball-spin-clockwise {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.la-ball-spin-clockwise > div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: -4px;
|
||||
margin-left: -4px;
|
||||
border-radius: 100%;
|
||||
-webkit-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
-moz-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
-o-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(1) {
|
||||
top: 5%;
|
||||
left: 50%;
|
||||
-webkit-animation-delay: -0.875s;
|
||||
-moz-animation-delay: -0.875s;
|
||||
-o-animation-delay: -0.875s;
|
||||
animation-delay: -0.875s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(2) {
|
||||
top: 18.1801948466%;
|
||||
left: 81.8198051534%;
|
||||
-webkit-animation-delay: -0.75s;
|
||||
-moz-animation-delay: -0.75s;
|
||||
-o-animation-delay: -0.75s;
|
||||
animation-delay: -0.75s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(3) {
|
||||
top: 50%;
|
||||
left: 95%;
|
||||
-webkit-animation-delay: -0.625s;
|
||||
-moz-animation-delay: -0.625s;
|
||||
-o-animation-delay: -0.625s;
|
||||
animation-delay: -0.625s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(4) {
|
||||
top: 81.8198051534%;
|
||||
left: 81.8198051534%;
|
||||
-webkit-animation-delay: -0.5s;
|
||||
-moz-animation-delay: -0.5s;
|
||||
-o-animation-delay: -0.5s;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(5) {
|
||||
top: 94.9999999966%;
|
||||
left: 50.0000000005%;
|
||||
-webkit-animation-delay: -0.375s;
|
||||
-moz-animation-delay: -0.375s;
|
||||
-o-animation-delay: -0.375s;
|
||||
animation-delay: -0.375s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(6) {
|
||||
top: 81.8198046966%;
|
||||
left: 18.1801949248%;
|
||||
-webkit-animation-delay: -0.25s;
|
||||
-moz-animation-delay: -0.25s;
|
||||
-o-animation-delay: -0.25s;
|
||||
animation-delay: -0.25s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(7) {
|
||||
top: 49.9999750815%;
|
||||
left: 5.0000051215%;
|
||||
-webkit-animation-delay: -0.125s;
|
||||
-moz-animation-delay: -0.125s;
|
||||
-o-animation-delay: -0.125s;
|
||||
animation-delay: -0.125s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(8) {
|
||||
top: 18.179464974%;
|
||||
left: 18.1803700518%;
|
||||
-webkit-animation-delay: 0s;
|
||||
-moz-animation-delay: 0s;
|
||||
-o-animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-sm {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-sm > div {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
margin-top: -2px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-2x {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-2x > div {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-3x {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-3x > div {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: -12px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
/*
|
||||
* Animation
|
||||
*/
|
||||
@-webkit-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-moz-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-moz-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@-o-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-o-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
-o-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -9,8 +9,8 @@
|
|||
<div v-else-if="books.length" class="flex justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
|
||||
<covers-book-cover :audiobook="books[0]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-book-cover v-if="books.length > 1" :audiobook="books[1]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-book-cover :library-item="books[0]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-book-cover v-if="books.length > 1" :library-item="books[1]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
|
|
|
@ -17,6 +17,7 @@ export default {
|
|||
},
|
||||
width: Number,
|
||||
height: Number,
|
||||
groupTo: String,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
|
@ -31,7 +32,6 @@ export default {
|
|||
isFannedOut: false,
|
||||
isDetached: false,
|
||||
isAttaching: false,
|
||||
windowWidth: 0,
|
||||
isInit: false
|
||||
}
|
||||
},
|
||||
|
@ -48,8 +48,11 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
sizeMultiplier() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.width / (100 * 1.6 * 2)
|
||||
return this.width / 200
|
||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||
return this.width / 240
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.store.state.showExperimentalFeatures
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
|
@ -59,44 +62,8 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
detchCoverWrapper() {
|
||||
if (!this.coverWrapperEl || !this.$refs.wrapper || this.isDetached) return
|
||||
|
||||
this.coverWrapperEl.remove()
|
||||
|
||||
this.isDetached = true
|
||||
document.body.appendChild(this.coverWrapperEl)
|
||||
this.coverWrapperEl.addEventListener('mouseleave', this.mouseleaveCover)
|
||||
|
||||
this.coverWrapperEl.style.position = 'absolute'
|
||||
this.coverWrapperEl.style.zIndex = 40
|
||||
|
||||
this.updatePosition()
|
||||
},
|
||||
attachCoverWrapper() {
|
||||
if (!this.coverWrapperEl || !this.$refs.wrapper || !this.isDetached) return
|
||||
|
||||
this.coverWrapperEl.remove()
|
||||
this.coverWrapperEl.style.position = 'relative'
|
||||
this.coverWrapperEl.style.left = 'unset'
|
||||
this.coverWrapperEl.style.top = 'unset'
|
||||
this.coverWrapperEl.style.width = this.$refs.wrapper.clientWidth + 'px'
|
||||
|
||||
this.$refs.wrapper.appendChild(this.coverWrapperEl)
|
||||
|
||||
this.isDetached = false
|
||||
},
|
||||
updatePosition() {
|
||||
var rect = this.$refs.wrapper.getBoundingClientRect()
|
||||
this.coverWrapperEl.style.top = rect.top + window.scrollY + 'px'
|
||||
|
||||
this.coverWrapperEl.style.left = rect.left + window.scrollX + 4 + 'px'
|
||||
|
||||
this.coverWrapperEl.style.height = rect.height + 'px'
|
||||
this.coverWrapperEl.style.width = rect.width + 'px'
|
||||
},
|
||||
getCoverUrl(book) {
|
||||
return this.store.getters['audiobooks/getBookCoverSrc'](book, '')
|
||||
return this.store.getters['globals/getLibraryItemCoverSrc'](book, '')
|
||||
},
|
||||
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
||||
var src = coverData.coverUrl
|
||||
|
@ -156,6 +123,22 @@ export default {
|
|||
imgdiv.appendChild(img)
|
||||
return imgdiv
|
||||
},
|
||||
createSeriesNameCover(offsetLeft) {
|
||||
var imgdiv = document.createElement('div')
|
||||
imgdiv.style.height = this.height + 'px'
|
||||
imgdiv.style.width = this.height / this.bookCoverAspectRatio + 'px'
|
||||
imgdiv.style.left = offsetLeft + 'px'
|
||||
imgdiv.className = 'absolute top-0 box-shadow-book transition-transform flex items-center justify-center'
|
||||
imgdiv.style.boxShadow = '4px 0px 4px #11111166'
|
||||
imgdiv.style.backgroundColor = '#111'
|
||||
|
||||
var innerP = document.createElement('p')
|
||||
innerP.textContent = this.name
|
||||
innerP.className = 'text-sm font-book text-white'
|
||||
imgdiv.appendChild(innerP)
|
||||
|
||||
return imgdiv
|
||||
},
|
||||
async init() {
|
||||
if (this.isInit) return
|
||||
this.isInit = true
|
||||
|
@ -168,7 +151,6 @@ export default {
|
|||
.map((bookItem) => {
|
||||
return {
|
||||
id: bookItem.id,
|
||||
volumeNumber: bookItem.book ? bookItem.book.volumeNumber : null,
|
||||
coverUrl: this.getCoverUrl(bookItem)
|
||||
}
|
||||
})
|
||||
|
@ -179,6 +161,8 @@ export default {
|
|||
}
|
||||
this.noValidCovers = false
|
||||
|
||||
validCovers = validCovers.slice(0, 10)
|
||||
|
||||
var coverWidth = this.width
|
||||
var widthPer = this.width
|
||||
if (validCovers.length > 1) {
|
||||
|
@ -189,7 +173,7 @@ export default {
|
|||
this.offsetIncrement = widthPer
|
||||
|
||||
var outerdiv = document.createElement('div')
|
||||
outerdiv.id = `group-cover-${this.id || this.$encode(this.name)}`
|
||||
outerdiv.id = `group-cover-${this.id}`
|
||||
this.coverWrapperEl = outerdiv
|
||||
outerdiv.className = 'w-full h-full relative box-shadow-book'
|
||||
|
||||
|
@ -211,9 +195,7 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.windowWidth = window.innerWidth
|
||||
},
|
||||
mounted() {},
|
||||
beforeDestroy() {
|
||||
if (this.coverWrapperEl) this.coverWrapperEl.remove()
|
||||
if (this.coverImageEls && this.coverImageEls.length) {
|
||||
|
|
241
components/widgets/LoadingSpinner.vue
Normal file
241
components/widgets/LoadingSpinner.vue
Normal file
|
@ -0,0 +1,241 @@
|
|||
<template>
|
||||
<div class="la-ball-spin-clockwise" :class="`${size}`">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
default: 'la-sm'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*!
|
||||
* Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
|
||||
* Copyright 2015 Daniel Cardoso <@DanielCardoso>
|
||||
* Licensed under MIT
|
||||
*/
|
||||
.la-ball-spin-clockwise,
|
||||
.la-ball-spin-clockwise > div {
|
||||
position: relative;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.la-ball-spin-clockwise {
|
||||
display: block;
|
||||
font-size: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-dark {
|
||||
color: #262626;
|
||||
}
|
||||
.la-ball-spin-clockwise > div {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
background-color: currentColor;
|
||||
border: 0 solid currentColor;
|
||||
}
|
||||
.la-ball-spin-clockwise {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.la-ball-spin-clockwise > div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: -4px;
|
||||
margin-left: -4px;
|
||||
border-radius: 100%;
|
||||
-webkit-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
-moz-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
-o-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(1) {
|
||||
top: 5%;
|
||||
left: 50%;
|
||||
-webkit-animation-delay: -0.875s;
|
||||
-moz-animation-delay: -0.875s;
|
||||
-o-animation-delay: -0.875s;
|
||||
animation-delay: -0.875s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(2) {
|
||||
top: 18.1801948466%;
|
||||
left: 81.8198051534%;
|
||||
-webkit-animation-delay: -0.75s;
|
||||
-moz-animation-delay: -0.75s;
|
||||
-o-animation-delay: -0.75s;
|
||||
animation-delay: -0.75s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(3) {
|
||||
top: 50%;
|
||||
left: 95%;
|
||||
-webkit-animation-delay: -0.625s;
|
||||
-moz-animation-delay: -0.625s;
|
||||
-o-animation-delay: -0.625s;
|
||||
animation-delay: -0.625s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(4) {
|
||||
top: 81.8198051534%;
|
||||
left: 81.8198051534%;
|
||||
-webkit-animation-delay: -0.5s;
|
||||
-moz-animation-delay: -0.5s;
|
||||
-o-animation-delay: -0.5s;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(5) {
|
||||
top: 94.9999999966%;
|
||||
left: 50.0000000005%;
|
||||
-webkit-animation-delay: -0.375s;
|
||||
-moz-animation-delay: -0.375s;
|
||||
-o-animation-delay: -0.375s;
|
||||
animation-delay: -0.375s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(6) {
|
||||
top: 81.8198046966%;
|
||||
left: 18.1801949248%;
|
||||
-webkit-animation-delay: -0.25s;
|
||||
-moz-animation-delay: -0.25s;
|
||||
-o-animation-delay: -0.25s;
|
||||
animation-delay: -0.25s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(7) {
|
||||
top: 49.9999750815%;
|
||||
left: 5.0000051215%;
|
||||
-webkit-animation-delay: -0.125s;
|
||||
-moz-animation-delay: -0.125s;
|
||||
-o-animation-delay: -0.125s;
|
||||
animation-delay: -0.125s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(8) {
|
||||
top: 18.179464974%;
|
||||
left: 18.1803700518%;
|
||||
-webkit-animation-delay: 0s;
|
||||
-moz-animation-delay: 0s;
|
||||
-o-animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-sm {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-sm > div {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
margin-top: -2px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-2x {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-2x > div {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-3x {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-3x > div {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: -12px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
/*
|
||||
* Animation
|
||||
*/
|
||||
@-webkit-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-moz-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-moz-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@-o-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-o-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
-o-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -51,7 +51,7 @@ export default {
|
|||
async connected(isConnected) {
|
||||
if (isConnected) {
|
||||
console.log('[Default] Connected socket sync user ab data')
|
||||
this.$store.dispatch('user/syncUserAudiobookData')
|
||||
// this.$store.dispatch('user/syncUserAudiobookData')
|
||||
|
||||
this.initSocketListeners()
|
||||
|
||||
|
@ -326,51 +326,49 @@ export default {
|
|||
}
|
||||
}
|
||||
},
|
||||
audiobookAdded(audiobook) {
|
||||
this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
||||
},
|
||||
audiobookUpdated(audiobook) {
|
||||
this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
||||
},
|
||||
audiobookRemoved(audiobook) {
|
||||
if (this.$route.name.startsWith('audiobook')) {
|
||||
if (this.$route.params.id === audiobook.id) {
|
||||
// audiobookAdded(audiobook) {
|
||||
// this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
||||
// },
|
||||
// audiobookUpdated(audiobook) {
|
||||
// this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
||||
// },
|
||||
itemRemoved(libraryItem) {
|
||||
if (this.$route.name.startsWith('item')) {
|
||||
if (this.$route.params.id === libraryItem.id) {
|
||||
this.$router.replace(`/bookshelf`)
|
||||
}
|
||||
}
|
||||
},
|
||||
audiobooksAdded(audiobooks) {
|
||||
audiobooks.forEach((ab) => {
|
||||
this.audiobookAdded(ab)
|
||||
})
|
||||
},
|
||||
audiobooksUpdated(audiobooks) {
|
||||
audiobooks.forEach((ab) => {
|
||||
this.audiobookUpdated(ab)
|
||||
})
|
||||
},
|
||||
// audiobooksAdded(audiobooks) {
|
||||
// audiobooks.forEach((ab) => {
|
||||
// this.audiobookAdded(ab)
|
||||
// })
|
||||
// },
|
||||
// audiobooksUpdated(audiobooks) {
|
||||
// audiobooks.forEach((ab) => {
|
||||
// this.audiobookUpdated(ab)
|
||||
// })
|
||||
// },
|
||||
userLoggedOut() {
|
||||
// Only cancels stream if streamining not playing downloaded
|
||||
this.$eventBus.$emit('close_stream')
|
||||
},
|
||||
initSocketListeners() {
|
||||
if (this.$server.socket) {
|
||||
// Audiobook Listeners
|
||||
this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
|
||||
this.$server.socket.on('audiobook_added', this.audiobookAdded)
|
||||
this.$server.socket.on('audiobook_removed', this.audiobookRemoved)
|
||||
this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
|
||||
this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
|
||||
// this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
|
||||
// this.$server.socket.on('audiobook_added', this.audiobookAdded)
|
||||
this.$server.socket.on('item_removed', this.itemRemoved)
|
||||
// this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
|
||||
// this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
|
||||
}
|
||||
},
|
||||
removeSocketListeners() {
|
||||
if (this.$server.socket) {
|
||||
// Audiobook Listeners
|
||||
this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
|
||||
this.$server.socket.off('audiobook_added', this.audiobookAdded)
|
||||
this.$server.socket.off('audiobook_removed', this.audiobookRemoved)
|
||||
this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated)
|
||||
this.$server.socket.off('audiobooks_added', this.audiobooksAdded)
|
||||
// this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
|
||||
// this.$server.socket.off('audiobook_added', this.audiobookAdded)
|
||||
this.$server.socket.off('item_removed', this.itemRemoved)
|
||||
// this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated)
|
||||
// this.$server.socket.off('audiobooks_added', this.audiobooksAdded)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -382,6 +380,7 @@ export default {
|
|||
console.log('Syncing on default mount')
|
||||
this.connected(true)
|
||||
}
|
||||
|
||||
this.$server.on('logout', this.userLoggedOut)
|
||||
this.$server.on('connected', this.connected)
|
||||
this.$server.on('connectionFailed', this.socketConnectionFailed)
|
||||
|
|
|
@ -28,16 +28,7 @@ export default {
|
|||
if (this.entityComponentRefs[index]) {
|
||||
var bookComponent = this.entityComponentRefs[index]
|
||||
shelfEl.appendChild(bookComponent.$el)
|
||||
if (this.isSelectionMode) {
|
||||
bookComponent.setSelectionMode(true)
|
||||
if (this.selectedAudiobooks.includes(bookComponent.audiobookId) || this.isSelectAll) {
|
||||
bookComponent.selected = true
|
||||
} else {
|
||||
bookComponent.selected = false
|
||||
}
|
||||
} else {
|
||||
bookComponent.setSelectionMode(false)
|
||||
}
|
||||
bookComponent.isHovering = false
|
||||
return
|
||||
}
|
||||
|
@ -78,12 +69,6 @@ export default {
|
|||
if (this.entities[index]) {
|
||||
instance.setEntity(this.entities[index])
|
||||
}
|
||||
if (this.isSelectionMode) {
|
||||
instance.setSelectionMode(true)
|
||||
if (instance.audiobookId && this.selectedAudiobooks.includes(instance.audiobookId) || this.isSelectAll) {
|
||||
instance.selected = true
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -122,7 +122,7 @@ export default {
|
|||
methods: {
|
||||
async fetchCategories() {
|
||||
var categories = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/categories`)
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
|
@ -131,6 +131,7 @@ export default {
|
|||
return []
|
||||
})
|
||||
this.shelves = categories
|
||||
console.log('Shelves', this.shelves)
|
||||
},
|
||||
async socketInit(isConnected) {
|
||||
if (isConnected && this.currentLibraryId) {
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
</nuxt-link>
|
||||
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
|
||||
<img src="/Logo.png" class="h-20 w-20 mb-2" />
|
||||
<h1 class="text-2xl font-book">Audiobookshelf</h1>
|
||||
<h1 class="text-2xl font-book">audiobookshelf</h1>
|
||||
</div>
|
||||
<p class="hidden absolute short:block top-1.5 left-12 p-2 font-book text-xl">Audiobookshelf</p>
|
||||
<p class="hidden absolute short:block top-1.5 left-12 p-2 font-book text-xl">audiobookshelf</p>
|
||||
|
||||
<p class="absolute bottom-16 left-0 right-0 px-2 text-center text-error"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p>
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
|||
<p>Connecting socket..</p>
|
||||
</div>
|
||||
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6 w-full">
|
||||
<h2 class="text-lg leading-7 mb-4">Enter an <span class="font-book font-normal">Audiobookshelf</span><br />server address:</h2>
|
||||
<h2 class="text-lg leading-7 mb-4">Server address</h2>
|
||||
<form v-show="!showAuth" @submit.prevent="submit" novalidate class="w-full">
|
||||
<ui-text-input v-model="serverUrl" :disabled="processing || !networkConnected" placeholder="http://55.55.55.55:13378" type="url" class="w-full sm:w-72 h-10" />
|
||||
<div class="flex justify-end">
|
||||
|
|
445
pages/item/_id.vue
Normal file
445
pages/item/_id.vue
Normal file
|
@ -0,0 +1,445 @@
|
|||
<template>
|
||||
<div class="w-full h-full px-3 py-4 overflow-y-auto">
|
||||
<div class="flex">
|
||||
<div class="w-32">
|
||||
<div class="relative">
|
||||
<covers-book-cover :library-item="libraryItem" :download-cover="downloadedCover" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
||||
</div>
|
||||
<div class="flex my-4">
|
||||
<p v-if="numTracks" class="text-sm">{{ numTracks }} Tracks</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow px-3">
|
||||
<h1 class="text-lg">{{ title }}</h1>
|
||||
<!-- <h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3> -->
|
||||
<p class="text-sm text-gray-400">by {{ author }}</p>
|
||||
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
||||
{{ $elapsedPretty(duration) }}
|
||||
<span class="px-4">{{ $bytesPretty(size) }}</span>
|
||||
</p>
|
||||
|
||||
<div v-if="progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
|
||||
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
||||
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
|
||||
<span class="material-icons text-sm">close</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="(isConnected && (showPlay || showRead)) || isDownloadPlayable" class="flex mt-4 -mr-2">
|
||||
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isDownloadPlayable ? 'Play local' : 'Play stream' }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="showRead && isConnected" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
||||
<span class="material-icons">auto_stories</span>
|
||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="isConnected && showPlay && !isIos" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span>
|
||||
</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full py-4">
|
||||
<p>{{ description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Path from 'path'
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import AudioDownloader from '@/plugins/audio-downloader'
|
||||
import StorageManager from '@/plugins/storage-manager'
|
||||
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, app }) {
|
||||
var libraryItemId = params.id
|
||||
var libraryItem = null
|
||||
|
||||
if (app.$server.connected) {
|
||||
libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
var download = store.getters['downloads/getDownload'](libraryItemId)
|
||||
if (download) {
|
||||
libraryItem = download.libraryItem
|
||||
}
|
||||
}
|
||||
|
||||
if (!libraryItem) {
|
||||
console.error('No item...', params.id)
|
||||
return redirect('/')
|
||||
}
|
||||
return {
|
||||
libraryItem
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
resettingProgress: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isIos() {
|
||||
return this.$platform === 'ios'
|
||||
},
|
||||
isConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
},
|
||||
libraryItemId() {
|
||||
return this.libraryItem.id
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
title() {
|
||||
return this.mediaMetadata.title
|
||||
},
|
||||
author() {
|
||||
return this.mediaMetadata.authorName
|
||||
},
|
||||
description() {
|
||||
return this.mediaMetadata.description || ''
|
||||
},
|
||||
series() {
|
||||
return this.mediaMetadata.series || []
|
||||
},
|
||||
audiobooks() {
|
||||
return this.media.audiobooks || []
|
||||
},
|
||||
defaultAudiobook() {
|
||||
if (!this.audiobooks.length) return null
|
||||
return this.audiobooks[0]
|
||||
},
|
||||
duration() {
|
||||
if (!this.defaultAudiobook) return 0
|
||||
return this.defaultAudiobook.duration
|
||||
},
|
||||
size() {
|
||||
if (!this.defaultAudiobook) return 0
|
||||
return this.defaultAudiobook.size
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
userItemProgress() {
|
||||
return this.$store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
||||
},
|
||||
userIsFinished() {
|
||||
return this.userItemProgress ? !!this.userItemProgress.isFinished : false
|
||||
},
|
||||
userTimeRemaining() {
|
||||
if (!this.userItemProgress) return 0
|
||||
var duration = this.userItemProgress.duration || this.duration
|
||||
return duration - this.userItemProgress.currentTime
|
||||
},
|
||||
progressPercent() {
|
||||
return this.userItemProgress ? Math.max(Math.min(1, this.userItemProgress.progress), 0) : 0
|
||||
},
|
||||
userProgressStartedAt() {
|
||||
return this.userItemProgress ? this.userItemProgress.startedAt : 0
|
||||
},
|
||||
userProgressFinishedAt() {
|
||||
return this.userItemProgress ? this.userItemProgress.finishedAt : 0
|
||||
},
|
||||
isStreaming() {
|
||||
return this.$store.getters['isAudiobookStreaming'](this.libraryItemId)
|
||||
},
|
||||
isPlaying() {
|
||||
return this.$store.getters['isAudiobookPlaying'](this.libraryItemId)
|
||||
},
|
||||
numTracks() {
|
||||
if (!this.defaultAudiobook) return 0
|
||||
return this.defaultAudiobook.tracks.length || 0
|
||||
},
|
||||
isMissing() {
|
||||
return this.libraryItem.isMissing
|
||||
},
|
||||
isIncomplete() {
|
||||
return this.libraryItem.isIncomplete
|
||||
},
|
||||
isDownloading() {
|
||||
return this.downloadObj ? this.downloadObj.isDownloading : false
|
||||
},
|
||||
showPlay() {
|
||||
return !this.isMissing && !this.isIncomplete && this.defaultAudiobook
|
||||
},
|
||||
showRead() {
|
||||
return this.ebooks.length && this.ebookFormat !== '.pdf'
|
||||
},
|
||||
ebooks() {
|
||||
return this.media.ebooks || []
|
||||
},
|
||||
ebookFormat() {
|
||||
if (!this.ebooks.length) return null
|
||||
return this.ebooks[0].ebookFile.ebookFormat
|
||||
},
|
||||
isDownloadPreparing() {
|
||||
return this.downloadObj ? this.downloadObj.isPreparing : false
|
||||
},
|
||||
isDownloadPlayable() {
|
||||
return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing
|
||||
},
|
||||
downloadedCover() {
|
||||
return this.downloadObj ? this.downloadObj.cover : null
|
||||
},
|
||||
downloadObj() {
|
||||
return this.$store.getters['downloads/getDownload'](this.libraryItemId)
|
||||
},
|
||||
hasStoragePermission() {
|
||||
return this.$store.state.hasStoragePermission
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
readBook() {
|
||||
this.$store.commit('openReader', this.libraryItem)
|
||||
},
|
||||
playClick() {
|
||||
this.$eventBus.$emit('play-item', this.libraryItem.id)
|
||||
|
||||
// this.$store.commit('setPlayOnLoad', true)
|
||||
// if (!this.isDownloadPlayable) {
|
||||
// Stream
|
||||
// console.log('[PLAYCLICK] Set Playing STREAM ' + this.title)
|
||||
// this.$store.commit('setStreamAudiobook', this.libraryItem)
|
||||
// this.$server.socket.emit('open_stream', this.libraryItem.id)
|
||||
// } else {
|
||||
// Local
|
||||
// console.log('[PLAYCLICK] Set Playing Local Download ' + this.title)
|
||||
// this.$store.commit('setPlayingDownload', this.downloadObj)
|
||||
// }
|
||||
},
|
||||
async clearProgressClick() {
|
||||
if (!this.$server.connected) {
|
||||
this.$toast.info('Clear downloaded book progress not yet implemented')
|
||||
return
|
||||
}
|
||||
|
||||
const { value } = await Dialog.confirm({
|
||||
title: 'Confirm',
|
||||
message: 'Are you sure you want to reset your progress?'
|
||||
})
|
||||
if (value) {
|
||||
this.resettingProgress = true
|
||||
this.$axios
|
||||
.$delete(`/api/me/progress/${this.libraryItemId}`)
|
||||
.then(() => {
|
||||
console.log('Progress reset complete')
|
||||
this.$toast.success(`Your progress was reset`)
|
||||
this.resettingProgress = false
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Progress reset failed', error)
|
||||
this.resettingProgress = false
|
||||
})
|
||||
}
|
||||
// if (value) {
|
||||
// this.resettingProgress = true
|
||||
// this.$store.dispatch('user/updateUserAudiobookData', {
|
||||
// libraryItemId: this.libraryItemId,
|
||||
// currentTime: 0,
|
||||
// totalDuration: this.duration,
|
||||
// progress: 0,
|
||||
// lastUpdate: Date.now(),
|
||||
// isRead: false
|
||||
// })
|
||||
// }
|
||||
},
|
||||
itemUpdated(libraryItem) {
|
||||
if (libraryItem.id === this.libraryItemId) {
|
||||
console.log('Item Updated')
|
||||
this.libraryItem = libraryItem
|
||||
}
|
||||
},
|
||||
downloadClick() {
|
||||
console.log('downloadClick ' + this.$server.connected + ' | ' + !!this.downloadObj)
|
||||
if (!this.$server.connected) return
|
||||
|
||||
if (this.downloadObj) {
|
||||
console.log('Already downloaded', this.downloadObj)
|
||||
} else {
|
||||
this.prepareDownload()
|
||||
}
|
||||
},
|
||||
async changeDownloadFolderClick() {
|
||||
if (!this.hasStoragePermission) {
|
||||
console.log('Requesting Storage Permission')
|
||||
await StorageManager.requestStoragePermission()
|
||||
} else {
|
||||
var folderObj = await StorageManager.selectFolder()
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
}
|
||||
|
||||
var permissionsGood = await StorageManager.checkFolderPermissions({ folderUrl: folderObj.uri })
|
||||
console.log('Storage Permission check folder ' + permissionsGood)
|
||||
|
||||
if (!permissionsGood) {
|
||||
this.$toast.error('Folder permissions failed')
|
||||
return
|
||||
} else {
|
||||
this.$toast.success('Folder permission success')
|
||||
}
|
||||
|
||||
await this.$localStore.setDownloadFolder(folderObj)
|
||||
}
|
||||
},
|
||||
async prepareDownload() {
|
||||
var audiobook = this.libraryItem
|
||||
if (!audiobook) {
|
||||
return
|
||||
}
|
||||
|
||||
// Download Path
|
||||
var dlFolder = this.$localStore.downloadFolder
|
||||
console.log('Prepare download: ' + this.hasStoragePermission + ' | ' + dlFolder)
|
||||
|
||||
if (!this.hasStoragePermission || !dlFolder) {
|
||||
console.log('No download folder, request from user')
|
||||
// User to select download folder from download modal to ensure permissions
|
||||
// this.$store.commit('downloads/setShowModal', true)
|
||||
this.changeDownloadFolderClick()
|
||||
return
|
||||
} else {
|
||||
console.log('Has Download folder: ' + JSON.stringify(dlFolder))
|
||||
}
|
||||
|
||||
var downloadObject = {
|
||||
id: this.libraryItemId,
|
||||
downloadFolderUrl: dlFolder.uri,
|
||||
audiobook: {
|
||||
...audiobook
|
||||
},
|
||||
isPreparing: true,
|
||||
isDownloading: false,
|
||||
toastId: this.$toast(`Preparing download for "${this.title}"`, { timeout: false })
|
||||
}
|
||||
if (audiobook.tracks.length === 1) {
|
||||
// Single track should not need preparation
|
||||
console.log('Single track, start download no prep needed')
|
||||
var track = audiobook.tracks[0]
|
||||
var fileext = track.ext
|
||||
|
||||
console.log('Download Single Track Path: ' + track.path)
|
||||
|
||||
var relTrackPath = track.path.replace('\\', '/').replace(this.libraryItem.path.replace('\\', '/'), '')
|
||||
|
||||
var url = `${this.$store.state.serverUrl}/s/book/${this.libraryItemId}${relTrackPath}?token=${this.userToken}`
|
||||
this.startDownload(url, fileext, downloadObject)
|
||||
} else {
|
||||
// Multi-track merge
|
||||
this.$store.commit('downloads/addUpdateDownload', downloadObject)
|
||||
|
||||
var prepareDownloadPayload = {
|
||||
audiobookId: this.libraryItemId,
|
||||
audioFileType: 'same',
|
||||
type: 'singleAudio'
|
||||
}
|
||||
this.$server.socket.emit('download', prepareDownloadPayload)
|
||||
}
|
||||
},
|
||||
getCoverUrlForDownload() {
|
||||
if (!this.book || !this.book.cover) return null
|
||||
|
||||
var cover = this.book.cover
|
||||
if (cover.startsWith('http')) return cover
|
||||
var coverSrc = this.$store.getters['global/getLibraryItemCoverSrc'](this.libraryItem)
|
||||
return coverSrc
|
||||
},
|
||||
async startDownload(url, fileext, download) {
|
||||
this.$toast.update(download.toastId, { content: `Downloading "${download.audiobook.book.title}"...` })
|
||||
|
||||
var coverDownloadUrl = this.getCoverUrlForDownload()
|
||||
var coverFilename = null
|
||||
if (coverDownloadUrl) {
|
||||
var coverNoQueryString = coverDownloadUrl.split('?')[0]
|
||||
|
||||
var coverExt = Path.extname(coverNoQueryString) || '.jpg'
|
||||
coverFilename = `cover-${download.id}${coverExt}`
|
||||
}
|
||||
|
||||
download.isDownloading = true
|
||||
download.isPreparing = false
|
||||
download.filename = `${download.audiobook.book.title}${fileext}`
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
|
||||
console.log('Starting Download URL', url)
|
||||
var downloadRequestPayload = {
|
||||
audiobookId: download.id,
|
||||
filename: download.filename,
|
||||
coverFilename,
|
||||
coverDownloadUrl,
|
||||
downloadUrl: url,
|
||||
title: download.audiobook.book.title,
|
||||
downloadFolderUrl: download.downloadFolderUrl
|
||||
}
|
||||
var downloadRes = await AudioDownloader.download(downloadRequestPayload)
|
||||
if (downloadRes.error) {
|
||||
var errorMsg = downloadRes.error || 'Unknown error'
|
||||
console.error('Download error', errorMsg)
|
||||
this.$toast.update(download.toastId, { content: `Error: ${errorMsg}`, options: { timeout: 5000, type: 'error' } })
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
}
|
||||
},
|
||||
downloadReady(prepareDownload) {
|
||||
var download = this.$store.getters['downloads/getDownload'](prepareDownload.audiobookId)
|
||||
if (download) {
|
||||
var fileext = prepareDownload.ext
|
||||
var url = `${this.$store.state.serverUrl}/downloads/${prepareDownload.id}/${prepareDownload.filename}?token=${this.userToken}`
|
||||
this.startDownload(url, fileext, download)
|
||||
} else {
|
||||
console.error('Prepare download killed but download not found', prepareDownload)
|
||||
}
|
||||
},
|
||||
downloadKilled(prepareDownload) {
|
||||
var download = this.$store.getters['downloads/getDownload'](prepareDownload.audiobookId)
|
||||
if (download) {
|
||||
this.$toast.update(download.toastId, { content: `Prepare download killed for "${download.audiobook.book.title}"`, options: { timeout: 5000, type: 'error' } })
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
} else {
|
||||
console.error('Prepare download killed but download not found', prepareDownload)
|
||||
}
|
||||
},
|
||||
downloadFailed(prepareDownload) {
|
||||
var download = this.$store.getters['downloads/getDownload'](prepareDownload.audiobookId)
|
||||
if (download) {
|
||||
this.$toast.update(download.toastId, { content: `Prepare download failed for "${download.audiobook.book.title}"`, options: { timeout: 5000, type: 'error' } })
|
||||
this.$store.commit('downloads/removeDownload', download)
|
||||
} else {
|
||||
console.error('Prepare download failed but download not found', prepareDownload)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$server.socket) {
|
||||
console.warn('Library Item Page mounted: Server socket not set')
|
||||
} else {
|
||||
this.$server.socket.on('download_ready', this.downloadReady)
|
||||
this.$server.socket.on('download_killed', this.downloadKilled)
|
||||
this.$server.socket.on('download_failed', this.downloadFailed)
|
||||
this.$server.socket.on('item_updated', this.itemUpdated)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (!this.$server.socket) {
|
||||
console.warn('Library Item Page beforeDestroy: Server socket not set')
|
||||
} else {
|
||||
this.$server.socket.off('download_ready', this.downloadReady)
|
||||
this.$server.socket.off('download_killed', this.downloadKilled)
|
||||
this.$server.socket.off('download_failed', this.downloadFailed)
|
||||
this.$server.socket.off('item_updated', this.itemUpdated)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
32
store/globals.js
Normal file
32
store/globals.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
export const state = () => ({
|
||||
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
|
||||
if (!libraryItem) return placeholder
|
||||
var media = libraryItem.media
|
||||
if (!media || !media.coverPath || media.coverPath === placeholder) return placeholder
|
||||
|
||||
// Absolute URL covers (should no longer be used)
|
||||
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
||||
|
||||
var userToken = rootGetters['user/getToken']
|
||||
var lastUpdate = libraryItem.updatedAt || Date.now()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||
// return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||
}
|
||||
|
||||
var url = new URL(`/api/items/${libraryItem.id}/cover`, rootState.serverUrl)
|
||||
return `${url}?token=${userToken}&ts=${lastUpdate}`
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@ import Vue from 'vue'
|
|||
import { Network } from '@capacitor/network'
|
||||
|
||||
export const state = () => ({
|
||||
streamLibraryItem: null,
|
||||
streamAudiobook: null,
|
||||
playingDownload: null,
|
||||
playOnLoad: false,
|
||||
|
@ -80,6 +81,9 @@ export const mutations = {
|
|||
setPlayOnLoad(state, val) {
|
||||
state.playOnLoad = val
|
||||
},
|
||||
setLibraryItemStream(state, libraryItem) {
|
||||
state.streamLibraryItem = libraryItem
|
||||
},
|
||||
setStreamAudiobook(state, audiobook) {
|
||||
if (audiobook) {
|
||||
state.playingDownload = null
|
||||
|
@ -111,12 +115,6 @@ export const mutations = {
|
|||
state.networkConnected = val.connected
|
||||
state.networkConnectionType = val.connectionType
|
||||
},
|
||||
setStreamListener(state, val) {
|
||||
state.streamListener = val
|
||||
},
|
||||
removeStreamListener(state) {
|
||||
state.streamListener = null
|
||||
},
|
||||
openReader(state, audiobook) {
|
||||
state.selectedBook = audiobook
|
||||
state.showReader = true
|
||||
|
|
|
@ -20,6 +20,10 @@ export const getters = {
|
|||
getToken: (state) => {
|
||||
return state.user ? state.user.token : null
|
||||
},
|
||||
getUserLibraryItemProgress: (state) => (libraryItemId) => {
|
||||
if (!state.user.libraryItemProgress) return null
|
||||
return state.user.libraryItemProgress.find(li => li.id == libraryItemId)
|
||||
},
|
||||
getUserAudiobookData: (state, getters) => (audiobookId) => {
|
||||
return getters.getUserAudiobook(audiobookId)
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue