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

492 lines
14 KiB
Vue

<template>
<Loading v-if="loading" />
<MediaView :plugin-name="pluginName" :status="status" :track="track" @play="$emit('play', $event)"
@pause="$emit('pause')" @stop="$emit('stop')" @previous="$emit('previous')" @next="$emit('next')"
@set-volume="$emit('set-volume', $event)" @seek="$emit('seek', $event)" @consume="$emit('consume', $event)"
@repeat="$emit('repeat', $event)" @random="$emit('random', $event)" @search="search" v-else>
<main>
<div class="nav-container mobile" v-if="navVisible">
<Nav :selected-view="selectedView"
@input="selectedView = $event"
@toggle="navVisible = !navVisible" />
</div>
<div class="nav-container from tablet">
<Nav :selected-view="selectedView"
@input="selectedView = $event"
@toggle="navVisible = !navVisible" />
</div>
<div class="view-container">
<Playlist :tracks="tracks"
:status="status"
:loading="loading"
:devices="devices"
:selected-device="selectedDevice"
:active-device="activeDevice"
:show-nav-button="!navVisible"
v-if="selectedView === 'playing'"
@play="$emit('play', $event)"
@clear="$emit('clear')"
@swap="$emit('swap-tracks', $event)"
@add="$emit('add-to-tracklist', $event)"
@remove="$emit('remove-from-tracklist', $event)"
@move="$emit('tracklist-move', $event)"
@save="$emit('tracklist-save', $event)"
@info="$emit('info', $event)"
@add-to-playlist="openAddToPlaylist"
@search="search"
@toggle-nav="navVisible = !navVisible"
@refresh-status="refreshStatus"
@select-device="selectDevice" />
<Playlists :playlists="playlists"
:loading="loading"
:devices="devices"
:selected-device="selectedDevice"
:active-device="activeDevice"
v-else-if="selectedView === 'playlists'"
:edited-playlist="editedPlaylist"
:tracks="editedPlaylistTracks"
@play="$emit('play-playlist', $event)"
@load="$emit('load-playlist', $event)"
@remove="$emit('remove-playlist', $event)"
@playlist-edit="$emit('playlist-edit', $event)"
@load-track="$emit('add-to-tracklist-from-edited-playlist', $event)"
@remove-track="$emit('remove-from-playlist', $event)"
@info="$emit('info', $event)"
@playlist-add="$emit('playlist-add', $event)"
@add-to-playlist="openAddToPlaylist"
@track-move="$emit('playlist-track-move', $event)"
@search="search"
@refresh-status="refreshStatus"
@select-device="selectDevice" />
<Search :loading="loading" v-else-if="selectedView === 'search'" :devices="devices"
:selected-device="selectedDevice" :active-device="activeDevice" @search="search"
:results="searchResults" @clear="$emit('search-clear')" @info="$emit('info', $event)"
@play="$emit('play', $event)" @load="$emit('add-to-tracklist', $event)"
@add-to-playlist="openAddToPlaylist" @refresh-status="refreshStatus" @select-device="selectDevice" />
<Library :loading="loading" v-else-if="selectedView === 'library'" :devices="devices"
:selected-device="selectedDevice" :active-device="activeDevice" @search="search"
:results="libraryResults" :path="path" @clear="$emit('search-clear')" @info="$emit('info', $event)"
@play="$emit('play', $event)" @load="$emit('add-to-tracklist', $event)"
@add-to-playlist="openAddToPlaylist" @cd="$emit('cd', $event)" @refresh-status="refreshStatus"
@select-device="selectDevice" />
</div>
</main>
</MediaView>
<div class="track-info-container">
<Modal title="Track info" ref="trackInfo">
<div class="track-info-content" v-if="trackInfo">
<div class="row file" v-if="trackInfo.file">
<div class="col-3 attr">File</div>
<div class="col-9 value" v-text="trackInfo.file" />
</div>
<div class="row artist" v-if="trackInfo.artist">
<div class="col-3 attr">Artist</div>
<div class="col-9 value">
<a :href="$route.fullPath" v-text="trackInfo.artist" @click.prevent="search({artist: trackInfo.artist})" />
</div>
</div>
<div class="row track-title" v-if="trackInfo.title">
<div class="col-3 attr">Title</div>
<div class="col-9 value" v-text="trackInfo.title" />
</div>
<div class="row album" v-if="trackInfo.album">
<div class="col-3 attr">Album</div>
<div class="col-9 value">
<a :href="$route.fullPath" v-text="trackInfo.album"
@click.prevent="search({artist: trackInfo.artist, album: trackInfo.album})" />
</div>
</div>
<div class="row date" v-if="trackInfo.date">
<div class="col-3 attr">Date</div>
<div class="col-9 value" v-text="trackInfo.date" />
</div>
<div class="row duration" v-if="trackInfo.time">
<div class="col-3 attr">Duration</div>
<div class="col-9 value" v-text="convertTime(trackInfo.time)" />
</div>
<div class="row track" v-if="trackInfo.track">
<div class="col-3 attr">Track</div>
<div class="col-9 value" v-text="trackInfo.track" />
</div>
<div class="row disc" v-if="trackInfo.disc">
<div class="col-3 attr">Disc</div>
<div class="col-9 value" v-text="trackInfo.disc" />
</div>
<div class="row url" v-if="trackInfo.url">
<div class="col-3 attr">URL</div>
<div class="col-9 value">
<a :href="trackInfo.url" v-text="trackInfo.uri || trackInfo.url" target="_blank" />
</div>
</div>
</div>
</Modal>
</div>
<div class="playlists-modal-container">
<Modal title="Playlists" ref="playlistsModal" @close="addToPlaylistTrack = null"
@open="selectedPlaylists = [...Array(playlists.length).keys()].map(() => false)">
<div class="filter">
<label>
<input type="search" placeholder="Filter" v-model="playlistFilter">
</label>
</div>
<div class="playlists">
<label class="row playlist" v-for="(playlist, i) in playlists" :key="i"
:class="{hidden: playlistFilter?.length > 0 && playlist.name.toLowerCase().indexOf(playlistFilter.toLowerCase()) < 0}">
<input type="checkbox" :checked="selectedPlaylists[i]"
@change="selectedPlaylists[i] = $event.target.checked" />
<span class="name" v-text="playlist.name" />
</label>
</div>
<FormFooter>
<button @click="addToPlaylist">
<i class="fa fa-plus" /> &nbsp; Add
</button>
</FormFooter>
</Modal>
</div>
</template>
<script>
import FormFooter from "@/components/elements/FormFooter";
import Loading from "@/components/Loading";
import Modal from "@/components/Modal";
import MediaUtils from "@/components/Media/Utils";
import MediaView from "@/components/Media/View";
import Nav from "@/components/panels/Music/Nav";
import Playlist from "@/components/panels/Music/Playlist";
import Playlists from "@/components/panels/Music/Playlists";
import Search from "@/components/panels/Music/Search";
import Library from "@/components/panels/Music/Library";
import Utils from "@/Utils";
export default {
name: "Music",
emits: [
'add-to-playlist',
'add-to-tracklist',
'add-to-tracklist-from-edited-playlist',
'cd',
'clear',
'consume',
'info',
'load-playlist',
'new-playing-track',
'next',
'pause',
'play',
'play-playlist',
'playlist-add',
'playlist-edit',
'playlist-track-move',
'playlist-update',
'previous',
'random',
'refresh-status',
'remove-from-playlist',
'remove-from-tracklist',
'remove-playlist',
'repeat',
'search',
'search-clear',
'seek',
'select-device',
'set-volume',
'status-update',
'stop',
'swap-tracks',
'tracklist-move',
'tracklist-save',
],
mixins: [Utils, MediaUtils],
components: {Loading, Modal, Nav, MediaView, Playlist, Playlists, FormFooter, Search, Library},
props: {
pluginName: {
type: String,
required: true,
},
loading: {
type: Boolean,
default: false,
},
config: {
type: Object,
default: () => {},
},
tracks: {
type: Array,
default: () => [],
},
editedPlaylistTracks: {
type: Array,
default: () => [],
},
playlists: {
type: Array,
default: () => [],
},
status: {
type: Object,
default: () => {},
},
editedPlaylist: {
type: Number,
},
trackInfo: {
type: String,
},
searchResults: {
type: Array,
},
libraryResults: {
type: Array,
},
path: {
type: String,
},
devices: {
type: Object,
},
activeDevice: {
type: String,
},
selectedDevice: {
type: String,
},
},
data() {
return {
selectedView: 'playing',
selectedPlaylists: [],
addToPlaylistTrack: null,
playlistFilter: '',
navVisible: false,
}
},
computed: {
track() {
if (this.status?.playingPos == null)
return null
return this.tracks[this.status.playingPos]
}
},
methods: {
async onStatusEvent(event) {
if (event.plugin_name !== this.pluginName)
return
this.$emit('status-update', event)
},
async onPlaylistEvent(event) {
if (event.plugin_name !== this.pluginName)
return
this.$emit('playlist-update', event)
},
async onNewPlayingTrack(event) {
if (event.plugin_name !== this.pluginName)
return
this.notify({
html: `<b>${event.track?.artist}</b><br>${event.track?.title}`,
image: {
iconClass: 'fa fa-play',
},
})
this.$emit('new-playing-track', event)
},
async openAddToPlaylist(track) {
this.addToPlaylistTrack = track
this.$refs.playlistsModal.isVisible = true
},
async addToPlaylist() {
this.$emit('add-to-playlist', {
track: this.addToPlaylistTrack,
playlists: [...Array(this.selectedPlaylists.length).keys()].filter((i) => this.selectedPlaylists[i])
})
this.$refs.playlistsModal.isVisible = false
this.addToPlaylistTrack = null
this.playlistFilter = ''
},
async search(filter) {
this.$emit('search', filter)
this.$refs.trackInfo.isVisible = false
this.selectedView = 'search'
},
selectDevice(id) {
this.$emit('select-device', id)
},
refreshStatus() {
this.$emit('refresh-status')
},
},
mounted() {
this.subscribe(this.onStatusEvent, 'on-status-update',
'platypush.message.event.music.MusicPlayEvent',
'platypush.message.event.music.MusicPauseEvent',
'platypush.message.event.music.MusicStopEvent',
'platypush.message.event.music.SeekChangeEvent',
'platypush.message.event.music.VolumeChangeEvent',
'platypush.message.event.music.MuteChangeEvent',
'platypush.message.event.music.PlaybackRepeatModeChangeEvent',
'platypush.message.event.music.PlaybackRandomModeChangeEvent',
'platypush.message.event.music.PlaybackConsumeModeChangeEvent',
'platypush.message.event.music.PlaybackSingleModeChangeEvent',
)
this.subscribe(this.onPlaylistEvent, 'on-playlist-update',
'platypush.message.event.music.PlaylistChangeEvent')
this.subscribe(this.onNewPlayingTrack, 'on-new-playing-track',
'platypush.message.event.music.NewPlayingTrackEvent')
this.$watch(() => this.trackInfo, (info) => {
if (info != null)
this.$refs.trackInfo.isVisible = true
})
},
unmounted() {
this.unsubscribe('on-status-update')
this.unsubscribe('on-playlist-update')
},
}
</script>
<style lang="scss" scoped>
main {
height: 100%;
background: $background-color;
display: flex;
flex-direction: row-reverse;
.view-container {
display: flex;
flex-grow: 1;
overflow: auto;
}
:deep(button) {
background: none;
padding: .5em .75em;
border: 0;
&:hover {
border: 0;
color: $default-hover-fg;
}
}
:deep(a) {
color: $default-fg;
opacity: 0.65;
&:hover {
opacity: 0.75;
border-bottom: 1px dotted;
}
}
}
.playlists-modal-container {
:deep(.body) {
display: flex;
flex-direction: column;
padding: 0 !important;
}
:deep(.filter) {
padding: .33em;
background-color: $default-bg-6;
border-bottom: $default-border-2;
input {
width: 90%;
}
}
:deep(.playlists) {
overflow: auto;
padding: 1.5em;
label {
display: flex;
align-items: center;
&:not(:last-child) {
margin-bottom: .5em;
}
.name {
margin-left: .5em;
}
}
}
}
.track-info-container {
:deep(.body) {
height: 15em;
overflow: auto;
@include until($tablet) {
width: 25em;
}
@include from($tablet) {
width: 35em;
}
.file {
user-select: text;
}
}
}
</style>