mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-27 09:08:57 +01:00
Fix:Native audio player set time and play on load #227, Change:Mobile UI updates & cleanup old bookshelf
This commit is contained in:
parent
4592e1f494
commit
6d5f6bc46e
@ -22,7 +22,7 @@
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="updateVolume" />
|
||||
</div>
|
||||
|
||||
<div class="flex pb-8 sm:pb-4 md:pb-2">
|
||||
<div class="flex pb-4 md:pb-2">
|
||||
<div class="flex-grow" />
|
||||
<template v-if="!loading">
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-8" @mousedown.prevent @mouseup.prevent @click.stop="restart">
|
||||
@ -82,7 +82,7 @@
|
||||
<p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||
</div>
|
||||
|
||||
<audio ref="audio" @progress="progress" @timeupdate="timeupdate" @loadedmetadata="audioLoadedMetadata" @loadeddata="audioLoadedData" @play="audioPlayed" @pause="audioPaused" @error="audioError" @ended="audioEnded" @stalled="audioStalled" @suspend="audioSuspended" />
|
||||
<audio ref="audio" @progress="progress" @timeupdate="timeupdate" @loadedmetadata="audioLoadedMetadata" @play="audioPlayed" @pause="audioPaused" @error="audioError" @ended="audioEnded" @stalled="audioStalled" @suspend="audioSuspended" />
|
||||
|
||||
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||
</div>
|
||||
@ -109,6 +109,9 @@ export default {
|
||||
return {
|
||||
hlsInstance: null,
|
||||
staleHlsInstance: null,
|
||||
usingNativeAudioPlayer: false,
|
||||
playOnLoad: false,
|
||||
startTime: 0,
|
||||
volume: 1,
|
||||
playbackRate: 1,
|
||||
trackWidth: 0,
|
||||
@ -194,7 +197,7 @@ export default {
|
||||
},
|
||||
audioStalled() {
|
||||
if (!this.$refs.audio) return
|
||||
console.warn('Audio Ended', this.$refs.audio.paused, this.$refs.audio.currentTime)
|
||||
console.warn('Audio Stalled', this.$refs.audio.paused, this.$refs.audio.currentTime)
|
||||
},
|
||||
audioSuspended() {
|
||||
if (!this.$refs.audio) return
|
||||
@ -555,11 +558,13 @@ export default {
|
||||
this.playedTrackWidth = ptWidth
|
||||
},
|
||||
audioLoadedMetadata() {
|
||||
console.log('Audio METADATA Loaded, total duration', this.audioEl.duration)
|
||||
this.totalDuration = this.audioEl.duration
|
||||
this.$emit('loaded', this.totalDuration)
|
||||
if (this.usingNativeAudioPlayer) {
|
||||
this.audioEl.currentTime = this.startTime
|
||||
this.play()
|
||||
}
|
||||
},
|
||||
audioLoadedData() {},
|
||||
set(url, currentTime, playOnLoad = false) {
|
||||
if (this.hlsInstance) {
|
||||
this.terminateStream()
|
||||
@ -570,6 +575,8 @@ export default {
|
||||
}
|
||||
this.listeningTimeSinceLastUpdate = 0
|
||||
|
||||
this.playOnLoad = playOnLoad
|
||||
this.startTime = currentTime
|
||||
this.url = url
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
url = `${process.env.serverUrl}${url}`
|
||||
@ -584,6 +591,7 @@ export default {
|
||||
// iOS does not support Media Elements but allows for HLS in the native audio player
|
||||
if (!Hls.isSupported()) {
|
||||
console.warn('HLS is not supported - fallback to using audio element')
|
||||
this.usingNativeAudioPlayer = true
|
||||
audio.src = this.src + '?token=' + this.token
|
||||
audio.currentTime = currentTime
|
||||
return
|
||||
@ -604,10 +612,10 @@ export default {
|
||||
// console.log('[HLS] MEDIA ATTACHED')
|
||||
this.hlsInstance.loadSource(url)
|
||||
|
||||
this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, function () {
|
||||
this.hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log('[HLS] Manifest Parsed')
|
||||
if (playOnLoad) {
|
||||
audio.play()
|
||||
this.play()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1,449 +0,0 @@
|
||||
<template>
|
||||
<div id="bookshelf" class="overflow-hidden relative block max-h-full">
|
||||
<div ref="wrapper" class="h-full w-full relative" :class="isGridMode ? 'overflow-y-scroll' : 'overflow-hidden'">
|
||||
<!-- Cover size widget -->
|
||||
<div v-show="!isSelectionMode && isGridMode" class="fixed bottom-4 right-4 z-30">
|
||||
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span>
|
||||
<p class="px-2 font-mono">{{ bookCoverWidth }}</p>
|
||||
<span class="material-icons" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize">add</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Experimental Bookshelf Texture -->
|
||||
<div v-show="showExperimentalFeatures" class="fixed bottom-4 right-28 z-40">
|
||||
<div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
|
||||
</div>
|
||||
|
||||
<div v-if="!audiobooks.length" class="w-full flex flex-col items-center justify-center py-12">
|
||||
<p class="text-center text-2xl font-book mb-4 py-4">Your Audiobookshelf is empty!</p>
|
||||
<div class="flex">
|
||||
<ui-btn to="/config" color="primary" class="w-52 mr-2" @click="scan">Configure Scanner</ui-btn>
|
||||
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="page === 'search'" id="bookshelf-categorized" class="w-full flex flex-col items-center">
|
||||
<template v-for="(shelf, index) in categorizedShelves">
|
||||
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" />
|
||||
</template>
|
||||
<div v-show="!categorizedShelves.length" class="w-full py-16 text-center text-xl">
|
||||
<div class="py-4 mb-6"><p class="text-2xl">No Results</p></div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="w-full">
|
||||
<template v-if="viewMode === 'grid'">
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<template v-for="(shelf, index) in shelves">
|
||||
<div :key="index" class="w-full bookshelfRow relative">
|
||||
<div class="flex justify-center items-center">
|
||||
<template v-for="entity in shelf">
|
||||
<cards-collection-card v-if="isCollections" :key="entity.id" :width="bookCoverWidth" :collection="entity" @click="clickGroup" />
|
||||
<cards-group-card v-else-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" />
|
||||
|
||||
<!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> -->
|
||||
<cards-book-card v-else :ref="`book-card-${entity.id}`" :key="entity.id" :is-bookshelf-book="!isSeries" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" @hook:mounted="mountedBookCard(entity)" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-10" :class="isCollections || isSeriesGroups ? 'h-6' : 'h-4'" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<app-book-list :books="entities" />
|
||||
</template>
|
||||
<div v-show="!shelves.length" class="w-full py-16 text-center text-xl">
|
||||
<div v-if="page === 'search'" class="py-4 mb-6"><p class="text-2xl">No Results</p></div>
|
||||
<div v-else class="py-4 capitalize">No {{ showGroups ? page : 'Audiobooks' }}</div>
|
||||
<ui-btn v-if="!showGroups && (filterBy !== 'all' || keywordFilter)" @click="clearFilter">Clear Filter</ui-btn>
|
||||
<ui-btn v-else-if="page === 'search'" to="/library">Back to Library</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
page: String,
|
||||
selectedSeries: String,
|
||||
searchResults: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
searchQuery: String,
|
||||
viewMode: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shelves: [],
|
||||
currSearchParams: null,
|
||||
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
|
||||
selectedSizeIndex: 3,
|
||||
rowPaddingX: 40,
|
||||
keywordFilterTimeout: null,
|
||||
scannerParseSubtitle: false,
|
||||
wrapperClientWidth: 0,
|
||||
observer: null,
|
||||
booksObserved: [],
|
||||
booksVisible: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
keywordFilter() {
|
||||
this.checkKeywordFilter()
|
||||
},
|
||||
selectedSeries() {
|
||||
this.$nextTick(() => {
|
||||
this.$store.commit('audiobooks/setSelectedSeries', this.selectedSeries)
|
||||
this.setBookshelfEntities()
|
||||
})
|
||||
},
|
||||
searchResults() {
|
||||
this.$nextTick(() => {
|
||||
this.setBookshelfEntities()
|
||||
})
|
||||
},
|
||||
'$route.query.filter'() {
|
||||
if (this.$route.query.filter && this.$route.query.filter !== this.filterBy) {
|
||||
this.$store.dispatch('user/updateUserSettings', { filterBy: this.$route.query.filter })
|
||||
} else if (!this.$route.query.filter && this.filterBy) {
|
||||
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
isGridMode() {
|
||||
return this.viewMode === 'grid'
|
||||
},
|
||||
keywordFilter() {
|
||||
return this.$store.state.audiobooks.keywordFilter
|
||||
},
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||
},
|
||||
audiobooks() {
|
||||
return this.$store.state.audiobooks.audiobooks
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.bookCoverWidth / 120
|
||||
},
|
||||
bookCoverWidth() {
|
||||
var coverWidth = this.availableSizes[this.selectedSizeIndex]
|
||||
return coverWidth
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.bookCoverWidth / 120
|
||||
},
|
||||
paddingX() {
|
||||
return 16 * this.sizeMultiplier
|
||||
},
|
||||
bookWidth() {
|
||||
var coverWidth = this.bookCoverWidth
|
||||
if (this.page === 'collections') coverWidth *= 2
|
||||
var _width = coverWidth + this.paddingX * 2
|
||||
return this.showGroups ? _width * 1.6 : _width
|
||||
},
|
||||
isSelectionMode() {
|
||||
return this.$store.getters['getNumAudiobooksSelected']
|
||||
},
|
||||
filterBy() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
orderDesc() {
|
||||
return this.$store.getters['user/getUserSetting']('orderDesc')
|
||||
},
|
||||
showGroups() {
|
||||
return this.page !== '' && this.page !== 'search' && !this.selectedSeries
|
||||
},
|
||||
isCollections() {
|
||||
return this.page === 'collections'
|
||||
},
|
||||
isSeries() {
|
||||
return this.page === 'series'
|
||||
},
|
||||
isSeriesGroups() {
|
||||
return this.isSeries && !this.selectedSeries
|
||||
},
|
||||
categorizedShelves() {
|
||||
if (this.page !== 'search') return []
|
||||
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
|
||||
const shelves = []
|
||||
|
||||
if (audiobookSearchResults.length) {
|
||||
shelves.push({
|
||||
label: 'Books',
|
||||
books: audiobookSearchResults.map((absr) => absr.audiobook)
|
||||
})
|
||||
}
|
||||
|
||||
if (this.searchResults.series && this.searchResults.series.length) {
|
||||
var seriesGroups = this.searchResults.series.map((seriesResult) => {
|
||||
return {
|
||||
type: 'series',
|
||||
name: seriesResult.series || '',
|
||||
books: seriesResult.audiobooks || []
|
||||
}
|
||||
})
|
||||
shelves.push({
|
||||
label: 'Series',
|
||||
series: seriesGroups
|
||||
})
|
||||
}
|
||||
|
||||
if (this.searchResults.tags && this.searchResults.tags.length) {
|
||||
var tagGroups = this.searchResults.tags.map((tagResult) => {
|
||||
return {
|
||||
type: 'tags',
|
||||
name: tagResult.tag || '',
|
||||
books: tagResult.audiobooks || []
|
||||
}
|
||||
})
|
||||
shelves.push({
|
||||
label: 'Tags',
|
||||
series: tagGroups
|
||||
})
|
||||
}
|
||||
|
||||
return shelves
|
||||
},
|
||||
entities() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showBookshelfTextureModal() {
|
||||
this.$store.commit('globals/setShowBookshelfTextureModal', true)
|
||||
},
|
||||
editBook(audiobook) {
|
||||
var bookIds = this.entities.map((e) => e.id)
|
||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||
this.$store.commit('showEditModal', audiobook)
|
||||
},
|
||||
clickGroup(group) {
|
||||
if (this.page === 'collections') return
|
||||
this.$emit('update:selectedSeries', group.name)
|
||||
},
|
||||
clearFilter() {
|
||||
this.$store.commit('audiobooks/setKeywordFilter', null)
|
||||
if (this.filterBy !== 'all') {
|
||||
this.$store.dispatch('user/updateUserSettings', {
|
||||
filterBy: 'all'
|
||||
})
|
||||
} else {
|
||||
this.setBookshelfEntities()
|
||||
}
|
||||
},
|
||||
checkKeywordFilter() {
|
||||
clearTimeout(this.keywordFilterTimeout)
|
||||
this.keywordFilterTimeout = setTimeout(() => {
|
||||
this.setBookshelfEntities()
|
||||
}, 500)
|
||||
},
|
||||
increaseSize() {
|
||||
this.selectedSizeIndex = Math.min(this.availableSizes.length - 1, this.selectedSizeIndex + 1)
|
||||
this.resize()
|
||||
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
|
||||
},
|
||||
decreaseSize() {
|
||||
this.selectedSizeIndex = Math.max(0, this.selectedSizeIndex - 1)
|
||||
this.resize()
|
||||
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
|
||||
},
|
||||
setBookshelfEntities() {
|
||||
this.wrapperClientWidth = this.$refs.wrapper.clientWidth
|
||||
var width = Math.max(0, this.wrapperClientWidth - this.rowPaddingX * 2)
|
||||
|
||||
var booksPerRow = Math.floor(width / this.bookWidth)
|
||||
|
||||
this.currSearchParams = this.buildSearchParams()
|
||||
|
||||
var entities = this.entities
|
||||
|
||||
var groups = []
|
||||
var currentRow = 0
|
||||
var currentGroup = []
|
||||
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
var row = Math.floor(i / booksPerRow)
|
||||
if (row > currentRow) {
|
||||
groups.push([...currentGroup])
|
||||
currentRow = row
|
||||
currentGroup = []
|
||||
}
|
||||
currentGroup.push(entities[i])
|
||||
}
|
||||
if (currentGroup.length) {
|
||||
groups.push([...currentGroup])
|
||||
}
|
||||
this.shelves = groups
|
||||
},
|
||||
async init() {
|
||||
this.checkUpdateSearchParams()
|
||||
|
||||
this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0
|
||||
|
||||
var bookshelfCoverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||
var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
|
||||
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
|
||||
|
||||
// var isLoading = await this.$store.dispatch('audiobooks/load')
|
||||
// if (!isLoading) {
|
||||
// this.setBookshelfEntities()
|
||||
// }
|
||||
},
|
||||
resize() {
|
||||
this.$nextTick(this.setBookshelfEntities)
|
||||
},
|
||||
audiobooksUpdated() {
|
||||
console.log('[Bookshelf] Audiobooks Updated')
|
||||
this.setBookshelfEntities()
|
||||
},
|
||||
collectionsUpdated() {
|
||||
if (!this.isCollections) return
|
||||
console.log('[Bookshelf] Collections Updated')
|
||||
this.setBookshelfEntities()
|
||||
},
|
||||
buildSearchParams() {
|
||||
if (this.page === 'search' || this.page === 'series' || this.page === 'collections') {
|
||||
return ''
|
||||
}
|
||||
|
||||
let searchParams = new URLSearchParams()
|
||||
if (this.filterBy && this.filterBy !== 'all') {
|
||||
searchParams.set('filter', this.filterBy)
|
||||
}
|
||||
if (this.orderBy) {
|
||||
searchParams.set('order', this.orderBy)
|
||||
searchParams.set('orderdesc', this.orderDesc ? 1 : 0)
|
||||
}
|
||||
return searchParams.toString()
|
||||
},
|
||||
checkUpdateSearchParams() {
|
||||
var newSearchParams = this.buildSearchParams()
|
||||
var currentQueryString = window.location.search
|
||||
|
||||
if (newSearchParams === '') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (newSearchParams !== this.currSearchParams || newSearchParams !== currentQueryString) {
|
||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
||||
window.history.replaceState({ path: newurl }, '', newurl)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
settingsUpdated(settings) {
|
||||
var wasUpdated = this.checkUpdateSearchParams()
|
||||
if (wasUpdated) this.setBookshelfEntities()
|
||||
|
||||
if (settings.bookshelfCoverSize !== this.bookCoverWidth && settings.bookshelfCoverSize !== undefined) {
|
||||
var index = this.availableSizes.indexOf(settings.bookshelfCoverSize)
|
||||
if (index >= 0) {
|
||||
this.selectedSizeIndex = index
|
||||
this.resize()
|
||||
}
|
||||
}
|
||||
},
|
||||
scan() {
|
||||
this.$root.socket.emit('scan', this.$store.state.libraries.currentLibraryId)
|
||||
},
|
||||
mountedBookCard(entity, shouldUnobserve = false) {
|
||||
if (!this.observer) {
|
||||
console.error('Observer not loaded', entity.id)
|
||||
return
|
||||
}
|
||||
var el = document.getElementById(`book-card-${entity.id}`)
|
||||
if (el) {
|
||||
if (shouldUnobserve) {
|
||||
console.warn('Unobserving el', el)
|
||||
this.observer.unobserve(el)
|
||||
}
|
||||
this.observer.observe(el)
|
||||
this.booksObserved.push(entity.id)
|
||||
// console.log('Book observed', this.booksObserved.length)
|
||||
} else {
|
||||
console.error('Could not get book card', entity.id)
|
||||
}
|
||||
},
|
||||
getBookCard(id) {
|
||||
if (!this.$refs[id] || !this.$refs[id].length) {
|
||||
return null
|
||||
}
|
||||
return this.$refs[id][0]
|
||||
},
|
||||
observerCallback(entries, observer) {
|
||||
entries.forEach((entry) => {
|
||||
var bookId = entry.target.getAttribute('data-bookId')
|
||||
if (!bookId) {
|
||||
console.error('Invalid observe no book id', entry)
|
||||
return
|
||||
}
|
||||
var component = this.getBookCard(entry.target.id)
|
||||
if (component) {
|
||||
if (entry.isIntersecting) {
|
||||
if (!this.booksVisible[bookId]) {
|
||||
this.booksVisible[bookId] = true
|
||||
component.setShowCard(true)
|
||||
}
|
||||
} else if (this.booksVisible[bookId]) {
|
||||
this.booksVisible[bookId] = false
|
||||
component.setShowCard(false)
|
||||
}
|
||||
} else {
|
||||
console.error('Could not get book card for id', entry.target.id)
|
||||
}
|
||||
})
|
||||
},
|
||||
initIO() {
|
||||
let observerOptions = {
|
||||
rootMargin: '0px',
|
||||
threshold: 0.1
|
||||
}
|
||||
|
||||
this.observer = new IntersectionObserver(this.observerCallback, observerOptions)
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (this.$refs.wrapper) {
|
||||
if (this.wrapperClientWidth !== this.$refs.wrapper.clientWidth) {
|
||||
this.$nextTick(this.setBookshelfEntities)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.resize)
|
||||
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||
this.$store.commit('user/addCollectionsListener', { id: 'bookshelf', meth: this.collectionsUpdated })
|
||||
|
||||
this.init()
|
||||
this.initIO()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
||||
this.$store.commit('user/removeCollectionsListener', 'bookshelf')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bookshelfRow {
|
||||
background-image: var(--bookshelf-texture-img);
|
||||
}
|
||||
.bookshelfDivider {
|
||||
background: rgb(149, 119, 90);
|
||||
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
||||
box-shadow: 2px 14px 8px #111111aa;
|
||||
}
|
||||
</style>
|
@ -149,7 +149,6 @@ export default {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.shelves = shelves
|
||||
},
|
||||
settingsUpdated(settings) {},
|
||||
|
@ -1,40 +1,28 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem' }" @scroll="scrolled">
|
||||
<div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
|
||||
<div v-if="shelf.type === 'books'" class="flex items-center -mb-2">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" :padding-y="24" :book-cover-aspect-ratio="bookCoverAspectRatio" @edit="editBook" />
|
||||
<div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem', height: shelfHeight + 'px' }" @scroll="scrolled">
|
||||
<div class="w-full h-full pt-6">
|
||||
<div v-if="shelf.type === 'books'" class="flex items-center">
|
||||
<template v-for="(entity, index) in shelf.entities">
|
||||
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectBook" @edit="editBook" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'series'" class="flex items-center -mb-2">
|
||||
<div v-if="shelf.type === 'series'" class="flex items-center">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<cards-group-card :key="entity.name" is-categorized :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" @click="$emit('clickSeries', entity)" />
|
||||
<cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'tags'" class="flex items-center -mb-2">
|
||||
<div v-if="shelf.type === 'tags'" class="flex items-center">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
|
||||
<cards-group-card is-categorized :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'authors'" class="flex items-center -mb-2">
|
||||
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<nuxt-link :key="entity.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(entity.name)}`">
|
||||
<cards-author-card :width="bookCoverWidth" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6" />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="shelf.series" class="flex items-center -mb-2">
|
||||
<template v-for="entity in shelf.series">
|
||||
<cards-group-card is-categorized :key="entity.name" :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" @click="$emit('clickSeries', entity)" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="shelf.tags" class="flex items-center -mb-2">
|
||||
<template v-for="entity in shelf.tags">
|
||||
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
|
||||
<cards-group-card is-categorized :width="bookCoverWidth" :group="entity" :book-cover-aspect-ratio="bookCoverAspectRatio" @hook:updated="updatedBookCard" />
|
||||
<cards-author-card :width="bookCoverWidth" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
@ -49,10 +37,10 @@
|
||||
|
||||
<div class="bookshelfDividerCategorized h-6 w-full absolute bottom-0 left-0 right-0 z-20"></div>
|
||||
|
||||
<div v-show="canScrollLeft && !isScrolling" class="absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left flex items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollLeft">
|
||||
<div v-show="canScrollLeft && !isScrolling" class="hidden sm:flex absolute top-0 left-0 w-32 pr-8 bg-black book-shelf-arrow-left items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollLeft">
|
||||
<span class="material-icons text-6xl text-white">chevron_left</span>
|
||||
</div>
|
||||
<div v-show="canScrollRight && !isScrolling" class="absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right flex items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight">
|
||||
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight">
|
||||
<span class="material-icons text-6xl text-white">chevron_right</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -79,7 +67,18 @@ export default {
|
||||
updateTimer: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isSelectionMode(newVal) {
|
||||
this.updateSelectionMode(newVal)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bookCoverHeight() {
|
||||
return this.bookCoverWidth * this.bookCoverAspectRatio
|
||||
},
|
||||
shelfHeight() {
|
||||
return this.bookCoverHeight + 48
|
||||
},
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||
},
|
||||
@ -89,6 +88,9 @@ export default {
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
isSelectionMode() {
|
||||
return this.$store.getters['getNumAudiobooksSelected'] > 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -97,6 +99,21 @@ export default {
|
||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||
this.$store.commit('showEditModal', audiobook)
|
||||
},
|
||||
updateSelectionMode(val) {
|
||||
var selectedAudiobooks = this.$store.state.selectedAudiobooks
|
||||
if (this.shelf.type === 'books') {
|
||||
this.shelf.entities.forEach((ent) => {
|
||||
var component = this.$refs[`shelf-book-${ent.id}`]
|
||||
if (!component || !component.length) return
|
||||
component = component[0]
|
||||
component.setSelectionMode(val)
|
||||
component.selected = selectedAudiobooks.includes(ent.id)
|
||||
})
|
||||
}
|
||||
},
|
||||
selectBook(audiobook) {
|
||||
this.$store.commit('toggleAudiobookSelected', audiobook.id)
|
||||
},
|
||||
scrolled() {
|
||||
clearTimeout(this.scrollTimer)
|
||||
this.scrollTimer = setTimeout(() => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div id="bookshelf" class="w-full overflow-y-auto">
|
||||
<template v-for="shelf in totalShelves">
|
||||
<div :key="shelf" class="w-full px-8 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
|
||||
<div :key="shelf" class="w-full px-4 sm:px-8 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
|
||||
<!-- <div class="absolute top-0 left-0 bottom-0 p-4 z-10">
|
||||
<p class="text-white text-2xl">{{ shelf }}</p>
|
||||
</div> -->
|
||||
@ -128,14 +128,21 @@ export default {
|
||||
if (this.isCoverSquareAspectRatio) return this.bookWidth
|
||||
return this.bookWidth * 1.6
|
||||
},
|
||||
shelfPadding() {
|
||||
if (this.bookshelfWidth < 640) return 32
|
||||
return 64
|
||||
},
|
||||
totalPadding() {
|
||||
return this.shelfPadding * 2
|
||||
},
|
||||
entityWidth() {
|
||||
if (this.entityName === 'series') return this.bookWidth * 2
|
||||
if (this.entityName === 'collections') return this.bookWidth * 2
|
||||
if (this.entityName === 'series' || this.entityName === 'collections') {
|
||||
if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6
|
||||
return this.bookWidth * 2
|
||||
}
|
||||
return this.bookWidth
|
||||
},
|
||||
entityHeight() {
|
||||
if (this.entityName === 'series') return this.bookHeight
|
||||
if (this.entityName === 'collections') return this.bookHeight
|
||||
return this.bookHeight
|
||||
},
|
||||
shelfDividerHeightIndex() {
|
||||
@ -225,7 +232,6 @@ export default {
|
||||
return
|
||||
}
|
||||
if (payload) {
|
||||
// console.log('Received payload', payload)
|
||||
if (!this.initialized) {
|
||||
this.initialized = true
|
||||
this.totalEntities = payload.total
|
||||
@ -237,7 +243,6 @@ export default {
|
||||
for (let i = 0; i < payload.results.length; i++) {
|
||||
var index = i + startIndex
|
||||
this.entities[index] = payload.results[i]
|
||||
|
||||
if (this.entityComponentRefs[index]) {
|
||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||
}
|
||||
@ -437,7 +442,7 @@ export default {
|
||||
this.mountWindowWidth = window.innerWidth
|
||||
this.bookshelfHeight = clientHeight
|
||||
this.bookshelfWidth = clientWidth
|
||||
this.entitiesPerShelf = Math.floor((this.bookshelfWidth - 64) / this.totalEntityCardWidth)
|
||||
this.entitiesPerShelf = Math.max(1, Math.floor((this.bookshelfWidth - this.shelfPadding) / this.totalEntityCardWidth))
|
||||
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
||||
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
||||
|
||||
@ -526,10 +531,12 @@ export default {
|
||||
this.initListeners()
|
||||
},
|
||||
updated() {
|
||||
if (window.innerWidth > 0 && window.innerWidth !== this.mountWindowWidth) {
|
||||
console.log('Updated window width', window.innerWidth, 'from', this.mountWindowWidth)
|
||||
this.rebuild()
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (window.innerWidth > 0 && window.innerWidth !== this.mountWindowWidth) {
|
||||
console.log('Updated window width', window.innerWidth, 'from', this.mountWindowWidth)
|
||||
this.rebuild()
|
||||
}
|
||||
}, 50)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.destroyEntityComponents()
|
||||
|
@ -1,433 +0,0 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative book-card" :data-bookId="audiobookId" :id="`book-card-${audiobookId}`">
|
||||
<template v-if="!showCard">
|
||||
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }">
|
||||
<div class="bg-bg flex items-center justify-center p-2" :style="{ height: height + 'px', width: width + 'px' }">
|
||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- New Book Flag -->
|
||||
<div v-show="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl z-20">
|
||||
<div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center">
|
||||
<p class="text-center text-sm">New</p>
|
||||
</div>
|
||||
<div class="absolute -bottom-4 left-0 triangle-right" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }">
|
||||
<nuxt-link :to="isSelectionMode ? '' : `/audiobook/${audiobookId}`" class="cursor-pointer">
|
||||
<div class="w-full relative box-shadow-book" :style="{ height: height + 'px' }" @click="clickCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<covers-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
<!-- Hidden SM and DOWN -->
|
||||
<div v-show="isHovering || isSelectionMode || isMoreMenuOpen" class="absolute top-0 left-0 w-full h-full bg-black rounded hidden md:block z-20" :class="overlayWrapperClasslist">
|
||||
<div v-show="showPlayButton" class="h-full flex items-center justify-center">
|
||||
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="play">
|
||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">play_circle_filled</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="showReadButton" class="h-full flex items-center justify-center">
|
||||
<div class="hover:text-gray-200 hover:scale-110 transform duration-200" @click.stop.prevent="clickReadEBook">
|
||||
<span class="material-icons" :style="{ fontSize: playIconFontSize + 'rem' }">auto_stories</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="userCanUpdate" v-show="!isSelectionMode" class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-50" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="editClick">
|
||||
<span class="material-icons" :style="{ fontSize: sizeMultiplier + 'rem' }">edit</span>
|
||||
</div>
|
||||
|
||||
<div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick">
|
||||
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- More Icon -->
|
||||
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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` }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p>
|
||||
</div>
|
||||
|
||||
<!-- EBook Icon -->
|
||||
<div
|
||||
v-if="showSmallEBookIcon"
|
||||
class="absolute rounded-full bg-blue-500 flex items-center justify-center bg-opacity-90 hover:scale-125 transform duration-200 z-10"
|
||||
:style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem`, width: 1.5 * sizeMultiplier + 'rem', height: 1.5 * sizeMultiplier + 'rem' }"
|
||||
@click.stop.prevent="clickReadEBook"
|
||||
>
|
||||
<!-- <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">EBook</p> -->
|
||||
<span class="material-icons text-white" :style="{ fontSize: sizeMultiplier * 1 + 'rem' }">auto_stories</span>
|
||||
</div>
|
||||
|
||||
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10" :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-20">
|
||||
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import MoreMenu from '@/components/widgets/MoreMenu'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
userProgress: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
paddingY: {
|
||||
type: Number,
|
||||
default: 16
|
||||
},
|
||||
isBookshelfBook: Boolean,
|
||||
showVolumeNumber: Boolean,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showCard: false,
|
||||
isHovering: false,
|
||||
isMoreMenuOpen: false,
|
||||
isProcessingReadUpdate: false,
|
||||
rescanning: false,
|
||||
timesVisible: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
isNew() {
|
||||
return this.tags.includes('New')
|
||||
},
|
||||
tags() {
|
||||
return this.audiobook.tags || []
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook.id
|
||||
},
|
||||
hasEbook() {
|
||||
return this.audiobook.numEbooks
|
||||
},
|
||||
hasTracks() {
|
||||
return this.audiobook.numTracks
|
||||
},
|
||||
isSelectionMode() {
|
||||
return !!this.selectedAudiobooks.length
|
||||
},
|
||||
selectedAudiobooks() {
|
||||
return this.$store.state.selectedAudiobooks
|
||||
},
|
||||
selected() {
|
||||
return this.$store.getters['getIsAudiobookSelected'](this.audiobookId)
|
||||
},
|
||||
processingBatch() {
|
||||
return this.$store.state.processingBatch
|
||||
},
|
||||
book() {
|
||||
return this.audiobook.book || {}
|
||||
},
|
||||
squareAspectRatio() {
|
||||
return this.bookCoverAspectRatio === 1
|
||||
},
|
||||
height() {
|
||||
return this.width * this.bookCoverAspectRatio
|
||||
},
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||
return this.width / baseSize
|
||||
},
|
||||
paddingX() {
|
||||
return 16 * this.sizeMultiplier
|
||||
},
|
||||
title() {
|
||||
return this.book.title
|
||||
},
|
||||
playIconFontSize() {
|
||||
return Math.max(2, 3 * this.sizeMultiplier)
|
||||
},
|
||||
author() {
|
||||
return this.book.author
|
||||
},
|
||||
authorFL() {
|
||||
return this.book.authorFL || this.author
|
||||
},
|
||||
authorLF() {
|
||||
return this.book.authorLF || this.author
|
||||
},
|
||||
authorFormat() {
|
||||
if (!this.orderBy || !this.orderBy.startsWith('book.author')) return null
|
||||
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
|
||||
},
|
||||
volumeNumber() {
|
||||
return this.book.volumeNumber || null
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
filterBy() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||
},
|
||||
userProgressPercent() {
|
||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||
},
|
||||
userIsRead() {
|
||||
return this.userProgress ? !!this.userProgress.isRead : false
|
||||
},
|
||||
showError() {
|
||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||
},
|
||||
isStreaming() {
|
||||
return this.$store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
||||
},
|
||||
showReadButton() {
|
||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||
},
|
||||
showPlayButton() {
|
||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasTracks && !this.isStreaming
|
||||
},
|
||||
showSmallEBookIcon() {
|
||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||
},
|
||||
isMissing() {
|
||||
return this.audiobook.isMissing
|
||||
},
|
||||
isInvalid() {
|
||||
return this.audiobook.isInvalid
|
||||
},
|
||||
hasMissingParts() {
|
||||
return this.audiobook.hasMissingParts
|
||||
},
|
||||
hasInvalidParts() {
|
||||
return this.audiobook.hasInvalidParts
|
||||
},
|
||||
errorText() {
|
||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||
else if (this.isInvalid) return 'Audiobook has no audio tracks & ebook'
|
||||
var txt = ''
|
||||
if (this.hasMissingParts) {
|
||||
txt = `${this.hasMissingParts} missing parts.`
|
||||
}
|
||||
if (this.hasInvalidParts) {
|
||||
if (this.hasMissingParts) txt += ' '
|
||||
txt += `${this.hasInvalidParts} invalid parts.`
|
||||
}
|
||||
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
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.$store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
userIsRoot() {
|
||||
return this.$store.getters['user/getIsRoot']
|
||||
},
|
||||
moreMenuItems() {
|
||||
var items = [
|
||||
{
|
||||
func: 'toggleRead',
|
||||
text: `Mark as ${this.userIsRead ? 'Not Read' : 'Read'}`
|
||||
},
|
||||
{
|
||||
func: 'openCollections',
|
||||
text: 'Add to Collection'
|
||||
}
|
||||
]
|
||||
if (this.userCanUpdate) {
|
||||
if (this.hasTracks) {
|
||||
items.push({
|
||||
func: 'showEditModalTracks',
|
||||
text: 'Tracks'
|
||||
})
|
||||
}
|
||||
items.push({
|
||||
func: 'showEditModalMatch',
|
||||
text: 'Match'
|
||||
})
|
||||
}
|
||||
if (this.userCanDownload) {
|
||||
items.push({
|
||||
func: 'showEditModalDownload',
|
||||
text: 'Download'
|
||||
})
|
||||
}
|
||||
if (this.userIsRoot) {
|
||||
items.push({
|
||||
func: 'rescan',
|
||||
text: 'Re-Scan'
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setShowCard(val) {
|
||||
if (val) this.timesVisible++
|
||||
this.showCard = val
|
||||
},
|
||||
selectBtnClick() {
|
||||
if (this.processingBatch) return
|
||||
this.$store.commit('toggleAudiobookSelected', this.audiobookId)
|
||||
},
|
||||
clickError(e) {
|
||||
e.stopPropagation()
|
||||
this.$router.push(`/audiobook/${this.audiobookId}`)
|
||||
},
|
||||
play() {
|
||||
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||
this.$root.socket.emit('open_stream', this.audiobookId)
|
||||
},
|
||||
editClick() {
|
||||
// this.$store.commit('showEditModal', this.audiobook)
|
||||
this.$emit('edit', this.audiobook)
|
||||
},
|
||||
clickCard(e) {
|
||||
if (this.isSelectionMode) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
this.selectBtnClick()
|
||||
}
|
||||
},
|
||||
clickReadEBook() {
|
||||
this.$store.commit('showEReader', this.audiobook)
|
||||
},
|
||||
toggleRead() {
|
||||
// More menu func
|
||||
var updatePayload = {
|
||||
isRead: !this.userIsRead
|
||||
}
|
||||
this.isProcessingReadUpdate = true
|
||||
this.$axios
|
||||
.$patch(`/api/me/audiobook/${this.audiobookId}`, updatePayload)
|
||||
.then(() => {
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.success(`"${this.title}" Marked as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
this.isProcessingReadUpdate = false
|
||||
this.$toast.error(`Failed to mark as ${updatePayload.isRead ? 'Read' : 'Not Read'}`)
|
||||
})
|
||||
},
|
||||
audiobookScanComplete(result) {
|
||||
this.rescanning = false
|
||||
if (!result) {
|
||||
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
|
||||
} else if (result === 'UPDATED') {
|
||||
this.$toast.success(`Re-Scan complete audiobook was updated`)
|
||||
} else if (result === 'UPTODATE') {
|
||||
this.$toast.success(`Re-Scan complete audiobook was up to date`)
|
||||
} else if (result === 'REMOVED') {
|
||||
this.$toast.error(`Re-Scan complete audiobook was removed`)
|
||||
}
|
||||
},
|
||||
rescan() {
|
||||
this.rescanning = true
|
||||
this.$root.socket.once('audiobook_scan_complete', this.audiobookScanComplete)
|
||||
this.$root.socket.emit('scan_audiobook', this.audiobookId)
|
||||
},
|
||||
showEditModalTracks() {
|
||||
// More menu func
|
||||
this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'tracks' })
|
||||
},
|
||||
showEditModalMatch() {
|
||||
// More menu func
|
||||
this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'match' })
|
||||
},
|
||||
showEditModalDownload() {
|
||||
// More menu func
|
||||
this.$store.commit('showEditModalOnTab', { audiobook: this.audiobook, tab: 'download' })
|
||||
},
|
||||
openCollections() {
|
||||
this.$store.commit('setSelectedAudiobook', this.audiobook)
|
||||
this.$store.commit('globals/setShowUserCollectionsModal', true)
|
||||
},
|
||||
createMoreMenu() {
|
||||
if (!this.$refs.moreIcon) return
|
||||
|
||||
var ComponentClass = Vue.extend(MoreMenu)
|
||||
|
||||
var _this = this
|
||||
var instance = new ComponentClass({
|
||||
propsData: {
|
||||
items: this.moreMenuItems
|
||||
},
|
||||
created() {
|
||||
this.$on('action', (func) => {
|
||||
if (_this[func]) _this[func]()
|
||||
})
|
||||
this.$on('close', () => {
|
||||
_this.isMoreMenuOpen = false
|
||||
})
|
||||
}
|
||||
})
|
||||
instance.$mount()
|
||||
|
||||
var wrapperBox = this.$refs.moreIcon.getBoundingClientRect()
|
||||
var el = instance.$el
|
||||
|
||||
var elHeight = this.moreMenuItems.length * 28 + 2
|
||||
var elWidth = 130
|
||||
|
||||
var bottomOfIcon = wrapperBox.top + wrapperBox.height
|
||||
var rightOfIcon = wrapperBox.left + wrapperBox.width
|
||||
|
||||
var elTop = bottomOfIcon
|
||||
var elLeft = rightOfIcon
|
||||
if (bottomOfIcon + elHeight > window.innerHeight - 100) {
|
||||
elTop = wrapperBox.top - elHeight
|
||||
elLeft = wrapperBox.left
|
||||
}
|
||||
|
||||
if (rightOfIcon + elWidth > window.innerWidth - 100) {
|
||||
elLeft = rightOfIcon - elWidth
|
||||
}
|
||||
|
||||
el.style.top = elTop + 'px'
|
||||
el.style.left = elLeft + 'px'
|
||||
|
||||
this.isMoreMenuOpen = true
|
||||
document.body.appendChild(el)
|
||||
},
|
||||
clickShowMore() {
|
||||
this.createMoreMenu()
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.showCard = !this.isBookshelfBook
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div class="rounded-sm h-full relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||
<div class="rounded-sm h-full relative" :style="{ padding: `0px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
||||
<covers-group-cover ref="groupcover" :id="seriesId" :name="groupName" :is-categorized="isCategorized" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
@ -38,10 +38,6 @@ export default {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
paddingY: {
|
||||
type: Number,
|
||||
default: 24
|
||||
},
|
||||
isCategorized: Boolean,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="card" :id="`book-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @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" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<!-- When cover image does not fill -->
|
||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
@ -78,7 +78,12 @@ export default {
|
||||
default: 192
|
||||
},
|
||||
bookCoverAspectRatio: Number,
|
||||
showVolumeNumber: Boolean
|
||||
showVolumeNumber: Boolean,
|
||||
bookMount: {
|
||||
// Book can be passed as prop or set with setEntity()
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -465,6 +470,11 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.bookMount) {
|
||||
this.setEntity(this.bookMount)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" :group-to="seriesBooksRoute" />
|
||||
@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
|
||||
</div> -->
|
||||
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
||||
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||
</div>
|
||||
@ -24,7 +24,12 @@ export default {
|
||||
index: Number,
|
||||
width: Number,
|
||||
height: Number,
|
||||
bookCoverAspectRatio: Number
|
||||
bookCoverAspectRatio: Number,
|
||||
isCategorized: Boolean,
|
||||
seriesMount: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -60,7 +65,7 @@ export default {
|
||||
return `/library/${this.currentLibraryId}/series/${this.$encode(this.title)}`
|
||||
},
|
||||
seriesId() {
|
||||
return this.series ? this.$encode(this.series.id) : null
|
||||
return this.series ? this.$encode(this.title) : null
|
||||
},
|
||||
hasValidCovers() {
|
||||
var validCovers = this.books.map((bookItem) => bookItem.book.cover)
|
||||
@ -69,6 +74,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
setEntity(_series) {
|
||||
console.log('setting entity', _series)
|
||||
this.series = _series
|
||||
},
|
||||
setSelectionMode(val) {
|
||||
@ -100,7 +106,11 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
mounted() {
|
||||
if (this.seriesMount) {
|
||||
this.setEntity(this.seriesMount)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
|
@ -71,6 +71,7 @@ export default {
|
||||
|
||||
instance.$mount()
|
||||
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
||||
instance.$el.classList.add('absolute', 'top-0', 'left-0')
|
||||
shelfEl.appendChild(instance.$el)
|
||||
|
||||
if (this.entities[index]) {
|
||||
|
@ -143,5 +143,6 @@ export {
|
||||
export default ({ app }, inject) => {
|
||||
app.$decode = decode
|
||||
app.$encode = encode
|
||||
app.$isDev = process.env.NODE_ENV !== 'production'
|
||||
// app.$isDev = process.env.NODE_ENV !== 'production'
|
||||
inject('isDev', process.env.NODE_ENV !== 'production')
|
||||
}
|
Loading…
Reference in New Issue
Block a user