Add:Authors landing page #187

This commit is contained in:
advplyr 2022-05-08 18:21:46 -05:00
parent f94c706fc8
commit fbd7ae10d1
9 changed files with 333 additions and 38 deletions

View File

@ -42,32 +42,17 @@
height: 8px;
}
/* ::-webkit-scrollbar:horizontal { */
/* height: 16px; */
/* height: 24px;
} */
/* Track */
::-webkit-scrollbar-track {
background-color: rgba(0, 0, 0, 0);
}
/* ::-webkit-scrollbar-track:horizontal { */
/* background: rgb(149, 119, 90); */
/* background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); */
/* background: linear-gradient(180deg, rgb(117, 88, 60) 0%, rgb(65, 41, 17) 17%, rgb(71, 43, 15) 88%, rgb(3, 2, 1) 100%);
box-shadow: 2px 14px 8px #111111aa;
} */
/* Handle */
::-webkit-scrollbar-thumb {
background: #855620;
border-radius: 4px;
}
/* ::-webkit-scrollbar-thumb:horizontal { */
/* background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); */
/* box-shadow: 2px 14px 8px #111111aa;
border-radius: 4px;
} */
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #704922;
@ -78,6 +63,13 @@
opacity: 0;
}
.no-scroll {
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
}
/* Chrome, Safari, Edge, Opera */
.no-spinner::-webkit-outer-spin-button,
.no-spinner::-webkit-inner-spin-button {

View File

@ -199,10 +199,7 @@ export default {
scroll-behavior: smooth;
width: calc(100vw - 80px);
/* background-color: rgb(214, 116, 36); */
background-image: var(--bookshelf-texture-img);
/* background-position: center; */
/* background-size: contain; */
background-repeat: repeat-x;
}
@media (max-width: 768px) {
@ -213,9 +210,7 @@ export default {
.bookshelfDividerCategorized {
background: rgb(149, 119, 90);
/* background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%); */
background: linear-gradient(180deg, rgb(122, 94, 68) 0%, rgb(92, 62, 31) 17%, rgb(82, 54, 26) 88%, rgba(71, 48, 25, 1) 100%);
/* background: linear-gradient(180deg, rgb(114, 85, 59) 0%, rgb(73, 48, 22) 17%, rgb(71, 43, 15) 88%, rgb(61, 41, 20) 100%); */
box-shadow: 2px 14px 8px #111111aa;
}

View File

@ -43,13 +43,13 @@
<script>
export default {
props: {
value: Boolean,
author: {
type: Object,
default: () => {}
}
},
// props: {
// value: Boolean,
// author: {
// type: Object,
// default: () => {}
// }
// },
data() {
return {
authorCopy: {
@ -73,12 +73,15 @@ export default {
computed: {
show: {
get() {
return this.value
return this.$store.state.globals.showEditAuthorModal
},
set(val) {
this.$emit('input', val)
this.$store.commit('globals/setShowEditAuthorModal', val)
}
},
author() {
return this.$store.state.globals.selectedAuthor
},
authorId() {
if (!this.author) return ''
return this.author.id

View File

@ -0,0 +1,138 @@
<template>
<div class="w-full">
<div class="flex items-center py-3">
<slot />
<div class="flex-grow" />
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
<span class="material-icons text-2xl">chevron_left</span>
</button>
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
<span class="material-icons text-2xl">chevron_right</span>
</button>
</div>
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
<div class="flex" :style="{ height: height + 'px' }">
<template v-for="(item, index) in items">
<cards-lazy-book-card :key="item.id" :ref="`slider-item-${item.id}`" :index="index" :book-mount="item" :height="cardHeight" :width="cardWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" :bookshelf-view="$constants.BookshelfView.TITLES" class="relative mx-2" @edit="editItem" @select="selectItem" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
height: {
type: Number,
default: 192
}
},
data() {
return {
isScrollable: false,
canScrollLeft: false,
canScrollRight: false,
clientWidth: 0
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
cardHeight() {
return this.height - 40
},
cardWidth() {
return this.cardHeight / this.bookCoverAspectRatio
},
booksPerPage() {
return Math.floor(this.clientWidth / (this.cardWidth + 16))
},
isSelectionMode() {
return this.$store.getters['getNumLibraryItemsSelected'] > 0
}
},
methods: {
clearSelectedEntities() {
this.updateSelectionMode(false)
},
editItem(libraryItem) {
var itemIds = this.items.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
},
selectItem(libraryItem) {
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
this.$nextTick(() => {
this.$eventBus.$emit('item-selected', libraryItem)
})
},
itemSelectedEvt() {
this.updateSelectionMode(this.isSelectionMode)
},
updateSelectionMode(val) {
var selectedLibraryItems = this.$store.state.selectedLibraryItems
this.items.forEach((item) => {
var component = this.$refs[`slider-item-${item.id}`]
if (!component || !component.length) return
component = component[0]
component.setSelectionMode(val)
component.selected = selectedLibraryItems.includes(item.id)
})
},
scrolled() {
this.setScrollVars()
},
scrollRight() {
if (!this.canScrollRight) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
slider.scrollLeft = newScrollLeft
},
scrollLeft() {
if (!this.canScrollLeft) return
const slider = this.$refs.slider
if (!slider) return
const scrollAmount = this.booksPerPage * this.cardWidth
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
slider.scrollLeft = newScrollLeft
},
setScrollVars() {
const slider = this.$refs.slider
if (!slider) return
const { scrollLeft, scrollWidth, clientWidth } = slider
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
this.clientWidth = clientWidth
this.isScrollable = scrollWidth > clientWidth
this.canScrollRight = scrollPercent < 1
this.canScrollLeft = scrollLeft > 0
}
},
updated() {
this.setScrollVars()
},
mounted() {
this.setScrollVars()
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
},
beforeDestroy() {
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
}
}
</script>

View File

@ -11,6 +11,7 @@
<modals-edit-collection-modal />
<modals-bookshelf-texture-modal />
<modals-podcast-edit-episode />
<modals-authors-edit-modal />
<readers-reader />
</div>
</template>

105
client/pages/author/_id.vue Normal file
View File

@ -0,0 +1,105 @@
<template>
<div id="page-wrapper" class="bg-bg page overflow-y-auto p-8" :class="streamLibraryItem ? 'streaming' : ''">
<div class="max-w-6xl mx-auto">
<div class="flex mb-6">
<div class="w-48 min-w-48">
<div class="w-full h-52">
<covers-author-image :author="author" rounded="0" />
</div>
</div>
<div class="flex-grow px-8">
<div class="flex items-center mb-8">
<h1 class="text-2xl">{{ author.name }}</h1>
<button class="w-8 h-8 rounded-full flex items-center justify-center mx-4 cursor-pointer text-gray-300 hover:text-warning transform hover:scale-125 duration-100" @click="editAuthor">
<span class="material-icons text-base">edit</span>
</button>
</div>
<p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">Description</p>
<p class="text-white max-w-3xl text-sm leading-5">{{ author.description }}</p>
</div>
</div>
<div class="py-4">
<widgets-item-slider :items="libraryItems">
<h2 class="text-lg">{{ libraryItems.length }} Books</h2>
</widgets-item-slider>
</div>
<div v-for="series in authorSeries" :key="series.id" class="py-4">
<widgets-item-slider :items="series.items">
<h2 class="text-lg">{{ series.name }}</h2>
<p class="text-white text-opacity-40 text-base px-2">Series</p>
</widgets-item-slider>
</div>
</div>
</div>
</template>
<script>
export default {
async asyncData({ store, app, params, redirect }) {
const author = await app.$axios.$get(`/api/authors/${params.id}?include=items,series`).catch((error) => {
console.error('Failed to get author', error)
return null
})
if (!author) {
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
}
return {
author
}
},
data() {
return {}
},
computed: {
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
libraryItems() {
return this.author.libraryItems || []
},
authorSeries() {
return this.author.series || []
}
},
methods: {
editAuthor() {
this.$store.commit('globals/showEditAuthorModal', this.author)
},
authorUpdated(author) {
if (author.id === this.author.id) {
console.log('Author was updated', author)
this.author = {
...author,
series: this.authorSeries,
libraryItems: this.libraryItems
}
}
},
authorRemoved(author) {
if (author.id === this.author.id) {
console.warn('Author was removed')
this.$router.replace(`/library/${this.currentLibraryId}/authors`)
}
}
},
mounted() {
if (!this.author) this.$router.replace('/')
this.$root.socket.on('author_updated', this.authorUpdated)
this.$root.socket.on('author_removed', this.authorRemoved)
},
beforeDestroy() {
this.$root.socket.off('author_updated', this.authorUpdated)
this.$root.socket.off('author_removed', this.authorRemoved)
}
}
</script>

View File

@ -7,7 +7,8 @@
<div id="bookshelf" class="w-full h-full p-8 overflow-y-auto">
<div class="flex flex-wrap justify-center">
<template v-for="author in authors">
<nuxt-link :key="author.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`">
<!-- <nuxt-link :key="author.id" :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`"> -->
<nuxt-link :key="author.id" :to="`/author/${author.id}`">
<cards-author-card :author="author" :width="160" :height="200" class="p-3" @edit="editAuthor" />
</nuxt-link>
</template>
@ -15,7 +16,6 @@
</div>
</div>
</div>
<modals-authors-edit-modal v-model="showAuthorModal" :author="selectedAuthor" />
</div>
</template>
@ -40,9 +40,7 @@ export default {
data() {
return {
loading: true,
authors: [],
showAuthorModal: false,
selectedAuthor: null
authors: []
}
},
computed: {
@ -51,6 +49,9 @@ export default {
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
selectedAuthor() {
return this.$store.state.globals.selectedAuthor
}
},
methods: {
@ -68,7 +69,7 @@ export default {
},
authorUpdated(author) {
if (this.selectedAuthor && this.selectedAuthor.id === author.id) {
this.selectedAuthor = author
this.$store.commit('globals/setSelectedAuthor', author)
}
this.authors = this.authors.map((au) => {
if (au.id === author.id) {
@ -81,8 +82,7 @@ export default {
this.authors = this.authors.filter((au) => au.id !== author.id)
},
editAuthor(author) {
this.selectedAuthor = author
this.showAuthorModal = true
this.$store.commit('globals/showEditAuthorModal', author)
}
},
mounted() {

View File

@ -6,8 +6,10 @@ export const state = () => ({
showUserCollectionsModal: false,
showEditCollectionModal: false,
showEditPodcastEpisode: false,
showEditAuthorModal: false,
selectedEpisode: null,
selectedCollection: null,
selectedAuthor: null,
showBookshelfTextureModal: false,
isCasting: false, // Actively casting
isChromecastInitialized: false // Script loaded
@ -61,6 +63,16 @@ export const mutations = {
setShowBookshelfTextureModal(state, val) {
state.showBookshelfTextureModal = val
},
showEditAuthorModal(state, author) {
state.selectedAuthor = author
state.showEditAuthorModal = true
},
setShowEditAuthorModal(state, val) {
state.showEditAuthorModal = val
},
setSelectedAuthor(state, author) {
state.selectedAuthor = author
},
setChromecastInitialized(state, val) {
state.isChromecastInitialized = val
},

View File

@ -1,11 +1,59 @@
const Logger = require('../Logger')
const { reqSupportsWebp } = require('../utils/index')
const { createNewSortInstance } = require('fast-sort')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
})
class AuthorController {
constructor() { }
async findOne(req, res) {
return res.json(req.author)
const include = (req.query.include || '').split(',')
const authorJson = req.author.toJSON()
// Used on author landing page to include library items and items grouped in series
if (include.includes('items')) {
authorJson.libraryItems = this.db.libraryItems.filter(li => {
return li.media.metadata.hasAuthor && li.media.metadata.hasAuthor(req.author.id)
})
if (include.includes('series')) {
const seriesMap = {}
// Group items into series
authorJson.libraryItems.forEach((li) => {
if (li.media.metadata.series) {
li.media.metadata.series.forEach((series) => {
const itemWithSeries = li.toJSONMinified()
itemWithSeries.media.metadata.series = series
if (seriesMap[series.id]) {
seriesMap[series.id].items.push(itemWithSeries)
} else {
seriesMap[series.id] = {
id: series.id,
name: series.name,
items: [itemWithSeries]
}
}
})
}
})
// Sort series items
for (const key in seriesMap) {
seriesMap[key].items = naturalSort(seriesMap[key].items).asc(li => li.media.metadata.series.sequence)
}
authorJson.series = Object.values(seriesMap)
}
// Minify library items
authorJson.libraryItems = authorJson.libraryItems.map(li => li.toJSONMinified())
}
return res.json(authorJson)
}
async update(req, res) {
@ -41,6 +89,7 @@ class AuthorController {
}).length
this.emitter('author_updated', req.author.toJSONExpanded(numBooks))
}
res.json({
author: req.author.toJSON(),
updated: hasUpdated