forked from rc/aircox
work on player
This commit is contained in:
parent
2a0d0b1758
commit
063d1f194e
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,6 +1,16 @@
|
||||||
{% extends "aircox/page_detail.html" %}
|
{% extends "aircox/page_detail.html" %}
|
||||||
{% comment %}List of a show's episodes for a specific{% endcomment %}
|
{% comment %}List of a show's episodes for a specific{% endcomment %}
|
||||||
{% load i18n %}
|
{% load i18n aircox %}
|
||||||
|
|
||||||
|
{% block head_extra %}
|
||||||
|
<script id="page">
|
||||||
|
window.addEventListener('load', ev => {
|
||||||
|
Vue.set(aircox.app, 'page', {
|
||||||
|
podcasts: new aircox.Set(aircox.Sound, {items: {{ podcasts|json|safe }}})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock head_extra %}
|
||||||
|
|
||||||
{% include "aircox/program_sidebar.html" %}
|
{% include "aircox/program_sidebar.html" %}
|
||||||
|
|
||||||
|
@ -38,10 +48,19 @@
|
||||||
|
|
||||||
{% if podcasts %}
|
{% if podcasts %}
|
||||||
<section class="column">
|
<section class="column">
|
||||||
|
<a-playlist v-if="page" :set="page.podcasts"
|
||||||
|
name="{{ page.title }}"
|
||||||
|
:player="player" :actions="['play']"
|
||||||
|
@select="player.playItems('queue', $event.item)">
|
||||||
|
<template v-slot:header>
|
||||||
<h5 class="title is-5">{% trans "Podcasts" %}</h5>
|
<h5 class="title is-5">{% trans "Podcasts" %}</h5>
|
||||||
|
</template>
|
||||||
|
</a-playlist>
|
||||||
|
{% comment %}
|
||||||
{% for object in podcasts %}
|
{% for object in podcasts %}
|
||||||
{% include "aircox/widgets/podcast_item.html" %}
|
{% include "aircox/widgets/podcast_item.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% endcomment %}
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
8
aircox/templates/aircox/program_sidebar.html
Normal file
8
aircox/templates/aircox/program_sidebar.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
{% block sidebar_title %}
|
||||||
|
{% with program.title as program %}
|
||||||
|
{% blocktrans %}Recently on {{ program }}{% endblocktrans %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ List item for a podcast.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<a-sound-item :data="{{ object|json }}" :player="player"
|
<a-sound-item :data="{{ object|json }}" :player="player"
|
||||||
:actions="['play','queue']" @click="player.play(item)">
|
:actions="['play']" @click="player.play(item)">
|
||||||
</a-sound-item>
|
</a-sound-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import random
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from aircox.models import Page, Diffusion, Log
|
from aircox.models import Page, Diffusion, Log
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,18 @@ export const defaultConfig = {
|
||||||
el: '#app',
|
el: '#app',
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
page: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
player() {
|
player() { return window.aircox.player; }
|
||||||
return window.aircox.player;
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function App(config) {
|
export default function App(config) {
|
||||||
return (new AppConfig(config)).load()
|
return (new AppConfig(config)).load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,16 +10,20 @@ import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
|
||||||
|
|
||||||
|
|
||||||
//-- aircox
|
//-- aircox
|
||||||
import {App} from './app';
|
import App from './app';
|
||||||
|
import Sound from './sound';
|
||||||
|
import {Set} from './model';
|
||||||
|
|
||||||
import './styles.scss';
|
import './styles.scss';
|
||||||
|
|
||||||
import Autocomplete from './autocomplete.vue';
|
import Autocomplete from './autocomplete.vue';
|
||||||
import Player from './player.vue';
|
import Player from './player.vue';
|
||||||
|
import Playlist from './playlist.vue';
|
||||||
import SoundItem from './soundItem';
|
import SoundItem from './soundItem';
|
||||||
|
|
||||||
Vue.component('a-autocomplete', Autocomplete)
|
Vue.component('a-autocomplete', Autocomplete)
|
||||||
Vue.component('a-player', Player)
|
Vue.component('a-player', Player)
|
||||||
|
Vue.component('a-playlist', Playlist)
|
||||||
Vue.component('a-sound-item', SoundItem)
|
Vue.component('a-sound-item', SoundItem)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,8 +40,11 @@ window.aircox = {
|
||||||
// player component
|
// player component
|
||||||
get player() {
|
get player() {
|
||||||
return this.playerApp && this.playerApp.$refs.player
|
return this.playerApp && this.playerApp.$refs.player
|
||||||
}
|
},
|
||||||
|
|
||||||
|
Set: Set, Sound: Sound,
|
||||||
};
|
};
|
||||||
|
window.Vue = Vue;
|
||||||
|
|
||||||
|
|
||||||
App({el: '#player'}).then(app => window.aircox.playerApp = app,
|
App({el: '#player'}).then(app => window.aircox.playerApp = app,
|
||||||
|
|
|
@ -2,13 +2,11 @@
|
||||||
<div>
|
<div>
|
||||||
<slot name="header"></slot>
|
<slot name="header"></slot>
|
||||||
<ul :class="listClass">
|
<ul :class="listClass">
|
||||||
<slot name="start"></slot>
|
|
||||||
<template v-for="(item,index) in items">
|
<template v-for="(item,index) in items">
|
||||||
<li :class="itemClass" @click="select(index)">
|
<li :class="itemClass" @click="select(index)">
|
||||||
<slot name="item" :set="set" :index="index" :item="item"></slot>
|
<slot name="item" :selected="index == selectedIndex" :set="set" :index="index" :item="item"></slot>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<slot name="end"></slot>
|
|
||||||
</ul>
|
</ul>
|
||||||
<slot name="footer"></slot>
|
<slot name="footer"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,14 +15,14 @@
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
selectedIndex: this.default,
|
selectedIndex: this.defaultIndex,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
listClass: String,
|
listClass: String,
|
||||||
itemClass: String,
|
itemClass: String,
|
||||||
default: { type: Number, default: -1},
|
defaultIndex: { type: Number, default: -1},
|
||||||
set: Object,
|
set: Object,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -34,31 +32,37 @@ export default {
|
||||||
length() { return this.set.length },
|
length() { return this.set.length },
|
||||||
|
|
||||||
selected() {
|
selected() {
|
||||||
return this.items && this.items.length > this.selectedIndex > -1
|
return this.selectedIndex > -1 && this.items.length > this.selectedIndex > -1
|
||||||
? this.items[this.selectedIndex] : null;
|
? this.items[this.selectedIndex] : null;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
select(index=null) {
|
get(index) { return this.set.get(index) },
|
||||||
if(index === null)
|
find(item) { return this.set.find(item) },
|
||||||
index = this.selectedIndex;
|
findIndex(item) { return this.set.findIndex(item) },
|
||||||
else if(this.selectedIndex == index)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.selectedIndex = Math.min(index, this.items.length-1);
|
push(...items) {
|
||||||
this.$emit('select', { item: this.selected, index: this.selectedIndex });
|
let index = this.set.length;
|
||||||
|
for(var item of items)
|
||||||
|
this.set.push(item);
|
||||||
|
},
|
||||||
|
|
||||||
|
remove(index, select=False) {
|
||||||
|
this.set.remove(index);
|
||||||
|
if(index < this.selectedIndex)
|
||||||
|
this.selectedIndex--;
|
||||||
|
if(select && this.selectedIndex == index)
|
||||||
|
this.select(index)
|
||||||
|
},
|
||||||
|
|
||||||
|
select(index) {
|
||||||
|
this.selectedIndex = index > -1 && this.items.length ? index % this.items.length : -1;
|
||||||
|
this.$emit('select', { target: this, item: this.selected, index: this.selectedIndex });
|
||||||
return this.selectedIndex;
|
return this.selectedIndex;
|
||||||
},
|
},
|
||||||
|
|
||||||
selectNext() {
|
|
||||||
let index = this.selectedIndex + 1;
|
|
||||||
return this.select(index >= this.items.length ? -1 : index);
|
|
||||||
},
|
|
||||||
|
|
||||||
// add()
|
|
||||||
// insert() + drag & drop
|
|
||||||
// remove()
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -107,16 +107,19 @@ export default class Model {
|
||||||
*/
|
*/
|
||||||
export class Set {
|
export class Set {
|
||||||
constructor(model, {items=[],url=null,args={},unique=null,max=null,storeKey=null}={}) {
|
constructor(model, {items=[],url=null,args={},unique=null,max=null,storeKey=null}={}) {
|
||||||
this.items = items.map(x => x instanceof model ? x : new model(x, args));
|
this.items = [];
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.unique = unique;
|
this.unique = unique;
|
||||||
this.max = max;
|
this.max = max;
|
||||||
this.storeKey = storeKey;
|
this.storeKey = storeKey;
|
||||||
|
|
||||||
|
for(var item of items)
|
||||||
|
this.push(item, {args: args, save: false});
|
||||||
}
|
}
|
||||||
|
|
||||||
get length() { return this.items.length }
|
get length() { return this.items.length }
|
||||||
indexOf(...args) { return this.items.indexOf(...args); }
|
get(index) { return this.items[index] }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch multiple items from server
|
* Fetch multiple items from server
|
||||||
|
@ -141,35 +144,54 @@ export class Set {
|
||||||
* Store list into localStorage
|
* Store list into localStorage
|
||||||
*/
|
*/
|
||||||
store() {
|
store() {
|
||||||
if(this.storeKey)
|
this.storeKey && window.localStorage.setItem(this.storeKey, JSON.stringify(
|
||||||
window.localStorage.setItem(this.storeKey, JSON.stringify(
|
|
||||||
this.items.map(i => i.data)));
|
this.items.map(i => i.data)));
|
||||||
}
|
}
|
||||||
|
|
||||||
push(item, {args={}}={}) {
|
/**
|
||||||
|
* Save item
|
||||||
|
*/
|
||||||
|
save() {
|
||||||
|
this.storeKey && this.store();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find item by id
|
||||||
|
*/
|
||||||
|
find(item) {
|
||||||
|
return this.items.find(x => x.id == item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find item index by id
|
||||||
|
*/
|
||||||
|
findIndex(item) {
|
||||||
|
return this.items.findIndex(x => x.id == item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add item to set
|
||||||
|
*/
|
||||||
|
push(item, {args={},save=true}={}) {
|
||||||
item = item instanceof this.model ? item : new this.model(item, args);
|
item = item instanceof this.model ? item : new this.model(item, args);
|
||||||
if(this.unique && this.items.find(x => x.id == item.id))
|
if(this.unique) {
|
||||||
return;
|
let index = this.findIndex(item);
|
||||||
|
if(index > -1)
|
||||||
|
this.items.splice(index,1);
|
||||||
|
}
|
||||||
if(this.max && this.items.length >= this.max)
|
if(this.max && this.items.length >= this.max)
|
||||||
this.items.splice(0,this.items.length-this.max)
|
this.items.splice(0,this.items.length-this.max)
|
||||||
|
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
this._updated()
|
save && this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(item, {args={}}={}) {
|
/**
|
||||||
item = item instanceof this.model ? item : new this.model(item, args);
|
* Remove item from set by index
|
||||||
let index = this.items.findIndex(x => x.id == item.id);
|
*/
|
||||||
if(index == -1)
|
remove(index, {save=true}={}) {
|
||||||
return;
|
|
||||||
|
|
||||||
this.items.splice(index,1);
|
this.items.splice(index,1);
|
||||||
this._updated()
|
save && this.save();
|
||||||
}
|
|
||||||
|
|
||||||
_updated() {
|
|
||||||
Vue.set(this, 'items', this.items);
|
|
||||||
this.store();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,39 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="player">
|
<div class="player">
|
||||||
<List ref="queue" class="player-panel menu" v-show="panel == 'queue' && queue.length"
|
<Playlist ref="history" class="panel-menu menu" v-show="panel == 'history'"
|
||||||
:set="queue" @select="onSelect"
|
name="History"
|
||||||
|
:editable="true" :player="self" :set="sets.history" @select="play('pin', $event.index)"
|
||||||
listClass="menu-list" itemClass="menu-item">
|
listClass="menu-list" itemClass="menu-item">
|
||||||
<template v-slot:header>
|
<template v-slot:header="">
|
||||||
<p class="menu-label">
|
|
||||||
<span class="icon"><span class="fa fa-list"></span></span>
|
|
||||||
Playlist
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-slot:item="{item,index,set}">
|
|
||||||
<SoundItem activeClass="is-active" :data="item" :player="self" :set="set"
|
|
||||||
:actions="['remove']">
|
|
||||||
<template v-slot:actions="{active,set}">
|
|
||||||
<!-- TODO: stop player if active -->
|
|
||||||
|
|
||||||
</template>
|
|
||||||
</SoundItem>
|
|
||||||
</template>
|
|
||||||
</List>
|
|
||||||
<List ref="history" class="player-panel menu" v-show="panel == 'history' && history.length"
|
|
||||||
:set="history" @select="onSelect"
|
|
||||||
listClass="menu-list" itemClass="menu-item">
|
|
||||||
<template v-slot:header>
|
|
||||||
<p class="menu-label">
|
<p class="menu-label">
|
||||||
<span class="icon"><span class="fa fa-clock"></span></span>
|
<span class="icon"><span class="fa fa-clock"></span></span>
|
||||||
History
|
History
|
||||||
</p>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:item="{item,index,set}">
|
</Playlist>
|
||||||
<SoundItem activeClass="is-active" :data="item" :player="self" :set="set"
|
<Playlist ref="pin" class="player-panel menu" v-show="panel == 'pin'"
|
||||||
:actions="['queue','remove']">
|
name="Pinned"
|
||||||
</SoundItem>
|
:editable="true" :player="self" :set="sets.pin" @select="play('pin', $event.index)"
|
||||||
|
listClass="menu-list" itemClass="menu-item">
|
||||||
|
<template v-slot:header="">
|
||||||
|
<p class="menu-label">
|
||||||
|
<span class="icon"><span class="fa fa-thumbtack"></span></span>
|
||||||
|
Pinned
|
||||||
|
</p>
|
||||||
</template>
|
</template>
|
||||||
</List>
|
</Playlist>
|
||||||
|
<Playlist ref="queue" class="player-panel menu" v-show="panel == 'queue'"
|
||||||
|
:editable="true" :player="self" :set="sets.queue" @select="play('queue', $event.index)"
|
||||||
|
listClass="menu-list" itemClass="menu-item">
|
||||||
|
<template v-slot:header="">
|
||||||
|
<p class="menu-label">
|
||||||
|
<span class="icon"><span class="fa fa-list"></span></span>
|
||||||
|
Playlist
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</Playlist>
|
||||||
|
|
||||||
<div class="player-bar media">
|
<div class="player-bar media">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<div class="button" @click="togglePlay()"
|
<div class="button" @click="togglePlay()"
|
||||||
|
@ -53,26 +52,31 @@
|
||||||
<slot name="content" :current="current"></slot>
|
<slot name="content" :current="current"></slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-right">
|
<div class="media-right">
|
||||||
<button class="button" v-if="active" @click="load() && play()">
|
<button class="button has-text-weight-bold" v-if="loaded" @click="playLive()">
|
||||||
<span class="icon has-text-danger">
|
<span class="icon has-text-danger">
|
||||||
<span class="fa fa-broadcast-tower"></span>
|
<span class="fa fa-broadcast-tower"></span>
|
||||||
</span>
|
</span>
|
||||||
<span>Live</span>
|
<span>Live</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button :class="playlistButtonClass('history')"
|
||||||
<button class="button" v-if="history.length"
|
@click="togglePanel('history')">
|
||||||
@click="togglePanel('history')"
|
<span class="mr-2 is-size-6" v-if="sets.history.length">
|
||||||
:class="[panel == 'history' ? 'is-info' : '']" >
|
{{ sets.history.length }}</span>
|
||||||
<span class="icon">
|
<span class="icon"><span class="fa fa-clock"></span></span>
|
||||||
<span class="fa fa-clock"></span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button class="button" v-if="queue.length"
|
<button :class="playlistButtonClass('pin')"
|
||||||
@click="togglePanel('queue')"
|
@click="togglePanel('pin')">
|
||||||
:class="[panel == 'queue' ? 'is-info' : '']" >
|
<span class="mr-2 is-size-6" v-if="sets.pin.length">
|
||||||
<span class="mr-2 is-size-6">{{ queue.length }}</span>
|
{{ sets.pin.length }}</span>
|
||||||
|
<span class="icon"><span class="fa fa-thumbtack"></span></span>
|
||||||
|
</button>
|
||||||
|
<button :class="playlistButtonClass('queue')"
|
||||||
|
@click="togglePanel('queue')">
|
||||||
|
<span class="mr-2 is-size-6" v-if="sets.queue.length">
|
||||||
|
{{ sets.queue.length }}</span>
|
||||||
<span class="icon"><span class="fa fa-list"></span></span>
|
<span class="icon"><span class="fa fa-list"></span></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="progress">
|
<div v-if="progress">
|
||||||
|
@ -84,8 +88,7 @@
|
||||||
<script>
|
<script>
|
||||||
import Vue, { ref } from 'vue';
|
import Vue, { ref } from 'vue';
|
||||||
import Live from './live';
|
import Live from './live';
|
||||||
import List from './list';
|
import Playlist from './playlist';
|
||||||
import SoundItem from './soundItem';
|
|
||||||
import Sound from './sound';
|
import Sound from './sound';
|
||||||
import {Set} from './model';
|
import {Set} from './model';
|
||||||
|
|
||||||
|
@ -100,12 +103,21 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
state: State.paused,
|
state: State.paused,
|
||||||
active: null,
|
/// Loaded item
|
||||||
|
loaded: null,
|
||||||
|
/// Live instance
|
||||||
live: this.liveArgs ? new Live(this.liveArgs) : null,
|
live: this.liveArgs ? new Live(this.liveArgs) : null,
|
||||||
|
//! Active panel name
|
||||||
panel: null,
|
panel: null,
|
||||||
queue: Set.storeLoad(Sound, "player.queue", { max: 30, unique: true }),
|
//! current playing playlist component
|
||||||
|
playlist: null,
|
||||||
|
//! players' playlists' sets
|
||||||
|
sets: {
|
||||||
|
queue: Set.storeLoad(Sound, "playlist.queue", { max: 30, unique: true }),
|
||||||
|
pin: Set.storeLoad(Sound, "player.pin", { max: 30, unique: true }),
|
||||||
history: Set.storeLoad(Sound, "player.history", { max: 30, unique: true }),
|
history: Set.storeLoad(Sound, "player.history", { max: 30, unique: true }),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -120,7 +132,7 @@ export default {
|
||||||
self() { return this; },
|
self() { return this; },
|
||||||
|
|
||||||
current() {
|
current() {
|
||||||
return this.active || this.live && this.live.current;
|
return this.loaded || this.live && this.live.current;
|
||||||
},
|
},
|
||||||
|
|
||||||
progress() {
|
progress() {
|
||||||
|
@ -137,15 +149,35 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
playlistButtonClass(name) {
|
||||||
|
let set = this.sets[name];
|
||||||
|
return (set ? (set.length ? "" : "has-text-grey-light ")
|
||||||
|
+ (this.panel == name ? "is-info "
|
||||||
|
: this.playlist && this.playlist == this.$refs[name] ? 'is-primary '
|
||||||
|
: '') : '')
|
||||||
|
+ "button has-text-weight-bold";
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Show/hide panel
|
||||||
togglePanel(panel) {
|
togglePanel(panel) {
|
||||||
this.panel = this.panel == panel ? null : panel;
|
this.panel = this.panel == panel ? null : panel;
|
||||||
},
|
},
|
||||||
|
|
||||||
isActive(item) {
|
/// Return True if item is loaded
|
||||||
return item && this.active && this.active.src == item.src;
|
isLoaded(item) {
|
||||||
|
return this.loaded && this.loaded.src == item.src;
|
||||||
},
|
},
|
||||||
|
|
||||||
_load(src) {
|
/// Return True if item is loaded
|
||||||
|
isPlaying(item) {
|
||||||
|
return this.isLoaded(item) && !this.player.paused;
|
||||||
|
},
|
||||||
|
|
||||||
|
load(playlist, {src=null, item=null}={}) {
|
||||||
|
src = src || item.src;
|
||||||
|
this.loaded = item;
|
||||||
|
this.playlist = playlist ? this.$refs[playlist] : null;
|
||||||
|
|
||||||
const audio = this.$refs.audio;
|
const audio = this.$refs.audio;
|
||||||
if(src instanceof Array) {
|
if(src instanceof Array) {
|
||||||
audio.innerHTML = '';
|
audio.innerHTML = '';
|
||||||
|
@ -161,22 +193,43 @@ export default {
|
||||||
audio.load();
|
audio.load();
|
||||||
},
|
},
|
||||||
|
|
||||||
load(item) {
|
/// Play a playlist's sound (by playlist name, and sound index)
|
||||||
|
play(playlist=null, index=0) {
|
||||||
|
if(!playlist)
|
||||||
|
playlist = 'queue';
|
||||||
|
|
||||||
|
let item = this.$refs[playlist].get(index);
|
||||||
if(item) {
|
if(item) {
|
||||||
this._load(item.src)
|
this.load(playlist, {item: item});
|
||||||
this.history.push(item);
|
this.sets.history.push(item);
|
||||||
|
this.$refs.audio.play().catch(e => console.error(e))
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
this._load(this.live.src);
|
throw `No sound at index ${index} for playlist ${playlist}`;
|
||||||
this.$set(this, 'active', item);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
play(item) {
|
/// Push items to playlist (by name)
|
||||||
if(item)
|
push(playlist, ...items) {
|
||||||
this.load(item);
|
this.$refs[playlist].push(...items);
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Push and play items
|
||||||
|
playItems(playlist, ...items) {
|
||||||
|
this.push(playlist, ...items);
|
||||||
|
|
||||||
|
let index = this.$refs[playlist].findIndex(items[0]);
|
||||||
|
this.$refs[playlist].selectedIndex = index;
|
||||||
|
this.play(playlist, index);
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Play live stream
|
||||||
|
playLive() {
|
||||||
|
this.load(null, {src: this.live.src});
|
||||||
this.$refs.audio.play().catch(e => console.error(e))
|
this.$refs.audio.play().catch(e => console.error(e))
|
||||||
|
this.panel = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Pause
|
||||||
pause() {
|
pause() {
|
||||||
this.$refs.audio.pause()
|
this.$refs.audio.pause()
|
||||||
},
|
},
|
||||||
|
@ -184,33 +237,30 @@ export default {
|
||||||
//! Play/pause
|
//! Play/pause
|
||||||
togglePlay() {
|
togglePlay() {
|
||||||
if(this.paused)
|
if(this.paused)
|
||||||
this.play()
|
this.$refs.audio.play().catch(e => console.error(e))
|
||||||
else
|
else
|
||||||
this.pause()
|
this.pause()
|
||||||
},
|
},
|
||||||
|
|
||||||
//! Push item to queue
|
//! Pin/Unpin an item
|
||||||
push(item) {
|
togglePin(item) {
|
||||||
this.queue.push(item);
|
let index = this.sets.pin.findIndex(item);
|
||||||
this.panel = 'queue';
|
if(index > -1)
|
||||||
|
this.sets.pin.remove(index);
|
||||||
|
else {
|
||||||
|
this.sets.pin.push(item);
|
||||||
|
if(!this.panel)
|
||||||
|
this.panel = 'pin';
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Audio player state change event
|
||||||
onState(event) {
|
onState(event) {
|
||||||
const audio = this.$refs.audio;
|
const audio = this.$refs.audio;
|
||||||
this.state = audio.paused ? State.paused : State.playing;
|
this.state = audio.paused ? State.paused : State.playing;
|
||||||
|
|
||||||
if(event.type == 'ended' && this.active) {
|
if(event.type == 'ended' && (!this.playlist || this.playlist.selectNext() == -1))
|
||||||
this.queue.remove(this.active);
|
this.playLive();
|
||||||
if(this.queue.length)
|
|
||||||
this.$refs.queue.select(0);
|
|
||||||
else
|
|
||||||
this.load();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onSelect({item,index}) {
|
|
||||||
if(!this.isActive(item))
|
|
||||||
this.play(item);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -218,9 +268,7 @@ export default {
|
||||||
this.sources = this.$slots.sources;
|
this.sources = this.$slots.sources;
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {
|
components: { Playlist },
|
||||||
List, SoundItem,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
58
assets/public/playlist.vue
Normal file
58
assets/public/playlist.vue
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot name="header"></slot>
|
||||||
|
<ul :class="listClass">
|
||||||
|
<li v-for="(item,index) in items" :class="itemClass" @click="!hasAction('play') && select(index)">
|
||||||
|
<a :class="index == selectedIndex ? 'is-active' : ''">
|
||||||
|
<SoundItem
|
||||||
|
:data="item" :index="index" :player="player" :set="set"
|
||||||
|
@togglePlay="togglePlay(index)"
|
||||||
|
:actions="actions">
|
||||||
|
<template v-slot:actions="{loaded,set}">
|
||||||
|
<button class="button" v-if="editable" @click.stop="remove(index,true)">
|
||||||
|
<span class="icon is-small"><span class="fa fa-minus"></span></span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</SoundItem>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import List from './list';
|
||||||
|
import SoundItem from './soundItem';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
extends: List,
|
||||||
|
|
||||||
|
props: {
|
||||||
|
actions: Array,
|
||||||
|
name: String,
|
||||||
|
player: Object,
|
||||||
|
editable: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
self() { return this; }
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
hasAction(action) { return this.actions && this.actions.indexOf(action) != -1; },
|
||||||
|
|
||||||
|
selectNext() {
|
||||||
|
let index = this.selectedIndex + 1;
|
||||||
|
return this.select(index >= this.items.length ? -1 : index);
|
||||||
|
},
|
||||||
|
|
||||||
|
togglePlay(index) {
|
||||||
|
if(this.player.isPlaying(this.set.get(index)))
|
||||||
|
this.player.pause();
|
||||||
|
else
|
||||||
|
this.select(index)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: { List, SoundItem },
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,8 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<a :class="[active ? activeClass : '']" @click="actions.indexOf('play') == -1 && play()">
|
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left" v-if="hasAction('play')">
|
<div class="media-left" v-if="hasAction('play')">
|
||||||
<button class="button" @click="play()">
|
<button class="button" @click="$emit('togglePlay')">
|
||||||
<div class="icon">
|
<div class="icon">
|
||||||
<span class="fa fa-pause" v-if="playing || loading"></span>
|
<span class="fa fa-pause" v-if="playing || loading"></span>
|
||||||
<span class="fa fa-play" v-else></span>
|
<span class="fa fa-play" v-else></span>
|
||||||
|
@ -10,23 +9,21 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<slot name="content" :player="player" :item="item" :active="active">
|
<slot name="content" :player="player" :item="item" :loaded="loaded">
|
||||||
<h4 class="title is-4 is-inline-block">
|
<h4 class="title is-4 is-inline-block">
|
||||||
{{ name || item.name }}
|
{{ name || item.name }}
|
||||||
</h4>
|
</h4>
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-right">
|
<div class="media-right">
|
||||||
<button class="button" v-if="hasAction('queue')" @click.stop="player.push(item)">
|
<button class="button" v-if="player.$refs.pin != $parent" @click.stop="player.togglePin(item)">
|
||||||
<span class="icon is-small"><span class="fa fa-list"></span></span>
|
<span class="icon is-small">
|
||||||
|
<span :class="(pinned ? '' : 'has-text-grey-light ') + 'fa fa-thumbtack'"></span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="button" v-if="hasAction('remove')" @click.stop="set.remove(item)">
|
<slot name="actions" :player="player" :item="item" :loaded="loaded"></slot>
|
||||||
<span class="icon is-small"><span class="fa fa-minus"></span></span>
|
|
||||||
</button>
|
|
||||||
<slot name="actions" :player="player" :item="item" :active="active"></slot>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import Model from './model';
|
import Model from './model';
|
||||||
|
@ -37,36 +34,25 @@ export default {
|
||||||
data: {type: Object, default: x => {}},
|
data: {type: Object, default: x => {}},
|
||||||
name: String,
|
name: String,
|
||||||
cover: String,
|
cover: String,
|
||||||
set: Object,
|
|
||||||
player: Object,
|
player: Object,
|
||||||
page_url: String,
|
page_url: String,
|
||||||
activeClass: String,
|
|
||||||
actions: {type:Array, default: x => []},
|
actions: {type:Array, default: x => []},
|
||||||
|
index: {type:Number, default: null},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
item() { return this.data instanceof Model ? this.data : new Sound(this.data || {}); },
|
item() { return this.data instanceof Model ? this.data : new Sound(this.data || {}); },
|
||||||
active() { return this.player && this.player.isActive(this.item) },
|
loaded() { return this.player && this.player.isLoaded(this.item) },
|
||||||
playing() { return this.player && this.player.playing && this.active },
|
playing() { return this.player && this.player.playing && this.loaded },
|
||||||
paused() { return this.player && this.player.paused && this.active },
|
paused() { return this.player && this.player.paused && this.loaded },
|
||||||
loading() { return this.player && this.player.loading && this.active },
|
loading() { return this.player && this.player.loading && this.loaded },
|
||||||
|
pinned() { return this.player && this.player.sets.pin.find(this.item) },
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
hasAction(action) {
|
hasAction(action) {
|
||||||
return this.actions && this.actions.indexOf(action) != -1;
|
return this.actions && this.actions.indexOf(action) != -1;
|
||||||
},
|
},
|
||||||
|
|
||||||
play() {
|
|
||||||
if(this.player && this.active)
|
|
||||||
this.player.togglePlay()
|
|
||||||
else
|
|
||||||
this.player.play(this.item);
|
|
||||||
},
|
|
||||||
|
|
||||||
push_to(playlist) {
|
|
||||||
this.player.playlists[playlist].push(this.item, {unique_key:'id'});
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user