forked from rc/aircox
rewrite tests + fix error in schedule generator
This commit is contained in:
@ -414,6 +414,9 @@ class DiffusionPage(Publication):
|
||||
related_name = 'page',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
limit_choices_to = {
|
||||
'initial__isnull': True,
|
||||
},
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -423,7 +426,7 @@ class DiffusionPage(Publication):
|
||||
content_panels = [
|
||||
FieldPanel('diffusion'),
|
||||
] + Publication.content_panels + [
|
||||
InlinePanel('tracks', label=_('Tracks'))
|
||||
InlinePanel('tracks', label=_('Tracks')),
|
||||
]
|
||||
|
||||
|
||||
|
@ -890,8 +890,8 @@ class SectionLogsList(SectionItem):
|
||||
@register_snippet
|
||||
class SectionTimetable(SectionItem,DatedListBase):
|
||||
class Meta:
|
||||
verbose_name = _('timetable')
|
||||
verbose_name_plural = _('timetable')
|
||||
verbose_name = _('Section: Timetable')
|
||||
verbose_name_plural = _('Sections: Timetable')
|
||||
|
||||
panels = SectionItem.panels + DatedListBase.panels
|
||||
|
||||
@ -914,8 +914,8 @@ class SectionTimetable(SectionItem,DatedListBase):
|
||||
@register_snippet
|
||||
class SectionPublicationInfo(SectionItem):
|
||||
class Meta:
|
||||
verbose_name = _('section with publication\'s info')
|
||||
verbose_name = _('sections with publication\'s info')
|
||||
verbose_name = _('Section: publication\'s info')
|
||||
verbose_name_plural = _('Sections: publication\'s info')
|
||||
|
||||
@register_snippet
|
||||
class SectionSearchField(SectionItem):
|
||||
@ -933,8 +933,8 @@ class SectionSearchField(SectionItem):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('search field')
|
||||
verbose_name_plural = _('search fields')
|
||||
verbose_name = _('Section: search field')
|
||||
verbose_name_plural = _('Sections: search field')
|
||||
|
||||
panels = SectionItem.panels + [
|
||||
PageChooserPanel('page'),
|
||||
@ -946,6 +946,32 @@ class SectionSearchField(SectionItem):
|
||||
context = super().get_context(request, page)
|
||||
list_page = self.page or ListPage.objects.live().first()
|
||||
context['list_page'] = list_page
|
||||
print(context, self.template)
|
||||
return context
|
||||
|
||||
|
||||
@register_snippet
|
||||
class SectionPlayer(SectionItem):
|
||||
live_title = models.CharField(
|
||||
_('live title'),
|
||||
max_length = 32,
|
||||
help_text = _('text to display when it plays live'),
|
||||
)
|
||||
streams = models.TextField(
|
||||
_('audio streams'),
|
||||
help_text = _('one audio stream per line'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Section: Player')
|
||||
|
||||
panels = SectionItem.panels + [
|
||||
FieldPanel('live_title'),
|
||||
FieldPanel('streams'),
|
||||
]
|
||||
|
||||
def get_context(self, request, page):
|
||||
context = super().get_context(request, page)
|
||||
context['streams'] = self.streams.split('\r\n')
|
||||
return context
|
||||
|
||||
|
||||
|
280
cms/static/cms/js/player.js
Normal file
280
cms/static/cms/js/player.js
Normal file
@ -0,0 +1,280 @@
|
||||
// TODO
|
||||
// - multiple sources for an item
|
||||
// - live streams as item;
|
||||
// - add to playlist button
|
||||
//
|
||||
|
||||
/// Return a human-readable string from seconds
|
||||
function duration_str(seconds) {
|
||||
seconds = Math.floor(seconds);
|
||||
var hours = Math.floor(seconds / 3600);
|
||||
seconds -= hours;
|
||||
var minutes = Math.floor(seconds / 60);
|
||||
seconds -= minutes;
|
||||
|
||||
var str = hours ? (hours < 10 ? '0' + hours : hours) + ':' : '';
|
||||
str += (minutes < 10 ? '0' + minutes : minutes) + ':';
|
||||
str += (seconds < 10 ? '0' + seconds : seconds);
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
function Sound(title, detail, stream, duration) {
|
||||
this.title = title;
|
||||
this.detail = detail;
|
||||
this.stream = stream;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
Sound.prototype = {
|
||||
title: '',
|
||||
detail: '',
|
||||
stream: '',
|
||||
duration: undefined,
|
||||
|
||||
item: undefined,
|
||||
|
||||
get seekable() {
|
||||
return this.duration != undefined;
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
function PlayerPlaylist(player) {
|
||||
this.player = player;
|
||||
this.playlist = player.player.querySelector('.playlist');
|
||||
this.item_ = player.player.querySelector('.playlist .item');
|
||||
this.items = []
|
||||
}
|
||||
|
||||
PlayerPlaylist.prototype = {
|
||||
items: undefined,
|
||||
|
||||
find: function(stream) {
|
||||
return this.items.find(function(v) {
|
||||
return v.stream == item;
|
||||
});
|
||||
},
|
||||
|
||||
add: function(sound, container) {
|
||||
if(this.find(sound.stream))
|
||||
return;
|
||||
|
||||
var item = this.item_.cloneNode(true);
|
||||
item.removeAttribute('style');
|
||||
|
||||
console.log(sound)
|
||||
item.querySelector('.title').innerHTML = sound.title;
|
||||
if(sound.seekable)
|
||||
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
|
||||
);
|
||||
|
||||
(container || this.playlist).appendChild(item);
|
||||
this.items.push(sound);
|
||||
this.save();
|
||||
},
|
||||
|
||||
remove: function(sound) {
|
||||
var index = this.items.indexOf(sound);
|
||||
if(index != -1)
|
||||
this.items.splice(index,1);
|
||||
this.playlist.removeChild(sound.item);
|
||||
this.save();
|
||||
},
|
||||
|
||||
save: function() {
|
||||
var list = [];
|
||||
for(var i in this.items) {
|
||||
var sound = Object.assign({}, this.items[i])
|
||||
delete sound.item;
|
||||
list.push(sound);
|
||||
}
|
||||
this.player.store.set('playlist', list);
|
||||
},
|
||||
|
||||
load: function() {
|
||||
var list = [];
|
||||
var container = document.createDocumentFragment();
|
||||
for(var i in list)
|
||||
this.add(list[i], container)
|
||||
this.playlist.appendChild(container);
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
function Player(id) {
|
||||
this.store = new Store('player');
|
||||
|
||||
// html items
|
||||
this.player = document.getElementById(id);
|
||||
this.box = this.player.querySelector('.box');
|
||||
this.audio = this.player.querySelector('audio');
|
||||
this.controls = {
|
||||
duration: this.box.querySelector('.duration'),
|
||||
progress: this.player.querySelector('progress'),
|
||||
single: this.player.querySelector('input.single'),
|
||||
}
|
||||
|
||||
this.playlist = new PlayerPlaylist(this);
|
||||
this.playlist.load();
|
||||
|
||||
this.init_events();
|
||||
this.load();
|
||||
}
|
||||
|
||||
Player.prototype = {
|
||||
/// current item being played
|
||||
sound: undefined,
|
||||
|
||||
init_events: function() {
|
||||
var self = this;
|
||||
|
||||
function time_from_progress(event) {
|
||||
bounding = self.controls.progress.getBoundingClientRect()
|
||||
offset = (event.clientX - bounding.left);
|
||||
return offset * self.audio.duration / bounding.width;
|
||||
}
|
||||
|
||||
function update_info() {
|
||||
var controls = self.controls;
|
||||
// progress
|
||||
if( !self.sound.seekable ||
|
||||
self.audio.duration == Infinity) {
|
||||
controls.duration.innerHTML = '';
|
||||
controls.progress.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var pos = self.audio.currentTime;
|
||||
controls.progress.value = pos;
|
||||
controls.progress.max = self.audio.duration;
|
||||
controls.duration.innerHTML = duration_str(sound.duration);
|
||||
}
|
||||
|
||||
// audio
|
||||
this.audio.addEventListener('playing', function() {
|
||||
self.player.setAttribute('state', 'playing');
|
||||
}, false);
|
||||
|
||||
this.audio.addEventListener('pause', function() {
|
||||
self.player.setAttribute('state', 'paused');
|
||||
}, false);
|
||||
|
||||
this.audio.addEventListener('loadstart', function() {
|
||||
self.player.setAttribute('state', 'stalled');
|
||||
}, false);
|
||||
|
||||
this.audio.addEventListener('loadeddata', function() {
|
||||
self.player.removeAttribute('state');
|
||||
}, false);
|
||||
|
||||
this.audio.addEventListener('timeupdate', update_info, false);
|
||||
|
||||
this.audio.addEventListener('ended', function() {
|
||||
if(!self.controls.single.checked)
|
||||
self.next(true);
|
||||
}, false);
|
||||
|
||||
// buttons
|
||||
this.box.querySelector('button.play').onclick = function() {
|
||||
self.play();
|
||||
};
|
||||
|
||||
// progress
|
||||
progress = this.controls.progress;
|
||||
progress.addEventListener('click', function(event) {
|
||||
player.audio.currentTime = time_from_progress(event);
|
||||
}, false);
|
||||
|
||||
progress.addEventListener('mouseout', update_info, false);
|
||||
|
||||
progress.addEventListener('mousemove', function(event) {
|
||||
if(self.audio.duration == Infinity)
|
||||
return;
|
||||
|
||||
var pos = time_from_progress(event);
|
||||
self.controls.duration.innerHTML = duration_str(pos);
|
||||
}, false);
|
||||
},
|
||||
|
||||
play: function() {
|
||||
if(this.audio.paused)
|
||||
this.audio.play();
|
||||
else
|
||||
this.audio.pause();
|
||||
},
|
||||
|
||||
unselect: function(sound) {
|
||||
sound.item.removeAttribute('selected');
|
||||
},
|
||||
|
||||
select: function(sound, play = true) {
|
||||
if(this.sound)
|
||||
this.unselect(this.sound);
|
||||
|
||||
this.audio.pause();
|
||||
|
||||
// if stream is a list, use <source>
|
||||
if(sound.stream.splice) {
|
||||
this.audio.src="";
|
||||
|
||||
var sources = this.audio.querySelectorAll('source');
|
||||
for(var i in sources)
|
||||
this.audio.removeChild(sources[i]);
|
||||
|
||||
for(var i in sound.stream) {
|
||||
var source = document.createElement('source');
|
||||
source.src = sound.stream[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
this.audio.src = sound.stream;
|
||||
this.audio.load();
|
||||
|
||||
this.sound = sound;
|
||||
sound.item.setAttribute('selected', 'true');
|
||||
|
||||
this.box.querySelector('.title').innerHTML = sound.title;
|
||||
if(play)
|
||||
this.play();
|
||||
},
|
||||
|
||||
next: function() {
|
||||
var index = this.playlist.items.indexOf(this.sound);
|
||||
if(index < 0)
|
||||
return;
|
||||
|
||||
index++;
|
||||
if(index < this.playlist.items.length)
|
||||
this.select(this.playlist.items[index], true);
|
||||
},
|
||||
|
||||
save: function() {
|
||||
this.store.set('player', {
|
||||
single: this.controls.single.checked,
|
||||
sound: this.sound && this.sound.stream,
|
||||
});
|
||||
},
|
||||
|
||||
load: function() {
|
||||
var data = this.store.get('player');
|
||||
this.controls.single.checked = data.single;
|
||||
this.sound = this.playlist.find(data.stream);
|
||||
},
|
||||
|
||||
update_on_air: function() {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
|
68
cms/static/cms/js/utils.js
Normal file
68
cms/static/cms/js/utils.js
Normal file
@ -0,0 +1,68 @@
|
||||
|
||||
/// Helper to provide a tab+panel functionnality; the tab and the selected
|
||||
/// element will have an attribute "selected".
|
||||
/// We assume a common ancestor between tab and panel at a maximum level
|
||||
/// of 2.
|
||||
/// * tab: corresponding tab
|
||||
/// * panel_selector is used to select the right panel object.
|
||||
function select_tab(tab, panel_selector) {
|
||||
var parent = tab.parentNode.parentNode;
|
||||
var panel = parent.querySelector(panel_selector);
|
||||
|
||||
// unselect
|
||||
var qs = parent.querySelectorAll('*[selected]');
|
||||
for(var i = 0; i < qs.length; i++)
|
||||
if(qs[i] != tab && qs[i] != panel)
|
||||
qs[i].removeAttribute('selected');
|
||||
|
||||
panel.setAttribute('selected', 'true');
|
||||
tab.setAttribute('selected', 'true');
|
||||
}
|
||||
|
||||
|
||||
/// Utility to store objects in local storage. Data are stringified in JSON
|
||||
/// format in order to keep type.
|
||||
function Store(prefix) {
|
||||
this.prefix = prefix;
|
||||
}
|
||||
|
||||
Store.prototype = {
|
||||
// save data to localstorage, or remove it if data is null
|
||||
set: function(key, data) {
|
||||
key = this.prefix + '.' + key;
|
||||
if(data == undefined) {
|
||||
localStorage.removeItem(prefix);
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(key, JSON.stringify(data))
|
||||
},
|
||||
|
||||
// load data from localstorage
|
||||
get: function(key) {
|
||||
try {
|
||||
key = this.prefix + '.' + key;
|
||||
var data = localStorage.getItem(key);
|
||||
if(data)
|
||||
return JSON.parse(data);
|
||||
}
|
||||
catch(e) { console.log(e, data); }
|
||||
},
|
||||
|
||||
// return true if the given item is stored
|
||||
exists: function(key) {
|
||||
key = this.prefix + '.' + key;
|
||||
return (localStorage.getItem(key) != null);
|
||||
},
|
||||
|
||||
// update a field in the stored data
|
||||
update: function(key, field_key, value) {
|
||||
data = this.get(key) || {};
|
||||
if(value)
|
||||
data[field_key] = value;
|
||||
else
|
||||
delete data[field_key];
|
||||
this.set(key, data);
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,9 @@
|
||||
{% block css_extras %}{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
<script src="{% static 'cms/js/utils.js' %}"></script>
|
||||
<script src="{% static 'cms/js/player.js' %}"></script>
|
||||
|
||||
<title>{{ page.title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
|
61
cms/templates/cms/sections/section_player.html
Normal file
61
cms/templates/cms/sections/section_player.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% extends 'cms/sections/section_item.html' %}
|
||||
|
||||
{% load staticfiles %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
<div id="player">
|
||||
<div class="box">
|
||||
<audio preload="metadata">
|
||||
{% trans "Your browser does not support the <code>audio</code> element." %}
|
||||
{% for stream in streams %}
|
||||
<source src="{{ stream }}" />
|
||||
{% endfor %}
|
||||
</audio>
|
||||
|
||||
<button class="play" onclick="Player.play()"
|
||||
title="{% trans "play/pause" %}"></button>
|
||||
|
||||
<h3 class="title">{{ self.live_title }}</h3>
|
||||
|
||||
<div>
|
||||
<div class="info duration"></div>
|
||||
<progress value="0" max="1"></progress>
|
||||
|
||||
<input type="checkbox" class="single" id="player_single_mode">
|
||||
<label for="player_single_mode"></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="playlist">
|
||||
<li class='item' style="display: none;">
|
||||
<h2 class="title">{{ self.live_title }}</h2>
|
||||
<div class="info duration"></div>
|
||||
<div class="actions">
|
||||
<a class="action detail" title="{% trans "more informations" %}">➔</a>
|
||||
<a class="action remove" title="{% trans "remove this sound" %}">✖</a>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
<div class='item on_air'>
|
||||
<h2 class="title"></h2>
|
||||
<a class="url">➔</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var player = new Player('player');
|
||||
player.playlist.add(new Sound('{{ self.live_title }}', '', [
|
||||
{% for stream in streams %}'{{ stream }}',{% endfor %}
|
||||
]));
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
@ -1,27 +1,5 @@
|
||||
{% load i18n %}
|
||||
|
||||
<script>
|
||||
/// Function used to select a panel on a tab selection.
|
||||
/// The tab should be at max level -2 of the main container
|
||||
/// The panel must have a class "panel"
|
||||
function select_tab(target) {
|
||||
parent = target.parentNode.parentNode;
|
||||
|
||||
var date = target.dataset.date;
|
||||
panel = parent.querySelector('.panel[data-date="' + date + '"]');
|
||||
|
||||
// unselect
|
||||
qs = parent.querySelectorAll('*[selected]');
|
||||
for(var i = 0; i < qs.length; i++)
|
||||
if(qs[i].dataset.date != date)
|
||||
qs[i].removeAttribute('selected');
|
||||
|
||||
console.log(panel, target, date);
|
||||
panel.setAttribute('selected', 'true');
|
||||
target.setAttribute('selected', 'true');
|
||||
}
|
||||
</script>
|
||||
|
||||
{# FIXME: get current complete URL #}
|
||||
<div class="list date_list">
|
||||
{% if nav_dates %}
|
||||
@ -31,7 +9,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% for day in nav_dates.dates %}
|
||||
<a onclick="select_tab(this);" data-date="day_{{day|date:"Y-m-d"}}"
|
||||
<a onclick="select_tab(this, '.panel[data-date=\'{{day|date:"Y-m-d"}}\']');"
|
||||
{% if day == nav_dates.date %}selected{% endif %}
|
||||
class="tab {% if day == nav_dates.date %}today{% endif %}">
|
||||
{{ day|date:'D. d' }}
|
||||
@ -47,7 +25,7 @@
|
||||
{% for day, list in object_list %}
|
||||
<ul class="panel {% if day == nav_dates.date %}class="today"{% endif %}"
|
||||
{% if day == nav_dates.date %}selected{% endif %}
|
||||
data-date="day_{{day|date:"Y-m-d"}}">
|
||||
data-date="{{day|date:"Y-m-d"}}">
|
||||
{# you might like to hide it by default -- this more for sections #}
|
||||
<h2>{{ day|date:'l d F' }}</h2>
|
||||
{% with object_list=list item_date_format="H:i" %}
|
||||
|
Reference in New Issue
Block a user