mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-30 18:48:55 +01:00
Add author edit modal & remove from experimental
This commit is contained in:
parent
deea6702f0
commit
4c2ad3ede5
@ -22,7 +22,7 @@
|
|||||||
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
<div v-if="shelf.type === 'authors'" class="flex items-center">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<nuxt-link :key="entity.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(entity.id)}`">
|
<nuxt-link :key="entity.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(entity.id)}`">
|
||||||
<cards-author-card :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" />
|
<cards-author-card :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@ -43,6 +43,7 @@
|
|||||||
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight">
|
<div v-show="canScrollRight && !isScrolling" class="hidden sm:flex absolute top-0 right-0 w-32 pl-8 bg-black book-shelf-arrow-right items-center justify-center cursor-pointer opacity-0 hover:opacity-100 z-30" @click="scrollRight">
|
||||||
<span class="material-icons text-6xl text-white">chevron_right</span>
|
<span class="material-icons text-6xl text-white">chevron_right</span>
|
||||||
</div>
|
</div>
|
||||||
|
<modals-authors-edit-modal v-model="showAuthorModal" :author="selectedAuthor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -64,7 +65,9 @@ export default {
|
|||||||
canScrollLeft: false,
|
canScrollLeft: false,
|
||||||
isScrolling: false,
|
isScrolling: false,
|
||||||
scrollTimer: null,
|
scrollTimer: null,
|
||||||
updateTimer: null
|
updateTimer: null,
|
||||||
|
showAuthorModal: false,
|
||||||
|
selectedAuthor: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -94,6 +97,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
editAuthor(author) {
|
||||||
|
this.selectedAuthor = author
|
||||||
|
this.showAuthorModal = true
|
||||||
|
},
|
||||||
editBook(audiobook) {
|
editBook(audiobook) {
|
||||||
var bookIds = this.shelf.entities.map((e) => e.id)
|
var bookIds = this.shelf.entities.map((e) => e.id)
|
||||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</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'">
|
<nuxt-link :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">
|
<svg class="w-6 h-6" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
@ -2,23 +2,7 @@
|
|||||||
<div @mouseover="mouseover" @mouseout="mouseout">
|
<div @mouseover="mouseover" @mouseout="mouseout">
|
||||||
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-lg relative overflow-hidden">
|
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-lg relative overflow-hidden">
|
||||||
<!-- Image or placeholder -->
|
<!-- Image or placeholder -->
|
||||||
<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">
|
<covers-author-image :author="author" />
|
||||||
<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 v-else class="w-full h-full relative overflow-hidden rounded-lg">
|
|
||||||
<div v-if="showCoverBg" class="cover-bg absolute" :style="{ backgroundImage: `url(${imgSrc})` }" />
|
|
||||||
<img ref="img" :src="imgSrc" @load="imageLoaded" class="absolute top-0 left-0 h-full w-full object-contain" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Author name & num books overlay -->
|
<!-- 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">
|
<div v-show="!searching" class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
|
||||||
@ -27,8 +11,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Search icon btn -->
|
<!-- 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">
|
<div v-show="!searching && isHovering" class="absolute top-0 left-0 p-2 cursor-pointer hover:text-white text-gray-200 transform transition-transform hover:scale-125" @click.prevent.stop="searchAuthor">
|
||||||
<span class="material-icons">search</span>
|
<span class="material-icons text-lg">search</span>
|
||||||
|
</div>
|
||||||
|
<div v-show="isHovering && !searching" class="absolute top-0 right-0 p-2 cursor-pointer hover:text-white text-gray-200 transform transition-transform hover:scale-125" @click.prevent.stop="$emit('edit', author)">
|
||||||
|
<span class="material-icons text-lg">edit</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading spinner -->
|
<!-- Loading spinner -->
|
||||||
@ -56,8 +43,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searching: false,
|
searching: false,
|
||||||
isHovering: false,
|
isHovering: false
|
||||||
showCoverBg: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -73,28 +59,8 @@ export default {
|
|||||||
name() {
|
name() {
|
||||||
return this._author.name || ''
|
return this._author.name || ''
|
||||||
},
|
},
|
||||||
imagePath() {
|
|
||||||
return this._author.imagePath || null
|
|
||||||
},
|
|
||||||
description() {
|
|
||||||
return this._author.description
|
|
||||||
},
|
|
||||||
updatedAt() {
|
|
||||||
return this._author.updatedAt
|
|
||||||
},
|
|
||||||
numBooks() {
|
numBooks() {
|
||||||
return this._author.numBooks || 0
|
return this._author.numBooks || 0
|
||||||
},
|
|
||||||
imgSrc() {
|
|
||||||
if (!this.imagePath) return null
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
// Testing
|
|
||||||
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
|
||||||
}
|
|
||||||
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
|
||||||
},
|
|
||||||
aspectRatio() {
|
|
||||||
return this.height / this.width
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -104,19 +70,6 @@ export default {
|
|||||||
mouseout() {
|
mouseout() {
|
||||||
this.isHovering = false
|
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() {
|
||||||
this.searching = true
|
this.searching = true
|
||||||
var response = await this.$axios.$post(`/api/authors/${this.authorId}/match`, { q: this.name }).catch((error) => {
|
var response = await this.$axios.$post(`/api/authors/${this.authorId}/match`, { q: this.name }).catch((error) => {
|
||||||
|
@ -1,26 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full px-1 overflow-hidden">
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
<div class="overflow-hidden bg-primary rounded-sm" style="height: 50px; width: 40px">
|
<div class="overflow-hidden bg-primary rounded" style="height: 50px; width: 40px">
|
||||||
<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">
|
<covers-author-image :author="author" />
|
||||||
<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 v-else class="w-full h-full relative overflow-hidden rounded-sm">
|
|
||||||
<div v-if="showCoverBg" class="cover-bg absolute" :style="{ backgroundImage: `url(${imgSrc})` }" />
|
|
||||||
<img ref="img" :src="imgSrc" @load="imageLoaded" class="absolute top-0 left-0 h-full w-full object-contain" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<!-- <img v-if="!imagePath" src="/icons/NoUserPhoto.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" />
|
|
||||||
<img v-else :src="imgSrc" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" /> -->
|
|
||||||
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
||||||
<p class="truncate text-sm">{{ name }}</p>
|
<p class="truncate text-sm">{{ name }}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -36,50 +18,17 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {}
|
||||||
showCoverBg: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
authorId() {
|
|
||||||
return this.author.id
|
|
||||||
},
|
|
||||||
name() {
|
name() {
|
||||||
return this.author.name
|
return this.author.name
|
||||||
},
|
|
||||||
imagePath() {
|
|
||||||
return this.author.imagePath
|
|
||||||
},
|
|
||||||
updatedAt() {
|
|
||||||
return this.author.updatedAt
|
|
||||||
},
|
|
||||||
imgSrc() {
|
|
||||||
if (!this.imagePath) return null
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
// Testing
|
|
||||||
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
|
||||||
}
|
|
||||||
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
methods: {},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
87
client/components/covers/AuthorImage.vue
Normal file
87
client/components/covers/AuthorImage.vue
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="wrapper" :class="`rounded-${rounded}`" class="w-full h-full bg-primary overflow-hidden">
|
||||||
|
<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 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 v-else class="w-full h-full relative">
|
||||||
|
<div v-if="showCoverBg" class="cover-bg absolute" :style="{ backgroundImage: `url(${imgSrc})` }" />
|
||||||
|
<img ref="img" :src="imgSrc" @load="imageLoaded" class="absolute top-0 left-0 h-full w-full" :class="coverContain ? 'object-contain' : 'object-cover'" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
author: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
type: String,
|
||||||
|
default: 'lg'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showCoverBg: false,
|
||||||
|
coverContain: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
_author() {
|
||||||
|
return this.author || {}
|
||||||
|
},
|
||||||
|
authorId() {
|
||||||
|
return this._author.id
|
||||||
|
},
|
||||||
|
imagePath() {
|
||||||
|
return this._author.imagePath
|
||||||
|
},
|
||||||
|
updatedAt() {
|
||||||
|
return this._author.updatedAt
|
||||||
|
},
|
||||||
|
imgSrc() {
|
||||||
|
if (!this.imagePath) return null
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
// Testing
|
||||||
|
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||||
|
}
|
||||||
|
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
imageLoaded() {
|
||||||
|
var aspectRatio = 1.25
|
||||||
|
if (this.$refs.wrapper) {
|
||||||
|
aspectRatio = this.$refs.wrapper.clientHeight / this.$refs.wrapper.clientWidth
|
||||||
|
}
|
||||||
|
if (this.$refs.img) {
|
||||||
|
var { naturalWidth, naturalHeight } = this.$refs.img
|
||||||
|
var imgAr = naturalHeight / naturalWidth
|
||||||
|
var arDiff = Math.abs(imgAr - aspectRatio)
|
||||||
|
if (arDiff > 0.15) {
|
||||||
|
this.showCoverBg = true
|
||||||
|
} else {
|
||||||
|
this.showCoverBg = false
|
||||||
|
this.coverContain = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
160
client/components/modals/authors/EditModal.vue
Normal file
160
client/components/modals/authors/EditModal.vue
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="edit-author" :width="800" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="p-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="w-40 p-2">
|
||||||
|
<div class="w-full h-45 relative">
|
||||||
|
<covers-author-image :author="author" />
|
||||||
|
<div v-show="!processing" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
|
||||||
|
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="w-3/4 p-2">
|
||||||
|
<ui-text-input-with-label v-model="authorCopy.name" :disabled="processing" label="Name" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow p-2">
|
||||||
|
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-2">
|
||||||
|
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" label="Description" :rows="8" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex pt-2 px-2">
|
||||||
|
<ui-btn type="button" @click="searchAuthor">Quick Match</ui-btn>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn type="submit">Submit</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
author: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
authorCopy: {
|
||||||
|
name: '',
|
||||||
|
asin: '',
|
||||||
|
description: ''
|
||||||
|
},
|
||||||
|
processing: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
author: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
authorId() {
|
||||||
|
if (!this.author) return ''
|
||||||
|
return this.author.id
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return 'Edit Author'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
init() {
|
||||||
|
this.authorCopy.name = this.author.name
|
||||||
|
this.authorCopy.asin = this.author.asin
|
||||||
|
this.authorCopy.description = this.author.description
|
||||||
|
},
|
||||||
|
async submitForm() {
|
||||||
|
var keysToCheck = ['name', 'asin', 'description']
|
||||||
|
var updatePayload = {}
|
||||||
|
keysToCheck.forEach((key) => {
|
||||||
|
if (this.authorCopy[key] !== this.author[key]) {
|
||||||
|
updatePayload[key] = this.authorCopy[key]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!Object.keys(updatePayload).length) {
|
||||||
|
this.$toast.info('No updates are necessary')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.processing = true
|
||||||
|
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Failed to update author')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (result) {
|
||||||
|
if (result.updated) this.$toast.success('Author updated')
|
||||||
|
else this.$toast.info('No updates were needed')
|
||||||
|
}
|
||||||
|
this.processing = false
|
||||||
|
},
|
||||||
|
async removeCover() {
|
||||||
|
var updatePayload = {
|
||||||
|
imagePath: null,
|
||||||
|
relImagePath: null
|
||||||
|
}
|
||||||
|
this.processing = true
|
||||||
|
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error('Failed to remove image')
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (result && result.updated) {
|
||||||
|
this.$toast.success('Author image removed')
|
||||||
|
}
|
||||||
|
this.processing = false
|
||||||
|
},
|
||||||
|
async searchAuthor() {
|
||||||
|
if (!this.authorCopy.name) {
|
||||||
|
this.$toast.error('Must enter an author name')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.processing = true
|
||||||
|
var response = await this.$axios.$post(`/api/authors/${this.authorId}/match`, { q: this.authorCopy.name }).catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
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.processing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -35,32 +35,32 @@ export default {
|
|||||||
{
|
{
|
||||||
id: 'details',
|
id: 'details',
|
||||||
title: 'Details',
|
title: 'Details',
|
||||||
component: 'modals-edit-tabs-details'
|
component: 'modals-item-tabs-details'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'cover',
|
id: 'cover',
|
||||||
title: 'Cover',
|
title: 'Cover',
|
||||||
component: 'modals-edit-tabs-cover'
|
component: 'modals-item-tabs-cover'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'chapters',
|
id: 'chapters',
|
||||||
title: 'Chapters',
|
title: 'Chapters',
|
||||||
component: 'modals-edit-tabs-chapters'
|
component: 'modals-item-tabs-chapters'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'files',
|
id: 'files',
|
||||||
title: 'Files',
|
title: 'Files',
|
||||||
component: 'modals-edit-tabs-files'
|
component: 'modals-item-tabs-files'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'download',
|
id: 'download',
|
||||||
title: 'Download',
|
title: 'Download',
|
||||||
component: 'modals-edit-tabs-download'
|
component: 'modals-item-tabs-download'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'match',
|
id: 'match',
|
||||||
title: 'Match',
|
title: 'Match',
|
||||||
component: 'modals-edit-tabs-match'
|
component: 'modals-item-tabs-match'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -9,11 +9,11 @@
|
|||||||
<draggable :list="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
<draggable :list="libraryCopies" v-bind="dragOptions" class="list-group" draggable=".item" tag="div" @start="startDrag" @end="endDrag">
|
||||||
<template v-for="library in libraryCopies">
|
<template v-for="library in libraryCopies">
|
||||||
<div :key="library.id" class="item">
|
<div :key="library.id" class="item">
|
||||||
<modals-libraries-library-item :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
|
<tables-library-item :library="library" :selected="currentLibraryId === library.id" :show-edit="true" :dragging="drag" @edit="editLibrary" @click="setLibrary" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
<modals-libraries-edit-modal v-model="showLibraryModal" :library="selectedLibrary" />
|
||||||
|
|
||||||
<p class="text-xs mt-4 text-gray-200">*<strong>Force Re-Scan</strong> will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed and used for book details.</p>
|
<p class="text-xs mt-4 text-gray-200">*<strong>Force Re-Scan</strong> will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed and used for book details.</p>
|
||||||
|
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<app-stream-container ref="streamContainer" />
|
<app-stream-container ref="streamContainer" />
|
||||||
|
|
||||||
<modals-edit-modal />
|
<modals-item-edit-modal />
|
||||||
<modals-user-collections-modal />
|
<modals-user-collections-modal />
|
||||||
<modals-edit-collection-modal />
|
<modals-edit-collection-modal />
|
||||||
<modals-bookshelf-texture-modal />
|
<modals-bookshelf-texture-modal />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<tables-libraries-table />
|
<tables-library-libraries-table />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -8,13 +8,14 @@
|
|||||||
<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="200" class="p-3" />
|
<cards-author-card :author="author" :width="160" :height="200" class="p-3" @edit="editAuthor" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<modals-authors-edit-modal v-model="showAuthorModal" :author="selectedAuthor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -34,7 +35,9 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: true,
|
loading: true,
|
||||||
authors: []
|
authors: [],
|
||||||
|
showAuthorModal: false,
|
||||||
|
selectedAuthor: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -59,6 +62,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
authorUpdated(author) {
|
authorUpdated(author) {
|
||||||
|
if (this.selectedAuthor && this.selectedAuthor.id === author.id) {
|
||||||
|
this.selectedAuthor = author
|
||||||
|
}
|
||||||
this.authors = this.authors.map((au) => {
|
this.authors = this.authors.map((au) => {
|
||||||
if (au.id === author.id) {
|
if (au.id === author.id) {
|
||||||
return author
|
return author
|
||||||
@ -68,6 +74,10 @@ export default {
|
|||||||
},
|
},
|
||||||
authorRemoved(author) {
|
authorRemoved(author) {
|
||||||
this.authors = this.authors.filter((au) => au.id !== author.id)
|
this.authors = this.authors.filter((au) => au.id !== author.id)
|
||||||
|
},
|
||||||
|
editAuthor(author) {
|
||||||
|
this.selectedAuthor = author
|
||||||
|
this.showAuthorModal = true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -16,7 +16,8 @@ module.exports = {
|
|||||||
extend: {
|
extend: {
|
||||||
height: {
|
height: {
|
||||||
'7.5': '1.75rem',
|
'7.5': '1.75rem',
|
||||||
'18': '4.5rem'
|
'18': '4.5rem',
|
||||||
|
'45': '11.25rem'
|
||||||
},
|
},
|
||||||
width: {
|
width: {
|
||||||
'18': '4.5rem'
|
'18': '4.5rem'
|
||||||
|
@ -152,6 +152,7 @@ class ApiController {
|
|||||||
//
|
//
|
||||||
this.router.get('/authors/search', AuthorController.search.bind(this))
|
this.router.get('/authors/search', AuthorController.search.bind(this))
|
||||||
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.patch('/authors/:id', AuthorController.middleware.bind(this), AuthorController.update.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/:id/image', AuthorController.middleware.bind(this), AuthorController.getImage.bind(this))
|
||||||
|
|
||||||
|
@ -43,13 +43,21 @@ class CacheManager {
|
|||||||
readStream.pipe(res)
|
readStream.pipe(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
async purgeCoverCache(libraryItemId) {
|
purgeCoverCache(libraryItemId) {
|
||||||
|
return this.purgeEntityCache(libraryItemId, this.CoverCachePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
purgeImageCache(entityId) {
|
||||||
|
return this.purgeEntityCache(entityId, this.ImageCachePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
async purgeEntityCache(entityId, cachePath) {
|
||||||
// If purgeAll has been called... The cover cache directory no longer exists
|
// If purgeAll has been called... The cover cache directory no longer exists
|
||||||
await fs.ensureDir(this.CoverCachePath)
|
await fs.ensureDir(cachePath)
|
||||||
return Promise.all((await fs.readdir(this.CoverCachePath)).reduce((promises, file) => {
|
return Promise.all((await fs.readdir(cachePath)).reduce((promises, file) => {
|
||||||
if (file.startsWith(libraryItemId)) {
|
if (file.startsWith(entityId)) {
|
||||||
Logger.debug(`[CacheManager] Going to purge ${file}`);
|
Logger.debug(`[CacheManager] Going to purge ${file}`);
|
||||||
promises.push(this.removeCache(Path.join(this.CoverCachePath, file)))
|
promises.push(this.removeCache(Path.join(cachePath, file)))
|
||||||
}
|
}
|
||||||
return promises
|
return promises
|
||||||
}, []))
|
}, []))
|
||||||
|
@ -248,6 +248,5 @@ class CoverController {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
module.exports = CoverController
|
module.exports = CoverController
|
@ -8,6 +8,29 @@ class AuthorController {
|
|||||||
return res.json(req.author)
|
return res.json(req.author)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async update(req, res) {
|
||||||
|
var payload = req.body
|
||||||
|
|
||||||
|
// If updating or removing cover image then clear cache
|
||||||
|
if (payload.imagePath !== undefined && req.author.imagePath && payload.imagePath !== req.author.imagePath) {
|
||||||
|
this.cacheManager.purgeImageCache(req.author.id)
|
||||||
|
if (!payload.imagePath) { // If removing image then remove file
|
||||||
|
var currentImagePath = req.author.imagePath
|
||||||
|
await this.coverController.removeFile(currentImagePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasUpdated = req.author.update(payload)
|
||||||
|
if (hasUpdated) {
|
||||||
|
await this.db.updateEntity('author', req.author)
|
||||||
|
this.emitter('author_updated', req.author.toJSON())
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
author: req.author.toJSON(),
|
||||||
|
updated: hasUpdated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async search(req, res) {
|
async search(req, res) {
|
||||||
var q = (req.query.q || '').toLowerCase()
|
var q = (req.query.q || '').toLowerCase()
|
||||||
if (!q) return res.json([])
|
if (!q) return res.json([])
|
||||||
|
@ -64,6 +64,21 @@ class Author {
|
|||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
update(payload) {
|
||||||
|
var json = this.toJSON()
|
||||||
|
delete json.id
|
||||||
|
delete json.addedAt
|
||||||
|
delete json.updatedAt
|
||||||
|
var hasUpdates = false
|
||||||
|
for (const key in json) {
|
||||||
|
if (payload[key] !== undefined && json[key] != payload[key]) {
|
||||||
|
this[key] = payload[key]
|
||||||
|
hasUpdates = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasUpdates
|
||||||
|
}
|
||||||
|
|
||||||
checkNameEquals(name) {
|
checkNameEquals(name) {
|
||||||
if (!name) return false
|
if (!name) return false
|
||||||
return this.name.toLowerCase() == name.toLowerCase().trim()
|
return this.name.toLowerCase() == name.toLowerCase().trim()
|
||||||
|
Loading…
Reference in New Issue
Block a user