Add:Playlists

This commit is contained in:
advplyr 2022-12-03 15:20:27 -06:00
parent 6d9e902fe7
commit b62ce27487
13 changed files with 404 additions and 16 deletions

View file

@ -95,16 +95,16 @@ export default {
var coverSize = 100
if (window.innerWidth <= 375) coverSize = 90
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
return coverSize
},
bookHeight() {
if (this.isCoverSquareAspectRatio) return this.bookWidth
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth
return this.bookWidth * 1.6
},
entityWidth() {
if (this.showBookshelfListView) return this.bookshelfWidth - 16
if (this.isBookEntity) return this.bookWidth
if (this.isBookEntity || this.entityName === 'playlists') return this.bookWidth
return this.bookWidth * 2
},
entityHeight() {

View file

@ -63,9 +63,6 @@ export default {
var router = this.$router || this.$nuxt.$router
router.push(`/collection/${this.collection.id}`)
},
clickEdit() {
this.$emit('edit', this.collection)
},
destroy() {
// destroy the vue listeners, etc
this.$destroy()

View file

@ -0,0 +1,86 @@
<template>
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @click="clickCard">
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
</div>
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
index: Number,
width: Number,
height: Number,
bookCoverAspectRatio: Number,
playlistMount: {
type: Object,
default: () => null
},
isAltViewEnabled: Boolean
},
data() {
return {
playlist: null,
isSelectionMode: false
}
},
computed: {
labelFontSize() {
if (this.width < 160) return 0.75
return 0.875
},
sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
return this.width / 240
},
title() {
return this.playlist ? this.playlist.name : ''
},
items() {
return this.playlist ? this.playlist.items || [] : []
},
store() {
return this.$store || this.$nuxt.$store
},
currentLibraryId() {
return this.store.state.libraries.currentLibraryId
}
},
methods: {
setEntity(playlist) {
this.playlist = playlist
},
setSelectionMode(val) {
this.isSelectionMode = val
},
clickCard() {
if (!this.playlist) return
var router = this.$router || this.$nuxt.$router
router.push(`/playlist/${this.playlist.id}`)
},
destroy() {
// destroy the vue listeners, etc
this.$destroy()
// remove the element from the DOM
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
} else if (this.$el && this.$el.remove) {
this.$el.remove()
}
}
},
mounted() {
if (this.playlistMount) {
this.setEntity(this.playlistMount)
}
}
}
</script>

View file

@ -0,0 +1,51 @@
<template>
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
<div v-if="items.length" class="flex flex-wrap justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
<covers-book-cover v-for="(li, index) in libraryItemCovers" :key="index" :library-item="li" :width="itemCoverWidth" :book-cover-aspect-ratio="1" />
</div>
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
</div>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => []
},
width: Number,
height: Number
},
data() {
return {}
},
computed: {
sizeMultiplier() {
return this.width / (120 * 1.6 * 2)
},
itemCoverWidth() {
if (this.libraryItemCovers.length === 1) return this.width
return this.width / 2
},
libraryItemCovers() {
if (!this.items.length) return []
if (this.items.length === 1) return [this.items[0].libraryItem]
const covers = []
for (let i = 0; i < 4; i++) {
let index = i % this.items.length
if (this.items.length === 2 && i >= 2) index = (i + 1) % 2 // for playlists with 2 items show covers in checker pattern
covers.push(this.items[index].libraryItem)
}
return covers
}
},
methods: {},
mounted() {}
}
</script>

View file

@ -84,14 +84,14 @@ export default {
iconClass: 'text-xl',
text: 'Collections'
},
{
to: '/bookshelf/authors',
routeName: 'bookshelf-authors',
iconPack: 'abs-icons',
icon: 'authors',
iconClass: 'text-2xl pb-px',
text: 'Authors'
},
// {
// to: '/bookshelf/authors',
// routeName: 'bookshelf-authors',
// iconPack: 'abs-icons',
// icon: 'authors',
// iconClass: 'text-2xl pb-px',
// text: 'Authors'
// },
{
to: '/bookshelf/playlists',
routeName: 'bookshelf-playlists',

View file

@ -64,6 +64,8 @@ export default {
return 'Series'
} else if (this.page === 'collections') {
return 'Collections'
} else if (this.page === 'playlists') {
return 'Playlists'
}
return ''
},

View file

@ -0,0 +1,47 @@
<template>
<div class="w-full bg-primary bg-opacity-40">
<div class="w-full h-14 flex items-center px-4 bg-primary">
<p class="pr-4">Playlist Items</p>
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
<span class="text-xs md:text-sm font-mono leading-none">{{ items.length }}</span>
</div>
<div class="flex-grow" />
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
</div>
<template v-for="item in items">
<tables-playlist-item-table-row :key="item.id" :item="item" :playlist-id="playlistId" />
</template>
</div>
</template>
<script>
export default {
props: {
playlistId: String,
items: {
type: Array,
default: () => []
}
},
data() {
return {}
},
computed: {
totalDuration() {
var _total = 0
this.items.forEach((item) => {
if (item.episode) _total += item.episode.duration
else _total += item.libraryItem.media.duration
})
return _total
},
totalDurationPretty() {
return this.$elapsedPrettyExtended(this.totalDuration)
}
},
methods: {},
mounted() {}
}
</script>

View file

@ -0,0 +1,103 @@
<template>
<div class="w-full px-2 py-2 overflow-hidden relative">
<nuxt-link v-if="libraryItem" :to="`/item/${libraryItem.id}`" class="flex w-full">
<div class="h-full relative" :style="{ width: '50px' }">
<covers-book-cover :library-item="libraryItem" :width="50" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div class="flex-grow item-table-content h-full px-2 flex items-center">
<div class="max-w-full">
<p class="truncate block text-sm">{{ itemTitle }}</p>
<p class="truncate block text-gray-400 text-xs">{{ bookAuthorName }}</p>
<p class="text-xxs text-gray-500">{{ itemDuration }}</p>
</div>
</div>
</nuxt-link>
</div>
</template>
<script>
export default {
props: {
playlistId: String,
item: {
type: Object,
default: () => {}
}
},
data() {
return {
isProcessingReadUpdate: false,
processingRemove: false
}
},
computed: {
libraryItem() {
return this.item.libraryItem || {}
},
episode() {
return this.item.episode
},
episodeId() {
return this.episode ? this.episode.id : null
},
media() {
return this.libraryItem.media || {}
},
mediaMetadata() {
return this.media.metadata || {}
},
tracks() {
if (this.episode) return []
return this.media.tracks || []
},
itemTitle() {
if (this.episode) return this.episode.title
return this.mediaMetadata.title || ''
},
bookAuthors() {
if (this.episode) return []
return this.mediaMetadata.authors || []
},
bookAuthorName() {
return this.bookAuthors.map((au) => au.name).join(', ')
},
itemDuration() {
if (this.episode) return this.$elapsedPretty(this.episode.duration)
return this.$elapsedPretty(this.media.duration)
},
isMissing() {
return this.libraryItem.isMissing
},
isInvalid() {
return this.libraryItem.isInvalid
},
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
coverWidth() {
return 50
},
isMissing() {
return this.libraryItem.isMissing
},
isInvalid() {
return this.libraryItem.isInvalid
},
isStreaming() {
return this.$store.getters['getIsItemStreaming'](this.item.id)
},
showPlayBtn() {
return !this.isMissing && !this.isInvalid && !this.isStreaming && (this.tracks.length || this.episode)
}
},
methods: {},
mounted() {}
}
</script>
<style>
.item-table-content {
width: calc(100% - 50px);
max-width: calc(100% - 50px);
}
</style>

View file

@ -3,6 +3,7 @@ import LazyBookCard from '@/components/cards/LazyBookCard'
import LazyListBookCard from '@/components/cards/LazyListBookCard'
import LazySeriesCard from '@/components/cards/LazySeriesCard'
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
export default {
data() {
@ -16,6 +17,7 @@ export default {
getComponentClass() {
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
if (this.showBookshelfListView) return Vue.extend(LazyListBookCard)
return Vue.extend(LazyBookCard)
},

View file

@ -1,5 +1,5 @@
<template>
<div>Playlists</div>
<bookshelf-lazy-bookshelf page="playlists" />
</template>
<script>

View file

@ -12,7 +12,7 @@
{{ collectionName }}
</h1>
<div class="flex-grow" />
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center h-9 mr-2 w-20" @click="clickPlay">
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center justify-center h-9 mr-2 w-24" @click="clickPlay">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? 'Streaming' : 'Play' }}
</ui-btn>

95
pages/playlist/_id.vue Normal file
View file

@ -0,0 +1,95 @@
<template>
<div class="w-full h-full">
<div class="w-full h-full overflow-y-auto px-2 py-6 md:p-8">
<div class="w-full flex justify-center md:block sm:w-32 md:w-52" style="min-width: 240px">
<div class="relative" style="height: fit-content">
<covers-playlist-cover :items="playlistItems" :width="240" :height="120 * bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
</div>
<div class="flex-grow py-6">
<div class="flex items-center px-2">
<h1 class="text-xl font-sans">
{{ playlistName }}
</h1>
<div class="flex-grow" />
<ui-btn v-if="showPlayButton" :disabled="streaming" color="success" :padding-x="4" small class="flex items-center justify-center text-center h-9 mr-2 w-24" @click="clickPlay">
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
{{ streaming ? 'Streaming' : 'Play' }}
</ui-btn>
</div>
<div class="my-8 max-w-2xl px-2">
<p class="text-base text-gray-100">{{ description }}</p>
</div>
<tables-playlist-items-table :items="playlistItems" :playlist-id="playlist.id" />
</div>
</div>
</div>
</template>
<script>
export default {
async asyncData({ store, params, app, redirect, route }) {
if (!store.state.user.user) {
return redirect(`/connect?redirect=${route.path}`)
}
const playlist = await app.$axios.$get(`/api/playlists/${params.id}`).catch((error) => {
console.error('Failed', error)
return false
})
if (!playlist) {
return redirect('/bookshelf/playlists')
}
return {
playlist
}
},
data() {
return {}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
playlistItems() {
return this.playlist.items || []
},
playlistName() {
return this.playlist.name || ''
},
description() {
return this.playlist.description || ''
},
playableItems() {
return this.playlistItems.filter((item) => {
const libraryItem = item.libraryItem
if (libraryItem.isMissing || libraryItem.isInvalid) return false
if (item.episode) return item.episode.audioFile
return libraryItem.media.tracks.length
})
},
streaming() {
return !!this.playableItems.find((i) => this.$store.getters['getIsMediaStreaming'](i.libraryItemId, i.episodeId))
},
showPlayButton() {
return this.playableItems.length
}
},
methods: {
clickPlay() {
const nextItem = this.playableItems.find((i) => {
var prog = this.$store.getters['user/getUserMediaProgress'](i.libraryItemId, i.episodeId)
return !prog || !prog.isFinished
})
if (nextItem) {
this.$eventBus.$emit('play-item', { libraryItemId: nextItem.libraryItemId })
}
}
},
mounted() {}
}
</script>

View file

@ -26,6 +26,11 @@ export const state = () => ({
})
export const getters = {
getIsMediaStreaming: state => (libraryItemId, episodeId) => {
if (!state.playerLibraryItemId) return null
if (!episodeId) return state.playerLibraryItemId == libraryItemId
return state.playerLibraryItemId == libraryItemId && state.playerEpisodeId == episodeId
},
getIsItemStreaming: state => libraryItemId => {
return state.playerLibraryItemId == libraryItemId
},