[Music UI] Use the Playlist component also for playlist editors.

This commit is contained in:
Fabio Manganiello 2024-01-08 22:17:04 +01:00
parent e881fedc59
commit 5ef7313bdc
Signed by: blacklight
GPG key ID: D90FBA7F76362774
5 changed files with 146 additions and 100 deletions

View file

@ -26,6 +26,9 @@
:selected-device="selectedDevice" :selected-device="selectedDevice"
:active-device="activeDevice" :active-device="activeDevice"
:show-nav-button="!navVisible" :show-nav-button="!navVisible"
:with-clear="true"
:with-save="true"
:with-swap="true"
v-if="selectedView === 'playing'" v-if="selectedView === 'playing'"
@play="$emit('play', $event)" @play="$emit('play', $event)"
@clear="$emit('clear')" @clear="$emit('clear')"
@ -44,6 +47,7 @@
<Playlists :playlists="playlists" <Playlists :playlists="playlists"
:loading="loading" :loading="loading"
:devices="devices" :devices="devices"
:status="status"
:selected-device="selectedDevice" :selected-device="selectedDevice"
:active-device="activeDevice" :active-device="activeDevice"
:edited-playlist="editedPlaylist" :edited-playlist="editedPlaylist"
@ -54,7 +58,7 @@
@load="$emit('load-playlist', $event)" @load="$emit('load-playlist', $event)"
@remove="$emit('remove-playlist', $event)" @remove="$emit('remove-playlist', $event)"
@playlist-edit="$emit('playlist-edit', $event)" @playlist-edit="$emit('playlist-edit', $event)"
@load-track="$emit('add-to-tracklist-from-edited-playlist', $event)" @load-tracks="$emit('add-to-tracklist-from-edited-playlist', $event)"
@remove-track="$emit('remove-from-playlist', $event)" @remove-track="$emit('remove-from-playlist', $event)"
@info="$emit('info', $event)" @info="$emit('info', $event)"
@playlist-add="$emit('playlist-add', $event)" @playlist-add="$emit('playlist-add', $event)"

View file

@ -5,6 +5,10 @@
<div class="header-container"> <div class="header-container">
<MusicHeader ref="header"> <MusicHeader ref="header">
<div class="col-7 filter"> <div class="col-7 filter">
<button class="back-btn" title="Back" @click="$emit('back')" v-if="withBack">
<i class="fas fa-arrow-left" />
</button>
<label> <label>
<input type="search" placeholder="Filter" v-model="filter"> <input type="search" placeholder="Filter" v-model="filter">
</label> </label>
@ -19,8 +23,9 @@
<DropdownItem text="Add track" icon-class="fa fa-plus" @click="addTrack" /> <DropdownItem text="Add track" icon-class="fa fa-plus" @click="addTrack" />
<DropdownItem text="Refresh status" icon-class="fa fa-sync" @click="$emit('refresh-status')" v-if="devices != null" /> <DropdownItem text="Refresh status" icon-class="fa fa-sync" @click="$emit('refresh-status')" v-if="devices != null" />
<DropdownItem text="Save as playlist" icon-class="fa fa-save" :disabled="!tracks?.length" <DropdownItem text="Save as playlist" icon-class="fa fa-save" :disabled="!tracks?.length"
@click="playlistSave" /> @click="playlistSave" v-if="withSave" />
<DropdownItem text="Swap tracks" icon-class="fa fa-retweet" v-if="selectedTracks?.length === 2" <DropdownItem text="Swap tracks" icon-class="fa fa-retweet"
v-if="withSwap && selectedTracks?.length === 2"
@click="$emit('swap', selectedTracks)" /> @click="$emit('swap', selectedTracks)" />
<DropdownItem :text="selectionMode ? 'End selection' : 'Start selection'" icon-class="far fa-check-square" <DropdownItem :text="selectionMode ? 'End selection' : 'Start selection'" icon-class="far fa-check-square"
:disabled="!tracks?.length" @click="selectionMode = !selectionMode" /> :disabled="!tracks?.length" @click="selectionMode = !selectionMode" />
@ -30,7 +35,8 @@
<DropdownItem :text="'Remove track' + (selectedTracks.length > 1 ? 's' : '')" <DropdownItem :text="'Remove track' + (selectedTracks.length > 1 ? 's' : '')"
icon-class="fa fa-trash" v-if="selectedTracks.length > 0" icon-class="fa fa-trash" v-if="selectedTracks.length > 0"
@click="$emit('remove', [...(new Set(selectedTracks))])" /> @click="$emit('remove', [...(new Set(selectedTracks))])" />
<DropdownItem text="Clear playlist" icon-class="fa fa-ban" :disabled="!tracks?.length" @click="$emit('clear')" /> <DropdownItem text="Clear playlist" icon-class="fa fa-ban"
:disabled="!tracks?.length" @click="$emit('clear')" v-if="withClear" />
</Dropdown> </Dropdown>
<Dropdown title="Players" icon-class="fa fa-volume-up" v-if="Object.keys(devices || {}).length"> <Dropdown title="Players" icon-class="fa fa-volume-up" v-if="Object.keys(devices || {}).length">
@ -84,7 +90,9 @@
<span class="actions"> <span class="actions">
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h"> <Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
<DropdownItem text="Play" icon-class="fa fa-play" @click="$emit('play', {pos: i})" /> <DropdownItem text="Play" icon-class="fa fa-play" @click="onMenuPlay" />
<DropdownItem text="Add to queue" icon-class="fa fa-plus"
@click="$emit('add-to-queue', [...(new Set([...selectedTracks, i]))])" v-if="withAddToQueue" />
<DropdownItem text="Add to playlist" icon-class="fa fa-list-ul" @click="$emit('add-to-playlist', track)" /> <DropdownItem text="Add to playlist" icon-class="fa fa-list-ul" @click="$emit('add-to-playlist', track)" />
<DropdownItem text="Remove" icon-class="fa fa-trash" @click="$emit('remove', [...(new Set([...selectedTracks, i]))])" /> <DropdownItem text="Remove" icon-class="fa fa-trash" @click="$emit('remove', [...(new Set([...selectedTracks, i]))])" />
<DropdownItem text="Info" icon-class="fa fa-info" @click="$emit('info', tracks[i])" /> <DropdownItem text="Info" icon-class="fa fa-info" @click="$emit('info', tracks[i])" />
@ -109,6 +117,10 @@ export default {
components: {DropdownItem, Dropdown, MusicHeader, Loading}, components: {DropdownItem, Dropdown, MusicHeader, Loading},
emits: [ emits: [
'add', 'add',
'add-to-playlist',
'add-to-queue',
'add-to-queue-and-play',
'back',
'clear', 'clear',
'info', 'info',
'move', 'move',
@ -159,6 +171,31 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
withAddToQueue: {
type: Boolean,
default: false,
},
withBack: {
type: Boolean,
default: false,
},
withClear: {
type: Boolean,
default: false,
},
withSave: {
type: Boolean,
default: false,
},
withSwap: {
type: Boolean,
default: false,
},
}, },
data() { data() {
@ -259,7 +296,7 @@ export default {
trackClass(i) { trackClass(i) {
return { return {
selected: this.selectedTracksSet.has(i), selected: this.selectedTracksSet.has(i),
active: this.status?.playingPos === i, active: !this.withAddToQueue && this.status?.playingPos === i,
} }
}, },
@ -271,6 +308,13 @@ export default {
this.$emit('add', track) this.$emit('add', track)
}, },
onMenuPlay(i) {
if (this.withAddToQueue)
this.$emit('add-to-queue-and-play', [...(new Set([...this.selectedTracks, i]))])
else
this.$emit('play', {pos: i})
},
onTrackDragStart(track) { onTrackDragStart(track) {
this.sourcePos = track this.sourcePos = track
}, },
@ -364,6 +408,7 @@ export default {
.playlist { .playlist {
width: 100%; width: 100%;
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -374,8 +419,16 @@ export default {
} }
.filter { .filter {
input { display: flex;
width: 100%; flex-direction: row;
align-items: center;
label {
flex-grow: 1;
input[type="search"] {
width: 100%;
}
} }
} }
@ -388,6 +441,12 @@ export default {
} }
} }
:deep(.header) {
.back-btn {
padding-left: .25em;
}
}
.body { .body {
height: calc(100% - #{$music-header-height} - #{$media-ctrl-panel-height}); height: calc(100% - #{$music-header-height} - #{$media-ctrl-panel-height});
overflow: auto; overflow: auto;

View file

@ -2,76 +2,28 @@
<Loading v-if="loading" /> <Loading v-if="loading" />
<div class="editor-container fade-in" v-else-if="editedPlaylist != null"> <div class="editor-container fade-in" v-else-if="editedPlaylist != null">
<div class="header-container"> <Playlist
<MusicHeader ref="header"> :tracks="tracks || []"
<div class="col-8 filter"> :status="status"
<button class="back-btn" title="Back" @click="$emit('playlist-edit', null)"> :devices="devices"
<i class="fas fa-arrow-left" /> :selected-device="selectedDevice"
</button> :active-device="activeDevice"
:show-nav-button="showNavButton"
<label class="search-box"> :with-add-to-queue="true"
<input type="search" placeholder="Filter" v-model="trackFilter"> :with-back="true"
</label> @add="$emit('playlist-add', $event)"
</div> @add-to-playlist="$emit('add-to-playlist', $event)"
@add-to-queue="$emit('load-tracks', {tracks: $event, play: false})"
<div class="col-4 buttons pull-right"> @add-to-queue-and-play="$emit('load-tracks', {tracks: $event, play: true})"
<Dropdown title="Players" icon-class="fa fa-volume-up" v-if="Object.keys(devices || {}).length"> @back="$emit('playlist-edit', null)"
<DropdownItem v-for="(device, id) in devices" :key="id" v-text="device.name" @info="$emit('info', $event)"
:item-class="{active: activeDevice === id, selected: selectedDevice === id}" @move="$emit('track-move', {...$event, playlist: editedPlaylist})"
icon-class="fa fa-volume-up" @click="$emit('select-device', id)" /> @play="$emit('load-tracks', {tracks: [$event], play: true})"
</Dropdown> @refresh-status="$emit('refresh-status')"
@remove="$emit('remove-track', $event)"
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h"> @search="$emit('search', $event)"
<DropdownItem text="Add track" icon-class="fa fa-plus" @click="addTrack" /> @select-device="$emit('select-device', $event)"
<DropdownItem text="Refresh status" icon-class="fa fa-sync" @click="$emit('refresh-status')" v-if="devices != null" /> @toggle-nav="$emit('toggle-nav')" />
</Dropdown>
<button class="mobile" title="Menu" @click="$emit('toggle-nav')" v-if="showNavButton">
<i class="fas fa-bars" />
</button>
</div>
</MusicHeader>
</div>
<div class="editor" ref="editor">
<div class="no-content" v-if="!tracks?.length">
No tracks found
</div>
<div class="row track" draggable="true" v-for="(track, i) in tracks" :key="i"
:class="{selected: selectedTracksSet.has(i), active: status?.playingPos === i, hidden: !displayedTracks.has(i)}"
@dragstart="onTrackDragStart(i)" @dragend="onTrackDragEnd(i)" @dragover="onTrackDragOver(i)"
@click="onTrackClick($event, i)" @dblclick="$emit('load-track', {pos: i, play: true})">
<div class="col-10">
<div class="title">
{{ track.title || '[No Title]' }}
</div>
<div class="artist" v-if="track.artist">
<a :href="$route.fullPath" v-text="track.artist" @click.prevent="$emit('search', {artist: track.artist})" />
</div>
<div class="album" v-if="track.album">
<a :href="$route.fullPath" v-text="track.album"
@click.prevent="$emit('search', {artist: track.artist, album: track.album})" />
</div>
</div>
<div class="col-2 right-side">
<span class="duration" v-text="track.time ? convertTime(track.time) : '-:--'" />
<span class="actions">
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
<DropdownItem text="Play" icon-class="fa fa-play" @click="$emit('load-track', {pos: i, play: true})" />
<DropdownItem text="Add to queue" icon-class="fa fa-plus" @click="$emit('load-track', {pos: i, play: false})" />
<DropdownItem text="Add to playlist" icon-class="fa fa-list-ul" @click="$emit('add-to-playlist', track)" />
<DropdownItem text="Remove" icon-class="fa fa-trash" @click="$emit('remove-track', [...(new Set([...selectedTracks, i]))])" />
<DropdownItem text="Info" icon-class="fa fa-info" @click.stop="$emit('info', tracks[i])" />
</Dropdown>
</span>
</div>
</div>
</div>
</div> </div>
<div class="playlists fade-in" v-else> <div class="playlists fade-in" v-else>
@ -134,13 +86,34 @@ import MediaUtils from "@/components/Media/Utils";
import Dropdown from "@/components/elements/Dropdown"; import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem"; import DropdownItem from "@/components/elements/DropdownItem";
import Loading from "@/components/Loading"; import Loading from "@/components/Loading";
import Playlist from "./Playlist";
export default { export default {
name: "Playlists", name: "Playlists",
mixins: [MediaUtils], mixins: [MediaUtils],
components: {DropdownItem, Dropdown, MusicHeader, Loading}, components: {
emits: ['play', 'load', 'remove', 'playlist-edit', 'search', 'remove-track', 'load-track', 'info', Dropdown,
'playlist-add', 'add-to-playlist', 'track-move', 'refresh-status', 'select-device'], DropdownItem,
MusicHeader,
Loading,
Playlist,
},
emits: [
'add-to-playlist',
'info',
'load',
'load-tracks',
'play',
'playlist-add',
'playlist-edit',
'refresh-status',
'remove',
'remove-track',
'search',
'select-device',
'track-move',
],
props: { props: {
playlists: { playlists: {
@ -166,6 +139,11 @@ export default {
type: Object, type: Object,
}, },
status: {
type: Object,
default: () => {},
},
selectedDevice: { selectedDevice: {
type: String, type: String,
}, },
@ -264,14 +242,6 @@ export default {
} }
}, },
addTrack() {
const track = prompt('Track path or URL')
if (!track?.length)
return
this.$emit('playlist-add', track)
},
onTrackDragStart(track) { onTrackDragStart(track) {
this.sourcePos = track this.sourcePos = track
}, },

View file

@ -210,15 +210,18 @@ export default {
}, },
async addToTracklistFromEditedPlaylist(event) { async addToTracklistFromEditedPlaylist(event) {
const track = this.editedPlaylistTracks[event.pos] const tracks = event?.tracks?.map(
if (!track) (pos) => this.editedPlaylistTracks[pos]
)?.filter((track) => track?.file)?.map((track) => track.file)
if (!tracks?.length)
return return
await this.request('music.mpd.add', {resource: track.file}) await Promise.all(tracks.map((track) => this.request('music.mpd.add', {resource: track})))
await this.refresh(true) await this.refresh(true)
if (event.play) if (event.play)
await this.request('music.mpd.play_pos', {pos: this.tracks.length-1}) await this.request('music.mpd.play_pos', {pos: this.tracks.length - tracks.length})
}, },
async removeFromPlaylist(positions) { async removeFromPlaylist(positions) {

View file

@ -224,15 +224,25 @@ export default {
}, },
async addToTracklistFromEditedPlaylist(event) { async addToTracklistFromEditedPlaylist(event) {
const track = this.editedPlaylistTracks[event.pos] const tracks = event?.tracks?.map(
if (!track) (pos) => this.editedPlaylistTracks[pos]
)?.filter((track) => track?.file)?.map((track) => track.file)
if (!tracks?.length)
return return
const method = event.play ? 'play' : 'add' if (event.play && tracks.length === 1) {
await this.request(`music.spotify.${method}`, { await this.request('music.spotify.play', {
device: this.selectedDevice, device: this.selectedDevice,
resource: track.uri resource: tracks[0],
}) })
} else {
await Promise.all(tracks.map((track) => this.request('music.spotify.add', {
device: this.selectedDevice,
resource: track,
})))
}
await this.refresh(true) await this.refresh(true)
}, },