forked from rc/aircox
streamer as separate application; working streamer monitor interface
This commit is contained in:
@ -1,17 +1,53 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
|
||||
export var app = null;
|
||||
export default app;
|
||||
export const appBaseConfig = {
|
||||
el: '#app',
|
||||
delimiters: ['[[', ']]'],
|
||||
}
|
||||
|
||||
function loadApp() {
|
||||
app = new Vue({
|
||||
el: '#app',
|
||||
delimiters: [ '[[', ']]' ],
|
||||
/**
|
||||
* Application config for the main application instance
|
||||
*/
|
||||
var appConfig = {};
|
||||
|
||||
export function setAppConfig(config) {
|
||||
for(var member in appConfig) delete appConfig[member];
|
||||
return Object.assign(appConfig, config)
|
||||
}
|
||||
|
||||
export function getAppConfig(config) {
|
||||
if(config instanceof Function)
|
||||
config = config()
|
||||
config = config == null ? appConfig : config;
|
||||
return {...appBaseConfig, ...config}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create Vue application at window 'load' event and return a Promise
|
||||
* resolving to the created app.
|
||||
*
|
||||
* config: defaults to appConfig (checked when window is loaded)
|
||||
*/
|
||||
export function loadApp(config=null) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
window.addEventListener('load', function() {
|
||||
try {
|
||||
config = getAppConfig(config)
|
||||
const el = document.querySelector(config.el)
|
||||
if(!el) {
|
||||
reject(`Error: missing element ${config.el}`);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(new Vue(config))
|
||||
}
|
||||
catch(error) { reject(error) }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('load', loadApp);
|
||||
|
||||
|
||||
|
||||
|
63
assets/public/autocomplete.vue
Normal file
63
assets/public/autocomplete.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="control">
|
||||
<Autocomplete ref="autocomplete" :data="data" :placeholder="placeholder" :field="field"
|
||||
:loading="isFetching" open-on-focus
|
||||
@typing="fetch" @select="object => onSelect(object)"
|
||||
>
|
||||
</Autocomplete>
|
||||
<input v-if="valueField" ref="value" type="hidden" :name="valueField"
|
||||
:value="selected && selected[valueAttr || valueField]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce'
|
||||
import {Autocomplete} from 'buefy/dist/components/autocomplete';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
url: String,
|
||||
model: Function,
|
||||
placeholder: String,
|
||||
field: {type: String, default: 'value'},
|
||||
count: {type: Number, count: 10},
|
||||
valueAttr: String,
|
||||
valueField: String,
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
data: [],
|
||||
selected: null,
|
||||
isFetching: false,
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSelect(option) {
|
||||
console.log('selected', option)
|
||||
Vue.set(this, 'selected', option);
|
||||
this.$emit('select', option);
|
||||
},
|
||||
|
||||
fetch: debounce(function(query) {
|
||||
if(!query)
|
||||
return;
|
||||
|
||||
this.isFetching = true;
|
||||
this.model.fetchAll(this.url.replace('${query}', query))
|
||||
.then(data => {
|
||||
this.data = data;
|
||||
this.isFetching = false;
|
||||
}, data => { this.isFetching = false; Promise.reject(data) })
|
||||
}),
|
||||
},
|
||||
|
||||
components: {
|
||||
Autocomplete,
|
||||
},
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -10,18 +10,37 @@ import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
|
||||
|
||||
|
||||
//-- aircox
|
||||
import app from './app';
|
||||
import LiveInfo from './liveInfo';
|
||||
import {appConfig, loadApp} from './app';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
import Player from './player.vue';
|
||||
import Autocomplete from './autocomplete.vue';
|
||||
|
||||
Vue.component('a-player', Player)
|
||||
Vue.component('a-autocomplete', Autocomplete)
|
||||
|
||||
|
||||
window.aircox = {
|
||||
app: app,
|
||||
LiveInfo: LiveInfo,
|
||||
}
|
||||
// main application
|
||||
app: null,
|
||||
|
||||
// main application config
|
||||
appConfig: {},
|
||||
|
||||
// player application
|
||||
playerApp: null,
|
||||
|
||||
// player component
|
||||
get player() {
|
||||
return this.playerApp && this.playerApp.$refs.player
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
loadApp({el: '#player'}).then(app => { window.aircox.playerApp = app },
|
||||
() => undefined)
|
||||
loadApp(() => window.aircox.appConfig ).then(app => { window.aircox.app = app },
|
||||
() => undefined)
|
||||
|
||||
|
||||
|
118
assets/public/model.js
Normal file
118
assets/public/model.js
Normal file
@ -0,0 +1,118 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
function getCookie(name) {
|
||||
if(document.cookie && document.cookie !== '') {
|
||||
const cookie = document.cookie.split(';')
|
||||
.find(c => c.trim().startsWith(name + '='))
|
||||
return cookie ? decodeURIComponent(cookie.split('=')[1]) : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var csrfToken = null;
|
||||
|
||||
export function getCsrf() {
|
||||
if(csrfToken === null)
|
||||
csrfToken = getCookie('csrftoken')
|
||||
return csrfToken;
|
||||
}
|
||||
|
||||
|
||||
// TODO: move in another module for reuse
|
||||
export default class Model {
|
||||
constructor(data, {url=null}={}) {
|
||||
this.commit(data);
|
||||
}
|
||||
|
||||
static getId(data) {
|
||||
return data.id;
|
||||
}
|
||||
|
||||
static getOptions(options) {
|
||||
return {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-CSRFToken': getCsrf(),
|
||||
},
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
static fetch(url, options=null, initArgs=null) {
|
||||
options = this.getOptions(options)
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(data => new this(d, {url: url, ...initArgs}));
|
||||
}
|
||||
|
||||
static fetchAll(url, options=null, initArgs=null) {
|
||||
options = this.getOptions(options)
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if(!(data instanceof Array))
|
||||
data = data.results;
|
||||
data = data.map(d => new this(d, {baseUrl: url, ...initArgs}));
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data from server.
|
||||
*/
|
||||
fetch(options) {
|
||||
options = this.constructor.getOptions(options)
|
||||
return fetch(this.url, options)
|
||||
.then(response => response.json())
|
||||
.then(data => this.commit(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Call API action on object.
|
||||
*/
|
||||
action(path, options, commit=false) {
|
||||
options = this.constructor.getOptions(options)
|
||||
const promise = fetch(this.url + path, options);
|
||||
return commit ? promise.then(data => data.json())
|
||||
.then(data => { this.commit(data); this.data })
|
||||
: promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update instance's data with provided data. Return None
|
||||
*/
|
||||
commit(data) {
|
||||
this.id = this.constructor.getId(data);
|
||||
this.url = data.url_;
|
||||
Vue.set(this, 'data', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return data as model with url prepent by `this.url`.
|
||||
*/
|
||||
asChild(model, data, prefix='') {
|
||||
return new model(data, {baseUrl: `${this.url}${prefix}/`})
|
||||
}
|
||||
|
||||
getChildOf(attr, id) {
|
||||
const index = this.data[attr].findIndex(o => o.id = id)
|
||||
return index == -1 ? null : this.data[attr][index];
|
||||
}
|
||||
|
||||
static updateList(list=[], old=[]) {
|
||||
return list.reduce((items, data) => {
|
||||
const id = this.getId(data);
|
||||
let [index, obj] = [old.findIndex(o => o.id == id), null];
|
||||
if(index != -1) {
|
||||
old[index].commit(data)
|
||||
items.push(old[index]);
|
||||
}
|
||||
else
|
||||
items.push(new this(data))
|
||||
return items;
|
||||
}, [])
|
||||
}
|
||||
}
|
||||
|
||||
|
10
assets/public/sound.js
Normal file
10
assets/public/sound.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Model from './model';
|
||||
|
||||
|
||||
export default class Sound extends Model {
|
||||
get name() { return this.data.name }
|
||||
|
||||
static getId(data) { return data.pk }
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
@charset "utf-8";
|
||||
@import "~bulma/sass/utilities/_all.sass";
|
||||
@import "~bulma/sass/components/dropdown.sass";
|
||||
|
||||
$body-background-color: $light;
|
||||
|
||||
@import "~buefy/src/scss/components/_autocomplete.scss";
|
||||
@import "~bulma";
|
||||
|
||||
//-- helpers/modifiers
|
||||
@ -15,6 +17,10 @@ $body-background-color: $light;
|
||||
}
|
||||
.is-borderless { border: none; }
|
||||
|
||||
.has-text-nowrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.has-background-transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
@ -56,6 +62,22 @@ a.navbar-item.is-active {
|
||||
margin: 0em;
|
||||
padding: 0em;
|
||||
}
|
||||
|
||||
&.toolbar {
|
||||
margin: 1em 0em;
|
||||
background-color: transparent;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.title {
|
||||
padding-right: 2em;
|
||||
margin-right: 1em;
|
||||
border-right: 1px $grey-light solid;
|
||||
|
||||
font-size: $size-5;
|
||||
color: $text-light;
|
||||
font-weight: $weight-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//-- cards
|
||||
@ -91,24 +113,6 @@ a.navbar-item.is-active {
|
||||
}
|
||||
|
||||
|
||||
//-- filters
|
||||
.filters {
|
||||
margin: 1em 0em;
|
||||
background-color: transparent;
|
||||
margin-bottom: 1em;
|
||||
|
||||
.title {
|
||||
padding-right: 2em;
|
||||
margin-right: 1em;
|
||||
border-right: 1px $grey-light solid;
|
||||
|
||||
font-size: $size-5;
|
||||
color: $text-light;
|
||||
font-weight: $weight-light;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//-- page
|
||||
.page {
|
||||
& > .cover {
|
||||
|
98
assets/streamer/controllers.js
Normal file
98
assets/streamer/controllers.js
Normal file
@ -0,0 +1,98 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import Model from 'public/model';
|
||||
|
||||
|
||||
export class Streamer extends Model {
|
||||
get queues() { return this.data ? this.data.queues : []; }
|
||||
get playlists() { return this.data ? this.data.playlists : []; }
|
||||
get sources() { return [...this.queues, ...this.playlists]; }
|
||||
get source() { return this.sources.find(o => o.id == this.data.source) }
|
||||
|
||||
commit(data) {
|
||||
if(!this.data)
|
||||
this.data = { id: data.id, playlists: [], queues: [] }
|
||||
|
||||
data.playlists = Playlist.updateList(data.playlists, this.playlists)
|
||||
data.queues = Queue.updateList(data.queues, this.queues)
|
||||
super.commit(data)
|
||||
}
|
||||
}
|
||||
|
||||
export class Request extends Model {
|
||||
static getId(data) { return data.rid; }
|
||||
}
|
||||
|
||||
export class Source extends Model {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
setInterval(() => this.tick(), 1000)
|
||||
}
|
||||
|
||||
get isQueue() { return false; }
|
||||
get isPlaylist() { return false; }
|
||||
get isPlaying() { return this.data.status == 'playing' }
|
||||
get isPaused() { return this.data.status == 'paused' }
|
||||
|
||||
get remainingString() {
|
||||
if(!this.remaining)
|
||||
return '00:00';
|
||||
|
||||
const seconds = Math.floor(this.remaining % 60);
|
||||
const minutes = Math.floor(this.remaining / 60);
|
||||
return String(minutes).padStart(2, '0') + ':' +
|
||||
String(seconds).padStart(2, '0');
|
||||
}
|
||||
|
||||
|
||||
sync() { return this.action('sync/', {method: 'POST'}, true); }
|
||||
skip() { return this.action('skip/', {method: 'POST'}, true); }
|
||||
restart() { return this.action('restart/', {method: 'POST'}, true); }
|
||||
|
||||
seek(count) {
|
||||
return this.action('seek/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({count: count})
|
||||
}, true)
|
||||
}
|
||||
|
||||
tick() {
|
||||
if(!this.data.remaining || !this.isPlaying)
|
||||
return;
|
||||
const delta = (Date.now() - this.commitDate) / 1000;
|
||||
Vue.set(this, 'remaining', this.data.remaining - delta)
|
||||
}
|
||||
|
||||
commit(data) {
|
||||
if(data.air_time)
|
||||
data.air_time = new Date(data.air_time);
|
||||
Vue.set(this, 'remaining', data.remaining)
|
||||
this.commitDate = Date.now()
|
||||
super.commit(data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Playlist extends Source {
|
||||
get isPlaylist() { return true; }
|
||||
}
|
||||
|
||||
|
||||
export class Queue extends Source {
|
||||
get isQueue() { return true; }
|
||||
get queue() { return this.data && this.data.queue; }
|
||||
|
||||
commit(data) {
|
||||
data.queue = Request.updateList(data.queue, this.queue)
|
||||
super.commit(data)
|
||||
}
|
||||
|
||||
push(soundId) {
|
||||
return this.action('push/', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({'sound_id': parseInt(soundId)})
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
56
assets/streamer/index.js
Normal file
56
assets/streamer/index.js
Normal file
@ -0,0 +1,56 @@
|
||||
import Vue from 'vue';
|
||||
import Button from 'buefy/dist/components/button';
|
||||
|
||||
Vue.use(Button)
|
||||
|
||||
import {setAppConfig} from 'public/app';
|
||||
import Model from 'public/model';
|
||||
import Sound from 'public/sound';
|
||||
import {Streamer, Queue} from './controllers';
|
||||
|
||||
window.aircox.appConfig = {
|
||||
data() {
|
||||
return {
|
||||
// current streamer
|
||||
streamer: null,
|
||||
// all streamers
|
||||
streamers: [],
|
||||
// fetch interval id
|
||||
fetchInterval: null,
|
||||
|
||||
Sound: Sound,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
apiUrl() {
|
||||
return this.$el && this.$el.dataset.apiUrl;
|
||||
},
|
||||
|
||||
sources() {
|
||||
var sources = this.streamer ? this.streamer.sources : [];
|
||||
return sources.filter(s => s.data)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchStreamers() {
|
||||
Streamer.fetchAll(this.apiUrl, null)
|
||||
.then(streamers => {
|
||||
Vue.set(this, 'streamers', streamers);
|
||||
Vue.set(this, 'streamer', streamers ? streamers[0] : null);
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.fetchStreamers();
|
||||
this.fetchInterval = setInterval(() => this.streamer && this.streamer.fetch(), 5000)
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
if(this.fetchInterval !== null)
|
||||
clearInterval(this.fetchInterval)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user