update assets dependencies; still work to be done to solve it all

This commit is contained in:
bkfox 2022-03-10 15:47:56 +01:00
parent 4e03abcac8
commit ab8858154b
24 changed files with 6863 additions and 4776 deletions

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

View File

@ -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 %}
@ -160,8 +161,8 @@
{% block pretitle %}{% endblock %}
{% block content_title %}{% if title %}<h1 class="title is-3">{{ title }}</h1>{% endif %}{% endblock %}
{% block content %}
{% block object-tools %}{% endblock %}
{{ content }}
{% block object-tools %}{% endblock %}
{{ content }}
{% endblock %}
{% block sidebar %}{% endblock %}
<br class="clear">
@ -172,5 +173,7 @@
</div>
<!-- END Container -->
{% block outside_bottom %}{% endblock %}
</body>
</html>

View File

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

View File

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

View File

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

View File

@ -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) })
}),
},

View File

@ -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()
})

View File

@ -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})))
}
/**

View File

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

View File

@ -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
})
},
},

View File

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

View File

@ -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',