mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-09-10 17:58:02 +02:00
Working 1.0. Removed duplicate api calls per image. Attempted testing, removed, unable to figure out either testing option.
This commit is contained in:
parent
28d98b4dbc
commit
a2c7d64544
113
client/components/covers/DisplayCover.vue
Normal file
113
client/components/covers/DisplayCover.vue
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// Displays a book cover, given a cover image object with url, width, and height properties.
|
||||||
|
// It supports showing the cover in a specific aspect ratio, displaying resolution,
|
||||||
|
// and providing an option to open the cover in a new tab.
|
||||||
|
// This component exists in order to avoid a second api call per image. Since the images are being queried and sorted on a parent component (Cover.vue).
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="relative rounded-xs" :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 overflow-hidden">
|
||||||
|
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-xs bg-primary">
|
||||||
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
|
</div>
|
||||||
|
<img ref="cover" :src="coverImage.url" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" @error="imageError" @load="imageLoaded" />
|
||||||
|
|
||||||
|
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="coverImage.url" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-xs rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
|
||||||
|
<span class="material-symbols" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
|
<img v-if="width > 100" src="/Logo.png" class="mb-2" :style="{ height: 40 * sizeMultiplier + 'px' }" />
|
||||||
|
<p class="text-center text-error" :style="{ fontSize: invalidCoverFontSize + 'rem' }">Invalid Cover</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!imageFailed && showResolution && resolution" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">
|
||||||
|
{{ resolution }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
coverImage: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
validator: (value) => {
|
||||||
|
return value.url && value.width && value.height
|
||||||
|
}
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 120
|
||||||
|
},
|
||||||
|
showOpenNewTab: Boolean,
|
||||||
|
bookCoverAspectRatio: Number,
|
||||||
|
showResolution: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
imageFailed: false,
|
||||||
|
showCoverBg: false,
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'coverImage.url': {
|
||||||
|
handler() {
|
||||||
|
this.imageFailed = false
|
||||||
|
this.calculateCoverBg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / 120
|
||||||
|
},
|
||||||
|
invalidCoverFontSize() {
|
||||||
|
return Math.max(this.sizeMultiplier * 0.8, 0.5)
|
||||||
|
},
|
||||||
|
placeholderCoverPadding() {
|
||||||
|
return 0.8 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
resolution() {
|
||||||
|
return `${this.coverImage.width}×${this.coverImage.height}px`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setCoverBg() {
|
||||||
|
if (this.$refs.coverBg) {
|
||||||
|
this.$refs.coverBg.style.backgroundImage = `url("${this.coverImage.url}")`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calculateCoverBg() {
|
||||||
|
const aspectRatio = this.coverImage.height / this.coverImage.width
|
||||||
|
const 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) {
|
||||||
|
this.showCoverBg = true
|
||||||
|
this.$nextTick(this.setCoverBg)
|
||||||
|
} else {
|
||||||
|
this.showCoverBg = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageLoaded() {
|
||||||
|
this.imageFailed = false
|
||||||
|
this.calculateCoverBg()
|
||||||
|
},
|
||||||
|
imageError(err) {
|
||||||
|
console.error('ImgError', err)
|
||||||
|
this.imageFailed = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.calculateCoverBg()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
97
client/components/covers/SortedCovers.vue
Normal file
97
client/components/covers/SortedCovers.vue
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// This component sorts and displays book covers based on their aspect ratio.
|
||||||
|
// It separates covers into primary (preferred aspect ratio) and secondary (opposite aspect ratio) sections.
|
||||||
|
// Covers are displayed with options to select, view resolution, and open in a new tab.
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center sm:max-h-80 sm:overflow-y-scroll max-w-full">
|
||||||
|
<!-- Primary Covers Section (based on preferred aspect ratio) -->
|
||||||
|
<div v-if="primaryCovers.length" class="flex items-center flex-wrap justify-center">
|
||||||
|
<template v-for="cover in primaryCovers">
|
||||||
|
<div :key="cover.url" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.url === selectedCover ? 'border-yellow-300' : ''" @click="$emit('select-cover', cover.url)">
|
||||||
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
|
<covers-display-cover :cover-image="cover" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="true" :show-open-new-tab="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Divider if both sections have covers -->
|
||||||
|
<div v-if="primaryCovers.length && secondaryCovers.length" class="w-full border-b border-white/10 my-4"></div>
|
||||||
|
|
||||||
|
<!-- Secondary Covers Section (opposite aspect ratio) -->
|
||||||
|
<div v-if="secondaryCovers.length" class="flex items-center flex-wrap justify-center">
|
||||||
|
<template v-for="cover in secondaryCovers">
|
||||||
|
<div :key="cover.url" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.url === selectedCover ? 'border-yellow-300' : ''" @click="$emit('select-cover', cover.url)">
|
||||||
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
|
<covers-display-cover :cover-image="cover" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="true" :show-open-new-tab="true" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DisplayCover from './DisplayCover.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SortedCovers',
|
||||||
|
components: {
|
||||||
|
'covers-display-cover': DisplayCover
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
covers: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
bookCoverAspectRatio: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
selectedCover: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// Sort covers by dimension and size
|
||||||
|
sortedCovers() {
|
||||||
|
return [...this.covers].sort((a, b) => {
|
||||||
|
// Sort by area (width * height)
|
||||||
|
return a.width * a.height - b.width * b.height
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// Get square covers (width === height)
|
||||||
|
squareCovers() {
|
||||||
|
return this.sortedCovers.filter((cover) => cover.width === cover.height)
|
||||||
|
},
|
||||||
|
// Get rectangular covers (width !== height)
|
||||||
|
rectangleCovers() {
|
||||||
|
return this.sortedCovers.filter((cover) => cover.width !== cover.height)
|
||||||
|
},
|
||||||
|
// Determine primary covers based on preferred aspect ratio
|
||||||
|
primaryCovers() {
|
||||||
|
return this.bookCoverAspectRatio === 1 ? this.squareCovers : this.rectangleCovers
|
||||||
|
},
|
||||||
|
// Determine secondary covers (opposite of primary)
|
||||||
|
secondaryCovers() {
|
||||||
|
return this.bookCoverAspectRatio === 1 ? this.rectangleCovers : this.squareCovers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Ensure proper height distribution */
|
||||||
|
.cover-grid-container {
|
||||||
|
min-height: 200px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.cover-grid-container {
|
||||||
|
max-height: calc(100vh - 400px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -70,63 +70,9 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Cover Search Results -->
|
<!-- Cover Search Results -->
|
||||||
<div v-if="hasSearched" class="flex flex-col items-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full">
|
<div v-if="hasSearched" class="mt-2">
|
||||||
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
|
<p v-if="!coversFound.length" class="text-center">{{ $strings.MessageNoCoversFound }}</p>
|
||||||
|
<covers-sorted-covers v-else :covers="sortedCovers" :book-cover-aspect-ratio="bookCoverAspectRatio" :selected-cover="coverPath" @select-cover="updateCover" />
|
||||||
<!-- Conditional Rendering Based on bookCoverAspectRatio -->
|
|
||||||
<template v-if="bookCoverAspectRatio === 1">
|
|
||||||
<!-- Square Covers First -->
|
|
||||||
<template v-if="squareCovers.length">
|
|
||||||
<div class="flex items-center flex-wrap justify-center">
|
|
||||||
<template v-for="cover in squareCovers">
|
|
||||||
<div :key="cover.src" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.src === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover.src)">
|
|
||||||
<covers-preview-cover :src="cover.src" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Divider if there are rectangle covers -->
|
|
||||||
<div v-if="rectangleCovers.length" class="w-full border-b border-white/10 my-4"></div>
|
|
||||||
|
|
||||||
<!-- Rectangle Covers -->
|
|
||||||
<template v-if="rectangleCovers.length">
|
|
||||||
<div class="flex items-center flex-wrap justify-center">
|
|
||||||
<template v-for="cover in rectangleCovers">
|
|
||||||
<div :key="cover.src" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.src === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover.src)">
|
|
||||||
<covers-preview-cover :src="cover.src" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<!-- Rectangle Covers First -->
|
|
||||||
<template v-if="rectangleCovers.length">
|
|
||||||
<div class="flex items-center flex-wrap justify-center">
|
|
||||||
<template v-for="cover in rectangleCovers">
|
|
||||||
<div :key="cover.src" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.src === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover.src)">
|
|
||||||
<covers-preview-cover :src="cover.src" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Divider if there are square covers -->
|
|
||||||
<div v-if="squareCovers.length" class="w-full border-b border-white/10 my-4"></div>
|
|
||||||
|
|
||||||
<!-- Square Covers -->
|
|
||||||
<template v-if="squareCovers.length">
|
|
||||||
<div class="flex items-center flex-wrap justify-center">
|
|
||||||
<template v-for="cover in squareCovers">
|
|
||||||
<div :key="cover.src" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.src === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover.src)">
|
|
||||||
<covers-preview-cover :src="cover.src" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
<div v-if="previewUpload" class="absolute top-0 left-0 w-full h-full z-10 bg-bg p-8">
|
||||||
@ -391,26 +337,32 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async resolveImages() {
|
async resolveImages() {
|
||||||
|
console.log(this.coversFound)
|
||||||
const resolvedImages = await Promise.all(
|
const resolvedImages = await Promise.all(
|
||||||
this.coversFound.map((cover) => {
|
this.coversFound.map((cover) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const img = new Image()
|
const img = new Image()
|
||||||
img.src = cover
|
img.src = cover
|
||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
resolve({ src: cover, width: img.width, height: img.height })
|
resolve({
|
||||||
|
url: cover,
|
||||||
|
width: img.width,
|
||||||
|
height: img.height
|
||||||
|
})
|
||||||
|
}
|
||||||
|
img.onerror = () => {
|
||||||
|
// Resolve with default dimensions if image fails to load
|
||||||
|
resolve({
|
||||||
|
url: cover,
|
||||||
|
width: 400,
|
||||||
|
height: 600
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
// Sort images: squares first, then rectangles by area
|
this.sortedCovers = resolvedImages
|
||||||
this.sortedCovers = resolvedImages.sort((a, b) => {
|
console.log('Resolved covers:', this.sortedCovers)
|
||||||
// Prioritize square images (-1 for square, 1 for non-square)
|
|
||||||
const squareComparison = (b.width === b.height) - (a.width === a.height)
|
|
||||||
if (squareComparison !== 0) return squareComparison
|
|
||||||
|
|
||||||
// Sub-sort by width (ascending order)
|
|
||||||
return a.width - b.width
|
|
||||||
})
|
|
||||||
return this.sortedCovers
|
return this.sortedCovers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user