mirror of
https://github.com/owntone/owntone-server.git
synced 2025-01-24 13:13:17 -05:00
Merge pull request #693 from chme/web_player
Add option to stream audio from web interface
This commit is contained in:
commit
09c1cf1563
@ -46,8 +46,6 @@ extern struct event_base *evbase_httpd;
|
||||
#define STREAMING_SILENCE_INTERVAL 1
|
||||
// Buffer size for transmitting from player to httpd thread
|
||||
#define STREAMING_RAWBUF_SIZE (STOB(AIRTUNES_V2_PACKET_SAMPLES))
|
||||
// Should prevent that we keep transcoding to dead connections
|
||||
#define STREAMING_CONNECTION_TIMEOUT 60
|
||||
|
||||
// Linked list of mp3 streaming requests
|
||||
struct streaming_session {
|
||||
@ -74,6 +72,7 @@ static struct player_status streaming_player_status;
|
||||
static int streaming_player_changed;
|
||||
static int streaming_pipe[2];
|
||||
|
||||
|
||||
static void
|
||||
streaming_fail_cb(struct evhttp_connection *evcon, void *arg)
|
||||
{
|
||||
@ -243,6 +242,8 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse
|
||||
evhttp_add_header(output_headers, "Pragma", "no-cache");
|
||||
evhttp_add_header(output_headers, "Expires", "Mon, 31 Aug 2015 06:00:00 GMT");
|
||||
evhttp_add_header(output_headers, "icy-name", name);
|
||||
evhttp_add_header(output_headers, "Access-Control-Allow-Origin", "*");
|
||||
evhttp_add_header(output_headers, "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||
|
||||
// TODO ICY metaint
|
||||
evhttp_send_reply_start(req, HTTP_OK, "OK");
|
||||
@ -263,7 +264,6 @@ streaming_request(struct evhttp_request *req, struct httpd_uri_parsed *uri_parse
|
||||
session->next = streaming_sessions;
|
||||
streaming_sessions = session;
|
||||
|
||||
evhttp_connection_set_timeout(evcon, STREAMING_CONNECTION_TIMEOUT);
|
||||
evhttp_connection_set_closecb(evcon, streaming_fail_cb, session);
|
||||
|
||||
return 0;
|
||||
|
54
web-src/src/audio.js
Normal file
54
web-src/src/audio.js
Normal file
@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Audio handler object
|
||||
* Taken from https://github.com/rainner/soma-fm-player (released under MIT licence)
|
||||
*/
|
||||
export default {
|
||||
_audio: new Audio(),
|
||||
_context: new AudioContext(),
|
||||
_source: null,
|
||||
_gain: null,
|
||||
|
||||
// setup audio routing
|
||||
setupAudio () {
|
||||
this._source = this._context.createMediaElementSource(this._audio)
|
||||
this._gain = this._context.createGain()
|
||||
|
||||
this._source.connect(this._gain)
|
||||
this._gain.connect(this._context.destination)
|
||||
|
||||
this._audio.addEventListener('canplaythrough', e => {
|
||||
this._audio.play()
|
||||
})
|
||||
this._audio.addEventListener('canplay', e => {
|
||||
this._audio.play()
|
||||
})
|
||||
return this._audio
|
||||
},
|
||||
|
||||
// set audio volume
|
||||
setVolume (volume) {
|
||||
if (!this._gain) return
|
||||
volume = parseFloat(volume) || 0.0
|
||||
volume = (volume < 0) ? 0 : volume
|
||||
volume = (volume > 1) ? 1 : volume
|
||||
this._gain.gain.value = volume
|
||||
},
|
||||
|
||||
// play audio source url
|
||||
playSource (source) {
|
||||
this.stopAudio()
|
||||
this._context.resume().then(() => {
|
||||
console.log('playSource')
|
||||
this._audio.src = String(source || '') + '?x=' + Date.now()
|
||||
this._audio.crossOrigin = 'anonymous'
|
||||
this._audio.load()
|
||||
})
|
||||
},
|
||||
|
||||
// stop playing audio
|
||||
stopAudio () {
|
||||
try { this._audio.pause() } catch (e) {}
|
||||
try { this._audio.stop() } catch (e) {}
|
||||
try { this._audio.close() } catch (e) {}
|
||||
}
|
||||
}
|
@ -98,7 +98,7 @@ export default {
|
||||
},
|
||||
|
||||
open_genre: function () {
|
||||
this.$router.push({ path: '/music/genres/' + this.item.genre })
|
||||
this.$router.push({ name: 'Genre', params: { genre: this.item.name } })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,9 +37,9 @@
|
||||
<span class="heading">Year</span>
|
||||
<span class="title is-6">{{ track.year }}</span>
|
||||
</p>
|
||||
<p>
|
||||
<p v-if="track.genre">
|
||||
<span class="heading">Genre</span>
|
||||
<span class="title is-6">{{ track.genre }}</span>
|
||||
<a class="title is-6 has-text-link" @click="open_genre">{{ track.genre }}</a>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Track / Disc</span>
|
||||
@ -55,7 +55,7 @@
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Type</span>
|
||||
<span class="title is-6">{{ track.media_kind }} - {{ track.data_kind }}</span>
|
||||
<span class="title is-6">{{ track.media_kind }} - {{ track.data_kind }} <span class="has-text-weight-normal" v-if="track.data_kind === 'spotify'">(<a @click="open_spotify_artist">artist</a>, <a @click="open_spotify_album">album</a>)</span></span>
|
||||
</p>
|
||||
<p>
|
||||
<span class="heading">Added at</span>
|
||||
@ -88,6 +88,7 @@
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
import SpotifyWebApi from 'spotify-web-api-js'
|
||||
|
||||
export default {
|
||||
name: 'ModalDialogTrack',
|
||||
@ -96,6 +97,7 @@ export default {
|
||||
|
||||
data () {
|
||||
return {
|
||||
spotify_track: {}
|
||||
}
|
||||
},
|
||||
|
||||
@ -131,6 +133,20 @@ export default {
|
||||
this.$router.push({ path: '/music/artists/' + this.track.album_artist_id })
|
||||
},
|
||||
|
||||
open_genre: function () {
|
||||
this.$router.push({ name: 'Genre', params: { genre: this.track.genre } })
|
||||
},
|
||||
|
||||
open_spotify_artist: function () {
|
||||
this.$emit('close')
|
||||
this.$router.push({ path: '/music/spotify/artists/' + this.spotify_track.artists[0].id })
|
||||
},
|
||||
|
||||
open_spotify_album: function () {
|
||||
this.$emit('close')
|
||||
this.$router.push({ path: '/music/spotify/albums/' + this.spotify_track.album.id })
|
||||
},
|
||||
|
||||
mark_new: function () {
|
||||
webapi.library_track_update(this.track.id, { 'play_count': 'reset' }).then(() => {
|
||||
this.$emit('play_count_changed')
|
||||
@ -144,6 +160,20 @@ export default {
|
||||
this.$emit('close')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'track' () {
|
||||
if (this.track && this.track.data_kind === 'spotify') {
|
||||
const spotifyApi = new SpotifyWebApi()
|
||||
spotifyApi.setAccessToken(this.$store.state.spotify.webapi_token)
|
||||
spotifyApi.getTrack(this.track.path.slice(this.track.path.lastIndexOf(':') + 1)).then((response) => {
|
||||
this.spotify_track = response
|
||||
})
|
||||
} else {
|
||||
this.spotify_track = {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<span class="icon fd-has-action" :class="{ 'has-text-grey-light': !output.selected }" v-on:click="set_enabled"><i class="mdi mdi-18px" v-bind:class="type_class"></i></span>
|
||||
<a class="button is-white is-small"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !output.selected }" v-on:click="set_enabled"><i class="mdi mdi-18px" v-bind:class="type_class"></i></span></a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
|
@ -39,7 +39,9 @@
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<span class="icon"><i class="mdi mdi-18px mdi-volume-high"></i></span>
|
||||
<a class="button is-white is-small" @click="toggle_mute_volume">
|
||||
<span class="icon"><i class="mdi mdi-18px" :class="{ 'mdi-volume-off': player.volume <= 0, 'mdi-volume-high': player.volume > 0 }"></i></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
@ -60,6 +62,31 @@
|
||||
<hr class="navbar-divider">
|
||||
<nav-bar-item-output v-for="output in outputs" :key="output.id" :output="output"></nav-bar-item-output>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<a class="button is-white is-small" :class="{ 'is-loading': loading }"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }" @click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i></span></a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream <a href="/stream.mp3"><span class="is-lowercase">(stream.mp3)</span></a></p>
|
||||
<range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:disabled="!playing"
|
||||
:value="stream_volume"
|
||||
@change="set_stream_volume">
|
||||
</range-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile">
|
||||
@ -104,6 +131,7 @@
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
import _audio from '@/audio'
|
||||
import NavBarItemOutput from './NavBarItemOutput'
|
||||
import PlayerButtonPlayPause from './PlayerButtonPlayPause'
|
||||
import PlayerButtonNext from './PlayerButtonNext'
|
||||
@ -120,7 +148,12 @@ export default {
|
||||
|
||||
data () {
|
||||
return {
|
||||
search_query: ''
|
||||
search_query: '',
|
||||
old_volume: 0,
|
||||
|
||||
playing: false,
|
||||
loading: false,
|
||||
stream_volume: 10
|
||||
}
|
||||
},
|
||||
|
||||
@ -163,10 +196,91 @@ export default {
|
||||
webapi.player_volume(newVolume)
|
||||
},
|
||||
|
||||
toggle_mute_volume: function () {
|
||||
if (this.player.volume > 0) {
|
||||
this.set_volume(0)
|
||||
} else {
|
||||
this.set_volume(this.old_volume)
|
||||
}
|
||||
},
|
||||
|
||||
open_about: function () {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, false)
|
||||
this.$router.push({ path: '/about' })
|
||||
},
|
||||
|
||||
setupAudio: function () {
|
||||
const a = _audio.setupAudio()
|
||||
|
||||
a.addEventListener('waiting', e => {
|
||||
this.playing = false
|
||||
this.loading = true
|
||||
})
|
||||
a.addEventListener('playing', e => {
|
||||
this.playing = true
|
||||
this.loading = false
|
||||
})
|
||||
a.addEventListener('ended', e => {
|
||||
this.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
a.addEventListener('error', e => {
|
||||
this.closeAudio()
|
||||
this.$store.dispatch('add_notification', { text: 'HTTP stream error: failed to load stream or stopped loading due to network problem', type: 'danger' })
|
||||
this.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// close active audio
|
||||
closeAudio: function () {
|
||||
_audio.stopAudio()
|
||||
this.playing = false
|
||||
},
|
||||
|
||||
playChannel: function () {
|
||||
if (this.playing) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = '/stream.mp3'
|
||||
this.loading = true
|
||||
_audio.playSource(channel)
|
||||
_audio.setVolume(this.stream_volume / 100)
|
||||
},
|
||||
|
||||
togglePlay: function () {
|
||||
if (this.loading) {
|
||||
return
|
||||
}
|
||||
if (this.playing) {
|
||||
return this.closeAudio()
|
||||
}
|
||||
return this.playChannel()
|
||||
},
|
||||
|
||||
set_stream_volume: function (newVolume) {
|
||||
this.stream_volume = newVolume
|
||||
_audio.setVolume(this.stream_volume / 100)
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
'$store.state.player.volume' () {
|
||||
if (this.player.volume > 0) {
|
||||
this.old_volume = this.player.volume
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// on app mounted
|
||||
mounted () {
|
||||
this.setupAudio()
|
||||
},
|
||||
|
||||
// on app destroyed
|
||||
destroyed () {
|
||||
this.closeAudio()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -6,14 +6,12 @@
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
|
||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||
</a>
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
<!--
|
||||
<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>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
@ -26,6 +24,7 @@
|
||||
</template>
|
||||
</list-item-track>
|
||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
||||
<modal-dialog-album :show="show_album_details_modal" :album="album" @close="show_album_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
@ -35,6 +34,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const albumData = {
|
||||
@ -54,7 +54,7 @@ const albumData = {
|
||||
export default {
|
||||
name: 'PageAlbum',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumData) ],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack },
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -62,7 +62,9 @@ export default {
|
||||
tracks: [],
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {}
|
||||
selected_track: {},
|
||||
|
||||
show_album_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -4,9 +4,14 @@
|
||||
<p class="title is-4">{{ artist.name }}</p>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="show_artist_details_modal = true">
|
||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||
</a>
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile">{{ artist.album_count }} albums | <a class="has-text-link" @click="open_tracks">{{ artist.track_count }} tracks</a></p>
|
||||
@ -18,6 +23,7 @@
|
||||
</template>
|
||||
</list-item-album>
|
||||
<modal-dialog-album :show="show_details_modal" :album="selected_album" @close="show_details_modal = false" />
|
||||
<modal-dialog-artist :show="show_artist_details_modal" :artist="artist" @close="show_artist_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
@ -27,6 +33,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemAlbum from '@/components/ListItemAlbum'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import ModalDialogArtist from '@/components/ModalDialogArtist'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const artistData = {
|
||||
@ -46,7 +53,7 @@ const artistData = {
|
||||
export default {
|
||||
name: 'PageArtist',
|
||||
mixins: [ LoadDataBeforeEnterMixin(artistData) ],
|
||||
components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum },
|
||||
components: { ContentWithHeading, ListItemAlbum, ModalDialogAlbum, ModalDialogArtist },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -54,7 +61,9 @@ export default {
|
||||
albums: {},
|
||||
|
||||
show_details_modal: false,
|
||||
selected_album: {}
|
||||
selected_album: {},
|
||||
|
||||
show_artist_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -5,12 +5,17 @@
|
||||
<div class="title is-4 has-text-grey has-text-weight-normal">{{ album.artist }}</div>
|
||||
</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>
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
|
||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
|
||||
@ -22,6 +27,7 @@
|
||||
</template>
|
||||
</list-item-track>
|
||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
||||
<modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'audiobook'" @close="show_album_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
@ -31,6 +37,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const albumData = {
|
||||
@ -50,7 +57,7 @@ const albumData = {
|
||||
export default {
|
||||
name: 'PageAudiobook',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumData) ],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack },
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -58,7 +65,9 @@ export default {
|
||||
tracks: [],
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {}
|
||||
selected_track: {},
|
||||
|
||||
show_album_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -6,9 +6,14 @@
|
||||
<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>
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="open_directory_dialog({ 'path': current_directory })">
|
||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<div class="media" v-if="$route.query.directory" @click="open_parent_directory()">
|
||||
|
@ -72,7 +72,7 @@ export default {
|
||||
methods: {
|
||||
open_tracks: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ path: '/music/genres/' + this.name + '/tracks' })
|
||||
this.$router.push({ name: 'GenreTracks', params: { genre: this.name } })
|
||||
},
|
||||
|
||||
play: function () {
|
||||
|
@ -71,7 +71,7 @@ export default {
|
||||
methods: {
|
||||
open_genre: function () {
|
||||
this.show_details_modal = false
|
||||
this.$router.push({ path: '/music/genres/' + this.genre })
|
||||
this.$router.push({ name: 'Genre', params: { genre: this.genre } })
|
||||
},
|
||||
|
||||
play: function () {
|
||||
|
@ -4,9 +4,14 @@
|
||||
<div class="title is-4">{{ playlist.name }}</div>
|
||||
</template>
|
||||
<template slot="heading-right">
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="show_playlist_details_modal = true">
|
||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||
</a>
|
||||
<a class="button is-small is-dark is-rounded" @click="play">
|
||||
<span class="icon"><i class="mdi mdi-shuffle"></i></span> <span>Shuffle</span>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile">{{ tracks.length }} tracks</p>
|
||||
@ -18,6 +23,7 @@
|
||||
</template>
|
||||
</list-item-track>
|
||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" />
|
||||
<modal-dialog-playlist :show="show_playlist_details_modal" :playlist="playlist" @close="show_playlist_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
@ -27,6 +33,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogPlaylist from '@/components/ModalDialogPlaylist'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
const playlistData = {
|
||||
@ -46,7 +53,7 @@ const playlistData = {
|
||||
export default {
|
||||
name: 'PagePlaylist',
|
||||
mixins: [ LoadDataBeforeEnterMixin(playlistData) ],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack },
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, ModalDialogPlaylist },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -54,7 +61,9 @@ export default {
|
||||
tracks: [],
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {}
|
||||
selected_track: {},
|
||||
|
||||
show_playlist_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -4,12 +4,17 @@
|
||||
<div class="title is-4">{{ album.name }}</div>
|
||||
</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>
|
||||
<div class="buttons is-centered">
|
||||
<a class="button is-small is-light is-rounded" @click="show_album_details_modal = true">
|
||||
<span class="icon"><i class="mdi mdi-dots-horizontal mdi-18px"></i></span>
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<p class="heading has-text-centered-mobile">{{ album.track_count }} tracks</p>
|
||||
@ -31,6 +36,7 @@
|
||||
</template>
|
||||
</list-item-track>
|
||||
<modal-dialog-track :show="show_details_modal" :track="selected_track" @close="show_details_modal = false" @play_count_changed="reload_tracks" />
|
||||
<modal-dialog-album :show="show_album_details_modal" :album="album" :media_kind="'podcast'" @close="show_album_details_modal = false" />
|
||||
</template>
|
||||
</content-with-heading>
|
||||
</template>
|
||||
@ -40,6 +46,7 @@ import { LoadDataBeforeEnterMixin } from './mixin'
|
||||
import ContentWithHeading from '@/templates/ContentWithHeading'
|
||||
import ListItemTrack from '@/components/ListItemTrack'
|
||||
import ModalDialogTrack from '@/components/ModalDialogTrack'
|
||||
import ModalDialogAlbum from '@/components/ModalDialogAlbum'
|
||||
import RangeSlider from 'vue-range-slider'
|
||||
import webapi from '@/webapi'
|
||||
|
||||
@ -60,7 +67,7 @@ const albumData = {
|
||||
export default {
|
||||
name: 'PagePodcast',
|
||||
mixins: [ LoadDataBeforeEnterMixin(albumData) ],
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, RangeSlider },
|
||||
components: { ContentWithHeading, ListItemTrack, ModalDialogTrack, RangeSlider, ModalDialogAlbum },
|
||||
|
||||
data () {
|
||||
return {
|
||||
@ -68,7 +75,9 @@ export default {
|
||||
tracks: [],
|
||||
|
||||
show_details_modal: false,
|
||||
selected_track: {}
|
||||
selected_track: {},
|
||||
|
||||
show_album_details_modal: false
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -24,10 +24,13 @@ module.exports = {
|
||||
// localhost:3689
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3689',
|
||||
target: 'http://localhost:3689'
|
||||
},
|
||||
'/artwork': {
|
||||
target: 'http://localhost:3689',
|
||||
target: 'http://localhost:3689'
|
||||
},
|
||||
'/stream.mp3': {
|
||||
target: 'http://localhost:3689'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user