mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-28 09:38:56 +01:00
This commit is contained in:
parent
9057afb5ee
commit
6fd3317454
@ -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
|
||||||
|
@ -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 === ''
|
||||||
},
|
},
|
||||||
|
216
client/components/app/BookshelfListRow.vue
Normal file
216
client/components/app/BookshelfListRow.vue
Normal 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>
|
@ -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() {
|
||||||
|
@ -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: {
|
||||||
|
@ -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>
|
@ -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: {
|
||||||
|
@ -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": {
|
||||||
|
@ -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 || ''
|
||||||
},
|
},
|
||||||
|
@ -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">
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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": {
|
||||||
|
Loading…
Reference in New Issue
Block a user