forked from rc/aircox
update assets dependencies; still work to be done to solve it all
This commit is contained in:
@ -1,12 +1,8 @@
|
||||
import Vue from 'vue';
|
||||
import App from 'public/app';
|
||||
import 'public';
|
||||
|
||||
import './admin.scss';
|
||||
|
||||
import Statistics from './statistics.vue';
|
||||
|
||||
Vue.component('a-statistics', Statistics)
|
||||
|
||||
import 'public';
|
||||
import AStatistics from './statistics.vue';
|
||||
|
||||
window.aircox_admin = {
|
||||
/**
|
||||
@ -23,7 +19,11 @@ window.aircox_admin = {
|
||||
for(var item of container.querySelectorAll('a.navbar-item'))
|
||||
item.style.display = null;
|
||||
},
|
||||
}
|
||||
|
||||
window.aircox.builder.config = {
|
||||
...App,
|
||||
components: {...App.components, AStatistics},
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,95 +1,25 @@
|
||||
import Vue from 'vue';
|
||||
import AAutocomplete from './autocomplete'
|
||||
import AEpisode from './episode'
|
||||
import APlayer from './player'
|
||||
import APlaylist from './playlist'
|
||||
import ASoundItem from './soundItem'
|
||||
|
||||
export const defaultConfig = {
|
||||
const App = {
|
||||
el: '#app',
|
||||
delimiters: ['[[', ']]'],
|
||||
|
||||
computed: {
|
||||
player() { return window.aircox.player; },
|
||||
},
|
||||
|
||||
components: {AAutocomplete, AEpisode, APlayer, APlaylist, ASoundItem},
|
||||
}
|
||||
|
||||
|
||||
export default class AppBuilder {
|
||||
constructor(config={}) {
|
||||
this._config = config;
|
||||
this.title = null;
|
||||
this.app = null;
|
||||
}
|
||||
|
||||
get config() {
|
||||
let config = this._config instanceof Function ? this._config() : this._config;
|
||||
for(var k of new Set([...Object.keys(config || {}), ...Object.keys(defaultConfig)])) {
|
||||
if(!config[k] && defaultConfig[k])
|
||||
config[k] = defaultConfig[k]
|
||||
else if(config[k] instanceof Object)
|
||||
config[k] = {...defaultConfig[k], ...config[k]}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
set config(value) {
|
||||
this._config = value;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
self.app && self.app.$destroy();
|
||||
self.app = null;
|
||||
}
|
||||
|
||||
fetch(url, {el='app', ...options}={}) {
|
||||
return fetch(url, options).then(response => response.text())
|
||||
.then(content => {
|
||||
let doc = new DOMParser().parseFromString(content, 'text/html');
|
||||
let app = doc.getElementById('app');
|
||||
content = app ? app.innerHTML : content;
|
||||
return this.load({sync: true, content, title: doc.title, url })
|
||||
})
|
||||
}
|
||||
|
||||
load({async=false,content=null,title=null,el='app'}={}) {
|
||||
var self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
let func = () => {
|
||||
try {
|
||||
let config = self.config;
|
||||
const el = document.querySelector(config.el);
|
||||
if(!el)
|
||||
return reject(`Error: can't get element ${config.el}`)
|
||||
|
||||
if(content)
|
||||
el.innerHTML = content
|
||||
if(title)
|
||||
document.title = title;
|
||||
this.app = new Vue(config);
|
||||
window.scroll(0, 0);
|
||||
resolve(self.app)
|
||||
} catch(error) {
|
||||
self.destroy();
|
||||
reject(error)
|
||||
}};
|
||||
async ? window.addEventListener('load', func) : func();
|
||||
});
|
||||
}
|
||||
|
||||
/// Save application state into browser history
|
||||
historySave(url,replace=false) {
|
||||
const el = document.querySelector(this.config.el);
|
||||
const state = {
|
||||
content: el.innerHTML,
|
||||
title: document.title,
|
||||
};
|
||||
|
||||
if(replace)
|
||||
history.replaceState(state, '', url)
|
||||
else
|
||||
history.pushState(state, '', url)
|
||||
}
|
||||
|
||||
/// Load application from browser history's state
|
||||
historyLoad(state) {
|
||||
return this.load({ content: state.content, title: state.title });
|
||||
}
|
||||
export const PlayerApp = {
|
||||
el: '#player',
|
||||
components: {APlayer},
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
|
||||
|
137
assets/public/appBuilder.js
Normal file
137
assets/public/appBuilder.js
Normal file
@ -0,0 +1,137 @@
|
||||
import {createApp} from 'vue'
|
||||
|
||||
/**
|
||||
* Utility class used to handle Vue applications. It provides way to load
|
||||
* remote application and update history.
|
||||
*/
|
||||
export default class Builder {
|
||||
constructor(config={}) {
|
||||
this.config = config
|
||||
this.title = null
|
||||
this.app = null
|
||||
this.vm = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch app from remote and mount application.
|
||||
*/
|
||||
fetch(url, {el='app', ...options}={}) {
|
||||
return fetch(url, options).then(response => response.text())
|
||||
.then(content => {
|
||||
let doc = new DOMParser().parseFromString(content, 'text/html')
|
||||
let app = doc.getElementById('app')
|
||||
content = app ? app.innerHTML : content
|
||||
return this.mount({content, title: doc.title, reset:true, url })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mount application, using `create_app` if required.
|
||||
*
|
||||
* @param {String} options.content: replace app container content with it
|
||||
* @param {String} options.title: set DOM document title.
|
||||
* @param {String} [options.el=this.config.el]: mount application on this element (querySelector argument)
|
||||
* @param {Boolean} [reset=False]: if True, force application recreation.
|
||||
* @return `app.mount`'s result.
|
||||
*/
|
||||
mount({content=null, title=null, el=null, reset=false}={}) {
|
||||
try {
|
||||
this.unmount()
|
||||
|
||||
let config = this.config
|
||||
if(el === null)
|
||||
el = config.el
|
||||
|
||||
if(reset || !this.app)
|
||||
this.app = this.createApp({title,content,el,...config})
|
||||
this.vm = this.app.mount(el)
|
||||
window.scroll(0, 0)
|
||||
return this.vm
|
||||
} catch(error) {
|
||||
this.unmount()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
createApp({el, title=null, content=null, ...config}) {
|
||||
const container = document.querySelector(el)
|
||||
if(!container)
|
||||
throw `Error: can't get element ${el}`
|
||||
if(content)
|
||||
container.innerHTML = content
|
||||
if(title)
|
||||
document.title = title
|
||||
return createApp(config)
|
||||
}
|
||||
|
||||
unmount() {
|
||||
this.app && this.app.unmount()
|
||||
this.app = null
|
||||
this.vm = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable hot reload: catch page change in order to fetch them and
|
||||
* load page without actually leaving current one.
|
||||
*/
|
||||
enableHotReload(node=null, historySave=true) {
|
||||
if(historySave)
|
||||
this.historySave(document.location, true)
|
||||
node.addEventListener('click', event => this._onPageChange(event), true)
|
||||
node.addEventListener('submit', event => this._onPageChange(event), true)
|
||||
node.addEventListener('popstate', event => this._onPopState(event), true)
|
||||
}
|
||||
|
||||
_onPageChange(event) {
|
||||
let submit = event.type == 'submit';
|
||||
let target = submit || event.target.tagName == 'A'
|
||||
? event.target : event.target.closest('a');
|
||||
if(!target || target.hasAttribute('target'))
|
||||
return;
|
||||
|
||||
let url = submit ? target.getAttribute('action') || ''
|
||||
: target.getAttribute('href');
|
||||
if(url===null || !(url === '' || url.startsWith('/') || url.startsWith('?')))
|
||||
return;
|
||||
|
||||
let options = {};
|
||||
if(submit) {
|
||||
let formData = new FormData(event.target);
|
||||
if(target.method == 'get')
|
||||
url += '?' + (new URLSearchParams(formData)).toString();
|
||||
else
|
||||
options = {...options, method: target.method, body: formData}
|
||||
}
|
||||
this.fetch(url, options).then(_ => this.historySave(url))
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
_onPopState(event) {
|
||||
if(event.state && event.state.content)
|
||||
// document.title = this.title;
|
||||
this.historyLoad(event.state);
|
||||
}
|
||||
|
||||
/// Save application state into browser history
|
||||
historySave(url,replace=false) {
|
||||
const el = document.querySelector(this.config.el)
|
||||
const state = {
|
||||
content: el.innerHTML,
|
||||
title: document.title,
|
||||
}
|
||||
|
||||
if(replace)
|
||||
history.replaceState(state, '', url)
|
||||
else
|
||||
history.pushState(state, '', url)
|
||||
}
|
||||
|
||||
/// Load application from browser history's state
|
||||
historyLoad(state) {
|
||||
return this.mount({ content: state.content, title: state.title })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -12,8 +12,7 @@
|
||||
|
||||
<script>
|
||||
import debounce from 'lodash/debounce'
|
||||
import {Autocomplete} from 'buefy/dist/components/autocomplete';
|
||||
import Vue from 'vue';
|
||||
import {Autocomplete} from 'buefy/dist/components/autocomplete'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -31,25 +30,25 @@ export default {
|
||||
data: [],
|
||||
selected: null,
|
||||
isFetching: false,
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
onSelect(option) {
|
||||
console.log('selected', option)
|
||||
Vue.set(this, 'selected', option);
|
||||
this.$emit('select', option);
|
||||
this.selected = option
|
||||
this.$emit('select', option)
|
||||
},
|
||||
|
||||
fetch: debounce(function(query) {
|
||||
if(!query)
|
||||
return;
|
||||
return
|
||||
|
||||
this.isFetching = true;
|
||||
this.isFetching = true
|
||||
this.model.fetchAll(this.url.replace('${query}', query))
|
||||
.then(data => {
|
||||
this.data = data;
|
||||
this.isFetching = false;
|
||||
this.data = data
|
||||
this.isFetching = false
|
||||
}, data => { this.isFetching = false; Promise.reject(data) })
|
||||
}),
|
||||
},
|
||||
|
@ -3,93 +3,37 @@
|
||||
* administration interface)
|
||||
*/
|
||||
//-- vendor
|
||||
import Vue from 'vue';
|
||||
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
import '@fortawesome/fontawesome-free/css/fontawesome.min.css'
|
||||
|
||||
|
||||
//-- aircox
|
||||
import AppBuilder from './app';
|
||||
import Sound from './sound';
|
||||
import {Set} from './model';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
import Autocomplete from './autocomplete';
|
||||
import Episode from './episode';
|
||||
import Player from './player';
|
||||
import Playlist from './playlist';
|
||||
import SoundItem from './soundItem';
|
||||
|
||||
Vue.component('a-autocomplete', Autocomplete)
|
||||
Vue.component('a-episode', Episode)
|
||||
Vue.component('a-player', Player)
|
||||
Vue.component('a-playlist', Playlist)
|
||||
Vue.component('a-sound-item', SoundItem)
|
||||
import App, {PlayerApp} from './app'
|
||||
import Builder from './appBuilder'
|
||||
import Sound from './sound'
|
||||
import {Set} from './model'
|
||||
|
||||
import './styles.scss'
|
||||
|
||||
window.aircox = {
|
||||
// main application
|
||||
appBuilder: null,
|
||||
appConfig: {},
|
||||
get app() { return this.appBuilder.app },
|
||||
builder: new Builder(App),
|
||||
get app() { return this.builder.app },
|
||||
|
||||
// player application
|
||||
playerBuilder: null,
|
||||
playerBuilder: new Builder(PlayerApp),
|
||||
get playerApp() { return this.playerBuilder && this.playerBuilder.app },
|
||||
get player() { return this.playerApp && this.playerApp.$refs.player },
|
||||
|
||||
// Handle hot-reload (link click and form submits).
|
||||
onPageFetch(event) {
|
||||
let submit = event.type == 'submit';
|
||||
let target = submit || event.target.tagName == 'A'
|
||||
? event.target : event.target.closest('a');
|
||||
if(!target || target.hasAttribute('target'))
|
||||
return;
|
||||
|
||||
let url = submit ? target.getAttribute('action') || ''
|
||||
: target.getAttribute('href');
|
||||
if(url===null || !(url === '' || url.startsWith('/') || url.startsWith('?')))
|
||||
return;
|
||||
|
||||
let options = {};
|
||||
if(submit) {
|
||||
let formData = new FormData(event.target);
|
||||
if(target.method == 'get')
|
||||
url += '?' + (new URLSearchParams(formData)).toString();
|
||||
else {
|
||||
options['method'] = target.method;
|
||||
options['body'] = formData;
|
||||
}
|
||||
}
|
||||
this.appBuilder.fetch(url, options).then(app => {
|
||||
this.appBuilder.historySave(url);
|
||||
});
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
get player() { return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player },
|
||||
|
||||
Set: Set, Sound: Sound,
|
||||
};
|
||||
window.Vue = Vue;
|
||||
}
|
||||
|
||||
window.addEventListener('load', e => {
|
||||
const [app, player] = [aircox.builder, aircox.playerBuilder]
|
||||
app.title = document.title
|
||||
app.mount()
|
||||
app.enableHotReload(window)
|
||||
|
||||
aircox.playerBuilder = new AppBuilder({el: '#player'});
|
||||
aircox.playerBuilder.load({async:true});
|
||||
aircox.appBuilder = new AppBuilder(x => window.aircox.appConfig);
|
||||
aircox.appBuilder.load({async:true}).then(app => {
|
||||
aircox.appBuilder.historySave(document.location, true);
|
||||
|
||||
//-- load page hooks
|
||||
window.addEventListener('click', event => aircox.onPageFetch(event), true);
|
||||
window.addEventListener('submit', event => aircox.onPageFetch(event), true);
|
||||
window.addEventListener('popstate', event => {
|
||||
if(event.state && event.state.content) {
|
||||
document.title = aircox.appBuilder.title;
|
||||
aircox.appBuilder.historyLoad(event.state);
|
||||
}
|
||||
});
|
||||
player.mount()
|
||||
})
|
||||
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
function getCookie(name) {
|
||||
if(document.cookie && document.cookie !== '') {
|
||||
@ -82,7 +81,7 @@ export default class Model {
|
||||
*/
|
||||
commit(data) {
|
||||
this.id = this.constructor.getId(data);
|
||||
Vue.set(this, 'data', data);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,12 +122,12 @@ export class Set {
|
||||
/**
|
||||
* Fetch multiple items from server
|
||||
*/
|
||||
static fetch(url, options=null, args=null) {
|
||||
static fetch(model, url, options=null, args=null) {
|
||||
options = this.getOptions(options)
|
||||
return fetch(url, options)
|
||||
.then(response => response.json())
|
||||
.then(data => (data instanceof Array ? data : data.results)
|
||||
.map(d => new this.model(d, {url: url, ...args})))
|
||||
.map(d => new model(d, {url: url, ...args})))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,3 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import Model, {Set} from 'public/model';
|
||||
import {setEcoInterval} from 'public/utils';
|
||||
|
||||
@ -61,7 +59,7 @@ export class Source extends Model {
|
||||
if(!this.data.remaining || !this.isPlaying)
|
||||
return;
|
||||
const delta = (Date.now() - this.commitDate) / 1000;
|
||||
Vue.set(this, 'remaining', this.data.remaining - delta)
|
||||
this.remaining = this.data.remaining - delta
|
||||
}
|
||||
|
||||
commit(data) {
|
||||
@ -70,7 +68,7 @@ export class Source extends Model {
|
||||
|
||||
this.commitDate = Date.now()
|
||||
super.commit(data)
|
||||
Vue.set(this, 'remaining', data.remaining)
|
||||
this.remaining = data.remaining
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
import {setAppConfig} from 'public/app';
|
||||
import Model from 'public/model';
|
||||
import App from 'public/app';
|
||||
import Model, {Set} from 'public/model';
|
||||
import Sound from 'public/sound';
|
||||
import {setEcoInterval} from 'public/utils';
|
||||
|
||||
import {Streamer, Queue} from './controllers';
|
||||
|
||||
window.aircox.appConfig = {
|
||||
window.aircox.builder.config = {
|
||||
...App,
|
||||
|
||||
data() {
|
||||
return {
|
||||
// current streamer
|
||||
@ -16,12 +16,13 @@ window.aircox.appConfig = {
|
||||
streamers: [],
|
||||
// fetch interval id
|
||||
fetchInterval: null,
|
||||
|
||||
Sound: Sound,
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
...(App.computed || {}),
|
||||
|
||||
apiUrl() {
|
||||
return this.$el && this.$el.dataset.apiUrl;
|
||||
},
|
||||
@ -33,12 +34,13 @@ window.aircox.appConfig = {
|
||||
},
|
||||
|
||||
methods: {
|
||||
...(App.methods || {}),
|
||||
|
||||
fetchStreamers() {
|
||||
Streamer.Set.fetch(this.apiUrl)
|
||||
.then(streamers => {
|
||||
Vue.set(this, 'streamers', streamers);
|
||||
Vue.set(this, 'streamer', streamers ? streamers[0] : null);
|
||||
})
|
||||
Set.fetch(Streamer, this.apiUrl).then(streamers => {
|
||||
this.streamers = streamers
|
||||
this.streamer = streamers ? streamers[0] : null
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
|
Reference in New Issue
Block a user