migrate to vue3; autocomplete still needs work

This commit is contained in:
bkfox
2022-03-11 18:37:57 +01:00
parent ab8858154b
commit 5b788ca28f
34 changed files with 457 additions and 17868 deletions

9
assets/admin/app.js Normal file
View File

@ -0,0 +1,9 @@
import App from 'public/app';
import AStatistics from './statistics.vue';
export default {
...App,
components: {...App.components, AStatistics},
}

View File

@ -1,8 +1,8 @@
import App from 'public/app';
import 'public';
import '@fortawesome/fontawesome-free/css/all.min.css'
import '@fortawesome/fontawesome-free/css/fontawesome.min.css'
import AdminApp from './app';
import './admin.scss';
import AStatistics from './statistics.vue';
window.aircox_admin = {
/**
@ -21,11 +21,5 @@ window.aircox_admin = {
},
}
window.aircox.builder.config = {
...App,
components: {...App.components, AStatistics},
}
window.AdminApp = AdminApp

View File

@ -12,7 +12,7 @@ const App = {
player() { return window.aircox.player; },
},
components: {AAutocomplete, AEpisode, APlayer, APlaylist, ASoundItem},
components: {AAutocomplete, AEpisode, APlaylist, ASoundItem},
}
export const PlayerApp = {

View File

@ -34,16 +34,16 @@ export default class Builder {
* @param {Boolean} [reset=False]: if True, force application recreation.
* @return `app.mount`'s result.
*/
mount({content=null, title=null, el=null, reset=false}={}) {
mount({content=null, title=null, el=null, reset=false, props=null}={}) {
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.app = this.createApp({title,content,el,...config}, props)
this.vm = this.app.mount(el)
window.scroll(0, 0)
return this.vm
@ -53,7 +53,7 @@ export default class Builder {
}
}
createApp({el, title=null, content=null, ...config}) {
createApp({el, title=null, content=null, ...config}, props) {
const container = document.querySelector(el)
if(!container)
throw `Error: can't get element ${el}`
@ -61,7 +61,7 @@ export default class Builder {
container.innerHTML = content
if(title)
document.title = title
return createApp(config)
return createApp(config, props)
}
unmount() {

View File

@ -1,60 +1,74 @@
<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]" />
<datalist :id="listId">
<template v-for="item in items" :key="item.path">
<option :value="item[field]"></option>
</template>
</datalist>
<input type="text" :name="name" :placeholder="placeholder"
:list="listId" @keyup="onKeyUp"/>
</div>
</template>
<script>
import debounce from 'lodash/debounce'
import {Autocomplete} from 'buefy/dist/components/autocomplete'
// import debounce from 'lodash/debounce'
export default {
props: {
url: String,
model: Function,
placeholder: String,
field: {type: String, default: 'value'},
name: String,
field: String,
valueField: {type: String, default: 'id'},
count: {type: Number, count: 10},
valueAttr: String,
valueField: String,
},
data() {
return {
data: [],
value: '',
items: [],
selected: null,
isFetching: false,
listId: `autocomplete-${ Math.random() }`.replace('.',''),
}
},
methods: {
onSelect(option) {
console.log('selected', option)
select(option, value=null) {
if(!option && value !== null)
option = this.items.find(item => item[this.field] == value)
this.selected = option
this.$emit('select', option)
},
fetch: debounce(function(query) {
if(!query)
onKeyUp: function(event) {
const value = event.target.value
if(value === this.value)
return
if(value !== undefined && value !== null)
this.value = value
if(!value)
return this.select(null)
this.fetch(value)
},
fetch: function(query) {
if(!query || this.isFetching)
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,
return this.model.fetch(this.url.replace('${query}', query), {many:true})
.then(items => { this.items = items || []
this.isFetching = false
this.select(null, query)
return items },
data => {this.isFetching = false; Promise.reject(data)})
},
},
}

View File

@ -15,6 +15,7 @@ import {Set} from './model'
import './styles.scss'
window.aircox = {
// main application
builder: new Builder(App),
@ -25,9 +26,28 @@ window.aircox = {
get playerApp() { return this.playerBuilder && this.playerBuilder.app },
get player() { return this.playerBuilder.vm && this.playerBuilder.vm.$refs.player },
Set: Set, Sound: Sound,
Set, Sound,
/**
* Initialize main application and player.
*/
init(props=null, {config=null, builder=null, initPlayer=true}={}) {
builder = builder || this.builder
this.builder = builder
if(config)
builder.config = config
builder.title = document.title
builder.mount({props})
if(initPlayer) {
let playerBuilder = this.playerBuilder
playerBuilder.mount()
}
},
}
/*
window.addEventListener('load', e => {
const [app, player] = [aircox.builder, aircox.playerBuilder]
app.title = document.title
@ -36,4 +56,5 @@ window.addEventListener('load', e => {
player.mount()
})
*/

View File

@ -20,7 +20,7 @@ export function getCsrf() {
// TODO: prevent duplicate simple fetch
export default class Model {
constructor(data, {url=null}={}) {
this.url = url;
this.url = url || data.url_;
this.commit(data);
}
@ -45,14 +45,24 @@ export default class Model {
}
}
static fromList(items, args=null) {
return items ? items.map(d => new this(d, args)) : []
}
/**
* Fetch item from server
*/
static fetch(url, options=null, args=null) {
static fetch(url, {many=false, ...options}={}, args={}) {
options = this.getOptions(options)
return fetch(url, options)
.then(response => response.json())
.then(data => new this(data, {url: url, ...args}));
const request = fetch(url, options).then(response => response.json());
if(many)
return request.then(data => {
if(!(data instanceof Array))
data = data.results
return this.fromList(data, args)
})
else
return request.then(data => new this(data, {url: url, ...args}));
}
/**
@ -123,7 +133,7 @@ export class Set {
* Fetch multiple items from server
*/
static fetch(model, url, options=null, args=null) {
options = this.getOptions(options)
options = model.getOptions(options)
return fetch(url, options)
.then(response => response.json())
.then(data => (data instanceof Array ? data : data.results)

60
assets/streamer/app.js Normal file
View File

@ -0,0 +1,60 @@
import AdminApp from 'admin/app';
import Model, {Set} from 'public/model';
import Sound from 'public/sound';
import {setEcoInterval} from 'public/utils';
import {Streamer, Queue} from './controllers';
export default {
...AdminApp,
props: {
...(AdminApp.props || {}),
apiUrl: String,
},
data() {
return {
// current streamer
streamer: null,
// all streamers
streamers: [],
// fetch interval id
fetchInterval: null,
Sound: Sound,
}
},
computed: {
...(AdminApp.computed || {}),
sources() {
var sources = this.streamer ? this.streamer.sources : [];
return sources.filter(s => s.data)
},
},
methods: {
...(AdminApp.methods || {}),
fetchStreamers() {
Streamer.fetch(this.apiUrl, {many:true}).then(streamers => {
this.streamers = streamers
this.streamer = streamers ? streamers[0] : null
})
},
},
mounted() {
this.fetchStreamers();
this.fetchInterval = setEcoInterval(() => this.streamer && this.streamer.fetch(), 5000)
},
destroyed() {
if(this.fetchInterval !== null)
clearInterval(this.fetchInterval)
}
}

View File

@ -1,4 +1,4 @@
import Model, {Set} from 'public/model';
import Model from 'public/model';
import {setEcoInterval} from 'public/utils';
@ -12,8 +12,8 @@ export class Streamer extends Model {
if(!this.data)
this.data = { id: data.id, playlists: [], queues: [] }
data.playlists = Playlist.Set(data.playlists, {args: {streamer: this}});
data.queues = Queue.Set(data.queues, {args: {streamer: this}});
data.playlists = Playlist.fromList(data.playlists, {streamer: this});
data.queues = Queue.fromList(data.queues, {streamer: this});
super.commit(data)
}
}
@ -83,7 +83,7 @@ export class Queue extends Source {
get queue() { return this.data && this.data.queue; }
commit(data) {
data.queue = Request.Set(data.queue);
data.queue = Request.fromList(data.queue);
super.commit(data)
}

View File

@ -1,12 +1,18 @@
import App from 'public/app';
import AdminApp from 'admin/app';
import Model, {Set} from 'public/model';
import Sound from 'public/sound';
import {setEcoInterval} from 'public/utils';
import {Streamer, Queue} from './controllers';
window.aircox.builder.config = {
...App,
export const StreamerApp = {
...AdminApp,
props: {
...(AdminApp.props || {}),
apiUrl: String,
},
data() {
return {
@ -21,12 +27,8 @@ window.aircox.builder.config = {
},
computed: {
...(App.computed || {}),
...(AdminApp.computed || {}),
apiUrl() {
return this.$el && this.$el.dataset.apiUrl;
},
sources() {
var sources = this.streamer ? this.streamer.sources : [];
return sources.filter(s => s.data)
@ -34,10 +36,10 @@ window.aircox.builder.config = {
},
methods: {
...(App.methods || {}),
...(AdminApp.methods || {}),
fetchStreamers() {
Set.fetch(Streamer, this.apiUrl).then(streamers => {
Streamer.fetch(this.apiUrl, {many:true}).then(streamers => {
this.streamers = streamers
this.streamer = streamers ? streamers[0] : null
})
@ -55,3 +57,5 @@ window.aircox.builder.config = {
}
}
window.StreamerApp = StreamerApp