mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-28 09:38:56 +01:00
Add:Experimental authors page layout #187,Add:Square covers #210
This commit is contained in:
parent
9a6ba3d0de
commit
d544540454
@ -65,7 +65,8 @@ export default {
|
||||
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.bookCoverWidth / 120
|
||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||
return this.bookCoverWidth / baseSize
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -105,16 +106,6 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Author shelves
|
||||
// if (this.results.authors) {
|
||||
// shelves.push({
|
||||
// id: 'authors',
|
||||
// label: 'Authors',
|
||||
// type: 'authors',
|
||||
// entities: this.results.authors.map((a) => a.author)
|
||||
// })
|
||||
// }
|
||||
|
||||
if (this.results.series) {
|
||||
shelves.push({
|
||||
id: 'series',
|
||||
@ -143,6 +134,22 @@ export default {
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.results.authors) {
|
||||
shelves.push({
|
||||
id: 'authors',
|
||||
label: 'Authors',
|
||||
type: 'authors',
|
||||
entities: this.results.authors.map((a) => {
|
||||
return {
|
||||
id: a.author,
|
||||
name: a.author,
|
||||
numBooks: a.numBooks,
|
||||
type: 'author'
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.shelves = shelves
|
||||
},
|
||||
settingsUpdated(settings) {},
|
||||
|
@ -19,6 +19,13 @@
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'authors'" class="flex items-center -mb-2">
|
||||
<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)" />
|
||||
@ -79,6 +86,9 @@ export default {
|
||||
paddingLeft() {
|
||||
if (window.innerWidth < 768) return 1
|
||||
return 2.5
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -226,7 +226,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if (payload) {
|
||||
// console.log('Received payload', payload)
|
||||
console.log('Received payload', payload)
|
||||
if (!this.initialized) {
|
||||
this.initialized = true
|
||||
this.totalEntities = payload.total
|
||||
|
@ -32,9 +32,6 @@
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<!-- <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||
</svg> -->
|
||||
<span class="material-icons-outlined">collections_bookmark</span>
|
||||
|
||||
<p class="font-book pt-1.5" style="font-size: 0.9rem">Collections</p>
|
||||
@ -42,6 +39,19 @@
|
||||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="showExperimentalFeatures" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<p class="font-book pt-1.5" style="font-size: 0.9rem">Authors</p>
|
||||
|
||||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
||||
<span class="material-icons text-2xl">warning</span>
|
||||
|
||||
@ -106,6 +116,9 @@ export default {
|
||||
isSeriesPage() {
|
||||
return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
|
||||
},
|
||||
isAuthorsPage() {
|
||||
return this.$route.name === 'library-library-authors'
|
||||
},
|
||||
libraryBookshelfPage() {
|
||||
return this.$route.name === 'library-library-bookshelf-id'
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-40 bg-primary px-4 pb-4 pt-2">
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute -top-16 left-4 cursor-pointer">
|
||||
<covers-book-cover :audiobook="streamAudiobook" :width="bookCoverWidth" />
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
||||
<covers-book-cover :audiobook="streamAudiobook" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</nuxt-link>
|
||||
<div class="flex items-start pl-24 mb-6 md:mb-0">
|
||||
<div>
|
||||
@ -45,9 +45,19 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverAspectRatio() {
|
||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||
},
|
||||
bookCoverWidth() {
|
||||
return 88
|
||||
},
|
||||
bookCoverPosTop() {
|
||||
if (this.bookCoverAspectRatio === 1) return -10
|
||||
return -64
|
||||
},
|
||||
cover() {
|
||||
if (this.streamAudiobook && this.streamAudiobook.cover) return this.streamAudiobook.cover
|
||||
return 'Logo.png'
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<covers-book-cover :audiobook="audiobook" :width="50" />
|
||||
<covers-book-cover :audiobook="audiobook" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="flex-grow px-2 audiobookSearchCardContent">
|
||||
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
||||
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
||||
@ -30,6 +30,13 @@ export default {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
},
|
||||
coverWidth() {
|
||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
||||
return 50
|
||||
},
|
||||
book() {
|
||||
return this.audiobook ? this.audiobook.book || {} : {}
|
||||
},
|
||||
|
77
client/components/cards/AuthorCard.vue
Normal file
77
client/components/cards/AuthorCard.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-lg relative">
|
||||
<div class="w-full h-full overflow-hidden max-w-full max-h-full relative">
|
||||
<svg width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" />
|
||||
<path d="M-38.0532 155.529C-31.5407 148.629 10.2694 150.865 40.7156 165.47C54.006 171.846 66.828 181.394 76.9443 190.899L95.0391 173.37C80.6681 159.403 64.7526 149.155 51.5747 142.834C21.3549 128.337 -46.2471 114.563 -60.6897 144.67L-71.5489 167.307L44.5864 223.019L63.3191 204.159L-38.0532 155.529Z" fill="white" />
|
||||
<path
|
||||
d="M105.87 29.6508C80.857 17.6515 50.8784 28.1923 38.879 53.2056C26.8797 78.219 37.4205 108.198 62.4338 120.197C87.4472 132.196 117.426 121.656 129.425 96.6422C141.425 71.6288 130.884 41.6502 105.87 29.6508ZM106.789 85.783C112.761 73.3329 107.461 58.2599 95.0112 52.2874C82.5611 46.3148 67.4881 51.6147 61.5156 64.0648C55.543 76.5149 60.8429 91.5879 73.293 97.5604C85.7431 103.533 100.816 98.2331 106.789 85.783Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01ZM181.725 108.497C179.624 108.491 177.436 109.326 175.835 110.918L160.415 126.257L191.848 157.856L207.268 142.517C210.554 139.248 210.568 133.954 207.299 130.667L187.685 110.95C186.009 109.264 183.91 108.502 181.725 108.497ZM151.399 135.226L58.2034 227.931L58.1203 259.447L89.6359 259.53L182.831 166.825L151.399 135.226Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01Z" fill="white" stroke="white" stroke-width="10px" />
|
||||
</svg>
|
||||
|
||||
<div class="absolute bottom-0 left-0 w-full py-2 bg-black bg-opacity-25 px-2">
|
||||
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p>
|
||||
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.85 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
author: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
width: Number,
|
||||
height: Number,
|
||||
sizeMultiplier: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
placeholder: '/Logo.png'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
_author() {
|
||||
return this.author || {}
|
||||
},
|
||||
name() {
|
||||
return this._author.name || ''
|
||||
},
|
||||
image() {
|
||||
return this._author.image || null
|
||||
},
|
||||
description() {
|
||||
return this._author.description
|
||||
},
|
||||
lastUpdate() {
|
||||
return this._author.lastUpdate
|
||||
},
|
||||
numBooks() {
|
||||
return this._author.numBooks || 0
|
||||
},
|
||||
imgSrc() {
|
||||
if (!this.image) return this.placeholder
|
||||
var encodedImg = this.image.replace(/%/g, '%25').replace(/#/g, '%23')
|
||||
|
||||
var url = new URL(encodedImg, document.baseURI)
|
||||
return url.href + `?token=${this.userToken}&ts=${this.lastUpdate}`
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<div class="h-24 flex">
|
||||
<div class="w-32">
|
||||
<img :src="imgSrc" class="w-full object-cover" />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<p>{{ name }}</p>
|
||||
<p class="text-sm text-gray-300">{{ description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
person: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
placeholder: '/Logo.png'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
},
|
||||
_person() {
|
||||
return this.person || {}
|
||||
},
|
||||
name() {
|
||||
return this._person.name || ''
|
||||
},
|
||||
image() {
|
||||
return this._person.image || null
|
||||
},
|
||||
description() {
|
||||
return this._person.description
|
||||
},
|
||||
lastUpdate() {
|
||||
return this._person.lastUpdate
|
||||
},
|
||||
imgSrc() {
|
||||
if (!this.image) return this.placeholder
|
||||
var encodedImg = this.image.replace(/%/g, '%25').replace(/#/g, '%23')
|
||||
|
||||
var url = new URL(encodedImg, document.baseURI)
|
||||
return url.href + `?token=${this.userToken}&ts=${this.lastUpdate}`
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<covers-group-cover :name="series" :book-items="bookItems" :width="60" :height="60" />
|
||||
<covers-group-cover :name="series" :book-items="bookItems" :width="60" :height="60" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="flex-grow px-2 seriesSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ series }}</p>
|
||||
</div>
|
||||
@ -19,7 +19,11 @@ export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<div class="w-full h-full relative">
|
||||
<div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
|
||||
<div class="w-full h-full z-0" ref="coverBg" />
|
||||
@ -28,7 +28,8 @@ export default {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
showOpenNewTab: Boolean
|
||||
showOpenNewTab: Boolean,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -72,7 +73,7 @@ export default {
|
||||
this.naturalWidth = naturalWidth
|
||||
|
||||
var aspectRatio = naturalHeight / naturalWidth
|
||||
var arDiff = Math.abs(aspectRatio - 1.6)
|
||||
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
||||
|
||||
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
||||
if (arDiff > 0.15) {
|
||||
|
@ -10,7 +10,7 @@
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="flex">
|
||||
<div>
|
||||
<covers-collection-cover :book-items="books" :width="200" :height="100 * 1.6" />
|
||||
<covers-collection-cover :book-items="books" :width="200" :height="100 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<!-- <ui-btn type="button" @click="showImageUploader = true">Upload</ui-btn> -->
|
||||
</div>
|
||||
<div class="flex-grow px-4">
|
||||
@ -74,6 +74,9 @@ export default {
|
||||
this.$store.commit('globals/setShowEditCollectionModal', val)
|
||||
}
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
},
|
||||
collection() {
|
||||
return this.$store.state.globals.selectedCollection || {}
|
||||
},
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6 relative">
|
||||
<div class="flex">
|
||||
<div class="relative">
|
||||
<covers-book-cover :audiobook="audiobook" />
|
||||
<covers-book-cover :audiobook="audiobook" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<!-- book cover overlay -->
|
||||
<div v-if="book.cover" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||
<div class="absolute top-0 left-0 w-full h-16 bg-gradient-to-b from-black-600 to-transparent" />
|
||||
@ -32,7 +32,7 @@
|
||||
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
||||
<template v-for="cover in localCovers">
|
||||
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.localPath === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
||||
<div class="h-24 bg-primary" style="width: 60px">
|
||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
@ -57,7 +57,7 @@
|
||||
<p v-if="!coversFound.length">No Covers Found</p>
|
||||
<template v-for="cover in coversFound">
|
||||
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
||||
<covers-preview-cover :src="cover" :width="80" show-open-new-tab />
|
||||
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@ -68,7 +68,7 @@
|
||||
<p class="text-lg">Preview Cover</p>
|
||||
<span class="absolute top-4 right-4 material-icons text-2xl cursor-pointer" @click="resetCoverPreview">close</span>
|
||||
<div class="flex justify-center py-4">
|
||||
<covers-preview-cover :src="previewUpload" :width="240" />
|
||||
<covers-preview-cover :src="previewUpload" :width="240" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0 flex py-4 px-5">
|
||||
<ui-btn :disabled="processingUpload" class="mx-2" @click="resetCoverPreview">Clear</ui-btn>
|
||||
@ -119,6 +119,12 @@ export default {
|
||||
this.$emit('update:processing', val)
|
||||
}
|
||||
},
|
||||
coverAspectRatio() {
|
||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook ? this.audiobook.id : null
|
||||
},
|
||||
|
@ -11,7 +11,7 @@
|
||||
<draggable v-model="booksCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||
<transition-group type="transition" :name="!drag ? 'collection-book' : null">
|
||||
<template v-for="book in booksCopy">
|
||||
<tables-collection-book-table-row :key="book.id" :is-dragging="drag" :book="book" :collection-id="collectionId" class="item" :class="drag ? '' : 'collection-book-item'" @edit="editBook" />
|
||||
<tables-collection-book-table-row :key="book.id" :is-dragging="drag" :book="book" :collection-id="collectionId" :book-cover-aspect-ratio="bookCoverAspectRatio" class="item" :class="drag ? '' : 'collection-book-item'" @edit="editBook" />
|
||||
</template>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
@ -51,6 +51,12 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverAspectRatio() {
|
||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||
},
|
||||
totalDuration() {
|
||||
var _total = 0
|
||||
this.books.forEach((book) => {
|
||||
|
@ -6,9 +6,9 @@
|
||||
<span class="material-icons drag-handle text-xl">menu</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-full relative" :style="{ width: '50px' }">
|
||||
<covers-book-cover :audiobook="book" :width="50" />
|
||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full" v-show="isHovering && showPlayBtn">
|
||||
<div class="h-full relative" :style="{ width: coverWidth + 'px' }">
|
||||
<covers-book-cover :audiobook="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||
<span class="material-icons">play_arrow</span>
|
||||
</div>
|
||||
@ -57,7 +57,8 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
isDragging: Boolean
|
||||
isDragging: Boolean,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -117,6 +118,10 @@ export default {
|
||||
},
|
||||
userIsRead() {
|
||||
return this.userAudiobook ? !!this.userAudiobook.isRead : false
|
||||
},
|
||||
coverWidth() {
|
||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.6
|
||||
return 50
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
||||
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 208px">
|
||||
<div class="relative" style="height: fit-content">
|
||||
<covers-book-cover :audiobook="audiobook" :width="bookCoverWidth" />
|
||||
<covers-book-cover :audiobook="audiobook" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -190,6 +190,12 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverAspectRatio() {
|
||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||
},
|
||||
bookCoverWidth() {
|
||||
return this.windowWidth < 800 ? 176 : 208
|
||||
},
|
||||
|
@ -4,7 +4,7 @@
|
||||
<template v-for="audiobook in audiobookCopies">
|
||||
<div :key="audiobook.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px flex">
|
||||
<div class="w-32">
|
||||
<covers-book-cover :audiobook="audiobook.originalAudiobook" :width="120" />
|
||||
<covers-book-cover :audiobook="audiobook.originalAudiobook" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div class="flex-grow pl-4">
|
||||
<ui-text-input-with-label v-model="audiobook.book.title" label="Title" />
|
||||
@ -86,6 +86,12 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverAspectRatio() {
|
||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||
},
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
|
@ -4,7 +4,7 @@
|
||||
<div class="flex flex-col sm:flex-row max-w-6xl mx-auto">
|
||||
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 240px">
|
||||
<div class="relative" style="height: fit-content">
|
||||
<covers-collection-cover :book-items="bookItems" :width="240" :height="120 * 1.6" />
|
||||
<covers-collection-cover :book-items="bookItems" :width="240" :height="120 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||
@ -66,6 +66,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
},
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
|
@ -65,13 +65,6 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div v-if="showExperimentalFeatures" class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
||||
|
||||
<div v-if="showExperimentalFeatures" class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 my-8">
|
||||
<p>Experimental Bookshelf Texture</p>
|
||||
<div class="flex"></div>
|
||||
</div>
|
||||
|
||||
<div class="h-0.5 bg-primary bg-opacity-30 w-full" />
|
||||
|
||||
<div class="py-12 mb-4 opacity-60 hover:opacity-100">
|
||||
|
@ -43,7 +43,7 @@
|
||||
</tr>
|
||||
<tr v-for="ab in userAudiobooks" :key="ab.audiobookId" :class="!ab.isRead ? '' : 'isRead'">
|
||||
<td>
|
||||
<covers-book-cover :width="50" :audiobook="ab" />
|
||||
<covers-book-cover :width="50" :audiobook="ab" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</td>
|
||||
<td class="font-book">
|
||||
<p>{{ ab.book ? ab.book.title : ab.audiobookTitle || 'Unknown' }}</p>
|
||||
@ -87,6 +87,12 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
coverAspectRatio() {
|
||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
|
61
client/pages/library/_library/authors/index.vue
Normal file
61
client/pages/library/_library/authors/index.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
||||
<div class="flex h-full">
|
||||
<app-side-rail class="hidden md:block" />
|
||||
<div class="flex-grow">
|
||||
<app-book-shelf-toolbar is-home />
|
||||
<div id="bookshelf" class="w-full h-full p-8 overflow-y-auto">
|
||||
<div class="flex flex-wrap justify-center">
|
||||
<template v-for="author in authors">
|
||||
<nuxt-link :key="author.name" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.name)}`">
|
||||
<cards-author-card :author="author" :width="160" :height="160" class="p-3" />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
async asyncData({ store, params, redirect, query, app }) {
|
||||
var libraryId = params.library
|
||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
||||
if (!library) {
|
||||
return redirect('/oops?message=Library not found')
|
||||
}
|
||||
|
||||
return {
|
||||
libraryId
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
authors: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
this.authors = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors`).catch((error) => {
|
||||
console.error('Failed to load authors', error)
|
||||
return []
|
||||
})
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -23,11 +23,6 @@ export default {
|
||||
if (query.filter) {
|
||||
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
||||
}
|
||||
|
||||
// if (libraryPage === 'collections') {
|
||||
// store.dispatch('user/loadUserCollections')
|
||||
// }
|
||||
|
||||
return {
|
||||
id: params.id || '',
|
||||
libraryId
|
||||
|
@ -32,6 +32,10 @@ export const getters = {
|
||||
if (!state.serverSettings) return null
|
||||
return state.serverSettings[key]
|
||||
},
|
||||
getBookCoverAspectRatio: state => {
|
||||
if (!state.serverSettings || !state.serverSettings.coverAspectRatio) return 1.6
|
||||
return state.serverSettings.coverAspectRatio === 0 ? 1.6 : 1
|
||||
},
|
||||
getNumAudiobooksSelected: state => state.selectedAudiobooks.length,
|
||||
getAudiobookIdStreaming: state => {
|
||||
return state.streamAudiobook ? state.streamAudiobook.id : null
|
||||
|
@ -62,6 +62,7 @@ class ApiController {
|
||||
this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
|
||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
||||
this.router.patch('/libraries/order', LibraryController.reorder.bind(this))
|
||||
|
||||
// TEMP: Support old syntax for mobile app
|
||||
|
@ -191,7 +191,7 @@ class LibraryController {
|
||||
return ab.book.volumeNumber
|
||||
})
|
||||
res.json({
|
||||
results: audiobooks,
|
||||
results: audiobooks.map(ab => ab.toJSONExpanded()),
|
||||
total: audiobooks.length
|
||||
})
|
||||
}
|
||||
@ -319,7 +319,7 @@ class LibraryController {
|
||||
var queryResult = ab.searchQuery(req.query.q)
|
||||
if (queryResult.book) {
|
||||
var bookMatchObj = {
|
||||
audiobook: ab,
|
||||
audiobook: ab.toJSONExpanded(),
|
||||
matchKey: queryResult.book,
|
||||
matchText: queryResult.bookMatchText
|
||||
}
|
||||
@ -329,8 +329,11 @@ class LibraryController {
|
||||
queryResult.authors.forEach((author) => {
|
||||
if (!authorMatches[author]) {
|
||||
authorMatches[author] = {
|
||||
author: author
|
||||
author: author,
|
||||
numBooks: 1
|
||||
}
|
||||
} else {
|
||||
authorMatches[author].numBooks++
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -338,10 +341,10 @@ class LibraryController {
|
||||
if (!seriesMatches[queryResult.series]) {
|
||||
seriesMatches[queryResult.series] = {
|
||||
series: queryResult.series,
|
||||
audiobooks: [ab]
|
||||
audiobooks: [ab.toJSONExpanded()]
|
||||
}
|
||||
} else {
|
||||
seriesMatches[queryResult.series].audiobooks.push(ab)
|
||||
seriesMatches[queryResult.series].audiobooks.push(ab.toJSONExpanded())
|
||||
}
|
||||
}
|
||||
if (queryResult.tags && queryResult.tags.length) {
|
||||
@ -349,10 +352,10 @@ class LibraryController {
|
||||
if (!tagMatches[tag]) {
|
||||
tagMatches[tag] = {
|
||||
tag,
|
||||
audiobooks: [ab]
|
||||
audiobooks: [ab.toJSONExpanded()]
|
||||
}
|
||||
} else {
|
||||
tagMatches[tag].audiobooks.push(ab)
|
||||
tagMatches[tag].audiobooks.push(ab.toJSONExpanded())
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -383,6 +386,27 @@ class LibraryController {
|
||||
res.json(stats)
|
||||
}
|
||||
|
||||
async getAuthors(req, res) {
|
||||
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
||||
var authors = {}
|
||||
audiobooksInLibrary.forEach((ab) => {
|
||||
if (ab.book._authorsList.length) {
|
||||
ab.book._authorsList.forEach((author) => {
|
||||
if (!author) return
|
||||
if (!authors[author]) {
|
||||
authors[author] = {
|
||||
name: author,
|
||||
numBooks: 1
|
||||
}
|
||||
} else {
|
||||
authors[author].numBooks++
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
res.json(Object.values(authors))
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
var library = this.db.libraries.find(lib => lib.id === req.params.id)
|
||||
if (!library) {
|
||||
|
Loading…
Reference in New Issue
Block a user