mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-27 09:08:57 +01:00
Add:Create playlist from a collection #1226
This commit is contained in:
parent
9b1f7f566f
commit
5165f11460
55
client/components/ui/ContextMenuDropdown.vue
Normal file
55
client/components/ui/ContextMenuDropdown.vue
Normal file
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<span class="material-icons">more_vert</span>
|
||||
</button>
|
||||
|
||||
<transition name="menu">
|
||||
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg max-h-56 w-48 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm">
|
||||
<template v-for="(item, index) in items">
|
||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||
<p>{{ item.text }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
clickOutsideObj: {
|
||||
handler: this.clickedOutside,
|
||||
events: ['mousedown'],
|
||||
isActive: true
|
||||
},
|
||||
showMenu: false
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
clickShowMenu() {
|
||||
if (this.disabled) return
|
||||
this.showMenu = !this.showMenu
|
||||
},
|
||||
clickedOutside() {
|
||||
this.showMenu = false
|
||||
},
|
||||
clickAction(action) {
|
||||
if (this.disabled) return
|
||||
this.showMenu = false
|
||||
this.$emit('action', action)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -19,9 +19,11 @@
|
||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||
</ui-btn>
|
||||
|
||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
||||
<button type="button" class="h-9 w-9 flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5 mx-px" @click.stop.prevent="editClick">
|
||||
<span class="material-icons text-xl">edit</span>
|
||||
</button>
|
||||
|
||||
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
||||
<ui-context-menu-dropdown :items="contextMenuItems" class="mx-px" @action="contextMenuAction" />
|
||||
</div>
|
||||
|
||||
<div class="my-8 max-w-2xl">
|
||||
@ -32,7 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="processingRemove" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||
<div v-show="processing" class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-40 flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
</div>
|
||||
@ -64,7 +66,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
processingRemove: false
|
||||
processing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -102,15 +104,55 @@ export default {
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.$store.getters['user/getUserCanDelete']
|
||||
},
|
||||
contextMenuItems() {
|
||||
const items = [
|
||||
{
|
||||
text: 'Create playlist from collection',
|
||||
action: 'create-playlist'
|
||||
}
|
||||
]
|
||||
if (this.userCanDelete) {
|
||||
items.push({
|
||||
text: 'Delete collection',
|
||||
action: 'delete'
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
contextMenuAction(action) {
|
||||
if (action === 'delete') {
|
||||
this.removeClick()
|
||||
} else if (action === 'create-playlist') {
|
||||
this.createPlaylistFromCollection()
|
||||
}
|
||||
},
|
||||
createPlaylistFromCollection() {
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$post(`/api/playlists/collection/${this.collectionId}`)
|
||||
.then((playlist) => {
|
||||
if (playlist) {
|
||||
this.$toast.success('Playlist created')
|
||||
this.$router.push(`/playlist/${playlist.id}`)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errMsg = error.response ? error.response.data || '' : ''
|
||||
this.$toast.error(errMsg || 'Failed to create playlist')
|
||||
})
|
||||
.finally(() => {
|
||||
this.processing = false
|
||||
})
|
||||
},
|
||||
editClick() {
|
||||
this.$store.commit('globals/setEditCollection', this.collection)
|
||||
},
|
||||
removeClick() {
|
||||
if (confirm(this.$getString('MessageConfirmRemoveCollection', [this.collectionName]))) {
|
||||
this.processingRemove = true
|
||||
this.processing = true
|
||||
this.$axios
|
||||
.$delete(`/api/collections/${this.collection.id}`)
|
||||
.then(() => {
|
||||
@ -121,7 +163,7 @@ export default {
|
||||
this.$toast.error(this.$strings.ToastCollectionRemoveFailed)
|
||||
})
|
||||
.finally(() => {
|
||||
this.processingRemove = false
|
||||
this.processing = false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
@ -123,7 +123,7 @@ class CollectionController {
|
||||
|
||||
middleware(req, res, next) {
|
||||
if (req.params.id) {
|
||||
var collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
const collection = this.db.collections.find(c => c.id === req.params.id)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
|
@ -174,9 +174,42 @@ class PlaylistController {
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
// POST: api/playlists/collection/:collectionId
|
||||
async createFromCollection(req, res) {
|
||||
let collection = this.db.collections.find(c => c.id === req.params.collectionId)
|
||||
if (!collection) {
|
||||
return res.status(404).send('Collection not found')
|
||||
}
|
||||
// Expand collection to get library items
|
||||
collection = collection.toJSONExpanded(this.db.libraryItems)
|
||||
|
||||
// Filter out library items not accessible to user
|
||||
const libraryItems = collection.books.filter(item => req.user.checkCanAccessLibraryItem(item))
|
||||
|
||||
if (!libraryItems.length) {
|
||||
return res.status(400).send('Collection has no books accessible to user')
|
||||
}
|
||||
|
||||
const newPlaylist = new Playlist()
|
||||
|
||||
const newPlaylistData = {
|
||||
userId: req.user.id,
|
||||
libraryId: collection.libraryId,
|
||||
name: collection.name,
|
||||
description: collection.description || null,
|
||||
items: libraryItems.map(li => ({ libraryItemId: li.id }))
|
||||
}
|
||||
newPlaylist.setData(newPlaylistData)
|
||||
|
||||
const jsonExpanded = newPlaylist.toJSONExpanded(this.db.libraryItems)
|
||||
await this.db.insertEntity('playlist', newPlaylist)
|
||||
SocketAuthority.clientEmitter(newPlaylist.userId, 'playlist_added', jsonExpanded)
|
||||
res.json(jsonExpanded)
|
||||
}
|
||||
|
||||
middleware(req, res, next) {
|
||||
if (req.params.id) {
|
||||
var playlist = this.db.playlists.find(p => p.id === req.params.id)
|
||||
const playlist = this.db.playlists.find(p => p.id === req.params.id)
|
||||
if (!playlist) {
|
||||
return res.status(404).send('Playlist not found')
|
||||
}
|
||||
|
@ -38,10 +38,10 @@ class Collection {
|
||||
}
|
||||
|
||||
toJSONExpanded(libraryItems, minifiedBooks = false) {
|
||||
var json = this.toJSON()
|
||||
const json = this.toJSON()
|
||||
json.books = json.books.map(bookId => {
|
||||
var _ab = libraryItems.find(li => li.id === bookId)
|
||||
return _ab ? minifiedBooks ? _ab.toJSONMinified() : _ab.toJSONExpanded() : null
|
||||
const book = libraryItems.find(li => li.id === bookId)
|
||||
return book ? minifiedBooks ? book.toJSONMinified() : book.toJSONExpanded() : null
|
||||
}).filter(b => !!b)
|
||||
return json
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ class ApiRouter {
|
||||
//
|
||||
// Playlist Routes
|
||||
//
|
||||
this.router.post('/playlists', PlaylistController.middleware.bind(this), PlaylistController.create.bind(this))
|
||||
this.router.post('/playlists', PlaylistController.create.bind(this))
|
||||
this.router.get('/playlists', PlaylistController.findAllForUser.bind(this))
|
||||
this.router.get('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.findOne.bind(this))
|
||||
this.router.patch('/playlists/:id', PlaylistController.middleware.bind(this), PlaylistController.update.bind(this))
|
||||
@ -152,6 +152,7 @@ class ApiRouter {
|
||||
this.router.delete('/playlists/:id/item/:libraryItemId/:episodeId?', PlaylistController.middleware.bind(this), PlaylistController.removeItem.bind(this))
|
||||
this.router.post('/playlists/:id/batch/add', PlaylistController.middleware.bind(this), PlaylistController.addBatch.bind(this))
|
||||
this.router.post('/playlists/:id/batch/remove', PlaylistController.middleware.bind(this), PlaylistController.removeBatch.bind(this))
|
||||
this.router.post('/playlists/collection/:collectionId', PlaylistController.createFromCollection.bind(this))
|
||||
|
||||
//
|
||||
// Current User Routes (Me)
|
||||
|
Loading…
Reference in New Issue
Block a user