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

View File

@ -63,9 +63,8 @@ class SoundSerializer(serializers.ModelSerializer):
def get_field_names(self, *args):
names = super().get_field_names(*args)
if 'request' in self.context and self.context['request'].user.is_staff and \
self.instance.is_public:
names.push('path')
if 'request' in self.context and self.context['request'].user.is_staff:
names.append('path')
return names
class PodcastSerializer(serializers.ModelSerializer):

View File

@ -0,0 +1 @@
export default __webpack_public_path__ + "fonts/fa-regular-400.woff2";

View File

@ -0,0 +1 @@
export default __webpack_public_path__ + "fonts/fa-regular-400.ttf";

View File

@ -0,0 +1 @@
export default __webpack_public_path__ + "fonts/fa-v4compatibility.woff2";

View File

@ -0,0 +1 @@
export default __webpack_public_path__ + "fonts/fa-brands-400.ttf";

View File

@ -0,0 +1 @@
export default __webpack_public_path__ + "fonts/fa-solid-900.ttf";

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
export default __webpack_public_path__ + "fonts/fa-solid-900.woff2";

View File

@ -0,0 +1 @@
export default __webpack_public_path__ + "fonts/fa-brands-400.woff2";

View File

@ -0,0 +1 @@
export default __webpack_public_path__ + "fonts/fa-v4compatibility.ttf";

Binary file not shown.

Binary file not shown.

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 it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -4,16 +4,17 @@
<head>
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="{% static "aircox/vendor.css" %}">
<!-- <link rel="stylesheet" type="text/css" href="{% static "aircox/vendor.css" %}"> -->
<link rel="stylesheet" type="text/css" href="{% static "admin/css/base.css" %}">
<script src="{% static "aircox/main.js" %}"></script>
<script src="{% static "aircox/vendor.js" %}"></script>
<script src="{% static "aircox/admin.js" %}"></script>
<script src="{% static "aircox/public.js" %}"></script>
{% block extrastyle %}{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static "aircox/main.css" %}">
<link rel="stylesheet" type="text/css" href="{% static "aircox/vendor.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "aircox/main.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "aircox/admin.css" %}">
{% if LANGUAGE_BIDI %}<link rel="stylesheet" type="text/css" href="{% block stylesheet_rtl %}{% static "admin/css/rtl.css" %}{% endblock %}">{% endif %}
@ -30,6 +31,13 @@
<body class="{% if is_popup %}popup {% endif %}{% block bodyclass %}{% endblock %}"
data-admin-utc-offset="{% now "Z" %}">
<script id="init-script">
window.addEventListener('load', function() {
{% block init-scripts %}
aircox.init({}, {config: window.AdminApp})
{% endblock %}
})
</script>
<!-- Container -->
<div>
@ -173,7 +181,9 @@
</div>
<!-- END Container -->
{% block outside_bottom %}{% endblock %}
{% block player %}
<div id="player">{% include "aircox/widgets/player.html" %}</div>
{% endblock %}
</body>
</html>

View File

@ -39,6 +39,14 @@ Usefull context:
{% block head_extra %}{% endblock %}
</head>
<body>
<script id="init-script">
window.addEventListener('load', function() {
{% block init-scripts %}
aircox.init()
aircox.app.enableHotReload(window)
{% endblock %}
})
</script>
<div id="app">
<nav class="navbar has-shadow" role="navigation" aria-label="main navigation">
<div class="container">

View File

@ -0,0 +1,18 @@
{% extends "aircox/page_list.html" %}
{% load i18n %}
{% block filters %}
{{ block.super }}
<div class="field is-horizontal">
<div class="field-label">
<label class="label">{% trans "Podcasts" %}</label>
</div>
<div class="field-body">
<div class="checkbox">
<input type="checkbox" class="checkbox" name="podcast" value="True"
{% if filterset_data.podcast %}checked{% endif %} />
</div>
</div>
</div>
{% endblock %}

Binary file not shown.

View File

@ -0,0 +1,130 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-01-06 14:14+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: models.py:37
msgid "input"
msgstr ""
#: models.py:38
msgid "output"
msgstr ""
#: models.py:56
msgid "station"
msgstr ""
#: models.py:58
msgid "direction"
msgstr ""
#: models.py:59
msgid "type"
msgstr ""
#: models.py:61
msgid "active"
msgstr ""
#: models.py:62
msgid "this port is active"
msgstr ""
#: models.py:65
msgid "port settings"
msgstr ""
#: models.py:66
msgid ""
"list of comma separated params available; this is put in the output config "
"file as raw code; plugin related"
msgstr ""
#: templates/aircox_streamer/source_item.html:19
msgid "Synchronize source with Liquidsoap"
msgstr ""
#: templates/aircox_streamer/source_item.html:23
msgid "Synchronise"
msgstr ""
#: templates/aircox_streamer/source_item.html:26
msgid "Restart current track"
msgstr ""
#: templates/aircox_streamer/source_item.html:30
msgid "Restart"
msgstr ""
#: templates/aircox_streamer/source_item.html:33
msgid "Skip current file"
msgstr ""
#: templates/aircox_streamer/source_item.html:34
msgid "Skip"
msgstr ""
#: templates/aircox_streamer/source_item.html:43
msgid "Add sound"
msgstr ""
#: templates/aircox_streamer/source_item.html:51
msgid "Select a sound"
msgstr ""
#: templates/aircox_streamer/source_item.html:53
msgid "Add a sound to the queue (queue may start playing)"
msgstr ""
#: templates/aircox_streamer/source_item.html:62
msgid "Add"
msgstr ""
#: templates/aircox_streamer/source_item.html:68
msgid "Sounds in queue"
msgstr ""
#: templates/aircox_streamer/source_item.html:86
msgid "Status"
msgstr ""
#: templates/aircox_streamer/source_item.html:96
msgid "Air time"
msgstr ""
#: templates/aircox_streamer/source_item.html:106
msgid "Time left"
msgstr ""
#: templates/aircox_streamer/source_item.html:114
msgid "Data source"
msgstr ""
#: templates/aircox_streamer/streamer.html:19
msgid "Reload"
msgstr ""
#: templates/aircox_streamer/streamer.html:26
#: templates/aircox_streamer/streamer.html:27
msgid "Select a station"
msgstr ""
#: urls.py:9 views.py:9
msgid "Streamer Monitor"
msgstr ""

View File

@ -51,10 +51,9 @@
<form class="columns" @submit.prevent="source.push($event.target.elements['sound_id'].value)">
<div class="column field is-small">
{# TODO: select station => change the shit #}
<a-autocomplete url="{% url "aircox:sound-list" %}?station={{ station.pk }}&search=${query}"
class="is-fullwidth"
:model="Sound" field="name" value-field="sound_id" value-attr="id"
{# FIXME dirty hack awaiting the vue component #}
<a-autocomplete class="is-fullwidth"
url="{% url "aircox:sound-list" %}?station={{ station.pk }}&search=${query}"
name="sound_id" :model="Sound" field="path" value-field="id"
placeholder="{% trans "Select a sound" %}"></a-autocomplete>
<p class="help">
{% trans "Add a sound to the queue (queue may start playing)" %}

View File

@ -6,9 +6,14 @@
<script src="{% static "aircox/streamer.js" %}"></script>
{% endblock %}
{% block init-scripts %}
aircox.init({apiUrl: "{% url "admin:api:streamer-list" %}"},
{config: window.StreamerApp})
{% endblock %}
{% block content %}
{{ block.super }}
<div id="app" v-if="streamers" data-api-url="{% url "admin:api:streamer-list" %}">
<div id="app" api-url="{% url "admin:api:streamer-list" %}">
<div class="navbar toolbar">
<div class="navbar-start">
<span class="navbar-item control">
@ -22,7 +27,7 @@
</div>
<div class="navbar-end">
<div class="select navbar-item">
<select ref="selectStreamer" onchange="selectStreamer" class="control"
<select onchange="selectStreamer" class="control"
title="{% trans "Select a station" %}"
aria-label="{% trans "Select a station" %}">
<option v-for="streamer of streamers" :value="streamer.id">[[ streamer.data.name ]]</option>
@ -39,7 +44,3 @@
</div>
{% endblock %}
{% block outside_bottom %}
<div id="player">{% include "aircox/widgets/player.html" %}</div>
{% endblock %}

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,11 +27,7 @@ window.aircox.builder.config = {
},
computed: {
...(App.computed || {}),
apiUrl() {
return this.$el && this.$el.dataset.apiUrl;
},
...(AdminApp.computed || {}),
sources() {
var sources = this.streamer ? this.streamer.sources : [];
@ -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