streamer as separate application; working streamer monitor interface

This commit is contained in:
bkfox
2019-09-21 17:14:40 +02:00
parent 4e61ec1520
commit d3f39c5ade
39 changed files with 1347 additions and 148 deletions

View File

@ -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);

View 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>

View File

@ -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
View 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
View 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 }
}

View File

@ -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 {