Update:Add libraries playlists API endpoint, add lazy playlists card

This commit is contained in:
advplyr 2022-11-26 17:24:46 -06:00
parent 0979b3e03d
commit 7e171576e0
13 changed files with 207 additions and 39 deletions

View File

@ -165,6 +165,9 @@ export default {
isCollectionsPage() { isCollectionsPage() {
return this.page === 'collections' return this.page === 'collections'
}, },
isPlaylistsPage() {
return this.page === 'playlists'
},
isHomePage() { isHomePage() {
return this.$route.name === 'library-library' return this.$route.name === 'library-library'
}, },
@ -185,6 +188,7 @@ export default {
if (!this.page) return this.$strings.LabelBooks if (!this.page) return this.$strings.LabelBooks
if (this.isSeriesPage) return this.$strings.LabelSeries if (this.isSeriesPage) return this.$strings.LabelSeries
if (this.isCollectionsPage) return this.$strings.LabelCollections if (this.isCollectionsPage) return this.$strings.LabelCollections
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
return '' return ''
}, },
seriesId() { seriesId() {

View File

@ -87,11 +87,11 @@ export default {
emptyMessage() { emptyMessage() {
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
if (this.hasFilter) { if (this.hasFilter) {
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
return this.$getString('MessageBookshelfNoResultsForFilter', [this.filterName, this.filterValue]) return this.$getString('MessageBookshelfNoResultsForFilter', [this.filterName, this.filterValue])
// return `No Results for filter "${this.filterName}: ${this.filterValue}"`
} }
return this.$strings.MessageNoResults return this.$strings.MessageNoResults
}, },
@ -178,7 +178,7 @@ export default {
return this.shelfPadding * 2 return this.shelfPadding * 2
}, },
entityWidth() { entityWidth() {
if (this.entityName === 'series' || this.entityName === 'collections') { if (this.entityName === 'series' || this.entityName === 'collections' || this.entityName === 'playlists') {
if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6 if (this.bookWidth * 2 > this.bookshelfWidth - this.shelfPadding) return this.bookWidth * 1.6
return this.bookWidth * 2 return this.bookWidth * 2
} }
@ -302,11 +302,11 @@ export default {
this.currentSFQueryString = this.buildSearchParams() this.currentSFQueryString = this.buildSearchParams()
} }
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? 'items' : this.entityName
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : '' const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1` const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => { const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
console.error('failed to fetch books', error) console.error('failed to fetch books', error)
return null return null
}) })

View File

@ -71,6 +71,14 @@
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" 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="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<span class="material-icons text-2.5xl">playlist_play</span>
<p class="font-book pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link>
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" 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-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'"> <nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" 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-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
<span class="material-icons text-2xl">warning</span> <span class="material-icons text-2xl">warning</span>
@ -143,6 +151,9 @@ export default {
isAuthorsPage() { isAuthorsPage() {
return this.$route.name === 'library-library-authors' return this.$route.name === 'library-library-authors'
}, },
isPlaylistsPage() {
return this.paramId === 'playlists'
},
libraryBookshelfPage() { libraryBookshelfPage() {
return this.$route.name === 'library-library-bookshelf-id' return this.$route.name === 'library-library-bookshelf-id'
}, },
@ -173,6 +184,9 @@ export default {
}, },
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
},
showPlaylists() {
return true
} }
}, },
methods: { methods: {

View File

@ -0,0 +1,115 @@
<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" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @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" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div v-show="isHovering && userCanUpdate" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
</div>
</div>
<div v-if="!isAlternativeBookshelfView" 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 shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
</div>
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
</div>
</div>
</template>
<script>
export default {
props: {
index: Number,
width: Number,
height: Number,
bookCoverAspectRatio: Number,
bookshelfView: {
type: Number,
default: 0
},
playlistMount: {
type: Object,
default: () => null
}
},
data() {
return {
playlist: null,
isSelectionMode: false,
selected: false,
isHovering: 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
},
isAlternativeBookshelfView() {
const constants = this.$constants || this.$nuxt.$constants
return this.bookshelfView == constants.BookshelfView.DETAIL
},
userCanUpdate() {
return this.store.getters['user/getUserCanUpdate']
}
},
methods: {
setEntity(playlist) {
this.playlist = playlist
},
setSelectionMode(val) {
this.isSelectionMode = val
},
mouseover() {
this.isHovering = true
},
mouseleave() {
this.isHovering = false
},
clickCard() {
if (!this.playlist) return
var router = this.$router || this.$nuxt.$router
router.push(`/playlist/${this.playlist.id}`)
},
clickEdit() {
this.$emit('edit', this.playlist)
},
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

@ -75,7 +75,7 @@ export default {
return selectedPlaylistItem.libraryItem.media.metadata.title || '' return selectedPlaylistItem.libraryItem.media.metadata.title || ''
}, },
playlists() { playlists() {
return this.$store.state.user.playlists || [] return this.$store.state.libraries.userPlaylists || []
}, },
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
@ -113,9 +113,9 @@ export default {
if (!this.playlists.length) { if (!this.playlists.length) {
this.processing = true this.processing = true
this.$axios this.$axios
.$get(`/api/playlists`) .$get(`/api/libraries/${this.currentLibraryId}/playlists`)
.then((data) => { .then((data) => {
this.$store.commit('user/setPlaylists', data.playlists || []) this.$store.commit('libraries/setUserPlaylists', data.results || [])
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to get playlists', error) console.error('Failed to get playlists', error)

View File

@ -54,9 +54,12 @@ export default {
isCasting() { isCasting() {
return this.$store.state.globals.isCasting return this.$store.state.globals.isCasting
}, },
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
isShowingSideRail() { isShowingSideRail() {
if (!this.$route.name) return false if (!this.$route.name) return false
return !this.$route.name.startsWith('config') && this.$store.state.libraries.currentLibraryId return !this.$route.name.startsWith('config') && this.currentLibraryId
}, },
isShowingToolbar() { isShowingToolbar() {
return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account' return this.isShowingSideRail && this.$route.name !== 'upload' && this.$route.name !== 'account'
@ -169,7 +172,7 @@ export default {
this.$store.commit('libraries/remove', library) this.$store.commit('libraries/remove', library)
// When removed currently selected library then set next accessible library // When removed currently selected library then set next accessible library
const currLibraryId = this.$store.state.libraries.currentLibraryId const currLibraryId = this.currentLibraryId
if (currLibraryId === library.id) { if (currLibraryId === library.id) {
var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary'] var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary']
if (nextLibrary) { if (nextLibrary) {
@ -208,7 +211,7 @@ export default {
libraryItemRemoved(item) { libraryItemRemoved(item) {
if (this.$route.name.startsWith('item')) { if (this.$route.name.startsWith('item')) {
if (this.$route.params.id === item.id) { if (this.$route.params.id === item.id) {
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`) this.$router.replace(`/library/${this.currentLibraryId}`)
} }
} }
}, },
@ -293,35 +296,39 @@ export default {
this.$store.commit('user/updateMediaProgress', payload) this.$store.commit('user/updateMediaProgress', payload)
}, },
collectionAdded(collection) { collectionAdded(collection) {
if (this.currentLibraryId !== collection.libraryId) return
this.$store.commit('libraries/addUpdateCollection', collection) this.$store.commit('libraries/addUpdateCollection', collection)
}, },
collectionUpdated(collection) { collectionUpdated(collection) {
if (this.currentLibraryId !== collection.libraryId) return
this.$store.commit('libraries/addUpdateCollection', collection) this.$store.commit('libraries/addUpdateCollection', collection)
}, },
collectionRemoved(collection) { collectionRemoved(collection) {
if (this.currentLibraryId !== collection.libraryId) return
if (this.$route.name.startsWith('collection')) { if (this.$route.name.startsWith('collection')) {
if (this.$route.params.id === collection.id) { if (this.$route.params.id === collection.id) {
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}/bookshelf/collections`) this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/collections`)
} }
} }
this.$store.commit('libraries/removeCollection', collection) this.$store.commit('libraries/removeCollection', collection)
}, },
playlistAdded(playlist) { playlistAdded(playlist) {
if (playlist.userId !== this.user.id) return if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
this.$store.commit('user/addUpdatePlaylist', playlist) this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
}, },
playlistUpdated(playlist) { playlistUpdated(playlist) {
if (playlist.userId !== this.user.id) return if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
this.$store.commit('user/addUpdatePlaylist', playlist) this.$store.commit('libraries/addUpdateUserPlaylist', playlist)
}, },
playlistRemoved(playlist) { playlistRemoved(playlist) {
if (playlist.userId !== this.user.id) return if (playlist.userId !== this.user.id || this.currentLibraryId !== playlist.libraryId) return
if (this.$route.name.startsWith('playlist')) { if (this.$route.name.startsWith('playlist')) {
if (this.$route.params.id === playlist.id) { if (this.$route.params.id === playlist.id) {
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}/bookshelf/playlists`) this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/playlists`)
} }
} }
this.$store.commit('user/removePlaylist', playlist) this.$store.commit('libraries/removeUserPlaylist', playlist)
}, },
rssFeedOpen(data) { rssFeedOpen(data) {
this.$store.commit('feeds/addFeed', data) this.$store.commit('feeds/addFeed', data)

View File

@ -2,6 +2,7 @@ import Vue from 'vue'
import LazyBookCard from '@/components/cards/LazyBookCard' import LazyBookCard from '@/components/cards/LazyBookCard'
import LazySeriesCard from '@/components/cards/LazySeriesCard' import LazySeriesCard from '@/components/cards/LazySeriesCard'
import LazyCollectionCard from '@/components/cards/LazyCollectionCard' import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
export default { export default {
data() { data() {
@ -15,6 +16,7 @@ export default {
getComponentClass() { getComponentClass() {
if (this.entityName === 'series') return Vue.extend(LazySeriesCard) if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard) if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
return Vue.extend(LazyBookCard) return Vue.extend(LazyBookCard)
}, },
async mountEntityCard(index) { async mountEntityCard(index) {

View File

@ -16,7 +16,6 @@ export default {
// Set series sort by // Set series sort by
if (params.id === 'series') { if (params.id === 'series') {
console.log('Series page', query)
if (query.sort) { if (query.sort) {
store.commit('libraries/setSeriesSortBy', query.sort) store.commit('libraries/setSeriesSortBy', query.sort)
store.commit('libraries/setSeriesSortDesc', !!query.desc) store.commit('libraries/setSeriesSortDesc', !!query.desc)

View File

@ -12,7 +12,8 @@ export const state = () => ({
seriesSortBy: 'name', seriesSortBy: 'name',
seriesSortDesc: false, seriesSortDesc: false,
seriesFilterBy: 'all', seriesFilterBy: 'all',
collections: [] collections: [],
userPlaylists: []
}) })
export const getters = { export const getters = {
@ -102,6 +103,8 @@ export const actions = {
return false return false
} }
const libraryChanging = state.currentLibraryId !== libraryId
return this.$axios return this.$axios
.$get(`/api/libraries/${libraryId}?include=filterdata`) .$get(`/api/libraries/${libraryId}?include=filterdata`)
.then((data) => { .then((data) => {
@ -115,7 +118,10 @@ export const actions = {
commit('setLibraryIssues', issues) commit('setLibraryIssues', issues)
commit('setLibraryFilterData', filterData) commit('setLibraryFilterData', filterData)
commit('setCurrentLibrary', libraryId) commit('setCurrentLibrary', libraryId)
commit('setCollections', []) if (libraryChanging) {
commit('setCollections', [])
commit('setUserPlaylists', [])
}
return data return data
}) })
.catch((error) => { .catch((error) => {
@ -320,5 +326,19 @@ export const mutations = {
}, },
removeCollection(state, collection) { removeCollection(state, collection) {
state.collections = state.collections.filter(c => c.id !== collection.id) state.collections = state.collections.filter(c => c.id !== collection.id)
},
setUserPlaylists(state, playlists) {
state.userPlaylists = playlists
},
addUpdateUserPlaylist(state, playlist) {
const index = state.userPlaylists.findIndex(p => p.id === playlist.id)
if (index >= 0) {
state.userPlaylists.splice(index, 1, playlist)
} else {
state.userPlaylists.push(playlist)
}
},
removeUserPlaylist(state, playlist) {
state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id)
} }
} }

View File

@ -9,8 +9,7 @@ export const state = () => ({
collapseSeries: false, collapseSeries: false,
collapseBookSeries: false collapseBookSeries: false
}, },
settingsListeners: [], settingsListeners: []
playlists: []
}) })
export const getters = { export const getters = {
@ -164,19 +163,5 @@ export const mutations = {
}, },
removeSettingsListener(state, listenerId) { removeSettingsListener(state, listenerId) {
state.settingsListeners = state.settingsListeners.filter(l => l.id !== listenerId) state.settingsListeners = state.settingsListeners.filter(l => l.id !== listenerId)
},
setPlaylists(state, playlists) {
state.playlists = playlists
},
addUpdatePlaylist(state, playlist) {
const indexOf = state.playlists.findIndex(p => p.id == playlist.id)
if (indexOf >= 0) {
state.playlists.splice(indexOf, 1, playlist)
} else {
state.playlists.push(playlist)
}
},
removePlaylist(state, playlist) {
state.playlists = state.playlists.filter(p => p.id !== playlist.id)
} }
} }

View File

@ -41,6 +41,7 @@
"ButtonOpenManager": "Open Manager", "ButtonOpenManager": "Open Manager",
"ButtonPlay": "Play", "ButtonPlay": "Play",
"ButtonPlaying": "Playing", "ButtonPlaying": "Playing",
"ButtonPlaylists": "Playlists",
"ButtonPurgeAllCache": "Purge All Cache", "ButtonPurgeAllCache": "Purge All Cache",
"ButtonPurgeItemsCache": "Purge Items Cache", "ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonPurgeMediaProgress": "Purge Media Progress", "ButtonPurgeMediaProgress": "Purge Media Progress",

View File

@ -422,6 +422,26 @@ class LibraryController {
res.json(payload) res.json(payload)
} }
// api/libraries/:id/playlists
async getUserPlaylistsForLibrary(req, res) {
let playlistsForUser = this.db.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(this.db.libraryItems))
const payload = {
results: [],
total: playlistsForUser.length,
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
}
if (payload.limit) {
const startIndex = payload.page * payload.limit
playlistsForUser = playlistsForUser.slice(startIndex, startIndex + payload.limit)
}
payload.results = playlistsForUser
res.json(payload)
}
async getLibraryFilterData(req, res) { async getLibraryFilterData(req, res) {
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems)) res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
} }

View File

@ -72,6 +72,7 @@ class ApiRouter {
this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this)) this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this)) this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this)) this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
this.router.get('/libraries/:id/playlists', LibraryController.middleware.bind(this), LibraryController.getUserPlaylistsForLibrary.bind(this))
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this)) this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this)) this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this)) this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))