mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-20 01:17:45 +02:00
Add:Authors page match authors and display author image
This commit is contained in:
parent
dad12537b6
commit
30f15d3575
@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div @mouseover="mouseover" @mouseout="mouseout">
|
||||||
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-lg relative">
|
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-lg relative overflow-hidden">
|
||||||
<div class="w-full h-full overflow-hidden max-w-full max-h-full relative">
|
<!-- Image or placeholder -->
|
||||||
<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">
|
<svg v-if="!imagePath" 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 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="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
|
<path
|
||||||
@ -15,15 +15,25 @@
|
|||||||
/>
|
/>
|
||||||
<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" />
|
<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>
|
</svg>
|
||||||
|
<div v-else class="w-full h-full relative overflow-hidden rounded-lg">
|
||||||
<div class="absolute bottom-0 left-0 w-full py-2 bg-black bg-opacity-25 px-2">
|
<div v-if="showCoverBg" class="cover-bg absolute" :style="{ backgroundImage: `url(${imgSrc})` }" />
|
||||||
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p>
|
<img ref="img" :src="imgSrc" @load="imageLoaded" class="absolute top-0 left-0 h-full w-full object-contain" />
|
||||||
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.85 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200" @click.stop="searchAuthor">
|
<!-- Author name & num books overlay -->
|
||||||
|
<div v-show="!searching" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
|
||||||
|
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
|
||||||
|
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search icon btn -->
|
||||||
|
<div v-show="!searching && isHovering" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200" @click.prevent.stop="searchAuthor">
|
||||||
<span class="material-icons">search</span>
|
<span class="material-icons">search</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading spinner -->
|
||||||
|
<div v-show="searching" class="absolute top-0 left-0 z-10 w-full h-full bg-black bg-opacity-50 flex items-center justify-center">
|
||||||
|
<widgets-loading-spinner size="" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -38,11 +48,16 @@ export default {
|
|||||||
},
|
},
|
||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: Number,
|
||||||
sizeMultiplier: Number
|
sizeMultiplier: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
placeholder: '/Logo.png'
|
searching: false,
|
||||||
|
isHovering: false,
|
||||||
|
showCoverBg: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -58,33 +73,65 @@ export default {
|
|||||||
name() {
|
name() {
|
||||||
return this._author.name || ''
|
return this._author.name || ''
|
||||||
},
|
},
|
||||||
image() {
|
imagePath() {
|
||||||
return this._author.image || null
|
return this._author.imagePath || null
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
return this._author.description
|
return this._author.description
|
||||||
},
|
},
|
||||||
lastUpdate() {
|
updatedAt() {
|
||||||
return this._author.lastUpdate
|
return this._author.updatedAt
|
||||||
},
|
},
|
||||||
numBooks() {
|
numBooks() {
|
||||||
return this._author.numBooks || 0
|
return this._author.numBooks || 0
|
||||||
},
|
},
|
||||||
imgSrc() {
|
imgSrc() {
|
||||||
if (!this.image) return this.placeholder
|
if (!this.imagePath) return null
|
||||||
var encodedImg = this.image.replace(/%/g, '%25').replace(/#/g, '%23')
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// Testing
|
||||||
var url = new URL(encodedImg, document.baseURI)
|
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||||
return url.href + `?token=${this.userToken}&ts=${this.lastUpdate}`
|
}
|
||||||
|
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||||
|
},
|
||||||
|
aspectRatio() {
|
||||||
|
return this.height / this.width
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseout() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
imageLoaded() {
|
||||||
|
if (this.$refs.img) {
|
||||||
|
var { naturalWidth, naturalHeight } = this.$refs.img
|
||||||
|
var aspectRatio = naturalHeight / naturalWidth
|
||||||
|
var arDiff = Math.abs(aspectRatio - this.aspectRatio)
|
||||||
|
|
||||||
|
if (arDiff > 0.15) {
|
||||||
|
this.showCoverBg = true
|
||||||
|
} else {
|
||||||
|
this.showCoverBg = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async searchAuthor() {
|
async searchAuthor() {
|
||||||
var author = await this.$axios.$post(`/api/authors/${this.authorId}/match`, { q: this.name }).catch((error) => {
|
this.searching = true
|
||||||
|
var response = await this.$axios.$post(`/api/authors/${this.authorId}/match`, { q: this.name }).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
console.log('Got author', author)
|
if (!response) {
|
||||||
|
this.$toast.error('Author not found')
|
||||||
|
} else if (response.updated) {
|
||||||
|
if (response.author.imagePath) this.$toast.success('Author was updated')
|
||||||
|
else this.$toast.success('Author was updated (no image found)')
|
||||||
|
} else {
|
||||||
|
this.$toast.info('No updates were made for Author')
|
||||||
|
}
|
||||||
|
this.searching = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
@ -9,16 +9,7 @@
|
|||||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
<div class="absolute top-2 right-2">
|
<div class="absolute top-2 right-2">
|
||||||
<div class="la-ball-spin-clockwise la-sm">
|
<widgets-loading-spinner />
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -171,214 +162,3 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
/*!
|
|
||||||
* Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
|
|
||||||
* Copyright 2015 Daniel Cardoso <@DanielCardoso>
|
|
||||||
* Licensed under MIT
|
|
||||||
*/
|
|
||||||
.la-ball-spin-clockwise,
|
|
||||||
.la-ball-spin-clockwise > div {
|
|
||||||
position: relative;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise {
|
|
||||||
display: block;
|
|
||||||
font-size: 0;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise.la-dark {
|
|
||||||
color: #262626;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div {
|
|
||||||
display: inline-block;
|
|
||||||
float: none;
|
|
||||||
background-color: currentColor;
|
|
||||||
border: 0 solid currentColor;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
margin-top: -4px;
|
|
||||||
margin-left: -4px;
|
|
||||||
border-radius: 100%;
|
|
||||||
-webkit-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
|
||||||
-moz-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
|
||||||
-o-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
|
||||||
animation: ball-spin-clockwise 1s infinite ease-in-out;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div:nth-child(1) {
|
|
||||||
top: 5%;
|
|
||||||
left: 50%;
|
|
||||||
-webkit-animation-delay: -0.875s;
|
|
||||||
-moz-animation-delay: -0.875s;
|
|
||||||
-o-animation-delay: -0.875s;
|
|
||||||
animation-delay: -0.875s;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div:nth-child(2) {
|
|
||||||
top: 18.1801948466%;
|
|
||||||
left: 81.8198051534%;
|
|
||||||
-webkit-animation-delay: -0.75s;
|
|
||||||
-moz-animation-delay: -0.75s;
|
|
||||||
-o-animation-delay: -0.75s;
|
|
||||||
animation-delay: -0.75s;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div:nth-child(3) {
|
|
||||||
top: 50%;
|
|
||||||
left: 95%;
|
|
||||||
-webkit-animation-delay: -0.625s;
|
|
||||||
-moz-animation-delay: -0.625s;
|
|
||||||
-o-animation-delay: -0.625s;
|
|
||||||
animation-delay: -0.625s;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div:nth-child(4) {
|
|
||||||
top: 81.8198051534%;
|
|
||||||
left: 81.8198051534%;
|
|
||||||
-webkit-animation-delay: -0.5s;
|
|
||||||
-moz-animation-delay: -0.5s;
|
|
||||||
-o-animation-delay: -0.5s;
|
|
||||||
animation-delay: -0.5s;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div:nth-child(5) {
|
|
||||||
top: 94.9999999966%;
|
|
||||||
left: 50.0000000005%;
|
|
||||||
-webkit-animation-delay: -0.375s;
|
|
||||||
-moz-animation-delay: -0.375s;
|
|
||||||
-o-animation-delay: -0.375s;
|
|
||||||
animation-delay: -0.375s;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div:nth-child(6) {
|
|
||||||
top: 81.8198046966%;
|
|
||||||
left: 18.1801949248%;
|
|
||||||
-webkit-animation-delay: -0.25s;
|
|
||||||
-moz-animation-delay: -0.25s;
|
|
||||||
-o-animation-delay: -0.25s;
|
|
||||||
animation-delay: -0.25s;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div:nth-child(7) {
|
|
||||||
top: 49.9999750815%;
|
|
||||||
left: 5.0000051215%;
|
|
||||||
-webkit-animation-delay: -0.125s;
|
|
||||||
-moz-animation-delay: -0.125s;
|
|
||||||
-o-animation-delay: -0.125s;
|
|
||||||
animation-delay: -0.125s;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise > div:nth-child(8) {
|
|
||||||
top: 18.179464974%;
|
|
||||||
left: 18.1803700518%;
|
|
||||||
-webkit-animation-delay: 0s;
|
|
||||||
-moz-animation-delay: 0s;
|
|
||||||
-o-animation-delay: 0s;
|
|
||||||
animation-delay: 0s;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise.la-sm {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise.la-sm > div {
|
|
||||||
width: 4px;
|
|
||||||
height: 4px;
|
|
||||||
margin-top: -2px;
|
|
||||||
margin-left: -2px;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise.la-2x {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise.la-2x > div {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-top: -8px;
|
|
||||||
margin-left: -8px;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise.la-3x {
|
|
||||||
width: 96px;
|
|
||||||
height: 96px;
|
|
||||||
}
|
|
||||||
.la-ball-spin-clockwise.la-3x > div {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
margin-top: -12px;
|
|
||||||
margin-left: -12px;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* Animation
|
|
||||||
*/
|
|
||||||
@-webkit-keyframes ball-spin-clockwise {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: scale(1);
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
80% {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@-moz-keyframes ball-spin-clockwise {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
-moz-transform: scale(1);
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
80% {
|
|
||||||
opacity: 0;
|
|
||||||
-moz-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@-o-keyframes ball-spin-clockwise {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
-o-transform: scale(1);
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
80% {
|
|
||||||
opacity: 0;
|
|
||||||
-o-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes ball-spin-clockwise {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: scale(1);
|
|
||||||
-moz-transform: scale(1);
|
|
||||||
-o-transform: scale(1);
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
20% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
80% {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: scale(0);
|
|
||||||
-moz-transform: scale(0);
|
|
||||||
-o-transform: scale(0);
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
241
client/components/widgets/LoadingSpinner.vue
Normal file
241
client/components/widgets/LoadingSpinner.vue
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
<template>
|
||||||
|
<div class="la-ball-spin-clockwise" :class="`${size}`">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'la-sm'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/*!
|
||||||
|
* Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
|
||||||
|
* Copyright 2015 Daniel Cardoso <@DanielCardoso>
|
||||||
|
* Licensed under MIT
|
||||||
|
*/
|
||||||
|
.la-ball-spin-clockwise,
|
||||||
|
.la-ball-spin-clockwise > div {
|
||||||
|
position: relative;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise {
|
||||||
|
display: block;
|
||||||
|
font-size: 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-dark {
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div {
|
||||||
|
display: inline-block;
|
||||||
|
float: none;
|
||||||
|
background-color: currentColor;
|
||||||
|
border: 0 solid currentColor;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-left: -4px;
|
||||||
|
border-radius: 100%;
|
||||||
|
-webkit-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||||
|
-moz-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||||
|
-o-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||||
|
animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(1) {
|
||||||
|
top: 5%;
|
||||||
|
left: 50%;
|
||||||
|
-webkit-animation-delay: -0.875s;
|
||||||
|
-moz-animation-delay: -0.875s;
|
||||||
|
-o-animation-delay: -0.875s;
|
||||||
|
animation-delay: -0.875s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(2) {
|
||||||
|
top: 18.1801948466%;
|
||||||
|
left: 81.8198051534%;
|
||||||
|
-webkit-animation-delay: -0.75s;
|
||||||
|
-moz-animation-delay: -0.75s;
|
||||||
|
-o-animation-delay: -0.75s;
|
||||||
|
animation-delay: -0.75s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(3) {
|
||||||
|
top: 50%;
|
||||||
|
left: 95%;
|
||||||
|
-webkit-animation-delay: -0.625s;
|
||||||
|
-moz-animation-delay: -0.625s;
|
||||||
|
-o-animation-delay: -0.625s;
|
||||||
|
animation-delay: -0.625s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(4) {
|
||||||
|
top: 81.8198051534%;
|
||||||
|
left: 81.8198051534%;
|
||||||
|
-webkit-animation-delay: -0.5s;
|
||||||
|
-moz-animation-delay: -0.5s;
|
||||||
|
-o-animation-delay: -0.5s;
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(5) {
|
||||||
|
top: 94.9999999966%;
|
||||||
|
left: 50.0000000005%;
|
||||||
|
-webkit-animation-delay: -0.375s;
|
||||||
|
-moz-animation-delay: -0.375s;
|
||||||
|
-o-animation-delay: -0.375s;
|
||||||
|
animation-delay: -0.375s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(6) {
|
||||||
|
top: 81.8198046966%;
|
||||||
|
left: 18.1801949248%;
|
||||||
|
-webkit-animation-delay: -0.25s;
|
||||||
|
-moz-animation-delay: -0.25s;
|
||||||
|
-o-animation-delay: -0.25s;
|
||||||
|
animation-delay: -0.25s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(7) {
|
||||||
|
top: 49.9999750815%;
|
||||||
|
left: 5.0000051215%;
|
||||||
|
-webkit-animation-delay: -0.125s;
|
||||||
|
-moz-animation-delay: -0.125s;
|
||||||
|
-o-animation-delay: -0.125s;
|
||||||
|
animation-delay: -0.125s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(8) {
|
||||||
|
top: 18.179464974%;
|
||||||
|
left: 18.1803700518%;
|
||||||
|
-webkit-animation-delay: 0s;
|
||||||
|
-moz-animation-delay: 0s;
|
||||||
|
-o-animation-delay: 0s;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-sm {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-sm > div {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-2x {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-2x > div {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-top: -8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-3x {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-3x > div {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -12px;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Animation
|
||||||
|
*/
|
||||||
|
@-webkit-keyframes ball-spin-clockwise {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-moz-keyframes ball-spin-clockwise {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0;
|
||||||
|
-moz-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-o-keyframes ball-spin-clockwise {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0;
|
||||||
|
-o-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes ball-spin-clockwise {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-moz-transform: scale(0);
|
||||||
|
-o-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -8,7 +8,7 @@
|
|||||||
<div class="flex flex-wrap justify-center">
|
<div class="flex flex-wrap justify-center">
|
||||||
<template v-for="author in authors">
|
<template v-for="author in authors">
|
||||||
<nuxt-link :key="author.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`">
|
<nuxt-link :key="author.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`">
|
||||||
<cards-author-card :author="author" :width="160" :height="160" class="p-3" />
|
<cards-author-card :author="author" :width="160" :height="200" class="p-3" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -52,10 +52,34 @@ export default {
|
|||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
},
|
||||||
|
authorAdded(author) {
|
||||||
|
if (!this.authors.some((au) => au.id === author.id)) {
|
||||||
|
this.authors.push(author)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
authorUpdated(author) {
|
||||||
|
this.authors = this.authors.map((au) => {
|
||||||
|
if (au.id === author.id) {
|
||||||
|
return author
|
||||||
|
}
|
||||||
|
return au
|
||||||
|
})
|
||||||
|
},
|
||||||
|
authorRemoved(author) {
|
||||||
|
this.authors = this.authors.filter((au) => au.id !== author.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
|
this.$root.socket.on('author_added', this.authorAdded)
|
||||||
|
this.$root.socket.on('author_updated', this.authorUpdated)
|
||||||
|
this.$root.socket.on('author_removed', this.authorRemoved)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$root.socket.off('author_added', this.authorAdded)
|
||||||
|
this.$root.socket.off('author_updated', this.authorUpdated)
|
||||||
|
this.$root.socket.off('author_removed', this.authorRemoved)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -164,6 +164,7 @@ class ApiController {
|
|||||||
//
|
//
|
||||||
this.router.get('/authors/:id', AuthorController.middleware.bind(this), AuthorController.findOne.bind(this))
|
this.router.get('/authors/:id', AuthorController.middleware.bind(this), AuthorController.findOne.bind(this))
|
||||||
this.router.post('/authors/:id/match', AuthorController.middleware.bind(this), AuthorController.match.bind(this))
|
this.router.post('/authors/:id/match', AuthorController.middleware.bind(this), AuthorController.match.bind(this))
|
||||||
|
this.router.get('/authors/:id/image', AuthorController.middleware.bind(this), AuthorController.getImage.bind(this))
|
||||||
this.router.get('/authors/search', AuthorController.search.bind(this))
|
this.router.get('/authors/search', AuthorController.search.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -8,6 +8,7 @@ class CacheManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this.CachePath = Path.join(global.MetadataPath, 'cache')
|
this.CachePath = Path.join(global.MetadataPath, 'cache')
|
||||||
this.CoverCachePath = Path.join(this.CachePath, 'covers')
|
this.CoverCachePath = Path.join(this.CachePath, 'covers')
|
||||||
|
this.ImageCachePath = Path.join(this.CachePath, 'images')
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleCoverCache(res, libraryItem, options = {}) {
|
async handleCoverCache(res, libraryItem, options = {}) {
|
||||||
@ -72,5 +73,37 @@ class CacheManager {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async handleAuthorCache(res, author, options = {}) {
|
||||||
|
const format = options.format || 'webp'
|
||||||
|
const width = options.width || 400
|
||||||
|
const height = options.height || null
|
||||||
|
|
||||||
|
res.type(`image/${format}`)
|
||||||
|
|
||||||
|
var path = Path.join(this.ImageCachePath, `${author.id}_${width}${height ? `x${height}` : ''}`) + '.' + format
|
||||||
|
|
||||||
|
// Cache exists
|
||||||
|
if (await fs.pathExists(path)) {
|
||||||
|
const r = fs.createReadStream(path)
|
||||||
|
const ps = new stream.PassThrough()
|
||||||
|
stream.pipeline(r, ps, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.log(err)
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return ps.pipe(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write cache
|
||||||
|
await fs.ensureDir(this.ImageCachePath)
|
||||||
|
|
||||||
|
let writtenFile = await resizeImage(author.imagePath, path, width, height)
|
||||||
|
if (!writtenFile) return res.sendStatus(400)
|
||||||
|
|
||||||
|
var readStream = fs.createReadStream(writtenFile)
|
||||||
|
readStream.pipe(res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = CacheManager
|
module.exports = CacheManager
|
@ -61,28 +61,7 @@ class StreamManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async tempCheckStrayStreams() {
|
|
||||||
try {
|
|
||||||
var dirs = await fs.readdir(global.MetadataPath)
|
|
||||||
if (!dirs || !dirs.length) return true
|
|
||||||
|
|
||||||
await Promise.all(dirs.map(async (dirname) => {
|
|
||||||
if (dirname !== 'streams' && dirname !== 'books' && dirname !== 'downloads' && dirname !== 'backups' && dirname !== 'logs' && dirname !== 'cache') {
|
|
||||||
var fullPath = Path.join(global.MetadataPath, dirname)
|
|
||||||
Logger.warn(`Removing OLD Orphan Stream ${dirname}`)
|
|
||||||
return fs.remove(fullPath)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
return true
|
|
||||||
} catch (error) {
|
|
||||||
Logger.debug('No old orphan streams', error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeOrphanStreams() {
|
async removeOrphanStreams() {
|
||||||
await this.tempCheckStrayStreams()
|
|
||||||
try {
|
try {
|
||||||
var dirs = await fs.readdir(this.StreamsPath)
|
var dirs = await fs.readdir(this.StreamsPath)
|
||||||
if (!dirs || !dirs.length) return true
|
if (!dirs || !dirs.length) return true
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const { reqSupportsWebp } = require('../utils/index')
|
||||||
|
|
||||||
class AuthorController {
|
class AuthorController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@ -21,20 +22,55 @@ class AuthorController {
|
|||||||
if (!authorData) {
|
if (!authorData) {
|
||||||
return res.status(404).send('Author not found')
|
return res.status(404).send('Author not found')
|
||||||
}
|
}
|
||||||
|
Logger.debug(`[AuthorController] match author with "${req.body.q}"`, authorData)
|
||||||
|
|
||||||
|
var hasUpdates = false
|
||||||
|
if (authorData.asin && req.author.asin !== authorData.asin) {
|
||||||
req.author.asin = authorData.asin
|
req.author.asin = authorData.asin
|
||||||
if (authorData.image) {
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only updates image if there was no image before or the author ASIN was updated
|
||||||
|
if (authorData.image && (!req.author.imagePath || hasUpdates)) {
|
||||||
var imageData = await this.authorFinder.saveAuthorImage(req.author.id, authorData.image)
|
var imageData = await this.authorFinder.saveAuthorImage(req.author.id, authorData.image)
|
||||||
if (imageData) {
|
if (imageData) {
|
||||||
req.author.imagePath = imageData.path
|
req.author.imagePath = imageData.path
|
||||||
req.author.relImagePath = imageData.relPath
|
req.author.relImagePath = imageData.relPath
|
||||||
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (authorData.description) {
|
|
||||||
|
if (authorData.description && req.author.description !== authorData.description) {
|
||||||
req.author.description = authorData.description
|
req.author.description = authorData.description
|
||||||
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasUpdates) {
|
||||||
|
req.author.updatedAt = Date.now()
|
||||||
|
|
||||||
await this.db.updateEntity('author', req.author)
|
await this.db.updateEntity('author', req.author)
|
||||||
this.emitter('author_updated', req.author)
|
var numBooks = this.db.libraryItems.filter(li => {
|
||||||
res.json(req.author)
|
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
|
||||||
|
}).length
|
||||||
|
this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
updated: hasUpdates,
|
||||||
|
author: req.author
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET api/authors/:id/image
|
||||||
|
async getImage(req, res) {
|
||||||
|
let { query: { width, height, format }, author } = req
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
format: format || (reqSupportsWebp(req) ? 'webp' : 'jpeg'),
|
||||||
|
height: height ? parseInt(height) : null,
|
||||||
|
width: width ? parseInt(width) : null
|
||||||
|
}
|
||||||
|
return this.cacheManager.handleAuthorCache(res, author, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
|
@ -40,6 +40,12 @@ class Author {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONExpanded(numBooks = 0) {
|
||||||
|
var json = this.toJSON()
|
||||||
|
json.numBooks = numBooks
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
toJSONMinimal() {
|
toJSONMinimal() {
|
||||||
return {
|
return {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
Loading…
Reference in New Issue
Block a user