update assets dependencies; still work to be done to solve it all
This commit is contained in:
parent
4e03abcac8
commit
ab8858154b
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
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
|
@ -9,6 +9,7 @@
|
|||
|
||||
<script src="{% static "aircox/vendor.js" %}"></script>
|
||||
<script src="{% static "aircox/admin.js" %}"></script>
|
||||
<script src="{% static "aircox/public.js" %}"></script>
|
||||
|
||||
{% block extrastyle %}{% endblock %}
|
||||
|
||||
|
@ -172,5 +173,7 @@
|
|||
</div>
|
||||
<!-- END Container -->
|
||||
|
||||
{% block outside_bottom %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
<script src="{% static "aircox/streamer.js" %}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}{{ block.super }}
|
||||
<div id="app" data-api-url="{% url "admin:api:streamer-list" %}">
|
||||
{% block content %}
|
||||
{{ block.super }}
|
||||
<div id="app" v-if="streamers" data-api-url="{% url "admin:api:streamer-list" %}">
|
||||
<div class="navbar toolbar">
|
||||
<div class="navbar-start">
|
||||
<span class="navbar-item control">
|
||||
|
@ -38,3 +39,7 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block outside_bottom %}
|
||||
<div id="player">{% include "aircox/widgets/player.html" %}</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -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,11 +34,12 @@ 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
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
34
package.json
34
package.json
|
@ -6,26 +6,24 @@
|
|||
"author": "bkfox",
|
||||
"license": "AGPL",
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.13.0",
|
||||
"buefy": "^0.9.3",
|
||||
"bulma": "^0.7.5",
|
||||
"css-loader": "^2.1.1",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"file-loader": "^3.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"mini-css-extract-plugin": "^0.5.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^7.3.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"ttf-loader": "^1.0.2",
|
||||
"vue-loader": "^15.9.3",
|
||||
"vue-style-loader": "^4.1.2",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
"@fortawesome/fontawesome-free": "^6.0.0",
|
||||
"buefy": "^0.9.19",
|
||||
"bulma": "^0.9.3",
|
||||
"css-loader": "^6.7.1",
|
||||
"file-loader": "^6.2.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^2.6.0",
|
||||
"node-sass": "^7.0.1",
|
||||
"sass-loader": "^12.6.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"vue-loader": "^17.0.0",
|
||||
"vue-style-loader": "^4.1.3",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^5.70.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^2.6.12"
|
||||
"vue": "^3.2.31"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.config.js"
|
||||
|
|
|
@ -86,7 +86,7 @@ module.exports = (env, argv) => Object({
|
|||
|
||||
resolve: {
|
||||
alias: {
|
||||
vue: 'vue/dist/vue.esm.browser.js',
|
||||
vue: 'vue/dist/vue.esm-browser.js',
|
||||
},
|
||||
modules: [
|
||||
'./assets',
|
||||
|
|
Loading…
Reference in New Issue
Block a user