Merge pull request #652 from chme/webinterface_4

Update player webinterface (v0.4.0)
This commit is contained in:
chme 2019-01-05 07:53:21 +01:00 committed by GitHub
commit 22cdd90a32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 318 additions and 14 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "forked-daapd-web",
"version": "0.3.0",
"version": "0.4.0",
"description": "forked-daapd web interface",
"author": "chme <christian.meffert@googlemail.com>",
"license": "GPL-2.0",

View File

@ -95,7 +95,7 @@ export default {
socket.onopen = function () {
vm.$store.dispatch('add_notification', { text: 'Connection to server established', type: 'primary', topic: 'connection', timeout: 2000 })
vm.reconnect_attempts = 0
socket.send(JSON.stringify({ notify: ['update', 'player', 'options', 'outputs', 'volume', 'spotify'] }))
socket.send(JSON.stringify({ notify: ['update', 'database', 'player', 'options', 'outputs', 'volume', 'spotify'] }))
vm.update_outputs()
vm.update_player_status()
@ -112,7 +112,7 @@ export default {
}
socket.onmessage = function (response) {
var data = JSON.parse(response.data)
if (data.notify.includes('update')) {
if (data.notify.includes('update') || data.notify.includes('database')) {
vm.update_library_stats()
}
if (data.notify.includes('player') || data.notify.includes('options') || data.notify.includes('volume')) {

View File

@ -0,0 +1,26 @@
<template functional>
<div class="media">
<figure class="media-left fd-has-action" @click="listeners.click">
<span class="icon">
<i class="mdi mdi-folder"></i>
</span>
</figure>
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ props.directory.path.substring(props.directory.path.lastIndexOf('/') + 1) }}</h1>
<h2 class="subtitle is-7 has-text-grey-light">{{ props.directory.path }}</h2>
</div>
<div class="media-right">
<slot name="actions"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'ListItemDirectory',
props: [ 'directory' ]
}
</script>
<style>
</style>

View File

@ -1,5 +1,8 @@
<template functional>
<div class="media">
<figure class="media-left fd-has-action" v-if="slots().icon" @click="listeners.click">
<slot name="icon"></slot>
</figure>
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ props.playlist.name }}</h1>
</div>

View File

@ -1,5 +1,8 @@
<template functional>
<div class="media" :id="'index_' + props.track.title_sort.charAt(0).toUpperCase()">
<figure class="media-left fd-has-action" v-if="slots().icon" @click="listeners.click">
<slot name="icon"></slot>
</figure>
<div class="media-content fd-has-action is-clipped" @click="listeners.click">
<h1 class="title is-6">{{ props.track.title }}</h1>
<h2 class="subtitle is-7 has-text-grey"><b>{{ props.track.artist }}</b></h2>

View File

@ -0,0 +1,65 @@
<template>
<div>
<transition name="fade">
<div class="modal is-active" v-if="show">
<div class="modal-background" @click="$emit('close')"></div>
<div class="modal-content fd-modal-card">
<div class="card">
<div class="card-content">
<p class="title is-4">
{{ directory.path }}
</p>
</div>
<footer class="card-footer">
<a class="card-footer-item has-text-dark" @click="queue_add">
<span class="icon"><i class="mdi mdi-playlist-plus"></i></span> <span class="is-size-7">Add</span>
</a>
<a class="card-footer-item has-text-dark" @click="queue_add_next">
<span class="icon"><i class="mdi mdi-playlist-play"></i></span> <span class="is-size-7">Add Next</span>
</a>
<a class="card-footer-item has-text-dark" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span class="is-size-7">Play</span>
</a>
</footer>
</div>
</div>
<button class="modal-close is-large" aria-label="close" @click="$emit('close')"></button>
</div>
</transition>
</div>
</template>
<script>
import webapi from '@/webapi'
export default {
name: 'ModalDialogDirectory',
props: [ 'show', 'directory' ],
methods: {
play: function () {
this.$emit('close')
webapi.search({ 'type': 'tracks', 'expression': 'path starts with "' + this.directory.path + '" order by path asc' }).then(({ data }) => {
webapi.player_play_uri(data.tracks.items.map(a => a.uri).join(','), false)
})
},
queue_add: function () {
this.$emit('close')
webapi.search({ 'type': 'tracks', 'expression': 'path starts with "' + this.directory.path + '" order by path asc' }).then(({ data }) => {
webapi.queue_add(data.tracks.items.map(a => a.uri).join(','))
})
},
queue_add_next: function () {
this.$emit('close')
webapi.search({ 'type': 'tracks', 'expression': 'path starts with "' + this.directory.path + '" order by path asc' }).then(({ data }) => {
webapi.queue_add_next(data.tracks.items.map(a => a.uri).join(','))
})
}
}
}
</script>
<style>
</style>

View File

@ -39,22 +39,22 @@ export default {
methods: {
play: function () {
this.$emit('close')
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.player_play_uri(data.albums.items.map(a => a.uri).join(','), false)
webapi.library_genre_tracks(this.genre.name).then(({ data }) =>
webapi.player_play_uri(data.tracks.items.map(a => a.uri).join(','), false)
)
},
queue_add: function () {
this.$emit('close')
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.queue_add(data.albums.items.map(a => a.uri).join(','))
webapi.library_genre_tracks(this.genre.name).then(({ data }) =>
webapi.queue_add(data.tracks.items.map(a => a.uri).join(','))
)
},
queue_add_next: function () {
this.$emit('close')
webapi.library_genre(this.genre.name).then(({ data }) =>
webapi.queue_add_next(data.albums.items.map(a => a.uri).join(','))
webapi.library_genre_tracks(this.genre.name).then(({ data }) =>
webapi.queue_add_next(data.tracks.items.map(a => a.uri).join(','))
)
},

View File

@ -13,6 +13,9 @@
<router-link to="/audiobooks" class="navbar-item" active-class="is-active" v-if="audiobooks.tracks > 0">
<span class="icon"><i class="mdi mdi-book-open-variant"></i></span>
</router-link>
<router-link to="/files" class="navbar-item" active-class="is-active">
<span class="icon"><i class="mdi mdi-folder-open"></i></span>
</router-link>
<router-link to="/search" class="navbar-item" active-class="is-active">
<span class="icon"><i class="mdi mdi-magnify"></i></span>
</router-link>

View File

@ -1,6 +1,6 @@
<template>
<a v-on:click="toggle_play_pause">
<span class="icon"><i class="mdi" v-bind:class="[icon_style, { 'mdi-play': !is_playing, 'mdi-pause': is_playing }]"></i></span>
<span class="icon"><i class="mdi" v-bind:class="[icon_style, { 'mdi-play': !is_playing, 'mdi-pause': is_playing && is_pause_allowed, 'mdi-stop': is_playing && !is_pause_allowed }]"></i></span>
</a>
</template>
@ -15,13 +15,19 @@ export default {
computed: {
is_playing () {
return this.$store.state.player.state === 'play'
},
is_pause_allowed () {
return this.$store.getters.now_playing && this.$store.getters.now_playing.data_kind !== 'url' && this.$store.getters.now_playing.data_kind !== 'pipe'
}
},
methods: {
toggle_play_pause: function () {
if (this.is_playing) {
if (this.is_playing && this.is_pause_allowed) {
webapi.player_pause()
} else if (this.is_playing && !this.is_pause_allowed) {
webapi.player_stop()
} else {
webapi.player_play()
}

View File

@ -0,0 +1,178 @@
<template>
<div>
<content-with-heading>
<template slot="heading-left">
<p class="title is-4">Files</p>
<p class="title is-7 has-text-grey">{{ current_directory }}</p>
</template>
<template slot="heading-right">
<a class="button is-small is-dark is-rounded" @click="play">
<span class="icon"><i class="mdi mdi-play"></i></span> <span>Play</span>
</a>
</template>
<template slot="content">
<div class="media" v-if="$route.query.directory" @click="open_parent_directory()">
<figure class="media-left fd-has-action">
<span class="icon">
<i class="mdi mdi-subdirectory-arrow-left"></i>
</span>
</figure>
<div class="media-content fd-has-action is-clipped">
<h1 class="title is-6">..</h1>
</div>
<div class="media-right">
<slot name="actions"></slot>
</div>
</div>
<list-item-directory v-for="directory in files.directories" :key="directory.path" :directory="directory" @click="open_directory(directory)">
<template slot="actions">
<a @click="open_directory_dialog(directory)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-directory>
<list-item-playlist v-for="playlist in files.playlists.items" :key="playlist.id" :playlist="playlist" @click="open_playlist(playlist)">
<template slot="icon">
<span class="icon">
<i class="mdi mdi-library-music"></i>
</span>
</template>
<template slot="actions">
<a @click="open_playlist_dialog(playlist)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-playlist>
<list-item-track v-for="(track, index) in files.tracks.items" :key="track.id" :track="track" @click="play_track(index)">
<template slot="icon">
<span class="icon">
<i class="mdi mdi-file-outline"></i>
</span>
</template>
<template slot="actions">
<a @click="open_track_dialog(track)">
<span class="icon has-text-dark"><i class="mdi mdi-dots-vertical mdi-18px"></i></span>
</a>
</template>
</list-item-track>
<modal-dialog-directory :show="show_directory_details_modal" :directory="selected_directory" @close="show_directory_details_modal = false" />
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="selected_playlist" @close="show_playlist_details_modal = false" />
<modal-dialog-track :show="show_track_details_modal" :track="selected_track" @close="show_track_details_modal = false" />
</template>
</content-with-heading>
</div>
</template>
<script>
import { LoadDataBeforeEnterMixin } from './mixin'
import ContentWithHeading from '@/templates/ContentWithHeading'
import ListItemDirectory from '@/components/ListItemDirectory'
import ListItemPlaylist from '@/components/ListItemPlaylist'
import ListItemTrack from '@/components/ListItemTrack'
import ModalDialogDirectory from '@/components/ModalDialogDirectory'
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
import ModalDialogTrack from '@/components/ModalDialogTrack'
import webapi from '@/webapi'
const filesData = {
load: function (to) {
if (to.query.directory) {
return webapi.library_files(to.query.directory)
}
return Promise.resolve()
},
set: function (vm, response) {
if (response) {
vm.files = response.data
} else {
vm.files = {
directories: vm.$store.state.config.directories.map(dir => { return { path: dir } }),
tracks: { items: [] },
playlists: { items: [] }
}
}
}
}
export default {
name: 'PageFiles',
mixins: [ LoadDataBeforeEnterMixin(filesData) ],
components: { ContentWithHeading, ListItemDirectory, ListItemPlaylist, ListItemTrack, ModalDialogDirectory, ModalDialogPlaylist, ModalDialogTrack },
data () {
return {
files: { directories: [], tracks: { items: [] }, playlists: { items: [] } },
show_directory_details_modal: false,
selected_directory: {},
show_playlist_details_modal: false,
selected_playlist: {},
show_track_details_modal: false,
selected_track: {}
}
},
computed: {
current_directory () {
if (this.$route.query && this.$route.query.directory) {
return this.$route.query.directory
}
return '/'
}
},
methods: {
open_parent_directory: function () {
var parent = this.current_directory.slice(0, this.current_directory.lastIndexOf('/'))
if (parent === '' || this.$store.state.config.directories.includes(this.current_directory)) {
this.$router.push({ path: '/files' })
} else {
this.$router.push({ path: '/files', query: { directory: this.current_directory.slice(0, this.current_directory.lastIndexOf('/')) } })
}
},
open_directory: function (directory) {
this.$router.push({ path: '/files', query: { directory: directory.path } })
},
open_directory_dialog: function (directory) {
this.selected_directory = directory
this.show_directory_details_modal = true
},
play: function () {
webapi.search({ 'type': 'tracks', 'expression': 'path starts with "' + this.current_directory + '" order by path asc' }).then(({ data }) => {
webapi.player_play_uri(data.tracks.items.map(a => a.uri).join(','), false)
})
},
play_track: function (position) {
webapi.player_play_uri(this.files.tracks.items.map(a => a.uri).join(','), false, position)
},
open_track_dialog: function (track) {
this.selected_track = track
this.show_track_details_modal = true
},
open_playlist: function (playlist) {
this.$router.push({ path: '/playlists/' + playlist.id })
},
open_playlist_dialog: function (playlist) {
this.selected_playlist = playlist
this.show_playlist_details_modal = true
}
}
}
</script>
<style>
</style>

View File

@ -76,7 +76,9 @@ export default {
},
play: function () {
webapi.player_play_uri(this.genre_albums.items.map(a => a.uri).join(','), true)
webapi.library_genre_tracks(this.name).then(({ data }) =>
webapi.player_play_uri(data.tracks.items.map(a => a.uri).join(','), true)
)
},
open_album: function (album) {

View File

@ -21,6 +21,7 @@ import PageAudiobooks from '@/pages/PageAudiobooks'
import PageAudiobook from '@/pages/PageAudiobook'
import PagePlaylists from '@/pages/PagePlaylists'
import PagePlaylist from '@/pages/PagePlaylist'
import PageFiles from '@/pages/PageFiles'
import PageSearch from '@/pages/PageSearch'
import PageAbout from '@/pages/PageAbout'
import SpotifyPageBrowse from '@/pages/SpotifyPageBrowse'
@ -144,6 +145,12 @@ export const router = new VueRouter({
component: PageAudiobook,
meta: { show_progress: true }
},
{
path: '/files',
name: 'Files',
component: PageFiles,
meta: { show_progress: true }
},
{
path: '/playlists',
name: 'Playlists',

View File

@ -91,6 +91,10 @@ export default {
return axios.put('/api/player/pause')
},
player_stop () {
return axios.put('/api/player/stop')
},
player_next () {
return axios.put('/api/player/next')
},
@ -214,6 +218,13 @@ export default {
return axios.get('/api/library/playlists/' + playlistId + '/tracks')
},
library_files (directory = undefined) {
var filesParams = { 'directory': directory }
return axios.get('/api/library/files', {
params: filesParams
})
},
search (searchParams) {
return axios.get('/api/search', {
params: searchParams