mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-29 07:04:31 +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)
|
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() {
|
setListeners() {
|
||||||
if (!this.$server.socket) {
|
if (!this.$server.socket) {
|
||||||
console.error('Invalid server socket not set')
|
console.error('Invalid server socket not set')
|
||||||
|
@ -481,6 +469,16 @@ export default {
|
||||||
this.$refs.audioPlayer.terminateStream()
|
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() {
|
mounted() {
|
||||||
|
@ -491,9 +489,9 @@ export default {
|
||||||
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||||
|
|
||||||
this.setListeners()
|
this.setListeners()
|
||||||
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$on('close_stream', this.closeStreamOnly)
|
this.$eventBus.$on('close_stream', this.closeStreamOnly)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||||
this.$store.commit('setStreamListener', this.streamUpdated)
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
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_ready', this.streamReady)
|
||||||
this.$server.socket.off('stream_reset', this.streamReset)
|
this.$server.socket.off('stream_reset', this.streamReset)
|
||||||
}
|
}
|
||||||
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$off('close_stream', this.closeStreamOnly)
|
this.$eventBus.$off('close_stream', this.closeStreamOnly)
|
||||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||||
this.$store.commit('removeStreamListener')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
|
@ -144,19 +144,29 @@ export default {
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.currentSFQueryString = this.buildSearchParams()
|
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 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') {
|
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||||
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)
|
console.error('failed to fetch books', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
this.isFetchingEntities = false
|
this.isFetchingEntities = false
|
||||||
if (this.pendingReset) {
|
if (this.pendingReset) {
|
||||||
this.pendingReset = false
|
this.pendingReset = false
|
||||||
|
@ -390,42 +400,42 @@ export default {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audiobookAdded(audiobook) {
|
libraryItemAdded(libraryItem) {
|
||||||
console.log('Audiobook added', audiobook)
|
console.log('libraryItem added', libraryItem)
|
||||||
// TODO: Check if audiobook would be on this shelf
|
// TODO: Check if item would be on this shelf
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
},
|
},
|
||||||
audiobookUpdated(audiobook) {
|
libraryItemUpdated(libraryItem) {
|
||||||
console.log('Audiobook updated', audiobook)
|
console.log('Item updated', libraryItem)
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
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) {
|
if (indexOf >= 0) {
|
||||||
this.entities[indexOf] = audiobook
|
this.entities[indexOf] = libraryItem
|
||||||
if (this.entityComponentRefs[indexOf]) {
|
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') {
|
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) {
|
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.totalEntities = this.entities.length
|
||||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||||
this.remountEntities()
|
this.executeRebuild()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audiobooksAdded(audiobooks) {
|
libraryItemsAdded(libraryItems) {
|
||||||
console.log('audiobooks added', audiobooks)
|
console.log('items added', libraryItems)
|
||||||
// TODO: Check if audiobook would be on this shelf
|
// TODO: Check if item would be on this shelf
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
},
|
},
|
||||||
audiobooksUpdated(audiobooks) {
|
libraryItemsUpdated(libraryItems) {
|
||||||
audiobooks.forEach((ab) => {
|
libraryItems.forEach((ab) => {
|
||||||
this.audiobookUpdated(ab)
|
this.libraryItemUpdated(ab)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
initListeners() {
|
initListeners() {
|
||||||
|
@ -433,19 +443,17 @@ export default {
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
bookshelf.addEventListener('scroll', this.scroll)
|
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('library-changed', this.libraryChanged)
|
||||||
this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
|
this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
||||||
|
|
||||||
if (this.$server.socket) {
|
if (this.$server.socket) {
|
||||||
this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
|
this.$server.socket.on('item_updated', this.libraryItemUpdated)
|
||||||
this.$server.socket.on('audiobook_added', this.audiobookAdded)
|
this.$server.socket.on('item_added', this.libraryItemAdded)
|
||||||
this.$server.socket.on('audiobook_removed', this.audiobookRemoved)
|
this.$server.socket.on('item_removed', this.libraryItemRemoved)
|
||||||
this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
|
this.$server.socket.on('items_updated', this.libraryItemsUpdated)
|
||||||
this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
|
this.$server.socket.on('items_added', this.libraryItemsAdded)
|
||||||
} else {
|
} else {
|
||||||
console.error('Bookshelf - Socket not initialized')
|
console.error('Bookshelf - Socket not initialized')
|
||||||
}
|
}
|
||||||
|
@ -455,16 +463,17 @@ export default {
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
bookshelf.removeEventListener('scroll', this.scroll)
|
bookshelf.removeEventListener('scroll', this.scroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$off('library-changed', this.libraryChanged)
|
this.$eventBus.$off('library-changed', this.libraryChanged)
|
||||||
this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
|
this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
|
||||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
||||||
|
|
||||||
if (this.$server.socket) {
|
if (this.$server.socket) {
|
||||||
this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
|
this.$server.socket.off('item_updated', this.libraryItemUpdated)
|
||||||
this.$server.socket.off('audiobook_added', this.audiobookAdded)
|
this.$server.socket.off('item_added', this.libraryItemAdded)
|
||||||
this.$server.socket.off('audiobook_removed', this.audiobookRemoved)
|
this.$server.socket.off('item_removed', this.libraryItemRemoved)
|
||||||
this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated)
|
this.$server.socket.off('items_updated', this.libraryItemsUpdated)
|
||||||
this.$server.socket.off('audiobooks_added', this.audiobooksAdded)
|
this.$server.socket.off('items_added', this.libraryItemsAdded)
|
||||||
} else {
|
} else {
|
||||||
console.error('Bookshelf - Socket not initialized')
|
console.error('Bookshelf - Socket not initialized')
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<div class="bookshelfRow flex items-end px-3 max-w-full overflow-x-auto" :style="{ height: shelfHeight + 'px' }">
|
<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">
|
<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" />
|
<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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,27 @@
|
||||||
<template>
|
<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 -->
|
<!-- 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 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 class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</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 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' }">
|
<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>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</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 -->
|
<!-- 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' }">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Downloaded indicator icon -->
|
<!-- No progress shown for collapsed series in library -->
|
||||||
<div v-if="hasDownload" class="absolute z-10" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }">
|
<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>
|
||||||
<span class="material-icons text-success" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">download_done</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Progress bar -->
|
<!-- Error widget -->
|
||||||
<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>
|
<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">
|
||||||
<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">
|
|
||||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||||
</div>
|
</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>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from 'vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
index: Number,
|
index: Number,
|
||||||
|
@ -55,15 +67,18 @@ export default {
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio: Number,
|
bookCoverAspectRatio: Number,
|
||||||
showVolumeNumber: Boolean,
|
showVolumeNumber: Boolean,
|
||||||
|
bookshelfView: Number,
|
||||||
bookMount: {
|
bookMount: {
|
||||||
|
// Book can be passed as prop or set with setEntity()
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
}
|
},
|
||||||
|
orderBy: String,
|
||||||
|
filterBy: String,
|
||||||
|
sortingIgnorePrefix: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isHovering: false,
|
|
||||||
isMoreMenuOpen: false,
|
|
||||||
isProcessingReadUpdate: false,
|
isProcessingReadUpdate: false,
|
||||||
audiobook: null,
|
audiobook: null,
|
||||||
imageReady: false,
|
imageReady: false,
|
||||||
|
@ -73,87 +88,145 @@ export default {
|
||||||
showCoverBg: false
|
showCoverBg: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
bookMount: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.audiobook = newVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
_audiobook() {
|
showExperimentalFeatures() {
|
||||||
|
return this.store.state.showExperimentalFeatures
|
||||||
|
},
|
||||||
|
_libraryItem() {
|
||||||
return this.audiobook || {}
|
return this.audiobook || {}
|
||||||
},
|
},
|
||||||
|
media() {
|
||||||
|
return this._libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
return '/book_placeholder.jpg'
|
return '/book_placeholder.jpg'
|
||||||
},
|
},
|
||||||
hasDownload() {
|
|
||||||
return !!this._audiobook.download
|
|
||||||
},
|
|
||||||
downloadedCover() {
|
|
||||||
if (!this._audiobook.download) return null
|
|
||||||
return this._audiobook.download.cover
|
|
||||||
},
|
|
||||||
bookCoverSrc() {
|
bookCoverSrc() {
|
||||||
if (this.downloadedCover) return this.downloadedCover
|
return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
||||||
return this.store.getters['audiobooks/getBookCoverSrc'](this._audiobook, this.placeholderUrl)
|
|
||||||
},
|
},
|
||||||
audiobookId() {
|
libraryItemId() {
|
||||||
return this._audiobook.id
|
return this._libraryItem.id
|
||||||
|
},
|
||||||
|
series() {
|
||||||
|
return this.mediaMetadata.series
|
||||||
|
},
|
||||||
|
libraryId() {
|
||||||
|
return this._libraryItem.libraryId
|
||||||
},
|
},
|
||||||
hasEbook() {
|
hasEbook() {
|
||||||
return this._audiobook.numEbooks
|
if (!this.media.ebooks) return 0
|
||||||
|
return this.media.ebooks.length
|
||||||
},
|
},
|
||||||
hasTracks() {
|
hasAudiobook() {
|
||||||
return this._audiobook.numTracks
|
if (!this.media.audiobooks) return 0
|
||||||
|
return this.media.audiobooks.length
|
||||||
},
|
},
|
||||||
book() {
|
processingBatch() {
|
||||||
return this._audiobook.book || {}
|
return this.store.state.processingBatch
|
||||||
|
},
|
||||||
|
booksInSeries() {
|
||||||
|
// Only added to audiobook object when collapseSeries is enabled
|
||||||
|
return this._libraryItem.booksInSeries
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.book.cover
|
return !!this.media.coverPath
|
||||||
},
|
},
|
||||||
squareAspectRatio() {
|
squareAspectRatio() {
|
||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.squareAspectRatio ? 160 : 100
|
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||||
return this.width / baseSize
|
return this.width / baseSize
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book.title || ''
|
return this.mediaMetadata.title || ''
|
||||||
|
},
|
||||||
|
playIconFontSize() {
|
||||||
|
return Math.max(2, 3 * this.sizeMultiplier)
|
||||||
|
},
|
||||||
|
authors() {
|
||||||
|
return this.mediaMetadata.authors || []
|
||||||
},
|
},
|
||||||
author() {
|
author() {
|
||||||
return this.book.author
|
return this.authors.map((au) => au.name).join(', ')
|
||||||
},
|
|
||||||
authorFL() {
|
|
||||||
return this.book.authorFL || this.author
|
|
||||||
},
|
},
|
||||||
authorLF() {
|
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() {
|
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() {
|
userProgress() {
|
||||||
return this.store.getters['user/getUserAudiobook'](this.audiobookId)
|
return this.store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||||
},
|
},
|
||||||
userIsRead() {
|
itemIsFinished() {
|
||||||
return this.userProgress ? !!this.userProgress.isRead : false
|
return this.userProgress ? !!this.userProgress.isFinished : false
|
||||||
},
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||||
},
|
},
|
||||||
isStreaming() {
|
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() {
|
isMissing() {
|
||||||
return this._audiobook.isMissing
|
return this._libraryItem.isMissing
|
||||||
},
|
},
|
||||||
isInvalid() {
|
isInvalid() {
|
||||||
return this._audiobook.isInvalid
|
return this._libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
hasMissingParts() {
|
hasMissingParts() {
|
||||||
return this._audiobook.hasMissingParts
|
return this._libraryItem.hasMissingParts
|
||||||
},
|
},
|
||||||
hasInvalidParts() {
|
hasInvalidParts() {
|
||||||
return this._audiobook.hasInvalidParts
|
return this._libraryItem.hasInvalidParts
|
||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||||
|
@ -168,6 +241,15 @@ export default {
|
||||||
}
|
}
|
||||||
return txt || 'Unknown Error'
|
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() {
|
store() {
|
||||||
return this.$store || this.$nuxt.$store
|
return this.$store || this.$nuxt.$store
|
||||||
},
|
},
|
||||||
|
@ -206,11 +288,21 @@ export default {
|
||||||
return this.title
|
return this.title
|
||||||
},
|
},
|
||||||
authorCleaned() {
|
authorCleaned() {
|
||||||
if (!this.authorFL) return ''
|
if (!this.author) return ''
|
||||||
if (this.authorFL.length > 30) {
|
if (this.author.length > 30) {
|
||||||
return this.authorFL.slice(0, 27) + '...'
|
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: {
|
methods: {
|
||||||
|
@ -218,8 +310,8 @@ export default {
|
||||||
this.isSelectionMode = val
|
this.isSelectionMode = val
|
||||||
if (!val) this.selected = false
|
if (!val) this.selected = false
|
||||||
},
|
},
|
||||||
setEntity(audiobook) {
|
setEntity(libraryItem) {
|
||||||
this.audiobook = audiobook
|
this.audiobook = libraryItem
|
||||||
},
|
},
|
||||||
clickCard(e) {
|
clickCard(e) {
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
|
@ -228,13 +320,85 @@ export default {
|
||||||
this.selectBtnClick()
|
this.selectBtnClick()
|
||||||
} else {
|
} else {
|
||||||
var router = this.$router || this.$nuxt.$router
|
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() {
|
selectBtnClick() {
|
||||||
|
if (this.processingBatch) return
|
||||||
this.selected = !this.selected
|
this.selected = !this.selected
|
||||||
this.$emit('select', this.audiobook)
|
this.$emit('select', this.audiobook)
|
||||||
},
|
},
|
||||||
|
play() {
|
||||||
|
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
|
eventBus.$emit('play-item', this.libraryItemId)
|
||||||
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
// destroy the vue listeners, etc
|
// destroy the vue listeners, etc
|
||||||
this.$destroy()
|
this.$destroy()
|
||||||
|
|
|
@ -5,20 +5,11 @@
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</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'" />
|
<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 && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<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>
|
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<div class="la-ball-spin-clockwise la-sm">
|
<widgets-loading-spinner />
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -44,11 +35,10 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
audiobook: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
authorOverride: String,
|
|
||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 120
|
default: 120
|
||||||
|
@ -76,12 +66,15 @@ export default {
|
||||||
height() {
|
height() {
|
||||||
return this.width * this.bookCoverAspectRatio
|
return this.width * this.bookCoverAspectRatio
|
||||||
},
|
},
|
||||||
book() {
|
media() {
|
||||||
if (!this.audiobook) return {}
|
if (!this.libraryItem) return {}
|
||||||
return this.audiobook.book || {}
|
return this.libraryItem.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.book.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
},
|
},
|
||||||
titleCleaned() {
|
titleCleaned() {
|
||||||
if (this.title.length > 60) {
|
if (this.title.length > 60) {
|
||||||
|
@ -89,9 +82,11 @@ export default {
|
||||||
}
|
}
|
||||||
return this.title
|
return this.title
|
||||||
},
|
},
|
||||||
|
authors() {
|
||||||
|
return this.mediaMetadata.authors || []
|
||||||
|
},
|
||||||
author() {
|
author() {
|
||||||
if (this.authorOverride) return this.authorOverride
|
return this.authors.map((au) => au.name).join(', ')
|
||||||
return this.book.author || 'Unknown'
|
|
||||||
},
|
},
|
||||||
authorCleaned() {
|
authorCleaned() {
|
||||||
if (this.author.length > 30) {
|
if (this.author.length > 30) {
|
||||||
|
@ -104,15 +99,15 @@ export default {
|
||||||
},
|
},
|
||||||
fullCoverUrl() {
|
fullCoverUrl() {
|
||||||
if (this.downloadCover) return this.downloadCover
|
if (this.downloadCover) return this.downloadCover
|
||||||
if (!this.audiobook) return null
|
if (!this.libraryItem) return null
|
||||||
var store = this.$store || this.$nuxt.$store
|
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() {
|
cover() {
|
||||||
return this.book.cover || this.placeholderUrl
|
return this.media.coverPath || this.placeholderUrl
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.book.cover
|
return !!this.media.coverPath
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||||
|
@ -140,7 +135,6 @@ export default {
|
||||||
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
|
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hideCoverBg() {},
|
|
||||||
imageLoaded() {
|
imageLoaded() {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
@ -170,214 +164,3 @@ export default {
|
||||||
}
|
}
|
||||||
</script>
|
</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 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" />
|
<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 :library-item="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 v-if="books.length > 1" :library-item="books[1]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
<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" />
|
<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,
|
width: Number,
|
||||||
height: Number,
|
height: Number,
|
||||||
|
groupTo: String,
|
||||||
bookCoverAspectRatio: Number
|
bookCoverAspectRatio: Number
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
@ -31,7 +32,6 @@ export default {
|
||||||
isFannedOut: false,
|
isFannedOut: false,
|
||||||
isDetached: false,
|
isDetached: false,
|
||||||
isAttaching: false,
|
isAttaching: false,
|
||||||
windowWidth: 0,
|
|
||||||
isInit: false
|
isInit: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -48,8 +48,11 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.width / (100 * 1.6 * 2)
|
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||||
return this.width / 200
|
return this.width / 240
|
||||||
|
},
|
||||||
|
showExperimentalFeatures() {
|
||||||
|
return this.store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
store() {
|
store() {
|
||||||
return this.$store || this.$nuxt.$store
|
return this.$store || this.$nuxt.$store
|
||||||
|
@ -59,44 +62,8 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
getCoverUrl(book) {
|
||||||
return this.store.getters['audiobooks/getBookCoverSrc'](book, '')
|
return this.store.getters['globals/getLibraryItemCoverSrc'](book, '')
|
||||||
},
|
},
|
||||||
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
||||||
var src = coverData.coverUrl
|
var src = coverData.coverUrl
|
||||||
|
@ -156,6 +123,22 @@ export default {
|
||||||
imgdiv.appendChild(img)
|
imgdiv.appendChild(img)
|
||||||
return imgdiv
|
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() {
|
async init() {
|
||||||
if (this.isInit) return
|
if (this.isInit) return
|
||||||
this.isInit = true
|
this.isInit = true
|
||||||
|
@ -168,7 +151,6 @@ export default {
|
||||||
.map((bookItem) => {
|
.map((bookItem) => {
|
||||||
return {
|
return {
|
||||||
id: bookItem.id,
|
id: bookItem.id,
|
||||||
volumeNumber: bookItem.book ? bookItem.book.volumeNumber : null,
|
|
||||||
coverUrl: this.getCoverUrl(bookItem)
|
coverUrl: this.getCoverUrl(bookItem)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -179,6 +161,8 @@ export default {
|
||||||
}
|
}
|
||||||
this.noValidCovers = false
|
this.noValidCovers = false
|
||||||
|
|
||||||
|
validCovers = validCovers.slice(0, 10)
|
||||||
|
|
||||||
var coverWidth = this.width
|
var coverWidth = this.width
|
||||||
var widthPer = this.width
|
var widthPer = this.width
|
||||||
if (validCovers.length > 1) {
|
if (validCovers.length > 1) {
|
||||||
|
@ -189,7 +173,7 @@ export default {
|
||||||
this.offsetIncrement = widthPer
|
this.offsetIncrement = widthPer
|
||||||
|
|
||||||
var outerdiv = document.createElement('div')
|
var outerdiv = document.createElement('div')
|
||||||
outerdiv.id = `group-cover-${this.id || this.$encode(this.name)}`
|
outerdiv.id = `group-cover-${this.id}`
|
||||||
this.coverWrapperEl = outerdiv
|
this.coverWrapperEl = outerdiv
|
||||||
outerdiv.className = 'w-full h-full relative box-shadow-book'
|
outerdiv.className = 'w-full h-full relative box-shadow-book'
|
||||||
|
|
||||||
|
@ -211,9 +195,7 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {},
|
||||||
this.windowWidth = window.innerWidth
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.coverWrapperEl) this.coverWrapperEl.remove()
|
if (this.coverWrapperEl) this.coverWrapperEl.remove()
|
||||||
if (this.coverImageEls && this.coverImageEls.length) {
|
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) {
|
async connected(isConnected) {
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
console.log('[Default] Connected socket sync user ab data')
|
console.log('[Default] Connected socket sync user ab data')
|
||||||
this.$store.dispatch('user/syncUserAudiobookData')
|
// this.$store.dispatch('user/syncUserAudiobookData')
|
||||||
|
|
||||||
this.initSocketListeners()
|
this.initSocketListeners()
|
||||||
|
|
||||||
|
@ -326,51 +326,49 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audiobookAdded(audiobook) {
|
// audiobookAdded(audiobook) {
|
||||||
this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
// this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
||||||
},
|
// },
|
||||||
audiobookUpdated(audiobook) {
|
// audiobookUpdated(audiobook) {
|
||||||
this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
// this.$store.commit('libraries/updateFilterDataWithAudiobook', audiobook)
|
||||||
},
|
// },
|
||||||
audiobookRemoved(audiobook) {
|
itemRemoved(libraryItem) {
|
||||||
if (this.$route.name.startsWith('audiobook')) {
|
if (this.$route.name.startsWith('item')) {
|
||||||
if (this.$route.params.id === audiobook.id) {
|
if (this.$route.params.id === libraryItem.id) {
|
||||||
this.$router.replace(`/bookshelf`)
|
this.$router.replace(`/bookshelf`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audiobooksAdded(audiobooks) {
|
// audiobooksAdded(audiobooks) {
|
||||||
audiobooks.forEach((ab) => {
|
// audiobooks.forEach((ab) => {
|
||||||
this.audiobookAdded(ab)
|
// this.audiobookAdded(ab)
|
||||||
})
|
// })
|
||||||
},
|
// },
|
||||||
audiobooksUpdated(audiobooks) {
|
// audiobooksUpdated(audiobooks) {
|
||||||
audiobooks.forEach((ab) => {
|
// audiobooks.forEach((ab) => {
|
||||||
this.audiobookUpdated(ab)
|
// this.audiobookUpdated(ab)
|
||||||
})
|
// })
|
||||||
},
|
// },
|
||||||
userLoggedOut() {
|
userLoggedOut() {
|
||||||
// Only cancels stream if streamining not playing downloaded
|
// Only cancels stream if streamining not playing downloaded
|
||||||
this.$eventBus.$emit('close_stream')
|
this.$eventBus.$emit('close_stream')
|
||||||
},
|
},
|
||||||
initSocketListeners() {
|
initSocketListeners() {
|
||||||
if (this.$server.socket) {
|
if (this.$server.socket) {
|
||||||
// Audiobook Listeners
|
// this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
|
||||||
this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
|
// this.$server.socket.on('audiobook_added', this.audiobookAdded)
|
||||||
this.$server.socket.on('audiobook_added', this.audiobookAdded)
|
this.$server.socket.on('item_removed', this.itemRemoved)
|
||||||
this.$server.socket.on('audiobook_removed', this.audiobookRemoved)
|
// this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
|
||||||
this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
|
// this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
|
||||||
this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeSocketListeners() {
|
removeSocketListeners() {
|
||||||
if (this.$server.socket) {
|
if (this.$server.socket) {
|
||||||
// Audiobook Listeners
|
// this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
|
||||||
this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
|
// this.$server.socket.off('audiobook_added', this.audiobookAdded)
|
||||||
this.$server.socket.off('audiobook_added', this.audiobookAdded)
|
this.$server.socket.off('item_removed', this.itemRemoved)
|
||||||
this.$server.socket.off('audiobook_removed', this.audiobookRemoved)
|
// this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated)
|
||||||
this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated)
|
// this.$server.socket.off('audiobooks_added', this.audiobooksAdded)
|
||||||
this.$server.socket.off('audiobooks_added', this.audiobooksAdded)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -382,6 +380,7 @@ export default {
|
||||||
console.log('Syncing on default mount')
|
console.log('Syncing on default mount')
|
||||||
this.connected(true)
|
this.connected(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$server.on('logout', this.userLoggedOut)
|
this.$server.on('logout', this.userLoggedOut)
|
||||||
this.$server.on('connected', this.connected)
|
this.$server.on('connected', this.connected)
|
||||||
this.$server.on('connectionFailed', this.socketConnectionFailed)
|
this.$server.on('connectionFailed', this.socketConnectionFailed)
|
||||||
|
|
|
@ -28,16 +28,7 @@ export default {
|
||||||
if (this.entityComponentRefs[index]) {
|
if (this.entityComponentRefs[index]) {
|
||||||
var bookComponent = this.entityComponentRefs[index]
|
var bookComponent = this.entityComponentRefs[index]
|
||||||
shelfEl.appendChild(bookComponent.$el)
|
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.setSelectionMode(false)
|
||||||
}
|
|
||||||
bookComponent.isHovering = false
|
bookComponent.isHovering = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -78,12 +69,6 @@ export default {
|
||||||
if (this.entities[index]) {
|
if (this.entities[index]) {
|
||||||
instance.setEntity(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: {
|
methods: {
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
var categories = await this.$axios
|
var categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/categories`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
@ -131,6 +131,7 @@ export default {
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
this.shelves = categories
|
this.shelves = categories
|
||||||
|
console.log('Shelves', this.shelves)
|
||||||
},
|
},
|
||||||
async socketInit(isConnected) {
|
async socketInit(isConnected) {
|
||||||
if (isConnected && this.currentLibraryId) {
|
if (isConnected && this.currentLibraryId) {
|
||||||
|
|
|
@ -6,9 +6,9 @@
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="absolute top-0 left-0 w-full p-6 flex items-center flex-col justify-center z-0 short:hidden">
|
<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" />
|
<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>
|
</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>
|
<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>
|
<p>Connecting socket..</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!loggedIn" class="mt-8 bg-primary overflow-hidden shadow rounded-lg p-6 w-full">
|
<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">
|
<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" />
|
<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">
|
<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'
|
import { Network } from '@capacitor/network'
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
|
streamLibraryItem: null,
|
||||||
streamAudiobook: null,
|
streamAudiobook: null,
|
||||||
playingDownload: null,
|
playingDownload: null,
|
||||||
playOnLoad: false,
|
playOnLoad: false,
|
||||||
|
@ -80,6 +81,9 @@ export const mutations = {
|
||||||
setPlayOnLoad(state, val) {
|
setPlayOnLoad(state, val) {
|
||||||
state.playOnLoad = val
|
state.playOnLoad = val
|
||||||
},
|
},
|
||||||
|
setLibraryItemStream(state, libraryItem) {
|
||||||
|
state.streamLibraryItem = libraryItem
|
||||||
|
},
|
||||||
setStreamAudiobook(state, audiobook) {
|
setStreamAudiobook(state, audiobook) {
|
||||||
if (audiobook) {
|
if (audiobook) {
|
||||||
state.playingDownload = null
|
state.playingDownload = null
|
||||||
|
@ -111,12 +115,6 @@ export const mutations = {
|
||||||
state.networkConnected = val.connected
|
state.networkConnected = val.connected
|
||||||
state.networkConnectionType = val.connectionType
|
state.networkConnectionType = val.connectionType
|
||||||
},
|
},
|
||||||
setStreamListener(state, val) {
|
|
||||||
state.streamListener = val
|
|
||||||
},
|
|
||||||
removeStreamListener(state) {
|
|
||||||
state.streamListener = null
|
|
||||||
},
|
|
||||||
openReader(state, audiobook) {
|
openReader(state, audiobook) {
|
||||||
state.selectedBook = audiobook
|
state.selectedBook = audiobook
|
||||||
state.showReader = true
|
state.showReader = true
|
||||||
|
|
|
@ -20,6 +20,10 @@ export const getters = {
|
||||||
getToken: (state) => {
|
getToken: (state) => {
|
||||||
return state.user ? state.user.token : null
|
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) => {
|
getUserAudiobookData: (state, getters) => (audiobookId) => {
|
||||||
return getters.getUserAudiobook(audiobookId)
|
return getters.getUserAudiobook(audiobookId)
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue