work on admin interface, player, list of sounds

This commit is contained in:
bkfox 2016-08-10 00:58:05 +02:00
parent f5dbc93f7f
commit 021b2a116a
19 changed files with 566 additions and 298 deletions

View File

@ -62,7 +62,7 @@ class Command (BaseCommand):
start__gt = tz.now().date() - tz.timedelta(days = 20), start__gt = tz.now().date() - tz.timedelta(days = 20),
page__isnull = True, page__isnull = True,
initial__isnull = True initial__isnull = True
) ).exclude(type = Diffusion.Type.unconfirmed)
for diffusion in qs: for diffusion in qs:
if not diffusion.program.page.count(): if not diffusion.program.page.count():
if not hasattr(diffusion.program, '__logged_diff_error'): if not hasattr(diffusion.program, '__logged_diff_error'):

View File

@ -31,6 +31,7 @@ import aircox.programs.models as programs
import aircox.controllers.models as controllers import aircox.controllers.models as controllers
import aircox.cms.settings as settings import aircox.cms.settings as settings
from aircox.cms.utils import image_url
from aircox.cms.sections import * from aircox.cms.sections import *
@ -163,6 +164,15 @@ class Comment(models.Model):
_('comment'), _('comment'),
) )
def __str__(self):
# Translators: text shown in the comments list (in admin)
return _('{date}, {author}: {content}...').format(
author = self.author,
date = self.date.strftime('%d %A %Y, %H:%M'),
content = self.content[:128]
)
def make_safe(self): def make_safe(self):
self.author = bleach.clean(self.author, tags=[]) self.author = bleach.clean(self.author, tags=[])
if self.email: if self.email:
@ -263,6 +273,15 @@ class Publication(Page):
index.FilterField('show_in_menus'), index.FilterField('show_in_menus'),
] ]
@property
def icon(self):
return image_url(self.cover, 'fill-64x64')
@property
def small_icon(self):
return image_url(self.cover, 'fill-32x32')
@property @property
def recents(self): def recents(self):
return self.get_children().type(Publication).not_in_menu().live() \ return self.get_children().type(Publication).not_in_menu().live() \
@ -390,10 +409,8 @@ class Track(programs.Track,Orderable):
diffusion = ParentalKey('DiffusionPage', diffusion = ParentalKey('DiffusionPage',
related_name='tracks') related_name='tracks')
panels = [ panels = [
FieldRowPanel([ FieldPanel('artist'),
FieldPanel('artist'), FieldPanel('title'),
FieldPanel('title'),
]),
FieldPanel('tags'), FieldPanel('tags'),
FieldPanel('info'), FieldPanel('info'),
] ]
@ -428,13 +445,15 @@ class DiffusionPage(Publication):
verbose_name = _('Diffusion') verbose_name = _('Diffusion')
verbose_name_plural = _('Diffusions') verbose_name_plural = _('Diffusions')
content_panels = [ content_panels = Publication.content_panels + [
FieldPanel('diffusion'),
FieldPanel('publish_archive'),
] + Publication.content_panels + [
InlinePanel('tracks', label=_('Tracks')), InlinePanel('tracks', label=_('Tracks')),
] ]
promote_panels = [
# FieldPanel('diffusion'),
FieldPanel('publish_archive'),
] + Publication.promote_panels
@classmethod @classmethod
def from_diffusion(cl, diff, model = None, **kwargs): def from_diffusion(cl, diff, model = None, **kwargs):
model = model or cl model = model or cl
@ -519,8 +538,6 @@ class DiffusionPage(Publication):
podcast.public = publish podcast.public = publish
podcast.save() podcast.save()
super().save(*args, **kwargs) super().save(*args, **kwargs)

View File

@ -221,19 +221,18 @@ class ListBase(models.Model):
else: else:
qs = qs.descendant_of(related) qs = qs.descendant_of(related)
date = self.related.date if hasattr('date', related) else \ date = self.related.date if hasattr(related, 'date') else \
self.related.first_published_at self.related.first_published_at
if self.date_filter == self.DateFilter.before_related: if self.date_filter == self.DateFilter.before_related:
qs = qs.filter(date__lt = date) qs = qs.filter(date__lt = date)
elif self.date_filter == self.DateFilter.after_related: elif self.date_filter == self.DateFilter.after_related:
qs = qs.filter(date__gte = date) qs = qs.filter(date__gte = date)
# date # date
else: date = tz.now()
date = tz.now() if self.date_filter == self.DateFilter.previous:
if self.date_filter == self.DateFilter.previous: qs = qs.filter(date__lt = date)
qs = qs.filter(date__lt = date) elif self.date_filter == self.DateFilter.next:
elif self.date_filter == self.DateFilter.next: qs = qs.filter(date__gte = date)
qs = qs.filter(date__gte = date)
# sort # sort
if self.asc: if self.asc:
@ -332,9 +331,7 @@ class ListBase(models.Model):
search = request.GET.get('search') search = request.GET.get('search')
if search: if search:
kwargs['terms'] = search kwargs['terms'] = search
print(search, qs)
qs = qs.search(search) qs = qs.search(search)
print(qs.count())
set('list_selector', kwargs) set('list_selector', kwargs)
@ -342,7 +339,7 @@ class ListBase(models.Model):
if qs: if qs:
paginator = Paginator(qs, 30) paginator = Paginator(qs, 30)
try: try:
qs = paginator.page('page') qs = paginator.page(request.GET.get('page') or 1)
except PageNotAnInteger: except PageNotAnInteger:
qs = paginator.page(1) qs = paginator.page(1)
except EmptyPage: except EmptyPage:
@ -803,7 +800,7 @@ class SectionList(ListBase, SectionRelativeItem):
'list. If empty, does not print an address'), 'list. If empty, does not print an address'),
) )
panels = SectionItem.panels + [ panels = SectionRelativeItem.panels + [
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('focus_available'), FieldPanel('focus_available'),
FieldPanel('count'), FieldPanel('count'),
@ -815,6 +812,9 @@ class SectionList(ListBase, SectionRelativeItem):
from aircox.cms.models import Publication from aircox.cms.models import Publication
context = super().get_context(request, page) context = super().get_context(request, page)
if self.is_related:
self.related = page
qs = self.get_queryset() qs = self.get_queryset()
qs = qs.live() qs = qs.live()
if self.focus_available: if self.focus_available:

View File

@ -24,6 +24,9 @@ ul {
float: left; float: left;
} }
.small {
font-size: 0.8em;
}
.icon { .icon {
max-width: 2em; max-width: 2em;
@ -67,9 +70,11 @@ nav.menu {
padding: 0.2em; padding: 0.2em;
height: 2.5em; height: 2.5em;
margin-bottom: 1em; margin-bottom: 1em;
background-color: white;
box-shadow: 0em 0em 0.2em black;
} }
.menu.top * { .menu.top * {
vertical-align: middle; vertical-align: bottom;
} }
.menu.top > section { .menu.top > section {
@ -81,6 +86,10 @@ nav.menu {
margin: 0.2em 1em; margin: 0.2em 1em;
} }
.page_left, .page_right {
max-width: 16em;
}
.page_left > section, .page_left > section,
.page_right > section { .page_right > section {
margin-bottom: 1em; margin-bottom: 1em;
@ -121,6 +130,12 @@ ul.list {
} }
.list nav {
text-align: center;
font-size: 0.9em;
}
/** content: date list **/ /** content: date list **/
.date_list nav { .date_list nav {
text-align:center; text-align:center;
@ -188,8 +203,6 @@ ul.list {
font-size: 0.8em; font-size: 0.8em;
} }
.comments ul { .comments ul {
margin-top: 2.5em; margin-top: 2.5em;
} }
@ -208,19 +221,23 @@ ul.list {
float: right; float: right;
} }
/** content: player **/ /** content: player **/
.player { .player {
width: 20em;
} }
.player:not([seekable]) > .controls { .player:not([seekable]) > .controls > .progress {
display: none; display: none;
} }
.player .controls > * { .player .controls {
margin: 0em 0.2em; margin-top: 1em;
text-align: right;
} }
.player .controls > * {
margin: 0em 0.2em;
}
.player .controls .single { .player .controls .single {
display: none; display: none;
@ -245,14 +262,9 @@ ul.list {
border-right: 2px #818181 solid; border-right: 2px #818181 solid;
} }
.player .on_air a:not([href]), .on_air a[href=""] {
display: none;
}
.player .playlist .item { .player .playlist .item {
margin: 0em; margin: 0em;
padding: 0.2em 0.4em; padding: 0.2em 0.4em;
height: 1em;
cursor: pointer; cursor: pointer;
} }
@ -266,25 +278,20 @@ ul.list {
.player .playlist .item .actions { .player .playlist .item .actions {
display: none; display: none;
font-size: 0.9em;
} }
.player .playlist .item:hover .actions { .player .playlist .item:hover .actions {
display: inline-block; display: inline;
} }
.player .playlist .item .info {
float: right;
width: 2em;
display: inline-block;
}
.player .item:not([selected]) .button {
display: none;
}
.player .item[selected] { .player .item[selected] {
height: auto; border-left: 1px #007EDF solid;
font-size: 1.1em; font-size: 1.0em;
}
.player .item:not([selected]) {
} }
.player .button { .player .button {
@ -300,36 +307,33 @@ ul.list {
max-height: 2.0em; max-height: 2.0em;
} }
.player:not([state]) .button > img:not(.play),
.player[state="paused"] .button > img:not(.play), .player:not([state]) .item[selected] .button > img:not(.play),
.player[state="playing"] .button > img:not(.pause), .player[state="paused"] .item[selected] .button > img:not(.play),
.player[state="loading"] .button > img:not(.loading) .player[state="playing"] .item[selected] .button > img:not(.pause),
.player[state="loading"] .item[selected] .button > img:not(.loading)
{ {
display: none; display: none;
} }
.player[state="loading"] .box .button > img.loading { .player .item:not([selected]) .button > img.play {
animation-duration: 2s; display: block;
animation-iteration-count: infinite; }
animation-name: rotate; .player .item:not([selected]) .button > img:not(.play) {
animation-timing-function: linear; display: none;
} }
@keyframes rotate {
from {
transform: rotate(0deg);
}
to { .player .list_item.live:hover .actions {
transform: rotate(360deg); display: none;
} }
}
/** content: page **/ /** content: page **/
main .body ~ section:not(.comments) { main .body ~ section:not(.comments) {
width: calc(50% - 1em); width: calc(50% - 1em);
float: left; vertical-align: top;
display: inline-block;
} }

View File

@ -50,6 +50,7 @@ time {
.info { .info {
font-size: 0.9em; font-size: 0.9em;
padding: 0.1em;
color: #007EDF; color: #007EDF;
} }
@ -81,14 +82,69 @@ main {
box-shadow: 0em 0em 0.2em black; box-shadow: 0em 0em 0.2em black;
} }
main h1 { main h1:not(.detail_title) {
margin: 0em;
margin: 0em 0em 0.4em 0em; margin: 0em 0em 0.4em 0em;
} }
main .content img.cover {
main h1.detail_title {
margin: 0em;
padding: 0.2em;
position: relative;
left: -0.7em;
width: 80%;
background-color: rgba(255,255,255,0.8);
}
main img.detail_cover {
width: calc(100% + 2em); width: calc(100% + 2em);
margin-top: -3.3em;
margin-left: -1em; margin-left: -1em;
} }
/** player **/
.player[state='playing'] .item[selected] .button > img {
animation-duration: 4s;
animation-iteration-count: infinite;
animation-name: blink;
}
@keyframes blink {
from {
opacity: 1.0;
}
50% {
opacity: 0.3;
}
to {
opacity: 1.0;
}
}
.player[state="loading"] .item[selected] .button > img.loading {
animation-duration: 2s;
animation-iteration-count: infinite;
animation-name: rotate;
animation-timing-function: linear;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -18,11 +18,13 @@ function duration_str(seconds) {
} }
function Sound(title, detail, duration, streams) { function Sound(title, detail, duration, streams, cover, on_air) {
this.title = title; this.title = title;
this.detail = detail; this.detail = detail;
this.duration = duration; this.duration = duration;
this.streams = streams.splice ? streams.sort() : [streams]; this.streams = streams.splice ? streams.sort() : [streams];
this.cover = cover;
this.on_air = on_air;
} }
Sound.prototype = { Sound.prototype = {
@ -30,25 +32,60 @@ Sound.prototype = {
detail: '', detail: '',
streams: undefined, streams: undefined,
duration: undefined, duration: undefined,
on_air_url: undefined, cover: undefined,
on_air: false,
item: undefined, item: undefined,
get seekable() { get seekable() {
return this.duration != undefined; return this.duration != undefined;
}, },
make_item: function(playlist, base_item) {
if(this.item)
return;
var item = base_item.cloneNode(true);
item.removeAttribute('style');
item.querySelector('.title').innerHTML = this.title;
if(this.seekable)
item.querySelector('.duration').innerHTML =
duration_str(this.duration);
if(this.detail)
item.querySelector('.detail').href = this.detail;
if(playlist.player.show_cover && this.cover)
item.querySelector('img.play').src = this.cover;
item.sound = this;
this.item = item;
// events
var self = this;
item.querySelector('.action.remove').addEventListener(
'click', function(event) { playlist.remove(self); }, false
);
item.addEventListener('click', function(event) {
if(event.target.className.indexOf('action') != -1)
return;
playlist.select(self, true)
}, false);
},
} }
function PlayerPlaylist(player) { function Playlist(player) {
this.player = player; this.player = player;
this.playlist = player.player.querySelector('.playlist'); this.playlist = player.player.querySelector('.playlist');
this.item_ = player.player.querySelector('.playlist .item'); this.item_ = player.player.querySelector('.playlist .item');
this.sounds = [] this.sounds = []
} }
PlayerPlaylist.prototype = { Playlist.prototype = {
on_air: undefined,
sounds: undefined, sounds: undefined,
sound: undefined,
/// Find a sound by its streams, and return it if found /// Find a sound by its streams, and return it if found
find: function(streams) { find: function(streams) {
@ -71,30 +108,12 @@ PlayerPlaylist.prototype = {
if(sound_) if(sound_)
return sound_; return sound_;
var item = this.item_.cloneNode(true); if(sound.on_air)
item.removeAttribute('style'); this.on_air = sound;
item.querySelector('.title').innerHTML = sound.title; sound.make_item(this, this.item_);
if(sound.seekable) (container || this.playlist).appendChild(sound.item);
item.querySelector('.duration').innerHTML =
duration_str(sound.duration);
if(sound.detail)
item.querySelector('.detail').href = sound.detail;
item.sound = sound;
sound.item = item;
var self = this;
item.querySelector('.action.remove').addEventListener(
'click', function(event) { self.remove(sound); }, false
);
item.addEventListener('click', function(event) {
if(event.target.className.indexOf('action') != -1)
return;
self.player.select(sound, true)
}, false);
(container || this.playlist).appendChild(item);
this.sounds.push(sound); this.sounds.push(sound);
this.save(); this.save();
return sound; return sound;
@ -106,8 +125,61 @@ PlayerPlaylist.prototype = {
this.sounds.splice(index,1); this.sounds.splice(index,1);
this.playlist.removeChild(sound.item); this.playlist.removeChild(sound.item);
this.save(); this.save();
this.player.stop()
this.next(false);
}, },
select: function(sound, play = true) {
this.player.playlist = this;
if(this.sound == sound) {
if(play)
this.player.play();
return;
}
if(this.sound)
this.unselect(this.sound);
this.sound = sound;
// audio
this.player.load_sound(this.sound);
// attributes
var container = this.player.player;
sound.item.setAttribute('selected', 'true');
if(!sound.on_air)
sound.item.querySelector('.content').insertBefore(
this.player.progress.item,
sound.item.querySelector('.content .duration')
)
if(sound.seekable)
container.setAttribute('seekable', 'true');
else
container.removeAttribute('seekable');
// play
if(play)
this.player.play();
},
unselect: function(sound) {
sound.item.removeAttribute('selected');
},
next: function(play = true) {
var index = this.sounds.indexOf(this.sound);
if(index < 0)
return;
index++;
if(index < this.sounds.length)
this.select(this.sounds[index]);
},
// storage
save: function() { save: function() {
var list = []; var list = [];
for(var i in this.sounds) { for(var i in this.sounds) {
@ -124,7 +196,7 @@ PlayerPlaylist.prototype = {
for(var i in list) { for(var i in list) {
var sound = list[i]; var sound = list[i];
sound = new Sound(sound.title, sound.detail, sound.duration, sound = new Sound(sound.title, sound.detail, sound.duration,
sound.streams) sound.streams, sound.cover, sound.on_air)
this.add(sound, container) this.add(sound, container)
} }
this.playlist.appendChild(container); this.playlist.appendChild(container);
@ -132,53 +204,67 @@ PlayerPlaylist.prototype = {
} }
function Player(id) { function Player(id, on_air_url, show_cover) {
this.store = new Store('player'); this.id = id;
this.on_air_url = on_air_url;
this.show_cover = show_cover;
this.store = new Store('player_' + id);
// html sounds // html sounds
this.player = document.getElementById(id); this.player = document.getElementById(id);
this.audio = this.player.querySelector('audio'); this.audio = this.player.querySelector('audio');
this.on_air = this.player.querySelector('.on_air'); this.on_air = this.player.querySelector('.on_air');
this.progress = {
item: this.player.querySelector('.controls .progress'),
bar: this.player.querySelector('.controls .progress progress'),
duration: this.player.querySelector('.controls .progress .duration')
}
console.log(this.progress)
this.controls = { this.controls = {
duration: this.player.querySelector('.controls .duration'),
progress: this.player.querySelector('progress'),
single: this.player.querySelector('input.single'), single: this.player.querySelector('input.single'),
} }
this.playlist = new PlayerPlaylist(this); this.playlist = new Playlist(this);
this.playlist.load(); this.playlist.load();
this.init_events(); this.init_events();
this.load(); this.load();
this.update_on_air();
} }
Player.prototype = { Player.prototype = {
/// current item being played /// current item being played
sound: undefined, sound: undefined,
on_air_url: undefined,
init_events: function() { init_events: function() {
var self = this; var self = this;
function time_from_progress(event) { function time_from_progress(event) {
bounding = self.controls.progress.getBoundingClientRect() bounding = self.progress.bar.getBoundingClientRect()
offset = (event.clientX - bounding.left); offset = (event.clientX - bounding.left);
return offset * self.audio.duration / bounding.width; return offset * self.audio.duration / bounding.width;
} }
function update_info() { function update_info() {
var controls = self.controls; var progress = self.progress;
var pos = self.audio.currentTime;
// progress // progress
if(!self.sound || !self.sound.seekable || if(!self.sound || !self.sound.seekable ||
self.audio.duration == Infinity) { !pos || self.audio.duration == Infinity)
controls.duration.innerHTML = ''; {
controls.progress.value = 0; progress.duration.innerHTML = '';
progress.bar.value = 0;
return; return;
} }
var pos = self.audio.currentTime; progress.bar.value = pos;
controls.progress.value = pos; progress.bar.max = self.audio.duration;
controls.progress.max = self.audio.duration; progress.duration.innerHTML = duration_str(pos);
controls.duration.innerHTML = duration_str(pos);
} }
// audio // audio
@ -207,7 +293,7 @@ Player.prototype = {
}, false); }, false);
// progress // progress
progress = this.controls.progress; progress = this.progress.bar;
progress.addEventListener('click', function(event) { progress.addEventListener('click', function(event) {
player.audio.currentTime = time_from_progress(event); player.audio.currentTime = time_from_progress(event);
}, false); }, false);
@ -215,43 +301,55 @@ Player.prototype = {
progress.addEventListener('mouseout', update_info, false); progress.addEventListener('mouseout', update_info, false);
progress.addEventListener('mousemove', function(event) { progress.addEventListener('mousemove', function(event) {
if(self.audio.duration == Infinity) if(self.audio.duration == Infinity || isNaN(self.audio.duration))
return; return;
var pos = time_from_progress(event); var pos = time_from_progress(event);
self.controls.duration.innerHTML = duration_str(pos); self.progress.duration.innerHTML = duration_str(pos);
}, false); }, false);
}, },
update_on_air: function(url) { update_on_air: function() {
if(!url) { if(!this.on_air_url)
// TODO HERE return;
}
var self = this; var self = this;
window.setTimeout(function() {
self.update_on_air();
}, 60*1000);
if(!this.playlist.on_air)
return;
var req = new XMLHttpRequest(); var req = new XMLHttpRequest();
req.open('GET', url, true); req.open('GET', this.on_air_url, true);
req.onreadystatechange = function() { req.onreadystatechange = function() {
if(req.readyState != 4 || (req.status != 200 && req.status != 0)) if(req.readyState != 4 || (req.status != 200 &&
req.status != 0))
return; return;
var data = JSON.parse(req.responseText) var data = JSON.parse(req.responseText)
if(data.type == 'track') { if(data.type == 'track')
self.on_air.querySelector('.info').innerHTML = '♫'; data = {
self.on_air.querySelector('.title') = title: '♫' + (data.artist ? data.artist + ' — ' : '') +
(data.artist || '') + ' — ' + (data.title); data.title,
self.on_air.querySelector('.url').removeAttribute('href'); url: ''
} }
else { else
self.on_air.querySelector('.info').innerHTML = ''; data = {
self.on_air.querySelector('.title').innerHTML = data.title; title: data.title,
self.on_air.querySelector('.url').setAttribute('href', data.url); info: '',
} url: data.url
}
if(timeout) var on_air = self.playlist.on_air;
window.setTimeout(function() { on_air = on_air.item.querySelector('.content');
self.update_on_air(url);
}, 60*1000); if(data.url)
on_air.innerHTML =
'<a href="' + data.url + '">' + data.title + '</a>';
else
on_air.innerHTML = data.title;
}; };
req.send(); req.send();
}, },
@ -263,78 +361,44 @@ Player.prototype = {
this.audio.pause(); this.audio.pause();
}, },
unselect: function(sound) {
sound.item.removeAttribute('selected');
},
__mime_type: function(path) { __mime_type: function(path) {
ext = path.substr(path.lastIndexOf('.')+1); ext = path.substr(path.lastIndexOf('.')+1);
return 'audio/' + ext; return 'audio/' + ext;
}, },
select: function(sound, play = true) { load_sound: function(sound) {
if(this.sound == sound) { var audio = this.audio;
if(play) audio.pause();
this.play();
return;
}
if(this.sound) var sources = audio.querySelectorAll('source');
this.unselect(this.sound); for(var i = 0; i < sources.length; i++)
audio.removeChild(sources[i]);
this.audio.pause();
// streams as <source>
var sources = this.audio.querySelectorAll('source');
for(var i = 0; i < sources.length; i++) {
this.audio.removeChild(sources[i]);
}
streams = sound.streams; streams = sound.streams;
for(var i = 0; i < streams.length; i++) { for(var i = 0; i < streams.length; i++) {
var source = document.createElement('source'); var source = document.createElement('source');
source.src = streams[i]; source.src = streams[i];
source.type = this.__mime_type(source.src); source.type = this.__mime_type(source.src);
this.audio.appendChild(source); audio.appendChild(source);
} }
this.audio.load(); audio.load();
// attributes
this.sound = sound;
sound.item.setAttribute('selected', 'true');
if(sound.seekable)
this.player.setAttribute('seekable', 'true');
else
this.player.removeAttribute('seekable');
// play
if(play)
this.play();
},
next: function() {
var index = this.playlist.sounds.indexOf(this.sound);
if(index < 0)
return;
index++;
if(index < this.playlist.sounds.length)
this.select(this.playlist.sounds[index], true);
}, },
save: function() { save: function() {
// TODO: move stored sound into playlist
this.store.set('player', { this.store.set('player', {
single: this.controls.single.checked, single: this.controls.single.checked,
sound: this.sound && this.sound.streams, sound: this.playlist.sound && this.playlist.sound.streams,
}); });
}, },
load: function() { load: function() {
var data = this.store.get('player'); var data = this.store.get('player');
if(!data)
return;
this.controls.single.checked = data.single; this.controls.single.checked = data.single;
if(data.sound) if(data.sound)
this.sound = this.playlist.find(data.sound); this.playlist.sound = this.playlist.find(data.sound);
}, },
} }

View File

@ -67,8 +67,13 @@
</nav> </nav>
</div> </div>
{% block footer %}
<footer class="footer"> <footer class="footer">
{% render_sections position="footer" %} {% render_sections position="footer" %}
<div class="small">Propulsed by
<a href="https://github.com/bkfox/aircox">Aircox</a>
</div>
</footer> </footer>
{% endblock %}
</body> </body>
</html> </html>

View File

@ -34,16 +34,26 @@
{% if podcasts %} {% if podcasts %}
<section class="podcasts list"> <section class="podcasts list">
<h2>{% trans "Podcasts" %}</h2> <h2>{% trans "Podcasts" %}</h2>
{% for item in podcasts %} <div id="player_diff_{{ page.id }}" class="player">
{% include 'cms/snippets/sound_list_item.html' %} {% include 'cms/snippets/player.html' %}
{% endfor %}
<script>
var podcasts = new Player('player_diff_{{ page.id }}', undefined, true)
{% for item in podcasts %}
podcasts.playlist.add(new Sound(
title='{{ item.name|escape }}',
detail='{{ item.detail_url }}',
duration={{ item.duration|date:"H*3600+i*60+s" }},
streams='{{ item.url }}',
{% if page and page.cover %}cover='{{ page.icon }}',{% endif %}
undefined
));
{% endfor %}
</script>
</div>
</section> </section>
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{# TODO: podcasts #}
{% endblock %} {% endblock %}

View File

@ -6,23 +6,30 @@
{% load aircox_cms %} {% load aircox_cms %}
{% if not object_list %}
{% block title %}
<h1 class="detail_title">{{ page.title }}</h1>
{% endblock %}
{% endif %}
{% block content %} {% block content %}
{% if object_list %} {% if object_list %}
{# list view #} {# list view #}
<div class="body summary"> <section class="body summary">
{{ page.summary }} {{ page.summary }}
<a href="?" class="go_back">{% trans "Go back to the publication" %}</a> <a href="?" class="go_back">{% trans "Go back to the publication" %}</a>
</div> </section>
{% with list_paginator=paginator %} {% with list_paginator=paginator %}
{% include "cms/snippets/list.html" %} {% include "cms/snippets/list.html" %}
{% endwith %} {% endwith %}
{% else %} {% else %}
{# detail view #} {# detail view #}
{% if page.cover %}
{% image page.cover max-600x480 class="detail_cover cover" height="" width="" %}
{% endif %}
<div class="content"> <div class="content">
{% if page.cover %}
{% image page.cover max-600x480 class="cover" height="" width="" %}
{% endif %}
<section class="body"> <section class="body">
{{ page.body|richtext}} {{ page.body|richtext}}
</section> </section>

View File

@ -7,11 +7,10 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
{% with url=url url_text=self.url_text %}
{% include "cms/snippets/list.html" %} {% include "cms/snippets/list.html" %}
{% endwith %}
{% if url %}
<nav><a href="{{ url }}">{{ self.url_text }}</a></nav>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -1,59 +1,24 @@
{% extends 'cms/sections/section_item.html' %} {% extends 'cms/sections/section_item.html' %}
{% load staticfiles %}
{% load i18n %}
{% block content %} {% block content %}
<style>
</style>
<div id="player" class="player"> <div id="player" class="player">
<audio preload="metadata"> {% include "cms/snippets/player.html" %}
{% trans "Your browser does not support the <code>audio</code> element." %}
{% for stream in streams %}
<source src="{{ stream }}" />
{% endfor %}
</audio>
<div class="controls flex_row">
<progress class="flex_item" value="0" max="1"></progress>
<span class="info duration"></span>
<input type="checkbox" class="single" id="player_single_mode">
<label for="player_single_mode" class="info" title="{% trans "single mode" %}"></label>
</div>
<div class="playlist">
<li class='item list_item flex_row' style="display: none;">
<div class="button">
<img src="{% static "cms/images/play.png" %}" class="play"
title="{% trans "play" %}" />
<img src="{% static "cms/images/pause.png" %}" class="pause"
title="{% trans "pause" %}" />
<img src="{% static "cms/images/loading.png" %}" class="loading"
title="{% trans "loading..." %}" />
</div>
<h3 class="title flex_item">{{ self.live_title }}</h3>
<div class="actions">
<a class="action detail" title="{% trans "more informations" %}"></a>
<a class="action remove" title="{% trans "remove this sound" %}"></a>
</div>
<span class="info duration"></span>
</li>
</div>
</div> </div>
<script> <script>
var player = new Player('player'); var player = new Player('player', '{% url 'controllers.on_air' %}');
var sound = player.playlist.add(new Sound('{{ self.live_title }}', '', undefined, var sound = player.playlist.add(
streams=[ new Sound(
{% for stream in streams %}'{{ stream }}',{% endfor %} '{{ self.live_title }}',
]), on_air_url = '{% url 'controllers.on_air' %}'); '', undefined,
player.select(sound, false); streams=[ {% for stream in streams %}'{{ stream }}',{% endfor %} ],
cover = undefined,
on_air = true
)
);
sound.item.className += ' live';
player.playlist.select(sound, false);
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,7 @@
{% extends "cms/sections/section_item.html" %} {% extends "cms/sections/section_item.html" %}
{% load i18n %} {% load i18n %}
{% block content %}
<div class="meta"> <div class="meta">
<div class="author"> <div class="author">
{% if page.publish_as %} {% if page.publish_as %}
@ -24,4 +25,5 @@
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
{% endblock %}

View File

@ -56,6 +56,8 @@ Options:
{% endif %} {% endif %}
{% endwith %} {% endwith %}
</nav> </nav>
{% elif url and url_text %}
<nav><a href="{{ url }}">{{ url_text }}</a></nav>
{% endif %} {% endif %}
</ul> </ul>

View File

@ -0,0 +1,45 @@
{% load staticfiles %}
{% load i18n %}
<audio preload="metadata">
{% trans "Your browser does not support the <code>audio</code> element." %}
{% for stream in streams %}
<source src="{{ stream }}" />
{% endfor %}
</audio>
<div class="playlist">
<li class='item list_item flex_row' style="display: none;">
<div class="button">
<img src="{% static "cms/images/play.png" %}" class="play"
title="{% trans "play" %}" />
<img src="{% static "cms/images/pause.png" %}" class="pause"
title="{% trans "pause" %}" />
<img src="{% static "cms/images/loading.png" %}" class="loading"
title="{% trans "loading..." %}" />
</div>
<div class="flex_item">
<h3 class="title flex_item">{{ self.live_title }}</h3>
<div class="content flex_row">
<span class="info duration flex_item"></span>
<span class="actions">
<a class="action detail" title="{% trans "more informations" %}"></a>
<a class="action remove" title="{% trans "remove this sound" %}"></a>
</span>
</div>
</div>
</li>
</div>
<div class="controls">
<span class="progress">
<span class="info duration"></span>
<progress class="flex_item progress" value="0" max="1"></progress>
</span>
<input type="checkbox" class="single" id="player_single_mode">
<label for="player_single_mode" class="info"
title="{% trans "enable and disable single mode" %}">↻</label>
</div>

View File

@ -1,22 +1,37 @@
{% load static %} {% load static %}
{% load i18n %}
{# TODO: complete archive podcast -> info #} {# TODO: complete archive podcast -> info #}
<div class="list_item sound"> <script>
<a onclick="player.select(player.playlist.add(new Sound( function add_sound_{{ item.id }}(event) {
var sound = new Sound(
title='{{ item.name|escape }}', title='{{ item.name|escape }}',
detail='{{ item.detail_url }}', detail='{{ item.detail_url }}',
duration={{ item.duration|date:"H*3600+i*60+s" }}, duration={{ item.duration|date:"H*3600+i*60+s" }},
streams='{{ item.url }}')));" class="flex_row"> streams='{{ item.url }}',
{% if page and page.cover %}cover='{{ page.icon }}'{% endif %}
);
sound = player.playlist.add(sound);
if(event.target.dataset.action != 'add')
player.select(sound, true);
}
</script>
<a class="list_item sound flex_row" onclick="add_sound_{{ item.id }}(event)">
<img src="{% static "cms/images/listen.png" %}" class="icon"/> <img src="{% static "cms/images/listen.png" %}" class="icon"/>
<h3 class="flex_item">{{ item.name }}</h3> <h3 class="flex_item">{{ item.name }}</h3>
<span class="info"> <time class="info">
{% if item.duration.hour > 0 %} {% if item.duration.hour > 0 %}
{{ item.duration|date:'H:i:s' }} {{ item.duration|date:'H:i:s' }}
{% else %} {% else %}
{{ item.duration|date:'i:s' }} {{ item.duration|date:'i:s' }}
{% endif %} {% endif %}
</span> </time>
</a>
</div> <img src="{% static "cms/images/add.png" %}" class="icon"
data-action='add' alt="{% trans "add this sound to the playlist" %}"/>
</a>

View File

@ -1,14 +1,84 @@
from django.utils import timezone as tz from django.utils import timezone as tz
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.utils.html import format_html
from wagtail.wagtailcore import hooks from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem, Menu, SubmenuMenuItem from wagtail.wagtailadmin.menu import MenuItem, Menu, SubmenuMenuItem
from wagtail.contrib.modeladmin.options import \
ModelAdmin, ModelAdminGroup, modeladmin_register
import aircox.programs.models as programs import aircox.programs.models as programs
import aircox.cms.models as models
class ProgramAdmin(ModelAdmin):
model = programs.Program
menu_label = _('Programs')
menu_icon = 'pick'
menu_order = 200
list_display = ('name', 'active')
search_fields = ('name',)
class DiffusionAdmin(ModelAdmin):
model = programs.Diffusion
menu_label = _('Diffusions')
menu_icon = 'date'
menu_order = 200
list_display = ('program', 'start', 'end', 'frequency', 'initial')
list_filter = ('frequency', 'start', 'program')
class ScheduleAdmin(ModelAdmin):
model = programs.Schedule
menu_label = _('Schedules')
menu_icon = 'time'
menu_order = 200
list_display = ('program', 'frequency', 'duration', 'initial')
list_filter = ('frequency', 'date', 'duration', 'program')
class StreamAdmin(ModelAdmin):
model = programs.Stream
menu_label = _('Streams')
menu_icon = 'time'
menu_order = 200
list_display = ('program', 'delay', 'begin', 'end')
list_filter = ('program', 'delay', 'begin', 'end')
class AdvancedAdminGroup(ModelAdminGroup):
menu_label = _("Advanced")
menu_icon = 'plus-inverse'
items = (ProgramAdmin, DiffusionAdmin, ScheduleAdmin, StreamAdmin)
modeladmin_register(AdvancedAdminGroup)
class SoundAdmin(ModelAdmin):
model = programs.Sound
menu_label = _('Sounds')
menu_icon = 'media'
menu_order = 350
list_display = ('name', 'duration', 'type', 'path', 'good_quality', 'public')
list_filter = ('type', 'good_quality', 'public')
search_fields = ('name', 'path')
modeladmin_register(SoundAdmin)
## Hooks
@hooks.register('insert_editor_css')
def editor_css():
return format_html(
'<link rel="stylesheet" href="{}">',
static('cms/css/cms.css')
)
class GenericMenu(Menu): class GenericMenu(Menu):
last_time = None page_model = models.Publication
def __init__(self): def __init__(self):
super().__init__('') super().__init__('')
@ -19,40 +89,47 @@ class GenericMenu(Menu):
def get_title(self, item): def get_title(self, item):
pass pass
def get_parent_page(self, item): def get_parent(self, item):
pass pass
def get_page_url(self, item): def get_page_url(self, page_model, item):
if item.page.count(): if item.page.count():
return reverse('wagtailadmin_pages:edit', args=[item.page.first().id]) return reverse('wagtailadmin_pages:edit', args=[item.page.first().id])
parent_page = self.get_parent_page(item)
parent_page = self.get_parent(item)
if not parent_page: if not parent_page:
return '' return ''
return reverse('wagtailadmin_pages:add_subpage', args=[parent_page.id])
return reverse(
'wagtailadmin_pages:add', args= [
page_model._meta.app_label, page_model._meta.model_name,
parent_page.id
]
)
@property @property
def registered_menu_items(self): def registered_menu_items(self):
now = tz.now() now = tz.now()
last_max = now - tz.timedelta(minutes = 10) last_max = now - tz.timedelta(minutes = 10)
if self._registered_menu_items is None or self.last_time < last_max: qs = self.get_queryset()
qs = self.get_queryset() return [
self._registered_menu_items = [ MenuItem(self.get_title(x), self.get_page_url(self.page_model, x))
MenuItem(self.get_title(x), self.get_page_url(x)) for x in qs
for x in qs ]
]
self.last_time = now
return self._registered_menu_items
class DiffusionsMenu(GenericMenu): class DiffusionsMenu(GenericMenu):
""" """
Menu to display diffusions of today Menu to display diffusions of today
""" """
page_model = models.DiffusionPage
def get_queryset(self): def get_queryset(self):
return programs.Diffusion.objects.filter( return programs.Diffusion.objects.filter(
type = programs.Diffusion.Type.normal, type = programs.Diffusion.Type.normal,
start__contains = tz.now().date(), start__contains = tz.now().date(),
initial__isnull = True,
).order_by('start') ).order_by('start')
def get_title(self, item): def get_title(self, item):
@ -74,6 +151,8 @@ class ProgramsMenu(GenericMenu):
""" """
Menu to display all active programs. Menu to display all active programs.
""" """
page_model = models.DiffusionPage
def get_queryset(self): def get_queryset(self):
return programs.Program.objects \ return programs.Program.objects \
.filter(active = True, page__isnull = False) \ .filter(active = True, page__isnull = False) \
@ -92,7 +171,6 @@ class ProgramsMenu(GenericMenu):
return settings.default_program_parent_page return settings.default_program_parent_page
@hooks.register('register_admin_menu_item') @hooks.register('register_admin_menu_item')
def register_programs_menu_item(): def register_programs_menu_item():
return SubmenuMenuItem( return SubmenuMenuItem(
@ -101,4 +179,3 @@ def register_programs_menu_item():
) )

View File

@ -64,7 +64,7 @@ def on_air(request):
last = { last = {
'type': 'diffusion', 'type': 'diffusion',
'title': publication.title if publication else last.program.name, 'title': last.program.name,
'date': last.start, 'date': last.start,
'url': publication.specific.url if publication else None, 'url': publication.specific.url if publication else None,
} }

View File

@ -26,10 +26,10 @@ This file is used as a reminder, can be used as crappy documentation too.
- controllers : - controllers :
- models to template -> note - models to template -> note
- input stream
- streamed program disable -> remote control on liquidsoap - streamed program disable -> remote control on liquidsoap
- tests: - tests:
- monitor - monitor
- check when a played sound has a temp blank
- config generation and sound diffusion - config generation and sound diffusion
- cms: - cms:

View File

@ -268,38 +268,6 @@ class Sound(Nameable):
verbose_name_plural = _('Sounds') verbose_name_plural = _('Sounds')
class Stream(models.Model):
"""
When there are no program scheduled, it is possible to play sounds
in order to avoid blanks. A Stream is a Program that plays this role,
and whose linked to a Stream.
All sounds that are marked as good and that are under the related
program's archive dir are elligible for the sound's selection.
"""
program = models.ForeignKey(
'Program',
verbose_name = _('related program'),
)
delay = models.TimeField(
_('delay'),
blank = True, null = True,
help_text = _('delay between two sound plays')
)
begin = models.TimeField(
_('begin'),
blank = True, null = True,
help_text = _('used to define a time range this stream is'
'played')
)
end = models.TimeField(
_('end'),
blank = True, null = True,
help_text = _('used to define a time range this stream is'
'played')
)
class Schedule(models.Model): class Schedule(models.Model):
""" """
A Schedule defines time slots of programs' diffusions. It can be an initial A Schedule defines time slots of programs' diffusions. It can be an initial
@ -636,6 +604,38 @@ class DiffusionManager(models.Manager):
).order_by('start') ).order_by('start')
class Stream(models.Model):
"""
When there are no program scheduled, it is possible to play sounds
in order to avoid blanks. A Stream is a Program that plays this role,
and whose linked to a Stream.
All sounds that are marked as good and that are under the related
program's archive dir are elligible for the sound's selection.
"""
program = models.ForeignKey(
'Program',
verbose_name = _('related program'),
)
delay = models.TimeField(
_('delay'),
blank = True, null = True,
help_text = _('delay between two sound plays')
)
begin = models.TimeField(
_('begin'),
blank = True, null = True,
help_text = _('used to define a time range this stream is'
'played')
)
end = models.TimeField(
_('end'),
blank = True, null = True,
help_text = _('used to define a time range this stream is'
'played')
)
class Diffusion(models.Model): class Diffusion(models.Model):
""" """
A Diffusion is an occurrence of a Program that is scheduled on the A Diffusion is an occurrence of a Program that is scheduled on the