Podcast home page shelves for currently listening episodes, newest episodes. Podcast episode card

This commit is contained in:
advplyr 2022-04-22 19:31:11 -05:00
parent 84fb7ce8b3
commit 5d12cc3f23
10 changed files with 164 additions and 100 deletions

View File

@ -7,6 +7,11 @@
<cards-lazy-book-card :key="entity.id" :ref="`shelf-book-${entity.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editBook" />
</template>
</div>
<div v-if="shelf.type === 'episode'" class="flex items-center">
<template v-for="(entity, index) in shelf.entities">
<cards-lazy-book-card :key="entity.recentEpisode.id" :ref="`shelf-episode-${entity.recentEpisode.id}`" :index="index" :width="bookCoverWidth" :height="bookCoverHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" :book-mount="entity" class="relative mx-2" @hook:updated="updatedBookCard" @select="selectItem" @edit="editEpisode" />
</template>
</div>
<div v-if="shelf.type === 'series'" class="flex items-center">
<template v-for="entity in shelf.entities">
<cards-lazy-series-card :key="entity.name" :series-mount="entity" :height="bookCoverHeight" :width="bookCoverWidth * 2" :book-cover-aspect-ratio="bookCoverAspectRatio" class="relative mx-2" @hook:updated="updatedBookCard" />
@ -70,11 +75,6 @@ export default {
selectedAuthor: null
}
},
watch: {
isSelectionMode(newVal) {
this.updateSelectionMode(newVal)
}
},
computed: {
bookCoverHeight() {
return this.bookCoverWidth * this.bookCoverAspectRatio
@ -103,9 +103,14 @@ export default {
this.$store.commit('setBookshelfBookIds', bookIds)
this.$store.commit('showEditModal', audiobook)
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setSelectedLibraryItem', libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
},
updateSelectionMode(val) {
var selectedLibraryItems = this.$store.state.selectedLibraryItems
if (this.shelf.type === 'book') {
if (this.shelf.type === 'book' || this.shelf.type === 'podcast') {
this.shelf.entities.forEach((ent) => {
var component = this.$refs[`shelf-book-${ent.id}`]
if (!component || !component.length) return
@ -113,10 +118,24 @@ export default {
component.setSelectionMode(val)
component.selected = selectedLibraryItems.includes(ent.id)
})
} else if (this.shelf.type === 'episode') {
this.shelf.entities.forEach((ent) => {
var component = this.$refs[`shelf-episode-${ent.recentEpisode.id}`]
if (!component || !component.length) return
component = component[0]
component.setSelectionMode(val)
component.selected = selectedLibraryItems.includes(ent.id)
})
}
},
selectItem(libraryItem) {
this.$store.commit('toggleLibraryItemSelected', libraryItem.id)
this.$nextTick(() => {
this.$eventBus.$emit('item-selected', libraryItem)
})
},
itemSelectedEvt() {
this.updateSelectionMode(this.isSelectionMode)
},
scrolled() {
clearTimeout(this.scrollTimer)
@ -160,6 +179,12 @@ export default {
this.canScrollLeft = false
}
}
},
mounted() {
this.$eventBus.$on('item-selected', this.itemSelectedEvt)
},
beforeDestroy() {
this.$eventBus.$off('item-selected', this.itemSelectedEvt)
}
}
</script>

View File

@ -35,8 +35,8 @@
</div>
</div>
<!-- No progress shown for collapsed series in library and podcasts -->
<div v-if="!booksInSeries && !isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- No progress shown for collapsed series in library and podcasts (unless showing podcast episode) -->
<div v-if="!booksInSeries && (!isPodcast || episodeProgress)" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<!-- Overlay is not shown if collapsing series in library -->
<div v-show="!booksInSeries && libraryItem && (isHovering || isSelectionMode || isMoreMenuOpen)" class="w-full h-full absolute top-0 left-0 z-10 bg-black rounded hidden md:block" :class="overlayWrapperClasslist">
@ -60,7 +60,7 @@
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div>
<div ref="moreIcon" v-show="!isSelectionMode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<div ref="moreIcon" v-show="!isSelectionMode && !recentEpisode" class="hidden md:block absolute cursor-pointer hover:text-yellow-300" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
</div>
</div>
@ -77,13 +77,18 @@
</div>
</ui-tooltip>
<!-- Volume number -->
<!-- Series sequence -->
<div v-if="seriesSequence && showSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
</div>
<!-- Podcast Episode # -->
<div v-if="recentEpisodeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">Episode #{{ recentEpisodeNumber }}</p>
</div>
<!-- Podcast Num Episodes -->
<div v-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<div v-else-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
</div>
</div>
@ -190,6 +195,17 @@ export default {
processingBatch() {
return this.store.state.processingBatch
},
recentEpisode() {
// Only added to item when getting currently listening podcasts
return this._libraryItem.recentEpisode
},
recentEpisodeNumber() {
if (!this.recentEpisode) return null
if (this.recentEpisode.episode) {
return this.recentEpisode.episode.replace(/^#/, '')
}
return this.recentEpisode.index
},
collapsedSeries() {
// Only added to item object when collapseSeries is enabled
return this._libraryItem.collapsedSeries
@ -240,7 +256,13 @@ export default {
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
return null
},
episodeProgress() {
// Only used on home page currently listening podcast shelf
if (!this.recentEpisode) return null
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
},
userProgress() {
if (this.episodeProgress) return this.episodeProgress
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
},
userProgressPercent() {
@ -259,7 +281,7 @@ export default {
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
},
showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
@ -406,6 +428,9 @@ export default {
}
},
editClick() {
if (this.recentEpisode) {
return this.$emit('edit', { libraryItem: this.libraryItem, episode: this.recentEpisode })
}
this.$emit('edit', this.libraryItem)
},
toggleFinished() {
@ -529,7 +554,8 @@ export default {
play() {
var eventBus = this.$eventBus || this.$nuxt.$eventBus
eventBus.$emit('play-item', {
libraryItemId: this.libraryItemId
libraryItemId: this.libraryItemId,
episodeId: this.recentEpisode ? this.recentEpisode.id : null
})
},
mouseover() {

View File

@ -35,17 +35,6 @@
<script>
export default {
props: {
value: Boolean,
libraryItem: {
type: Object,
default: () => {}
},
episode: {
type: Object,
default: () => {}
}
},
data() {
return {
processing: false,
@ -72,12 +61,18 @@ export default {
computed: {
show: {
get() {
return this.value
return this.$store.state.globals.showEditPodcastEpisode
},
set(val) {
this.$emit('input', val)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', val)
}
},
libraryItem() {
return this.$store.state.selectedLibraryItem
},
episode() {
return this.$store.state.globals.selectedEpisode
},
episodeId() {
return this.episode ? this.episode.id : null
},

View File

@ -16,8 +16,6 @@
</template>
</transition-group>
</draggable>
<modals-podcast-edit-episode v-model="showEditEpisodeModal" :library-item="libraryItem" :episode="selectedEpisode" />
</div>
</template>
@ -40,8 +38,6 @@ export default {
sortDesc: true,
drag: false,
episodesCopy: [],
selectedEpisode: null,
showEditEpisodeModal: false,
orderChanged: false,
savingOrder: false
}
@ -97,8 +93,9 @@ export default {
return false
},
editEpisode(episode) {
this.selectedEpisode = episode
this.showEditEpisodeModal = true
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowEditPodcastEpisodeModal', true)
},
draggableUpdate() {
this.orderChanged = this.checkHasOrderChanged()

View File

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

View File

@ -5,6 +5,8 @@ export const state = () => ({
showBatchUserCollectionModal: false,
showUserCollectionsModal: false,
showEditCollectionModal: false,
showEditPodcastEpisode: false,
selectedEpisode: null,
selectedCollection: null,
showBookshelfTextureModal: false,
isCasting: false, // Actively casting
@ -46,10 +48,16 @@ export const mutations = {
setShowEditCollectionModal(state, val) {
state.showEditCollectionModal = val
},
setShowEditPodcastEpisodeModal(state, val) {
state.showEditPodcastEpisode = val
},
setEditCollection(state, collection) {
state.selectedCollection = collection
state.showEditCollectionModal = true
},
setSelectedEpisode(state, episode) {
state.selectedEpisode = episode
},
setShowBookshelfTextureModal(state, val) {
state.showBookshelfTextureModal = val
},

View File

@ -300,12 +300,12 @@ class LibraryController {
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
var minified = req.query.minified === '1'
var itemsWithUserProgress = libraryHelpers.getItemsWithUserProgress(req.user, libraryItems)
var itemsWithUserProgress = libraryHelpers.getMediaProgressWithItems(req.user, libraryItems)
var categories = [
{
id: 'continue-listening',
label: 'Continue Listening',
type: req.library.mediaType,
type: isPodcastLibrary ? 'episode' : req.library.mediaType,
entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified)
},
{
@ -317,7 +317,7 @@ class LibraryController {
{
id: 'listen-again',
label: 'Listen Again',
type: req.library.mediaType,
type: isPodcastLibrary ? 'episode' : req.library.mediaType,
entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified)
}
].filter(cats => { // Remove categories with no items
@ -372,57 +372,17 @@ class LibraryController {
entities: authors
})
}
}
res.json(categories)
}
// LEGACY
// api/libraries/:id/books/categories
async getLibraryCategories(req, res) {
var library = req.library
var books = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
var minified = req.query.minified === '1'
var booksWithUserAb = libraryHelpers.getItemsWithUserProgress(req.user, books)
var series = libraryHelpers.getSeriesFromBooks(books, minified)
var seriesWithUserAb = libraryHelpers.getSeriesWithProgressFromBooks(req.user, books)
var categories = [
{
id: 'continue-reading',
label: 'Continue Reading',
type: 'books',
entities: libraryHelpers.getBooksMostRecentlyRead(booksWithUserAb, limitPerShelf, minified)
},
{
id: 'continue-series',
label: 'Continue Series',
type: 'books',
entities: libraryHelpers.getBooksNextInSeries(seriesWithUserAb, limitPerShelf, minified)
},
{
id: 'recently-added',
label: 'Recently Added',
type: 'books',
entities: libraryHelpers.getBooksMostRecentlyAdded(books, limitPerShelf, minified)
},
{
id: 'read-again',
label: 'Read Again',
type: 'books',
entities: libraryHelpers.getBooksMostRecentlyFinished(booksWithUserAb, limitPerShelf, minified)
},
{
id: 'recent-series',
label: 'Recent Series',
type: 'series',
entities: libraryHelpers.getSeriesMostRecentlyAdded(series, limitPerShelf)
} else {
var episodesRecentlyAdded = libraryHelpers.getEpisodesRecentlyAdded(libraryItems, limitPerShelf, minified)
if (episodesRecentlyAdded.length) {
categories.splice(1, 0, {
id: 'episodes-recently-added',
label: 'Newest Episodes',
type: 'episode',
entities: episodesRecentlyAdded
})
}
].filter(cats => { // Remove categories with no items
return cats.entities.length
})
}
res.json(categories)
}

View File

@ -257,5 +257,9 @@ class Podcast {
if (!episode) return 0
return episode.duration
}
getEpisode(episodeId) {
return this.episodes.find(ep => ep.id == episodeId)
}
}
module.exports = Podcast

View File

@ -251,6 +251,11 @@ class User {
})
}
getAllMediaProgressForLibraryItem(libraryItemId) {
if (!this.mediaProgress) return []
return this.mediaProgress.filter(li => li.libraryItemId === libraryItemId)
}
createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) {
var itemProgress = this.mediaProgress.find(li => {
if (episodeId && li.episodeId !== episodeId) return false

View File

@ -1,4 +1,5 @@
const { sort, createNewSortInstance } = require('fast-sort')
const Logger = require('../Logger')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
})
@ -172,14 +173,28 @@ module.exports = {
})
},
getItemsWithUserProgress(user, libraryItems) {
return libraryItems.map(li => {
var itemProgress = user.getMediaProgress(li.id)
return {
userProgress: itemProgress ? itemProgress.toJSON() : null,
libraryItem: li
}
}).filter(b => !!b.userProgress)
getMediaProgressWithItems(user, libraryItems) {
var mediaProgress = []
libraryItems.forEach(li => {
var itemProgress = user.getAllMediaProgressForLibraryItem(li.id).map(mp => {
var episode = null
if (mp.episodeId) {
episode = li.media.getEpisode(mp.episodeId)
if (!episode) {
// Episode not found for library item
return null
}
}
return {
userProgress: mp.toJSON(),
libraryItem: li,
episode
}
}).filter(mp => !!mp)
mediaProgress = mediaProgress.concat(itemProgress)
})
return mediaProgress
},
getItemsMostRecentlyListened(itemsWithUserProgress, limit, minified = false) {
@ -187,7 +202,13 @@ module.exports = {
itemsInProgress.sort((a, b) => {
return b.userProgress.lastUpdate - a.userProgress.lastUpdate
})
return itemsInProgress.map(b => minified ? b.libraryItem.toJSONMinified() : b.libraryItem.toJSONExpanded()).slice(0, limit)
return itemsInProgress.map(b => {
var libjson = minified ? b.libraryItem.toJSONMinified() : b.libraryItem.toJSONExpanded()
if (b.episode) {
libjson.recentEpisode = b.episode
}
return libjson
}).slice(0, limit)
},
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
@ -202,17 +223,39 @@ module.exports = {
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
},
getItemsMostRecentlyAdded(libraryItems, limit, minified = false) {
var itemsSortedByAddedAt = sort(libraryItems).desc(li => li.addedAt)
return itemsSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
},
getItemsMostRecentlyFinished(itemsWithUserProgress, limit, minified = false) {
var itemsFinished = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.isFinished)
itemsFinished.sort((a, b) => {
return b.userProgress.finishedAt - a.userProgress.finishedAt
})
return itemsFinished.map(i => minified ? i.libraryItem.toJSONMinified() : i.libraryItem.toJSONExpanded()).slice(0, limit)
return itemsFinished.map(i => {
var libjson = minified ? i.libraryItem.toJSONMinified() : i.libraryItem.toJSONExpanded()
if (i.episode) {
libjson.recentEpisode = i.episode
}
return libjson
}).slice(0, limit)
},
getItemsMostRecentlyAdded(libraryItems, limit, minified = false) {
var itemsSortedByAddedAt = sort(libraryItems).desc(li => li.addedAt)
return itemsSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
},
getEpisodesRecentlyAdded(libraryItems, limit, minified = false) {
var libraryItemsWithEpisode = []
libraryItems.forEach((li) => {
if (li.mediaType !== 'podcast' || !li.media.hasMediaEntities) return
var libjson = minified ? li.toJSONMinified() : li.toJSONExpanded()
var episodes = sort(li.media.episodes || []).desc(ep => ep.addedAt)
episodes.forEach((ep) => {
var lie = { ...libjson }
lie.recentEpisode = ep
libraryItemsWithEpisode.push(lie)
})
})
libraryItemsWithEpisode = sort(libraryItemsWithEpisode).desc(lie => lie.recentEpisode.addedAt)
return libraryItemsWithEpisode.slice(0, limit)
},
getSeriesMostRecentlyAdded(series, limit) {