platypush/platypush/backend/http/webapp/src/components/panels/Music/Playlists.vue

392 lines
11 KiB
Vue

<template>
<Loading v-if="loading" />
<div class="editor-container fade-in" v-else-if="editedPlaylist != null">
<div class="header-container">
<MusicHeader ref="header">
<div class="col-8 filter">
<button class="back-btn" title="Back" @click="$emit('playlist-edit', null)">
<i class="fas fa-arrow-left" />
</button>
<label class="search-box">
<input type="search" placeholder="Filter" v-model="trackFilter">
</label>
</div>
<div class="col-4 buttons pull-right">
<Dropdown title="Players" icon-class="fa fa-volume-up" v-if="Object.keys(devices || {}).length">
<DropdownItem v-for="(device, id) in devices" :key="id" v-text="device.name"
:item-class="{active: activeDevice === id, selected: selectedDevice === id}"
icon-class="fa fa-volume-up" @click="$emit('select-device', id)" />
</Dropdown>
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
<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" />
</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 class="playlists fade-in" v-else>
<div class="header-container">
<MusicHeader ref="header">
<div class="col-7 filter">
<label>
<input type="search" placeholder="Filter" v-model="filter">
</label>
</div>
<div class="col-5 buttons">
<Dropdown title="Players" icon-class="fa fa-volume-up" v-if="Object.keys(devices || {}).length">
<DropdownItem v-for="(device, id) in devices" :key="id" v-text="device.name"
:item-class="{active: activeDevice === id, selected: selectedDevice === id}"
icon-class="fa fa-volume-up" @click="$emit('select-device', id)" />
</Dropdown>
<button title="Refresh status" @click="$emit('refresh-status')" v-if="devices != null">
<i class="fa fa-sync"></i>
</button>
<button class="mobile" title="Menu" @click="$emit('toggle-nav')" v-if="showNavButton">
<i class="fas fa-bars" />
</button>
</div>
</MusicHeader>
</div>
<div class="body" ref="body">
<div class="no-content" v-if="!playlists?.length">
No playlists found
</div>
<div class="row playlist" :class="{hidden: !displayedPlaylists.has(i)}"
v-for="(playlist, i) in playlists" :key="i" @click="$emit('playlist-edit', i)"
@dblclick="$emit('load', i)">
<div class="col-10">
<div class="name" v-text="playlist.name || '[No Name]'" />
</div>
<div class="col-2 right-side">
<span class="actions">
<Dropdown title="Actions" icon-class="fa fa-ellipsis-h">
<DropdownItem text="Play" icon-class="fa fa-play" @click.stop="$emit('play', i)" />
<DropdownItem text="Load" icon-class="fa fa-list-ul" @click.stop="$emit('load', i)" />
<DropdownItem text="Edit" icon-class="fa fa-edit" @click.stop="$emit('playlist-edit', i)" />
<DropdownItem text="Remove" icon-class="fa fa-trash" @click.stop="$emit('remove', i)" />
</Dropdown>
</span>
</div>
</div>
</div>
</div>
</template>
<script>
import MusicHeader from "@/components/panels/Music/Header";
import MediaUtils from "@/components/Media/Utils";
import Dropdown from "@/components/elements/Dropdown";
import DropdownItem from "@/components/elements/DropdownItem";
import Loading from "@/components/Loading";
export default {
name: "Playlists",
mixins: [MediaUtils],
components: {DropdownItem, Dropdown, MusicHeader, Loading},
emits: ['play', 'load', 'remove', 'playlist-edit', 'search', 'remove-track', 'load-track', 'info',
'playlist-add', 'add-to-playlist', 'track-move', 'refresh-status', 'select-device'],
props: {
playlists: {
type: Array,
default: () => [],
},
loading: {
type: Boolean,
default: false,
},
tracks: {
type: Array,
default: () => [],
},
editedPlaylist: {
type: Number,
},
devices: {
type: Object,
},
selectedDevice: {
type: String,
},
activeDevice: {
type: String,
},
showNavButton: {
type: Boolean,
default: false,
},
},
data() {
return {
selectedTracks: [],
filter: '',
trackFilter: '',
sourcePos: null,
targetPos: null,
}
},
computed: {
selectedTracksSet() {
return new Set(this.selectedTracks)
},
displayedPlaylists() {
const positions = [...Array(this.playlists.length).keys()]
if (!this.filter?.length)
return new Set(positions)
const self = this
const filter = (self.filter || '').toLowerCase()
return new Set(
positions.filter((pos) => {
const track = this.playlists[pos]
return (track?.name || '').toLowerCase().indexOf(filter) >= 0
})
)
},
displayedTracks() {
const positions = [...Array(this.tracks.length).keys()]
if (!this.trackFilter?.length)
return new Set(positions)
const self = this
const filter = (self.trackFilter || '').toLowerCase()
return new Set(
positions.filter((pos) => {
const track = this.tracks[pos]
return (track?.artist || '').toLowerCase().indexOf(filter) >= 0
|| (track?.title || '').toLowerCase().indexOf(filter) >= 0
|| (track?.album || '').toLowerCase().indexOf(filter) >= 0
})
)
},
},
methods: {
onTrackClick(event, pos) {
if (event.shiftKey) {
const selectedTracks = this.selectedTracks.sort()
if (!selectedTracks.length) {
this.selectedTracks = [pos]
} else if (pos < selectedTracks[0]) {
this.selectedTracks = [
...this.selectedTracks,
...[...Array(selectedTracks[0] - pos).keys()].map((i) => i + pos)
]
} else if (pos > selectedTracks[selectedTracks.length - 1]) {
this.selectedTracks = [
...this.selectedTracks,
...[...Array(pos - selectedTracks[selectedTracks.length - 1] + 1).keys()].
map((i) => i + selectedTracks[selectedTracks.length - 1])
]
}
} else {
const idx = this.selectedTracks.indexOf(pos)
if (event.ctrlKey) {
if (idx >= 0)
this.selectedTracks.splice(idx, 1)
else
this.selectedTracks.push(pos)
} else {
if (idx >= 0)
this.selectedTracks = []
else
this.selectedTracks = [pos]
}
}
},
addTrack() {
const track = prompt('Track path or URL')
if (!track?.length)
return
this.$emit('playlist-add', track)
},
onTrackDragStart(track) {
this.sourcePos = track
},
onTrackDragEnd() {
this.$refs.editor.querySelectorAll('.track').forEach((track) => track.classList.remove('dragover'));
if (this.sourcePos == null || this.targetPos == null || this.sourcePos === this.targetPos)
return
this.$emit('track-move', {from: this.sourcePos, to: this.targetPos, playlist: this.editedPlaylist})
this.sourcePos = null
this.targetPos = null
},
onTrackDragOver(track) {
this.targetPos = track
const tracks = this.$refs.editor.querySelectorAll('.track')
tracks.forEach((track) => track.classList.remove('dragover'));
[...tracks][track].classList.add('dragover')
},
},
}
</script>
<style lang="scss" scoped>
@import 'vars.scss';
@import 'track.scss';
@import '../../Media/vars.scss';
.playlists {
width: 100%;
display: flex;
flex-direction: column;
.header-container {
.filter {
input {
width: 100%;
}
}
}
.body {
height: calc(100% - #{$music-header-height} - #{$media-ctrl-panel-height});
overflow: auto;
}
.no-content {
height: 100%;
}
.playlist {
display: flex;
justify-content: center;
padding: .75em .25em .25em .25em;
box-shadow: 0 2.5px 2px -1px $default-shadow-color;
cursor: pointer;
&:hover {
background: $hover-bg;
}
&.active {
background: $active-bg;
}
&.selected {
background: $selected-bg;
}
.right-side {
display: flex;
justify-content: flex-end;
}
}
.header {
.buttons {
display: flex;
align-items: flex-end;
justify-content: flex-end;
}
}
}
.editor-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.header-container {
width: 100%;
}
:deep(.header) {
.back-btn {
padding-left: .25em;
}
.search-box {
input {
width: 65%;
}
}
}
.editor {
width: 100%;
height: calc(100% - #{$music-header-height} - #{$media-ctrl-panel-height});
display: flex;
flex-direction: column;
overflow: auto;
}
}
</style>