Add: experimental library list view #149, Add: experimental series fan out on hover #149

This commit is contained in:
advplyr 2021-10-29 20:42:28 -05:00
parent 9057afb5ee
commit 6fd3317454
13 changed files with 427 additions and 47 deletions

View File

@ -1,7 +1,7 @@
<template> <template>
<div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative"> <div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative">
<!-- Cover size widget --> <!-- Cover size widget -->
<div v-show="!isSelectionMode" class="fixed bottom-2 right-4 z-30"> <div v-show="!isSelectionMode && isGridMode" class="fixed bottom-2 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> <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> <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> <p class="px-2 font-mono">{{ bookCoverWidth }}</p>
@ -25,17 +25,27 @@
</div> </div>
</div> </div>
<div v-else id="bookshelf" class="w-full flex flex-col items-center"> <div v-else id="bookshelf" class="w-full flex flex-col items-center">
<template v-for="(shelf, index) in shelves"> <template v-if="viewMode === 'grid'">
<div :key="index" class="w-full bookshelfRow relative"> <template v-for="(shelf, index) in shelves">
<div class="flex justify-center items-center"> <div :key="index" class="w-full bookshelfRow relative">
<template v-for="entity in shelf"> <div class="flex justify-center items-center">
<cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" /> <template v-for="entity in shelf">
<!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> --> <cards-group-card v-if="showGroups" :key="entity.id" :width="bookCoverWidth" :group="entity" @click="clickGroup" />
<cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" /> <!-- <cards-book-3d :key="entity.id" v-else :width="100" :src="$store.getters['audiobooks/getBookCoverSrc'](entity.book)" /> -->
</template> <cards-book-card v-else :key="entity.id" :show-volume-number="!!selectedSeries" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @edit="editBook" />
</template>
</div>
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
</div> </div>
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" /> </template>
</div> </template>
<template v-else>
<template v-for="(entity, index) in entities">
<div :key="index" class="w-full bookshelfRow relative">
<app-bookshelf-list-row :book-item="entity" :book-cover-width="bookCoverWidth" :user-audiobook="userAudiobooks[entity.id]" />
<div class="bookshelfDivider h-3 w-full absolute bottom-0 left-0 right-0 z-10" />
</div>
</template>
</template> </template>
<div v-show="!shelves.length" class="w-full py-16 text-center text-xl"> <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-if="page === 'search'" class="py-4 mb-6"><p class="text-2xl">No Results</p></div>
@ -56,7 +66,8 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
searchQuery: String searchQuery: String,
viewMode: String
}, },
data() { data() {
return { return {
@ -95,6 +106,9 @@ export default {
} }
}, },
computed: { computed: {
isGridMode() {
return this.viewMode === 'grid'
},
keywordFilter() { keywordFilter() {
return this.$store.state.audiobooks.keywordFilter return this.$store.state.audiobooks.keywordFilter
}, },
@ -108,7 +122,9 @@ export default {
return this.bookCoverWidth / 120 return this.bookCoverWidth / 120
}, },
bookCoverWidth() { bookCoverWidth() {
return this.availableSizes[this.selectedSizeIndex] if (this.viewMode === 'list') return 60
var coverWidth = this.availableSizes[this.selectedSizeIndex]
return coverWidth
}, },
sizeMultiplier() { sizeMultiplier() {
return this.bookCoverWidth / 120 return this.bookCoverWidth / 120

View File

@ -20,6 +20,14 @@
<ui-text-input v-show="!selectedSeries" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" clearable class="text-xs w-40" /> <ui-text-input v-show="!selectedSeries" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" clearable class="text-xs w-40" />
<controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" /> <controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
<controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" /> <controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
<div v-if="showExperimentalFeatures" class="h-7 ml-4 flex border border-white border-opacity-25 rounded-md">
<div class="h-full px-2 text-white flex items-center rounded-l-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'grid')">
<span class="material-icons" style="font-size: 1.4rem">view_module</span>
</div>
<div class="h-full px-2 text-white flex items-center rounded-r-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="!isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'list')">
<span class="material-icons" style="font-size: 1.4rem">view_list</span>
</div>
</div>
</template> </template>
<template v-else-if="!isHome"> <template v-else-if="!isHome">
<div @click="searchBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer"> <div @click="searchBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
@ -44,7 +52,8 @@ export default {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
searchQuery: String searchQuery: String,
viewMode: String
}, },
data() { data() {
return { return {
@ -53,6 +62,12 @@ export default {
} }
}, },
computed: { computed: {
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
isGridMode() {
return this.viewMode === 'grid'
},
showSortFilters() { showSortFilters() {
return this.page === '' return this.page === ''
}, },

View File

@ -0,0 +1,216 @@
<template>
<div :class="selected ? 'bg-success bg-opacity-10' : ''">
<div class="flex px-12 mx-auto" style="max-width: 1400px">
<div class="w-12 h-full flex items-center justify-center self-center">
<ui-checkbox v-model="selected" />
</div>
<div class="p-3">
<cards-book-cover :width="bookCoverWidth" :audiobook="bookItem" />
</div>
<div class="flex-grow p-3">
<div class="flex h-full">
<div class="w-full max-w-xl">
<nuxt-link :to="`/audiobook/${audiobookId}`" class="flex items-center hover:underline">
<p class="text-base font-book">{{ title }}<span v-if="subtitle">:</span></p>
<p class="text-base font-book pl-2 text-gray-200">{{ subtitle }}</p>
</nuxt-link>
<p class="text-gray-200 text-sm" v-if="seriesText">{{ seriesText }}</p>
<p class="text-sm text-gray-300">{{ author }}</p>
<div class="flex pt-2">
<div class="rounded-full bg-black bg-opacity-50 px-2 py-px text-xs text-gray-200">
<p>{{ numTracks }} Tracks</p>
</div>
<div class="rounded-full bg-black bg-opacity-50 px-2 py-px text-xs text-gray-200 mx-2">
<p>{{ durationPretty }}</p>
</div>
<div class="rounded-full bg-black bg-opacity-50 px-2 py-px text-xs text-gray-200">
<p>{{ sizePretty }}</p>
</div>
</div>
</div>
<div class="w-full max-w-xl pr-6 pl-12 items-center h-full pb-3 hidden xl:flex">
<p class="text-sm text-gray-200 max-3-lines">{{ description }}</p>
</div>
</div>
</div>
<div class="w-32 h-full self-center">
<div class="flex justify-center mb-2">
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9" @click="startStream">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? 'Streaming' : 'Play' }}
</ui-btn>
</div>
<div class="flex">
<ui-tooltip v-if="userCanUpdate" text="Edit" direction="top">
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
</ui-tooltip>
<ui-tooltip v-if="userCanDownload" :disabled="isMissing" text="Download" direction="top">
<ui-icon-btn icon="download" :disabled="isMissing" class="mx-0.5" @click="downloadClick" />
</ui-tooltip>
<ui-tooltip :text="userIsRead ? 'Mark as Not Read' : 'Mark as Read'" direction="top">
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsRead" class="mx-0.5" @click="toggleRead" />
</ui-tooltip>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
bookItem: {
type: Object,
default: () => {}
},
userAudiobook: {
type: Object,
default: () => {}
},
bookCoverWidth: Number
},
data() {
return {
isProcessingReadUpdate: false
}
},
computed: {
audiobookId() {
return this.bookItem.id
},
isSelectionMode() {
return !!this.selectedAudiobooks.length
},
selectedAudiobooks() {
return this.$store.state.selectedAudiobooks
},
selected: {
get() {
return this.$store.getters['getIsAudiobookSelected'](this.audiobookId)
},
set(val) {
if (this.processingBatch) return
this.$store.commit('setAudiobookSelected', { audiobookId: this.audiobookId, selected: val })
}
},
processingBatch() {
return this.$store.state.processingBatch
},
isMissing() {
return this.bookItem.isMissing
},
isIncomplete() {
return this.bookItem.isIncomplete
},
numTracks() {
return this.bookItem.numTracks
},
durationPretty() {
return this.$elapsedPretty(this.bookItem.duration)
},
sizePretty() {
return this.$bytesPretty(this.bookItem.size)
},
streamAudiobook() {
return this.$store.state.streamAudiobook
},
streaming() {
return this.streamAudiobook && this.streamAudiobook.id === this.audiobookId
},
book() {
return this.bookItem.book || {}
},
title() {
return this.book.title
},
subtitle() {
return this.book.subtitle
},
series() {
return this.book.series || null
},
volumeNumber() {
return this.book.volumeNumber || null
},
seriesText() {
if (!this.series) return ''
if (!this.volumeNumber) return this.series
return `${this.series} #${this.volumeNumber}`
},
description() {
return this.book.description
},
author() {
return this.book.authorFL
},
showPlayButton() {
return !this.isMissing && !this.isIncomplete && this.numTracks
},
userCurrentTime() {
return this.userAudiobook ? this.userAudiobook.currentTime : 0
},
userIsRead() {
return this.userAudiobook ? !!this.userAudiobook.isRead : false
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
}
},
methods: {
selectBtnClick() {
if (this.processingBatch) return
this.$store.commit('toggleAudiobookSelected', this.audiobookId)
},
openEbook() {
this.$store.commit('showEReader', this.bookItem)
},
downloadClick() {
this.$store.commit('showEditModalOnTab', { audiobook: this.bookItem, tab: 'download' })
},
toggleRead() {
var updatePayload = {
isRead: !this.userIsRead
}
this.isProcessingReadUpdate = true
this.$axios
.$patch(`/api/user/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'}`)
})
},
startStream() {
this.$store.commit('setStreamAudiobook', this.bookItem)
this.$root.socket.emit('open_stream', this.bookItem.id)
},
editClick() {
this.$store.commit('setBookshelfBookIds', [])
this.$store.commit('showEditModal', this.bookItem)
}
},
mounted() {}
}
</script>
<style>
.max-3-lines {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3; /* number of lines to show */
-webkit-box-orient: vertical;
}
</style>

View File

@ -8,7 +8,7 @@
<div class="absolute -bottom-4 left-0 triangle-right" /> <div class="absolute -bottom-4 left-0 triangle-right" />
</div> </div>
<div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `16px ${paddingX}px` }" @click.stop> <div class="rounded-sm h-full overflow-hidden relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @click.stop>
<nuxt-link :to="isSelectionMode ? '' : `/audiobook/${audiobookId}`" class="cursor-pointer"> <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"> <div class="w-full relative box-shadow-book" :style="{ height: height + 'px' }" @click="clickCard" @mouseover="isHovering = true" @mouseleave="isHovering = false">
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" /> <cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
@ -77,6 +77,10 @@ export default {
type: Number, type: Number,
default: 120 default: 120
}, },
paddingY: {
type: Number,
default: 16
},
showVolumeNumber: Boolean showVolumeNumber: Boolean
}, },
data() { data() {

View File

@ -3,9 +3,9 @@
<div class="rounded-sm h-full relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard"> <div class="rounded-sm h-full relative" :style="{ padding: `16px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
<nuxt-link :to="groupTo" class="cursor-pointer"> <nuxt-link :to="groupTo" class="cursor-pointer">
<div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }"> <div class="w-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: height + 'px', width: height + 'px' }">
<cards-group-cover ref="groupcover" :name="groupName" :book-items="bookItems" :width="height" :height="height" /> <cards-group-cover ref="groupcover" :name="groupName" :group-to="groupTo" :book-items="bookItems" :width="height" :height="height" />
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }"> <div v-if="hasValidCovers && !showExperimentalFeatures" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
<p class="font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p> <p class="font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p>
</div> </div>
@ -100,6 +100,9 @@ export default {
hasValidCovers() { hasValidCovers() {
var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover) var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover)
return !!validCovers.length return !!validCovers.length
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
} }
}, },
methods: { methods: {

View File

@ -1,5 +1,5 @@
<template> <template>
<div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative"> <div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative" @mouseover="mouseoverCover" @mouseleave="mouseleaveCover">
<div v-if="noValidCovers" class="absolute top-0 left-0 w-full h-full flex items-center justify-center box-shadow-book" :style="{ padding: `${sizeMultiplier}rem` }"> <div v-if="noValidCovers" class="absolute top-0 left-0 w-full h-full flex items-center justify-center box-shadow-book" :style="{ padding: `${sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p> <p :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p>
</div> </div>
@ -15,17 +15,21 @@ export default {
default: () => [] default: () => []
}, },
width: Number, width: Number,
height: Number height: Number,
groupTo: String
}, },
data() { data() {
return { return {
noValidCovers: false, noValidCovers: false,
coverDiv: null, coverDiv: null,
isHovering: false, isHovering: false,
coverWrapperEl: null,
coverImageEls: [], coverImageEls: [],
coverWidth: 0, coverWidth: 0,
offsetIncrement: 0, offsetIncrement: 0,
isFannedOut: false isFannedOut: false,
isDetached: false,
isAttaching: false
} }
}, },
watch: { watch: {
@ -42,14 +46,66 @@ export default {
computed: { computed: {
sizeMultiplier() { sizeMultiplier() {
return this.width / 192 return this.width / 192
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
} }
}, },
methods: { methods: {
mouseoverCover() {
if (this.showExperimentalFeatures) this.setHover(true)
},
mouseleaveCover() {
if (this.showExperimentalFeatures) this.setHover(false)
},
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)
console.log('Appended to wrapper', this.$refs.wrapper.children)
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'
},
setHover(val) { setHover(val) {
if (this.isAttaching) return
if (val && !this.isHovering) { if (val && !this.isHovering) {
this.detchCoverWrapper()
this.fanOutCovers() this.fanOutCovers()
} else if (!val && this.isHovering) { } else if (!val && this.isHovering) {
this.isAttaching = true
this.reverseFan() this.reverseFan()
setTimeout(() => {
this.attachCoverWrapper()
this.isAttaching = false
}, 100)
} }
this.isHovering = val this.isHovering = val
}, },
@ -57,15 +113,44 @@ export default {
if (this.coverImageEls.length < 2 || this.isFannedOut) return if (this.coverImageEls.length < 2 || this.isFannedOut) return
this.isFannedOut = true this.isFannedOut = true
var fanCoverWidth = this.coverWidth * 0.75 var fanCoverWidth = this.coverWidth * 0.75
var maximumWidth = window.innerWidth - 80
var totalFanWidth = (this.coverImageEls.length + 1) * fanCoverWidth
// If Fan width is too large, set new fan cover width
if (totalFanWidth > maximumWidth) {
fanCoverWidth = maximumWidth / (this.coverImageEls.length + 1)
}
var fanWidth = (this.coverImageEls.length - 1) * fanCoverWidth var fanWidth = (this.coverImageEls.length - 1) * fanCoverWidth
var offsetLeft = (-1 * fanWidth) / 2 var offsetLeft = (-1 * fanWidth) / 2
var rect = this.$refs.wrapper.getBoundingClientRect()
// If fan is going off page left or right, make adjustment
var leftEdge = rect.left + offsetLeft
var rightEdge = rect.left + rect.width - offsetLeft
if (leftEdge < 0) {
offsetLeft += leftEdge * -1
}
if (rightEdge + 80 > window.innerWidth) {
var difference = rightEdge + 80 - window.innerWidth
offsetLeft -= difference / 2
}
for (let i = 0; i < this.coverImageEls.length; i++) { for (let i = 0; i < this.coverImageEls.length; i++) {
var coverEl = this.coverImageEls[i] var coverEl = this.coverImageEls[i]
// Series name card pop out further
if (i === this.coverImageEls.length - 1) {
offsetLeft += fanCoverWidth * 0.25
}
coverEl.style.transform = `translateX(${offsetLeft}px)` coverEl.style.transform = `translateX(${offsetLeft}px)`
offsetLeft += fanCoverWidth offsetLeft += fanCoverWidth
var coverOverlay = document.createElement('div') var coverOverlay = document.createElement('div')
coverOverlay.className = 'absolute top-0 left-0 w-full h-full hover:bg-black hover:bg-opacity-40 text-white text-opacity-0 hover:text-opacity-100 flex items-center justify-center' coverOverlay.className = 'absolute top-0 left-0 w-full h-full hover:bg-black hover:bg-opacity-40 text-white text-opacity-0 hover:text-opacity-100 flex items-center justify-center cursor-pointer'
if (coverEl.dataset.volumeNumber) { if (coverEl.dataset.volumeNumber) {
var pEl = document.createElement('p') var pEl = document.createElement('p')
@ -73,13 +158,22 @@ export default {
pEl.textContent = `#${coverEl.dataset.volumeNumber}` pEl.textContent = `#${coverEl.dataset.volumeNumber}`
coverOverlay.appendChild(pEl) coverOverlay.appendChild(pEl)
} }
if (coverEl.dataset.audiobookId) {
let audiobookId = coverEl.dataset.audiobookId
coverOverlay.addEventListener('click', (e) => {
this.$router.push(`/audiobook/${audiobookId}`)
e.stopPropagation()
e.preventDefault()
})
} else {
// Is Series
coverOverlay.addEventListener('click', (e) => {
this.$router.push(this.groupTo)
e.stopPropagation()
e.preventDefault()
})
}
let audiobookId = coverEl.dataset.audiobookId
coverOverlay.addEventListener('click', (e) => {
this.$router.push(`/audiobook/${audiobookId}`)
e.stopPropagation()
e.preventDefault()
})
coverEl.appendChild(coverOverlay) coverEl.appendChild(coverOverlay)
} }
}, },
@ -89,7 +183,7 @@ export default {
for (let i = 0; i < this.coverImageEls.length; i++) { for (let i = 0; i < this.coverImageEls.length; i++) {
var coverEl = this.coverImageEls[i] var coverEl = this.coverImageEls[i]
coverEl.style.transform = 'translateX(0px)' coverEl.style.transform = 'translateX(0px)'
if (coverEl.lastChild) coverEl.lastChild.remove() if (coverEl.lastChild) coverEl.lastChild.remove() // Remove cover overlay
} }
}, },
getCoverUrl(book) { getCoverUrl(book) {
@ -157,6 +251,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 / 1.6 + '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.coverDiv) { if (this.coverDiv) {
this.coverDiv.remove() this.coverDiv.remove()
@ -187,16 +297,25 @@ export default {
this.offsetIncrement = widthPer this.offsetIncrement = widthPer
var outerdiv = document.createElement('div') var outerdiv = document.createElement('div')
this.coverWrapperEl = outerdiv
outerdiv.className = 'w-full h-full relative' outerdiv.className = 'w-full h-full relative'
var coverImageEls = [] var coverImageEls = []
var offsetLeft = 0
for (let i = 0; i < validCovers.length; i++) { for (let i = 0; i < validCovers.length; i++) {
var offsetLeft = widthPer * i offsetLeft = widthPer * i
var zIndex = validCovers.length - i var zIndex = validCovers.length - i
var img = await this.buildCoverImg(validCovers[i], coverWidth, offsetLeft, zIndex, validCovers.length === 1) var img = await this.buildCoverImg(validCovers[i], coverWidth, offsetLeft, zIndex, validCovers.length === 1)
outerdiv.appendChild(img) outerdiv.appendChild(img)
coverImageEls.push(img) coverImageEls.push(img)
} }
if (this.showExperimentalFeatures) {
var seriesNameCover = this.createSeriesNameCover(offsetLeft)
outerdiv.appendChild(seriesNameCover)
coverImageEls.push(seriesNameCover)
}
this.coverImageEls = coverImageEls this.coverImageEls = coverImageEls
if (this.$refs.wrapper) { if (this.$refs.wrapper) {
@ -205,6 +324,9 @@ export default {
} }
} }
}, },
mounted() {} mounted() {},
beforeDestroy() {
if (this.coverWrapperEl) this.coverWrapperEl.remove()
}
} }
</script> </script>

View File

@ -1,6 +1,6 @@
<template> <template>
<button class="icon-btn rounded-md border border-gray-600 flex items-center justify-center h-9 w-9 relative" :disabled="disabled" :class="className" @click="clickBtn"> <button class="icon-btn rounded-md border border-gray-600 flex items-center justify-center h-9 w-9 relative" :disabled="disabled" :class="className" @click="clickBtn">
<span class="material-icons icon-text">{{ icon }}</span> <span class="material-icons" :style="{ fontSize }">{{ icon }}</span>
</button> </button>
</template> </template>
@ -22,6 +22,10 @@ export default {
var classes = [] var classes = []
classes.push(`bg-${this.bgColor}`) classes.push(`bg-${this.bgColor}`)
return classes.join(' ') return classes.join(' ')
},
fontSize() {
if (this.icon === 'edit') return '1.25rem'
return '1.4rem'
} }
}, },
methods: { methods: {

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "1.5.7", "version": "1.5.8",
"description": "Audiobook manager and player", "description": "Audiobook manager and player",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -316,9 +316,6 @@ export default {
numEbooks() { numEbooks() {
return this.audiobook.numEbooks return this.audiobook.numEbooks
}, },
userToken() {
return this.$store.getters['user/getToken']
},
description() { description() {
return this.book.description || '' return this.book.description || ''
}, },

View File

@ -1,13 +1,5 @@
<template> <template>
<div class="page" :class="streamAudiobook ? 'streaming' : ''"> <div class="page" :class="streamAudiobook ? 'streaming' : ''">
<!-- <app-book-shelf-toolbar /> -->
<!-- <div class="flex h-full">
<app-side-rail />
<div class="flex-grow"> -->
<!-- <app-book-shelf /> -->
<!-- </div> -->
<!-- </div> -->
<div class="flex h-full"> <div class="flex h-full">
<app-side-rail /> <app-side-rail />
<div class="flex-grow"> <div class="flex-grow">

View File

@ -3,8 +3,8 @@
<div class="flex h-full"> <div class="flex h-full">
<app-side-rail /> <app-side-rail />
<div class="flex-grow"> <div class="flex-grow">
<app-book-shelf-toolbar :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" /> <app-book-shelf-toolbar :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode.sync="viewMode" />
<app-book-shelf :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" /> <app-book-shelf :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode="viewMode" />
</div> </div>
</div> </div>
</div> </div>
@ -56,7 +56,9 @@ export default {
} }
}, },
data() { data() {
return {} return {
viewMode: 'grid'
}
}, },
watch: { watch: {
'$route.query'(newVal) { '$route.query'(newVal) {

View File

@ -150,6 +150,15 @@ export const mutations = {
Vue.set(state, 'selectedAudiobooks', newSel) Vue.set(state, 'selectedAudiobooks', newSel)
} }
}, },
setAudiobookSelected(state, { audiobookId, selected }) {
var isThere = state.selectedAudiobooks.includes(audiobookId)
if (isThere && !selected) {
state.selectedAudiobooks = state.selectedAudiobooks.filter(a => a !== audiobookId)
} else if (selected && !isThere) {
var newSel = state.selectedAudiobooks.concat([audiobookId])
Vue.set(state, 'selectedAudiobooks', newSel)
}
},
setProcessingBatch(state, val) { setProcessingBatch(state, val) {
state.processingBatch = val state.processingBatch = val
}, },

View File

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf", "name": "audiobookshelf",
"version": "1.5.7", "version": "1.5.8",
"description": "Self-hosted audiobook server for managing and playing audiobooks", "description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {