work on admin interface, player, list of sounds
This commit is contained in:
		@ -62,7 +62,7 @@ class Command (BaseCommand):
 | 
			
		||||
                start__gt = tz.now().date() - tz.timedelta(days = 20),
 | 
			
		||||
                page__isnull = True,
 | 
			
		||||
                initial__isnull = True
 | 
			
		||||
            )
 | 
			
		||||
            ).exclude(type = Diffusion.Type.unconfirmed)
 | 
			
		||||
            for diffusion in qs:
 | 
			
		||||
                if not diffusion.program.page.count():
 | 
			
		||||
                    if not hasattr(diffusion.program, '__logged_diff_error'):
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,7 @@ import aircox.programs.models as programs
 | 
			
		||||
import aircox.controllers.models as controllers
 | 
			
		||||
import aircox.cms.settings as settings
 | 
			
		||||
 | 
			
		||||
from aircox.cms.utils import image_url
 | 
			
		||||
from aircox.cms.sections import *
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -163,6 +164,15 @@ class Comment(models.Model):
 | 
			
		||||
        _('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):
 | 
			
		||||
        self.author = bleach.clean(self.author, tags=[])
 | 
			
		||||
        if self.email:
 | 
			
		||||
@ -263,6 +273,15 @@ class Publication(Page):
 | 
			
		||||
        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
 | 
			
		||||
    def recents(self):
 | 
			
		||||
        return self.get_children().type(Publication).not_in_menu().live() \
 | 
			
		||||
@ -390,10 +409,8 @@ class Track(programs.Track,Orderable):
 | 
			
		||||
    diffusion = ParentalKey('DiffusionPage',
 | 
			
		||||
                            related_name='tracks')
 | 
			
		||||
    panels = [
 | 
			
		||||
        FieldRowPanel([
 | 
			
		||||
            FieldPanel('artist'),
 | 
			
		||||
            FieldPanel('title'),
 | 
			
		||||
        ]),
 | 
			
		||||
        FieldPanel('artist'),
 | 
			
		||||
        FieldPanel('title'),
 | 
			
		||||
        FieldPanel('tags'),
 | 
			
		||||
        FieldPanel('info'),
 | 
			
		||||
    ]
 | 
			
		||||
@ -428,13 +445,15 @@ class DiffusionPage(Publication):
 | 
			
		||||
        verbose_name = _('Diffusion')
 | 
			
		||||
        verbose_name_plural = _('Diffusions')
 | 
			
		||||
 | 
			
		||||
    content_panels = [
 | 
			
		||||
        FieldPanel('diffusion'),
 | 
			
		||||
        FieldPanel('publish_archive'),
 | 
			
		||||
    ] + Publication.content_panels + [
 | 
			
		||||
    content_panels = Publication.content_panels + [
 | 
			
		||||
        InlinePanel('tracks', label=_('Tracks')),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    promote_panels = [
 | 
			
		||||
        # FieldPanel('diffusion'),
 | 
			
		||||
        FieldPanel('publish_archive'),
 | 
			
		||||
    ] + Publication.promote_panels
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_diffusion(cl, diff, model = None, **kwargs):
 | 
			
		||||
        model = model or cl
 | 
			
		||||
@ -519,8 +538,6 @@ class DiffusionPage(Publication):
 | 
			
		||||
                    podcast.public = publish
 | 
			
		||||
                    podcast.save()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        super().save(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -221,19 +221,18 @@ class ListBase(models.Model):
 | 
			
		||||
            else:
 | 
			
		||||
                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
 | 
			
		||||
            if self.date_filter == self.DateFilter.before_related:
 | 
			
		||||
                qs = qs.filter(date__lt = date)
 | 
			
		||||
            elif self.date_filter == self.DateFilter.after_related:
 | 
			
		||||
                qs = qs.filter(date__gte = date)
 | 
			
		||||
        # date
 | 
			
		||||
        else:
 | 
			
		||||
            date = tz.now()
 | 
			
		||||
            if self.date_filter == self.DateFilter.previous:
 | 
			
		||||
                qs = qs.filter(date__lt = date)
 | 
			
		||||
            elif self.date_filter == self.DateFilter.next:
 | 
			
		||||
                qs = qs.filter(date__gte = date)
 | 
			
		||||
        date = tz.now()
 | 
			
		||||
        if self.date_filter == self.DateFilter.previous:
 | 
			
		||||
            qs = qs.filter(date__lt = date)
 | 
			
		||||
        elif self.date_filter == self.DateFilter.next:
 | 
			
		||||
            qs = qs.filter(date__gte = date)
 | 
			
		||||
 | 
			
		||||
        # sort
 | 
			
		||||
        if self.asc:
 | 
			
		||||
@ -332,9 +331,7 @@ class ListBase(models.Model):
 | 
			
		||||
        search = request.GET.get('search')
 | 
			
		||||
        if search:
 | 
			
		||||
            kwargs['terms'] = search
 | 
			
		||||
            print(search, qs)
 | 
			
		||||
            qs = qs.search(search)
 | 
			
		||||
            print(qs.count())
 | 
			
		||||
 | 
			
		||||
        set('list_selector', kwargs)
 | 
			
		||||
 | 
			
		||||
@ -342,7 +339,7 @@ class ListBase(models.Model):
 | 
			
		||||
        if qs:
 | 
			
		||||
            paginator = Paginator(qs, 30)
 | 
			
		||||
            try:
 | 
			
		||||
                qs = paginator.page('page')
 | 
			
		||||
                qs = paginator.page(request.GET.get('page') or 1)
 | 
			
		||||
            except PageNotAnInteger:
 | 
			
		||||
                qs = paginator.page(1)
 | 
			
		||||
            except EmptyPage:
 | 
			
		||||
@ -803,7 +800,7 @@ class SectionList(ListBase, SectionRelativeItem):
 | 
			
		||||
                      'list. If empty, does not print an address'),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    panels = SectionItem.panels + [
 | 
			
		||||
    panels = SectionRelativeItem.panels + [
 | 
			
		||||
        MultiFieldPanel([
 | 
			
		||||
        FieldPanel('focus_available'),
 | 
			
		||||
        FieldPanel('count'),
 | 
			
		||||
@ -815,6 +812,9 @@ class SectionList(ListBase, SectionRelativeItem):
 | 
			
		||||
        from aircox.cms.models import Publication
 | 
			
		||||
        context = super().get_context(request, page)
 | 
			
		||||
 | 
			
		||||
        if self.is_related:
 | 
			
		||||
            self.related = page
 | 
			
		||||
 | 
			
		||||
        qs = self.get_queryset()
 | 
			
		||||
        qs = qs.live()
 | 
			
		||||
        if self.focus_available:
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,9 @@ ul {
 | 
			
		||||
    float: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.small {
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
    max-width: 2em;
 | 
			
		||||
@ -67,9 +70,11 @@ nav.menu {
 | 
			
		||||
    padding: 0.2em;
 | 
			
		||||
    height: 2.5em;
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
    background-color: white;
 | 
			
		||||
    box-shadow: 0em 0em 0.2em black;
 | 
			
		||||
}
 | 
			
		||||
    .menu.top * {
 | 
			
		||||
        vertical-align: middle;
 | 
			
		||||
        vertical-align: bottom;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .menu.top > section {
 | 
			
		||||
@ -81,6 +86,10 @@ nav.menu {
 | 
			
		||||
        margin: 0.2em 1em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
.page_left, .page_right {
 | 
			
		||||
    max-width: 16em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page_left > section,
 | 
			
		||||
.page_right > section {
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
@ -121,6 +130,12 @@ ul.list {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.list nav {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    font-size: 0.9em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/** content: date list **/
 | 
			
		||||
.date_list nav {
 | 
			
		||||
    text-align:center;
 | 
			
		||||
@ -188,8 +203,6 @@ ul.list {
 | 
			
		||||
    font-size: 0.8em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.comments ul {
 | 
			
		||||
    margin-top: 2.5em;
 | 
			
		||||
}
 | 
			
		||||
@ -208,19 +221,23 @@ ul.list {
 | 
			
		||||
        float: right;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/** content: player **/
 | 
			
		||||
.player {
 | 
			
		||||
    width: 20em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    .player:not([seekable]) > .controls {
 | 
			
		||||
    .player:not([seekable]) > .controls > .progress {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.player .controls > * {
 | 
			
		||||
    margin: 0em 0.2em;
 | 
			
		||||
.player .controls {
 | 
			
		||||
    margin-top: 1em;
 | 
			
		||||
    text-align: right;
 | 
			
		||||
}
 | 
			
		||||
    .player .controls > * {
 | 
			
		||||
        margin: 0em 0.2em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .player .controls .single {
 | 
			
		||||
        display: none;
 | 
			
		||||
@ -245,14 +262,9 @@ ul.list {
 | 
			
		||||
            border-right: 2px #818181 solid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
.player .on_air a:not([href]), .on_air a[href=""] {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.player .playlist .item {
 | 
			
		||||
    margin: 0em;
 | 
			
		||||
    padding: 0.2em 0.4em;
 | 
			
		||||
    height: 1em;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -266,25 +278,20 @@ ul.list {
 | 
			
		||||
 | 
			
		||||
    .player .playlist .item .actions {
 | 
			
		||||
        display: none;
 | 
			
		||||
        font-size: 0.9em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .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] {
 | 
			
		||||
    height: auto;
 | 
			
		||||
    font-size: 1.1em;
 | 
			
		||||
    border-left: 1px #007EDF solid;
 | 
			
		||||
    font-size: 1.0em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.player .item:not([selected]) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.player .button {
 | 
			
		||||
@ -300,36 +307,33 @@ ul.list {
 | 
			
		||||
        max-height: 2.0em;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .player:not([state]) .button > img:not(.play),
 | 
			
		||||
    .player[state="paused"] .button > img:not(.play),
 | 
			
		||||
    .player[state="playing"] .button > img:not(.pause),
 | 
			
		||||
    .player[state="loading"] .button > img:not(.loading)
 | 
			
		||||
 | 
			
		||||
    .player:not([state]) .item[selected] .button > img:not(.play),
 | 
			
		||||
    .player[state="paused"] .item[selected] .button > img:not(.play),
 | 
			
		||||
    .player[state="playing"] .item[selected] .button > img:not(.pause),
 | 
			
		||||
    .player[state="loading"] .item[selected] .button > img:not(.loading)
 | 
			
		||||
    {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .player[state="loading"] .box .button > img.loading {
 | 
			
		||||
        animation-duration: 2s;
 | 
			
		||||
        animation-iteration-count: infinite;
 | 
			
		||||
        animation-name: rotate;
 | 
			
		||||
        animation-timing-function: linear;
 | 
			
		||||
    .player .item:not([selected]) .button > img.play {
 | 
			
		||||
        display: block;
 | 
			
		||||
    }
 | 
			
		||||
    .player .item:not([selected]) .button > img:not(.play) {
 | 
			
		||||
        display: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @keyframes rotate {
 | 
			
		||||
        from {
 | 
			
		||||
            transform: rotate(0deg);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        to {
 | 
			
		||||
            transform: rotate(360deg);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
.player .list_item.live:hover .actions {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/** content: page **/
 | 
			
		||||
main .body ~ section:not(.comments) {
 | 
			
		||||
    width: calc(50% - 1em);
 | 
			
		||||
    float: left;
 | 
			
		||||
    vertical-align: top;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -50,6 +50,7 @@ time {
 | 
			
		||||
 | 
			
		||||
.info {
 | 
			
		||||
    font-size: 0.9em;
 | 
			
		||||
    padding: 0.1em;
 | 
			
		||||
    color: #007EDF;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -81,14 +82,69 @@ main {
 | 
			
		||||
    box-shadow: 0em 0em 0.2em black;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    main h1 {
 | 
			
		||||
        margin: 0em;
 | 
			
		||||
    main h1:not(.detail_title) {
 | 
			
		||||
        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);
 | 
			
		||||
       margin-top: -3.3em;
 | 
			
		||||
       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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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.detail = detail;
 | 
			
		||||
    this.duration = duration;
 | 
			
		||||
    this.streams = streams.splice ? streams.sort() : [streams];
 | 
			
		||||
    this.cover = cover;
 | 
			
		||||
    this.on_air = on_air;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Sound.prototype = {
 | 
			
		||||
@ -30,25 +32,60 @@ Sound.prototype = {
 | 
			
		||||
    detail: '',
 | 
			
		||||
    streams: undefined,
 | 
			
		||||
    duration: undefined,
 | 
			
		||||
    on_air_url: undefined,
 | 
			
		||||
    cover: undefined,
 | 
			
		||||
    on_air: false,
 | 
			
		||||
 | 
			
		||||
    item: undefined,
 | 
			
		||||
 | 
			
		||||
    get seekable() {
 | 
			
		||||
        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.playlist = player.player.querySelector('.playlist');
 | 
			
		||||
    this.item_ = player.player.querySelector('.playlist .item');
 | 
			
		||||
    this.sounds = []
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PlayerPlaylist.prototype = {
 | 
			
		||||
Playlist.prototype = {
 | 
			
		||||
    on_air: undefined,
 | 
			
		||||
    sounds: undefined,
 | 
			
		||||
    sound: undefined,
 | 
			
		||||
 | 
			
		||||
    /// Find a sound by its streams, and return it if found
 | 
			
		||||
    find: function(streams) {
 | 
			
		||||
@ -71,30 +108,12 @@ PlayerPlaylist.prototype = {
 | 
			
		||||
        if(sound_)
 | 
			
		||||
            return sound_;
 | 
			
		||||
 | 
			
		||||
        var item = this.item_.cloneNode(true);
 | 
			
		||||
        item.removeAttribute('style');
 | 
			
		||||
        if(sound.on_air)
 | 
			
		||||
            this.on_air = 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;
 | 
			
		||||
        sound.make_item(this, this.item_);
 | 
			
		||||
        (container || this.playlist).appendChild(sound.item);
 | 
			
		||||
 | 
			
		||||
        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.save();
 | 
			
		||||
        return sound;
 | 
			
		||||
@ -106,8 +125,61 @@ PlayerPlaylist.prototype = {
 | 
			
		||||
            this.sounds.splice(index,1);
 | 
			
		||||
        this.playlist.removeChild(sound.item);
 | 
			
		||||
        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() {
 | 
			
		||||
        var list = [];
 | 
			
		||||
        for(var i in this.sounds) {
 | 
			
		||||
@ -124,7 +196,7 @@ PlayerPlaylist.prototype = {
 | 
			
		||||
        for(var i in list) {
 | 
			
		||||
            var sound = list[i];
 | 
			
		||||
            sound = new Sound(sound.title, sound.detail, sound.duration,
 | 
			
		||||
                              sound.streams)
 | 
			
		||||
                              sound.streams, sound.cover, sound.on_air)
 | 
			
		||||
            this.add(sound, container)
 | 
			
		||||
        }
 | 
			
		||||
        this.playlist.appendChild(container);
 | 
			
		||||
@ -132,53 +204,67 @@ PlayerPlaylist.prototype = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function Player(id) {
 | 
			
		||||
    this.store = new Store('player');
 | 
			
		||||
function Player(id, on_air_url, show_cover) {
 | 
			
		||||
    this.id = id;
 | 
			
		||||
    this.on_air_url = on_air_url;
 | 
			
		||||
    this.show_cover = show_cover;
 | 
			
		||||
 | 
			
		||||
    this.store = new Store('player_' + id);
 | 
			
		||||
 | 
			
		||||
    // html sounds
 | 
			
		||||
    this.player = document.getElementById(id);
 | 
			
		||||
    this.audio = this.player.querySelector('audio');
 | 
			
		||||
    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 = {
 | 
			
		||||
        duration: this.player.querySelector('.controls .duration'),
 | 
			
		||||
        progress: this.player.querySelector('progress'),
 | 
			
		||||
        single: this.player.querySelector('input.single'),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.playlist = new PlayerPlaylist(this);
 | 
			
		||||
    this.playlist = new Playlist(this);
 | 
			
		||||
    this.playlist.load();
 | 
			
		||||
 | 
			
		||||
    this.init_events();
 | 
			
		||||
    this.load();
 | 
			
		||||
 | 
			
		||||
    this.update_on_air();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Player.prototype = {
 | 
			
		||||
    /// current item being played
 | 
			
		||||
    sound: undefined,
 | 
			
		||||
    on_air_url: undefined,
 | 
			
		||||
 | 
			
		||||
    init_events: function() {
 | 
			
		||||
        var self = this;
 | 
			
		||||
 | 
			
		||||
        function time_from_progress(event) {
 | 
			
		||||
            bounding = self.controls.progress.getBoundingClientRect()
 | 
			
		||||
            bounding = self.progress.bar.getBoundingClientRect()
 | 
			
		||||
            offset = (event.clientX - bounding.left);
 | 
			
		||||
            return offset * self.audio.duration / bounding.width;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        function update_info() {
 | 
			
		||||
            var controls = self.controls;
 | 
			
		||||
            var progress = self.progress;
 | 
			
		||||
            var pos = self.audio.currentTime;
 | 
			
		||||
 | 
			
		||||
            // progress
 | 
			
		||||
            if(!self.sound || !self.sound.seekable ||
 | 
			
		||||
                    self.audio.duration == Infinity) {
 | 
			
		||||
                controls.duration.innerHTML = '';
 | 
			
		||||
                controls.progress.value = 0;
 | 
			
		||||
                    !pos || self.audio.duration == Infinity)
 | 
			
		||||
            {
 | 
			
		||||
                progress.duration.innerHTML = '';
 | 
			
		||||
                progress.bar.value = 0;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var pos = self.audio.currentTime;
 | 
			
		||||
            controls.progress.value = pos;
 | 
			
		||||
            controls.progress.max = self.audio.duration;
 | 
			
		||||
            controls.duration.innerHTML = duration_str(pos);
 | 
			
		||||
            progress.bar.value = pos;
 | 
			
		||||
            progress.bar.max = self.audio.duration;
 | 
			
		||||
            progress.duration.innerHTML = duration_str(pos);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // audio
 | 
			
		||||
@ -207,7 +293,7 @@ Player.prototype = {
 | 
			
		||||
        }, false);
 | 
			
		||||
 | 
			
		||||
        // progress
 | 
			
		||||
        progress = this.controls.progress;
 | 
			
		||||
        progress = this.progress.bar;
 | 
			
		||||
        progress.addEventListener('click', function(event) {
 | 
			
		||||
            player.audio.currentTime = time_from_progress(event);
 | 
			
		||||
        }, false);
 | 
			
		||||
@ -215,43 +301,55 @@ Player.prototype = {
 | 
			
		||||
        progress.addEventListener('mouseout', update_info, false);
 | 
			
		||||
 | 
			
		||||
        progress.addEventListener('mousemove', function(event) {
 | 
			
		||||
            if(self.audio.duration == Infinity)
 | 
			
		||||
            if(self.audio.duration == Infinity || isNaN(self.audio.duration))
 | 
			
		||||
               return;
 | 
			
		||||
 | 
			
		||||
            var pos = time_from_progress(event);
 | 
			
		||||
            self.controls.duration.innerHTML = duration_str(pos);
 | 
			
		||||
            self.progress.duration.innerHTML = duration_str(pos);
 | 
			
		||||
        }, false);
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    update_on_air: function(url) {
 | 
			
		||||
        if(!url) {
 | 
			
		||||
            // TODO HERE
 | 
			
		||||
        }
 | 
			
		||||
    update_on_air: function() {
 | 
			
		||||
        if(!this.on_air_url)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var self = this;
 | 
			
		||||
        window.setTimeout(function() {
 | 
			
		||||
            self.update_on_air();
 | 
			
		||||
        }, 60*1000);
 | 
			
		||||
 | 
			
		||||
        if(!this.playlist.on_air)
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var req = new XMLHttpRequest();
 | 
			
		||||
        req.open('GET', url, true);
 | 
			
		||||
        req.open('GET', this.on_air_url, true);
 | 
			
		||||
        req.onreadystatechange = function() {
 | 
			
		||||
            if(req.readyState != 4 || (req.status != 200 && req.status != 0))
 | 
			
		||||
            if(req.readyState != 4 || (req.status != 200 &&
 | 
			
		||||
                    req.status != 0))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var data = JSON.parse(req.responseText)
 | 
			
		||||
            if(data.type == 'track') {
 | 
			
		||||
                self.on_air.querySelector('.info').innerHTML = '♫';
 | 
			
		||||
                self.on_air.querySelector('.title') =
 | 
			
		||||
                    (data.artist || '') + ' — ' + (data.title);
 | 
			
		||||
                self.on_air.querySelector('.url').removeAttribute('href');
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                self.on_air.querySelector('.info').innerHTML = '';
 | 
			
		||||
                self.on_air.querySelector('.title').innerHTML = data.title;
 | 
			
		||||
                self.on_air.querySelector('.url').setAttribute('href', data.url);
 | 
			
		||||
            }
 | 
			
		||||
            if(data.type == 'track')
 | 
			
		||||
                data = {
 | 
			
		||||
                    title: '♫' + (data.artist ? data.artist + ' — ' : '') +
 | 
			
		||||
                           data.title,
 | 
			
		||||
                    url: ''
 | 
			
		||||
                }
 | 
			
		||||
            else
 | 
			
		||||
                data = {
 | 
			
		||||
                    title: data.title,
 | 
			
		||||
                    info: '',
 | 
			
		||||
                    url: data.url
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            if(timeout)
 | 
			
		||||
                window.setTimeout(function() {
 | 
			
		||||
                    self.update_on_air(url);
 | 
			
		||||
                }, 60*1000);
 | 
			
		||||
            var on_air = self.playlist.on_air;
 | 
			
		||||
            on_air = on_air.item.querySelector('.content');
 | 
			
		||||
 | 
			
		||||
            if(data.url)
 | 
			
		||||
                on_air.innerHTML =
 | 
			
		||||
                    '<a href="' + data.url + '">' + data.title + '</a>';
 | 
			
		||||
            else
 | 
			
		||||
                on_air.innerHTML = data.title;
 | 
			
		||||
        };
 | 
			
		||||
        req.send();
 | 
			
		||||
    },
 | 
			
		||||
@ -263,78 +361,44 @@ Player.prototype = {
 | 
			
		||||
            this.audio.pause();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    unselect: function(sound) {
 | 
			
		||||
        sound.item.removeAttribute('selected');
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    __mime_type: function(path) {
 | 
			
		||||
        ext = path.substr(path.lastIndexOf('.')+1);
 | 
			
		||||
        return 'audio/' + ext;
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    select: function(sound, play = true) {
 | 
			
		||||
        if(this.sound == sound) {
 | 
			
		||||
            if(play)
 | 
			
		||||
                this.play();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    load_sound: function(sound) {
 | 
			
		||||
        var audio = this.audio;
 | 
			
		||||
        audio.pause();
 | 
			
		||||
 | 
			
		||||
        if(this.sound)
 | 
			
		||||
            this.unselect(this.sound);
 | 
			
		||||
 | 
			
		||||
        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]);
 | 
			
		||||
        }
 | 
			
		||||
        var sources = audio.querySelectorAll('source');
 | 
			
		||||
        for(var i = 0; i < sources.length; i++)
 | 
			
		||||
            audio.removeChild(sources[i]);
 | 
			
		||||
 | 
			
		||||
        streams = sound.streams;
 | 
			
		||||
        for(var i = 0; i < streams.length; i++) {
 | 
			
		||||
            var source = document.createElement('source');
 | 
			
		||||
            source.src = streams[i];
 | 
			
		||||
            source.type = this.__mime_type(source.src);
 | 
			
		||||
            this.audio.appendChild(source);
 | 
			
		||||
            audio.appendChild(source);
 | 
			
		||||
        }
 | 
			
		||||
        this.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);
 | 
			
		||||
        audio.load();
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    save: function() {
 | 
			
		||||
        // TODO: move stored sound into playlist
 | 
			
		||||
        this.store.set('player', {
 | 
			
		||||
            single: this.controls.single.checked,
 | 
			
		||||
            sound: this.sound && this.sound.streams,
 | 
			
		||||
            sound: this.playlist.sound && this.playlist.sound.streams,
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
    load: function() {
 | 
			
		||||
        var data = this.store.get('player');
 | 
			
		||||
        if(!data)
 | 
			
		||||
            return;
 | 
			
		||||
        this.controls.single.checked = data.single;
 | 
			
		||||
        if(data.sound)
 | 
			
		||||
            this.sound = this.playlist.find(data.sound);
 | 
			
		||||
            this.playlist.sound = this.playlist.find(data.sound);
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -67,8 +67,13 @@
 | 
			
		||||
            </nav>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        {% block footer %}
 | 
			
		||||
        <footer class="footer">
 | 
			
		||||
            {% render_sections position="footer" %}
 | 
			
		||||
            <div class="small">Propulsed by
 | 
			
		||||
                <a href="https://github.com/bkfox/aircox">Aircox</a>
 | 
			
		||||
            </div>
 | 
			
		||||
        </footer>
 | 
			
		||||
        {% endblock %}
 | 
			
		||||
    </body>
 | 
			
		||||
</html>
 | 
			
		||||
 | 
			
		||||
@ -34,16 +34,26 @@
 | 
			
		||||
{% if podcasts %}
 | 
			
		||||
<section class="podcasts list">
 | 
			
		||||
    <h2>{% trans "Podcasts" %}</h2>
 | 
			
		||||
    {% for item in podcasts %}
 | 
			
		||||
    {% include 'cms/snippets/sound_list_item.html' %}
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    <div id="player_diff_{{ page.id }}" class="player">
 | 
			
		||||
        {% include 'cms/snippets/player.html' %}
 | 
			
		||||
 | 
			
		||||
        <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>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
{# TODO: podcasts #}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,23 +6,30 @@
 | 
			
		||||
 | 
			
		||||
{% load aircox_cms %}
 | 
			
		||||
 | 
			
		||||
{% if not object_list %}
 | 
			
		||||
{% block title %}
 | 
			
		||||
<h1 class="detail_title">{{ page.title }}</h1>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
{% if object_list %}
 | 
			
		||||
{# list view #}
 | 
			
		||||
    <div class="body summary">
 | 
			
		||||
    <section class="body summary">
 | 
			
		||||
    {{ page.summary }}
 | 
			
		||||
    <a href="?" class="go_back">{% trans "Go back to the publication" %}</a>
 | 
			
		||||
    </div>
 | 
			
		||||
    </section>
 | 
			
		||||
 | 
			
		||||
    {% with list_paginator=paginator %}
 | 
			
		||||
    {% include "cms/snippets/list.html" %}
 | 
			
		||||
    {% endwith %}
 | 
			
		||||
{% else %}
 | 
			
		||||
{# detail view #}
 | 
			
		||||
    {% if page.cover %}
 | 
			
		||||
    {% image page.cover max-600x480 class="detail_cover cover" height="" width="" %}
 | 
			
		||||
    {% endif %}
 | 
			
		||||
 | 
			
		||||
    <div class="content">
 | 
			
		||||
        {% if page.cover %}
 | 
			
		||||
        {% image page.cover max-600x480 class="cover" height="" width="" %}
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        <section class="body">
 | 
			
		||||
        {{ page.body|richtext}}
 | 
			
		||||
        </section>
 | 
			
		||||
 | 
			
		||||
@ -7,11 +7,10 @@
 | 
			
		||||
{% endwith %}
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
{% with url=url url_text=self.url_text %}
 | 
			
		||||
{% include "cms/snippets/list.html" %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
 | 
			
		||||
{% if url %}
 | 
			
		||||
<nav><a href="{{ url }}">{{ self.url_text }}</a></nav>
 | 
			
		||||
{% endif %}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,59 +1,24 @@
 | 
			
		||||
{% extends 'cms/sections/section_item.html' %}
 | 
			
		||||
 | 
			
		||||
{% load staticfiles %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<style>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
<div id="player" class="player">
 | 
			
		||||
    <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="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>
 | 
			
		||||
    {% include "cms/snippets/player.html" %}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
var player = new Player('player');
 | 
			
		||||
var sound = player.playlist.add(new Sound('{{ self.live_title }}', '', undefined,
 | 
			
		||||
    streams=[
 | 
			
		||||
        {% for stream in streams %}'{{ stream }}',{% endfor %}
 | 
			
		||||
    ]), on_air_url = '{% url 'controllers.on_air' %}');
 | 
			
		||||
player.select(sound, false);
 | 
			
		||||
var player = new Player('player', '{% url 'controllers.on_air' %}');
 | 
			
		||||
var sound = player.playlist.add(
 | 
			
		||||
    new Sound(
 | 
			
		||||
        '{{ self.live_title }}',
 | 
			
		||||
        '', undefined,
 | 
			
		||||
        streams=[ {% for stream in streams %}'{{ stream }}',{% endfor %} ],
 | 
			
		||||
        cover = undefined,
 | 
			
		||||
        on_air = true
 | 
			
		||||
    )
 | 
			
		||||
);
 | 
			
		||||
sound.item.className += ' live';
 | 
			
		||||
player.playlist.select(sound, false);
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
{% extends "cms/sections/section_item.html" %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
<div class="meta">
 | 
			
		||||
    <div class="author">
 | 
			
		||||
        {% if page.publish_as %}
 | 
			
		||||
@ -24,4 +25,5 @@
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -56,6 +56,8 @@ Options:
 | 
			
		||||
    {% endif %}
 | 
			
		||||
{% endwith %}
 | 
			
		||||
</nav>
 | 
			
		||||
{% elif url and url_text %}
 | 
			
		||||
<nav><a href="{{ url }}">{{ url_text }}</a></nav>
 | 
			
		||||
{% endif %}
 | 
			
		||||
 | 
			
		||||
</ul>
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										45
									
								
								cms/templates/cms/snippets/player.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								cms/templates/cms/snippets/player.html
									
									
									
									
									
										Normal 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>
 | 
			
		||||
 | 
			
		||||
@ -1,22 +1,37 @@
 | 
			
		||||
{% load static %}
 | 
			
		||||
{% load i18n %}
 | 
			
		||||
 | 
			
		||||
{# TODO: complete archive podcast -> info #}
 | 
			
		||||
<div class="list_item sound">
 | 
			
		||||
<a onclick="player.select(player.playlist.add(new Sound(
 | 
			
		||||
<script>
 | 
			
		||||
function add_sound_{{ item.id }}(event) {
 | 
			
		||||
    var sound = new Sound(
 | 
			
		||||
        title='{{ item.name|escape }}',
 | 
			
		||||
        detail='{{ item.detail_url }}',
 | 
			
		||||
        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"/>
 | 
			
		||||
    <h3 class="flex_item">{{ item.name }}</h3>
 | 
			
		||||
 | 
			
		||||
    <span class="info">
 | 
			
		||||
    <time class="info">
 | 
			
		||||
        {% if item.duration.hour > 0 %}
 | 
			
		||||
        {{ item.duration|date:'H:i:s' }}
 | 
			
		||||
        {% else %}
 | 
			
		||||
        {{ item.duration|date:'i:s' }}
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </span>
 | 
			
		||||
</a>
 | 
			
		||||
</div>
 | 
			
		||||
    </time>
 | 
			
		||||
 | 
			
		||||
    <img src="{% static "cms/images/add.png" %}" class="icon"
 | 
			
		||||
         data-action='add' alt="{% trans "add this sound to the playlist" %}"/>
 | 
			
		||||
</a>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,84 @@
 | 
			
		||||
from django.utils import timezone as tz
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
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.wagtailadmin.menu import MenuItem, Menu, SubmenuMenuItem
 | 
			
		||||
 | 
			
		||||
from wagtail.contrib.modeladmin.options import \
 | 
			
		||||
    ModelAdmin, ModelAdminGroup, modeladmin_register
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
    last_time = None
 | 
			
		||||
    page_model = models.Publication
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        super().__init__('')
 | 
			
		||||
@ -19,40 +89,47 @@ class GenericMenu(Menu):
 | 
			
		||||
    def get_title(self, item):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def get_parent_page(self, item):
 | 
			
		||||
    def get_parent(self, item):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    def get_page_url(self, item):
 | 
			
		||||
    def get_page_url(self, page_model, item):
 | 
			
		||||
        if item.page.count():
 | 
			
		||||
            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:
 | 
			
		||||
            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
 | 
			
		||||
    def registered_menu_items(self):
 | 
			
		||||
        now = tz.now()
 | 
			
		||||
        last_max = now - tz.timedelta(minutes = 10)
 | 
			
		||||
 | 
			
		||||
        if self._registered_menu_items is None or self.last_time < last_max:
 | 
			
		||||
            qs = self.get_queryset()
 | 
			
		||||
            self._registered_menu_items =  [
 | 
			
		||||
                MenuItem(self.get_title(x), self.get_page_url(x))
 | 
			
		||||
                for x in qs
 | 
			
		||||
            ]
 | 
			
		||||
            self.last_time = now
 | 
			
		||||
        return self._registered_menu_items
 | 
			
		||||
        qs = self.get_queryset()
 | 
			
		||||
        return [
 | 
			
		||||
            MenuItem(self.get_title(x), self.get_page_url(self.page_model, x))
 | 
			
		||||
            for x in qs
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiffusionsMenu(GenericMenu):
 | 
			
		||||
    """
 | 
			
		||||
    Menu to display diffusions of today
 | 
			
		||||
    """
 | 
			
		||||
    page_model = models.DiffusionPage
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return programs.Diffusion.objects.filter(
 | 
			
		||||
            type = programs.Diffusion.Type.normal,
 | 
			
		||||
            start__contains = tz.now().date(),
 | 
			
		||||
            initial__isnull = True,
 | 
			
		||||
        ).order_by('start')
 | 
			
		||||
 | 
			
		||||
    def get_title(self, item):
 | 
			
		||||
@ -74,6 +151,8 @@ class ProgramsMenu(GenericMenu):
 | 
			
		||||
    """
 | 
			
		||||
    Menu to display all active programs.
 | 
			
		||||
    """
 | 
			
		||||
    page_model = models.DiffusionPage
 | 
			
		||||
 | 
			
		||||
    def get_queryset(self):
 | 
			
		||||
        return programs.Program.objects \
 | 
			
		||||
                    .filter(active = True, page__isnull = False) \
 | 
			
		||||
@ -92,7 +171,6 @@ class ProgramsMenu(GenericMenu):
 | 
			
		||||
        return settings.default_program_parent_page
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@hooks.register('register_admin_menu_item')
 | 
			
		||||
def register_programs_menu_item():
 | 
			
		||||
    return SubmenuMenuItem(
 | 
			
		||||
@ -101,4 +179,3 @@ def register_programs_menu_item():
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,7 @@ def on_air(request):
 | 
			
		||||
 | 
			
		||||
        last = {
 | 
			
		||||
            'type': 'diffusion',
 | 
			
		||||
            'title': publication.title if publication else last.program.name,
 | 
			
		||||
            'title': last.program.name,
 | 
			
		||||
            'date': last.start,
 | 
			
		||||
            'url': publication.specific.url if publication else None,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								notes.md
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								notes.md
									
									
									
									
									
								
							@ -26,10 +26,10 @@ This file is used as a reminder, can be used as crappy documentation too.
 | 
			
		||||
 | 
			
		||||
- controllers :
 | 
			
		||||
    - models to template -> note
 | 
			
		||||
    - input stream
 | 
			
		||||
    - streamed program disable -> remote control on liquidsoap
 | 
			
		||||
    - tests:
 | 
			
		||||
        - monitor
 | 
			
		||||
        - check when a played sound has a temp blank
 | 
			
		||||
        - config generation and sound diffusion
 | 
			
		||||
 | 
			
		||||
- cms:
 | 
			
		||||
 | 
			
		||||
@ -268,38 +268,6 @@ class Sound(Nameable):
 | 
			
		||||
        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):
 | 
			
		||||
    """
 | 
			
		||||
    A Schedule defines time slots of programs' diffusions. It can be an initial
 | 
			
		||||
@ -636,6 +604,38 @@ class DiffusionManager(models.Manager):
 | 
			
		||||
        ).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):
 | 
			
		||||
    """
 | 
			
		||||
    A Diffusion is an occurrence of a Program that is scheduled on the
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user