hot reload

This commit is contained in:
bkfox 2020-11-08 00:45:49 +01:00
parent 222300945e
commit 5fd72c33cc
19 changed files with 473 additions and 105 deletions

View File

@ -6,6 +6,7 @@ from django.utils import timezone as tz
from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from easy_thumbnails.files import get_thumbnailer
from aircox import settings, utils
from .program import Program, ProgramChildQuerySet, \
@ -25,6 +26,24 @@ class Episode(Page):
def program(self):
return getattr(self.parent, 'program', None)
@cached_property
def podcasts(self):
""" Return serialized data about podcasts. """
from ..serializers import PodcastSerializer
podcasts = [PodcastSerializer(s).data
for s in self.sound_set.public().order_by('type') ]
if self.cover:
options = {'size': (128,128), 'crop':'scale'}
cover = get_thumbnailer(self.cover).get_thumbnail(options).url
else:
cover = None
for index, podcast in enumerate(podcasts):
podcasts[index]['cover'] = cover
podcasts[index]['page_url'] = self.get_absolute_url()
podcasts[index]['page_title'] = self.title
return podcasts
@program.setter
def program(self, value):
self.parent = value

View File

@ -7463,22 +7463,31 @@ section > .toolbar {
padding: 1em;
margin-bottom: 1.5em; }
main .cover {
margin: 1em 0em;
border: 0.2em black solid; }
main .small-cover {
main .cover.is-small {
width: 10em; }
main .cover.is-tiny {
height: 2em; }
.sound-item .cover {
height: 5em; }
aside > section {
margin-bottom: 2em; }
aside .cover {
margin-bottom: 2em; }
aside .small-cover {
width: 4em; }
aside .cover.is-small {
width: 10em; }
aside .cover.is-tiny {
height: 2em; }
aside .media .subtitle {
font-size: 1em; }
.is-round, .sound-item .button {
border: 1px #7a7a7a solid;
border-radius: 1em; }

File diff suppressed because one or more lines are too long

View File

@ -7442,22 +7442,31 @@ section > .toolbar {
padding: 1em;
margin-bottom: 1.5em; }
main .cover {
margin: 1em 0em;
border: 0.2em black solid; }
main .small-cover {
main .cover.is-small {
width: 10em; }
main .cover.is-tiny {
height: 2em; }
.sound-item .cover {
height: 5em; }
aside > section {
margin-bottom: 2em; }
aside .cover {
margin-bottom: 2em; }
aside .small-cover {
width: 4em; }
aside .cover.is-small {
width: 10em; }
aside .cover.is-tiny {
height: 2em; }
aside .media .subtitle {
font-size: 1em; }
.is-round, .sound-item .button {
border: 1px #7a7a7a solid;
border-radius: 1em; }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,19 +2,11 @@
{% comment %}List of a show's episodes for a specific{% endcomment %}
{% load i18n aircox %}
{% block head_extra %}
<script id="page">
window.addEventListener('load', ev => {
Vue.set(aircox.app, 'page', {
podcasts: new aircox.Set(aircox.Sound, {items: {{ podcasts|json|safe }}})
});
});
</script>
{% endblock head_extra %}
{% include "aircox/program_sidebar.html" %}
{% block content %}
<a-episode :page="{title: &quot;{{ page.title }}&quot;, podcasts: {{ object.podcasts|json }}}">
<template v-slot="{podcasts,page}">
{{ block.super }}
<div class="columns is-desktop">
@ -46,9 +38,9 @@
</ul>
</section>
{% if podcasts %}
{% if object.podcasts %}
<section class="column">
<a-playlist v-if="page" :set="page.podcasts"
<a-playlist v-if="page" :set="podcasts"
name="{{ page.title }}"
:player="player" :actions="['play']"
@select="player.playItems('queue', $event.item)">
@ -81,5 +73,6 @@
</section>
{% endif %}
</template></a-episode>
{% endblock %}

View File

@ -41,3 +41,15 @@ Context variables:
{% endif %}
{% endblock %}
{% block actions %}
{% if object.sound_set.public.count %}
<button class="button is-round" @click="player.playButtonClick($event)"
data-sounds="{{ object.podcasts|json }}">
<span class="icon is-small">
<span class="fas fa-play"></span>
</span>
</button>
{% endif %}
{% endblock %}

View File

@ -30,7 +30,7 @@ Context variables:
{% if has_cover|default_if_none:True %}
<div class="media-left">
<img src="{% thumbnail object.cover|default:station.default_cover 128x128 crop=scale %}"
class="small-cover">
class="cover is-small">
</div>
{% endif %}
<div class="media-content">
@ -55,6 +55,8 @@ Context variables:
</div>
{% endif %}
</div>
{% block actions %}{% endblock %}
</article>
{% endif %}

View File

@ -4,7 +4,6 @@ import datetime
from django.views.generic import ListView
from ..models import Diffusion, Episode, Program, StaticPage, Sound
from ..serializers import PodcastSerializer
from .base import BaseView
from .program import ProgramPageDetailView
from .page import PageListView
@ -20,8 +19,6 @@ class EpisodeDetailView(ProgramPageDetailView):
def get_context_data(self, **kwargs):
if not 'tracks' in kwargs:
kwargs['tracks'] = self.object.track_set.order_by('position')
if not 'podcasts' in kwargs:
kwargs['podcasts'] = [PodcastSerializer(s).data for s in self.object.sound_set.public() ]
return super().get_context_data(**kwargs)

View File

@ -1,6 +1,5 @@
import Vue from 'vue';
export const defaultConfig = {
el: '#app',
delimiters: ['[[', ']]'],
@ -12,12 +11,12 @@ export const defaultConfig = {
},
computed: {
player() { return window.aircox.player; }
player() { return window.aircox.player; },
},
}
export default function App(config) {
return (new AppConfig(config)).load()
export default function App(config, sync=false) {
return (new AppConfig(config)).load(sync)
}
/**
@ -30,30 +29,36 @@ export class AppConfig {
get config() {
let config = this._config instanceof Function ? this._config() : this._config;
return {...defaultConfig, ...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;
}
load() {
load(sync=false) {
var self = this;
return new Promise(function(resolve, reject) {
window.addEventListener('load', () => {
try {
let config = self.config;
const el = document.querySelector(config.el)
if(!el) {
reject(`Error: missing element ${config.el}`);
return;
}
resolve(new Vue(config))
}
catch(error) { reject(error) }
})
let func = () => { try { resolve(self.build()) } catch(error) { reject(error) }};
sync ? func() : window.addEventListener('load', func);
});
}
build() {
let config = this.config;
const el = document.querySelector(config.el)
if(!el) {
reject(`Error: missing element ${config.el}`);
return;
}
return new Vue(config);
}
}

23
assets/public/episode.vue Normal file
View File

@ -0,0 +1,23 @@
<template>
<div>
<slot :page="page" :podcasts="podcasts"></slot>
</div>
</template>
<script>
import {Set} from './model';
import Sound from './sound';
import Page from './page';
export default {
extends: Page,
data() {
return {
podcasts: new Set(Sound, {items:this.page.podcasts}),
}
},
}
</script>

View File

@ -16,12 +16,14 @@ import {Set} from './model';
import './styles.scss';
import Autocomplete from './autocomplete.vue';
import Player from './player.vue';
import Playlist from './playlist.vue';
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)
@ -42,14 +44,40 @@ window.aircox = {
return this.playerApp && this.playerApp.$refs.player
},
loadPage(url) {
fetch(url).then(response => response.text())
.then(response => {
let doc = new DOMParser().parseFromString(response, 'text/html');
aircox.app && aircox.app.$destroy();
document.getElementById('app').innerHTML = doc.getElementById('app').innerHTML;
App(() => window.aircox.appConfig, true).then(app => {
aircox.app = app;
document.title = doc.title;
})
});
},
Set: Set, Sound: Sound,
};
window.Vue = Vue;
App({el: '#player'}).then(app => window.aircox.playerApp = app,
() => undefined);
App(() => window.aircox.appConfig).then(app => { window.aircox.app = app },
() => undefined)
App({el: '#player'}).then(app => window.aircox.playerApp = app);
App(() => window.aircox.appConfig).then(app => {
window.aircox.app = app;
window.addEventListener('click', event => {
let target = event.target.tagName == 'A' ? event.target : event.target.closest('a');
if(!target || !target.hasAttribute('href'))
return;
let href = target.getAttribute('href');
if(href && href !='#') {
window.aircox.loadPage(href);
event.preventDefault();
event.stopPropagation();
}
}, true);
})

View File

@ -42,9 +42,14 @@ export default {
find(pred) { return this.set.find(pred) },
findIndex(pred) { return this.set.findIndex(pred) },
push(...items) {
/**
* Add items to list, return index of the first provided item.
*/
push(item, ...items) {
let index = this.set.push(item);
for(var item of items)
this.set.push(item);
return index;
},
remove(index, select=False) {

View File

@ -176,20 +176,21 @@ export class Set {
}
/**
* Add item to set
* Add item to set, return index.
*/
push(item, {args={},save=true}={}) {
item = item instanceof this.model ? item : new this.model(item, args);
if(this.unique) {
let index = this.findIndex(item);
if(index > -1)
return;
return index;
}
if(this.max && this.items.length >= this.max)
this.items.splice(0,this.items.length-this.max)
this.items.push(item);
save && this.save();
return this.items.length-1;
}
/**

20
assets/public/page.vue Normal file
View File

@ -0,0 +1,20 @@
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
data() {
return {}
},
props: {
page: Object,
title: String,
},
}
</script>

View File

@ -3,6 +3,7 @@
<div :class="['player-panels', panel ? 'is-open' : '']">
<Playlist ref="pin" class="player-panel menu" v-show="panel == 'pin'"
name="Pinned"
:actions="['page']"
:editable="true" :player="self" :set="sets.pin" @select="play('pin', $event.index)"
listClass="menu-list" itemClass="menu-item">
<template v-slot:header="">
@ -13,6 +14,7 @@
</template>
</Playlist>
<Playlist ref="queue" class="player-panel menu" v-show="panel == 'queue'"
:actions="['page']"
:editable="true" :player="self" :set="sets.queue" @select="play('queue', $event.index)"
listClass="menu-list" itemClass="menu-item">
<template v-slot:header="">
@ -26,12 +28,11 @@
<div class="player-bar media">
<div class="media-left">
<div class="button" @click="togglePlay()"
<button class="button" @click="togglePlay()"
:title="buttonTitle" :aria-label="buttonTitle">
<span class="fas fa-pause" v-if="playing"></span>
<span class="fas fa-play" v-else></span>
</div>
<slot name="sources"></slot>
</button>
</div>
<div class="media-left media-cover" v-if="current && current.cover">
<img :src="current.cover" class="cover" />
@ -218,13 +219,16 @@ export default {
/// Push and play items
playItems(playlist, ...items) {
this.push(playlist, ...items);
let index = this.$refs[playlist].findIndex(items[0]);
let index = this.push(playlist, ...items);
this.$refs[playlist].selectedIndex = index;
this.play(playlist, index);
},
playButtonClick(event) {
var items = JSON.parse(event.currentTarget.dataset.sounds);
this.playItems('queue', ...items);
},
/// Play live stream
playLive() {
this.load(null, {src: this.live.src});
@ -266,10 +270,6 @@ export default {
},
},
mounted() {
this.sources = this.$slots.sources;
},
components: { Playlist, Progress },
}
</script>

View File

@ -1,7 +1,10 @@
<template>
<div class="media">
<div class="media-left" v-if="hasAction('play')">
<button class="button" @click="$emit('togglePlay')">
<div class="media sound-item">
<div class="media-left">
<img class="cover is-tiny" :src="item.data.cover" v-if="item.data.cover">
</div>
<div class="media-left">
<button class="button" @click.stop="$emit('togglePlay')">
<div class="icon">
<span class="fa fa-pause" v-if="playing"></span>
<span class="fa fa-play" v-else></span>
@ -10,9 +13,14 @@
</div>
<div class="media-content">
<slot name="content" :player="player" :item="item" :loaded="loaded">
<h4 class="title is-4 is-inline-block">
{{ name || item.name }}
</h4>
<h4 class="title is-4">{{ name || item.name }}</h4>
<a class="subtitle is-6" v-if="hasAction('page') && item.data.page_url"
:href="item.data.page_url">
<i class="icon">
<i class="fas fa-link"></i>
</i>
{{ item.data.page_title }}
</a>
</slot>
</div>
<div class="media-right">
@ -33,7 +41,6 @@ export default {
props: {
data: {type: Object, default: x => {}},
name: String,
cover: String,
player: Object,
page_url: String,
actions: {type:Array, default: x => []},

View File

@ -167,7 +167,6 @@ a.navbar-item.is-active {
}
//-- player
.player {
box-shadow: 0em 1.5em 2.5em rgba(0, 0, 0, 0.6);
@ -257,16 +256,12 @@ section > .toolbar {
main {
.cover {
margin: 1em 0em;
border: 0.2em black solid;
}
.small-cover {
width: 10em;
}
.cover.is-small { width: 10em; }
.cover.is-tiny { height: 2em; }
}
.sound-item .cover { height: 5em; }
aside {
& > section {
margin-bottom: 2em;
@ -276,9 +271,8 @@ aside {
margin-bottom: 2em;
}
.small-cover {
width: 4em;
}
.cover.is-small { width: 10em; }
.cover.is-tiny { height: 2em; }
.media .subtitle {
font-size: 1em;
@ -286,4 +280,8 @@ aside {
}
.is-round, .sound-item .button {
border: 1px $grey solid;
border-radius: 1em;
}