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

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