Add author edit modal & remove from experimental

This commit is contained in:
advplyr 2022-03-14 18:53:49 -05:00
parent deea6702f0
commit 4c2ad3ede5
26 changed files with 344 additions and 131 deletions

View File

@ -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)

View File

@ -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"

View File

@ -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) => {

View File

@ -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>

View 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>

View 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>

View File

@ -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'
} }
] ]
} }

View File

@ -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>

View File

@ -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 />

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<tables-libraries-table /> <tables-library-libraries-table />
</div> </div>
</template> </template>

View File

@ -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() {

View File

@ -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'

View File

@ -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))

View File

@ -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
}, [])) }, []))

View File

@ -248,6 +248,5 @@ class CoverController {
} }
return false return false
} }
} }
module.exports = CoverController module.exports = CoverController

View File

@ -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([])

View File

@ -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()