work on player ui

This commit is contained in:
bkfox 2018-02-09 18:11:04 +01:00
parent d80322dd15
commit e05cdb343d
7 changed files with 196 additions and 253 deletions

View File

@ -543,9 +543,15 @@ class SectionPlaylist(Section):
""" """
fields = { fields = {
'name': 'name', 'name': 'name',
'duration': lambda e, o: ( 'embed': 'embed',
o.duration.hour, o.duration.minute, o.duration.second 'duration': lambda e, o:
), o.duration.hour * 3600 + o.duration.minute * 60 +
o.duration.second
,
'duration_str': lambda e, o:
(str(o.duration.hour) + '"' if o.duration.hour else '') +
str(o.duration.minute) + "'" + str(o.duration.second)
,
'sources': lambda e, o: [ o.url() ], 'sources': lambda e, o: [ o.url() ],
'detail_url': 'detail_url':
lambda e, o: o.diffusion and hasattr(o.diffusion, 'page') \ lambda e, o: o.diffusion and hasattr(o.diffusion, 'page') \

View File

@ -1,22 +0,0 @@
.nav-submenu h2 {
display: none;
}
.nav-submenu .menu-item .info {
margin-right: 1em;
}
.nav-submenu .menu-item a {
white-space: normal;
padding: 0.9em 1em 0.9em 1em;
}
.nav-submenu .menu-item a:hover {
background-color: rgba(100, 100, 100, 0.2);
}
.nav-submenu li {
border: 0;
}

View File

@ -486,153 +486,111 @@ ul.list, .list > ul {
} }
/** content: player **/ /** component: sound **/
.player { .component.sound {
} display: flex;
flex-direction: row;
.player:not([seekable]) > .controls > .progress { margin: 0.2em;
display: none;
}
.player .controls {
margin-top: 1em;
text-align: right;
}
.player .controls > * {
margin: 0em 0.2em;
}
.player .controls .single {
display: none;
}
.player .controls .single + label {
display: inline-block;
font-size: 1em;
padding: 0.1em;
width: 1.5em;
height: 1.0em;
text-align: center;
box-shadow: inset 0em 0em 0.1em #818181;
}
.player .controls .single:not(:checked) + label {
border-left: 2px #818181 solid;
color: black;
}
.player .controls .single:checked + label {
border-right: 2px #818181 solid;
}
.player .playlist .item {
position: relative;
margin: 0em;
padding: 0.2em 0.4em;
cursor: pointer;
}
.player .playlist .item:hover {
color: #007EDF;
}
.player .item > * {
vertical-align: middle;
}
.player .playlist .item .actions {
display: none;
height: 100%;
max-width: 2.9em;
position: absolute;
right: 0px;
font-size: 1.2em;
text-align: right;
}
.player .playlist .item:hover .actions {
display: inline;
}
.player .playlist .item .action {
display: inline-block;
width: 1.2em;
height: 1.2em;
border-radius: 0.2em;
text-align: center;
line-height: 1.2em;
background-color: #F2F2F2;
}
.player .playlist .item .action:hover {
background-color: rgba(0, 126, 223, 0.1);
}
.player .playlist .duration {
text-align: right;
}
.player .playlist progress {
width: 100%; width: 100%;
} }
.component.sound[state="play"] button {
.player .item[selected] { animation-name: sound-blink;
border-left: 1px #007EDF solid; animation-duration: 4s;
font-size: 1.0em; animation-iteration-count: infinite;
animation-direction: alternate;
} }
.player .item:not([selected]) { @keyframes sound-blink {
from { background-color: rgba(255, 255, 255, 0); }
to { background-color: rgba(255, 255, 255, 0.6); }
} }
.player .button {
display: inline-block; .component.sound .button {
width: 4em;
height: 4em;
cursor: pointer; cursor: pointer;
height: 2.0em; position: relative;
background: none; margin-right: 0.4em;
border: none;
font-size: 1.4em;
} }
.player .button > img { .component.sound .button > img {
max-height: 2.0em; width: 100%;
height: 100%;
} }
.player:not([state]) .item[selected] .button > img:not(.play), .component.sound button {
.player[state="paused"] .item[selected] .button > img:not(.play), transition: background-color 0.5s;
.player[state="playing"] .item[selected] .button > img:not(.pause), background-color: rgba(255,255,255,0.1);
.player[state="loading"] .item[selected] .button > img:not(.loading)
{
display: none;
}
.player .item:not([selected]) .button > img.play {
display: block;
}
.player .item:not([selected]) .button > img:not(.play):not(.cover) {
display: none;
}
.player .item .button > img.cover {
display: block;
position: absolute; position: absolute;
transition: opacity 0.2s; cursor: pointer;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: 0;
} }
.player .item:hover .button > img.cover { .component.sound button:hover {
opacity: 0.2; background-color: rgba(255,255,255,0.5);
}
.component.sound button > img {
background-color: rgba(255,255,255,0.9);
border-radius: 50%;
}
.component.sound .content {
position: relative;
}
.component.sound .info {
text-align: right;
}
.component.sound progress {
width: 100%;
position: absolute;
bottom: 0;
height: 0.4em;
}
.component.sound progress:hover {
height: 1em;
} }
/** component: playlist **/
.component.playlist footer {
text-align: right;
display: block;
}
main .player .actions .action:not(.add), .component.playlist .read_all {
.section_player .actions .action.add,
.player .list_item.live:hover .actions {
display: none; display: none;
} }
.component.playlist .read_all + label {
display: inline-block;
padding: 0.1em;
margin-left: 0.2em;
cursor: pointer;
font-size: 1em;
box-shadow: inset 0em 0em 0.1em #818181;
}
.component.playlist .read_all:not(:checked) + label {
border-left: 0.1em #818181 solid;
margin-right: 0em;
}
.component.playlist .read_all:checked + label {
border-right: 0.1em #007EDF solid;
box-shadow: inset 0em 0em 0.1em #007EDF;
margin-right: 0em;
}
/** content: page **/ /** content: page **/
main .body ~ section:not(.comments) { main .body ~ section:not(.comments) {

View File

@ -19,36 +19,8 @@
/** detail view **/ /** detail view **/
/** 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 { @keyframes rotate {
from { from {
transform: rotate(0deg); transform: rotate(0deg);

View File

@ -1,21 +1,13 @@
/* Implementation status: -- TODO /* Implementation status: -- TODO
* - actions:
* - add to user playlist
* - go to detail
* - remove from playlist: for user playlist
* - save sound infos:
* - while playing: save current position
* - otherwise: remove from localstorage
* - save playlist in localstorage
* - proper design * - proper design
* - mini-button integration in lists (list of diffusion articles) * - mini-button integration in lists (list of diffusion articles)
*/ */
var State = Object.freeze({ var State = Object.freeze({
Stop: Symbol('Stop'), Stop: 'stop',
Loading: Symbol('Loading'), Loading: 'loading',
Play: Symbol('Play'), Play: 'play',
}); });
@ -110,28 +102,29 @@ var Sound = Vue.extend({
state: State.Stop, state: State.Stop,
// current position in playing sound // current position in playing sound
position: 0, position: 0,
// estimated position when user mouse over progress bar
seek_position: null,
// url to the page related to the sound // url to the page related to the sound
detail_url: '', detail_url: '',
// estimated position when user mouse over progress bar
user_seek: null,
}; };
}, },
computed: { computed: {
// sound can be seeked // sound can be seeked
seekable: function() { seekable() {
// seekable: for the moment only when we have a podcast file // seekable: for the moment only when we have a podcast file
// note: need mounted because $refs is not reactive // note: need mounted because $refs is not reactive
return this.mounted && this.duration && this.$refs.audio.seekable; return this.mounted && this.duration && this.$refs.audio.seekable;
}, },
// sound duration in seconds // sound duration in seconds
duration: function() { duration() {
if(this.track.duration) return this.track.duration;
return this.track.duration[0] * 3600 + },
this.track.duration[1] * 60 +
this.track.duration[2]; seek_position() {
return null; return (this.user_seek === null && this.position) ||
this.user_seek;
}, },
}, },
@ -196,6 +189,29 @@ var Sound = Vue.extend({
tracks.splice(i, 1); tracks.splice(i, 1);
}, },
//
// Utils functions
//
_as_progress_time(event) {
bounding = this.$refs.progress.getBoundingClientRect()
offset = (event.clientX - bounding.left);
return offset * this.$refs.audio.duration / bounding.width;
},
// format seconds into time string such as: [h"m]m'ss
format_time(seconds) {
seconds = Math.floor(seconds);
var hours = Math.floor(seconds / 3600);
seconds -= hours * 3600;
var minutes = Math.floor(seconds / 60);
seconds -= minutes * 60;
return (hours ? ((hours < 10 ? '0' + hours : hours) + '"') : '') +
minutes + "'" + seconds
;
},
// //
// Events // Events
// //
@ -214,21 +230,15 @@ var Sound = Vue.extend({
this.$emit('ended', this); this.$emit('ended', this);
}, },
_as_progress_time(event) {
bounding = this.$refs.progress.getBoundingClientRect()
offset = (event.clientX - bounding.left);
return offset * this.$refs.audio.duration / bounding.width;
},
progress_mouse_out(event) { progress_mouse_out(event) {
this.seek_position = null; this.user_seek = null;
}, },
progress_mouse_move(event) { progress_mouse_move(event) {
if(this.$refs.audio.duration == Infinity || if(this.$refs.audio.duration == Infinity ||
isNaN(this.$refs.audio.duration)) isNaN(this.$refs.audio.duration))
return; return;
this.seek_position = this._as_progress_time(event); this.user_seek = this._as_progress_time(event);
}, },
progress_clicked(event) { progress_clicked(event) {
@ -250,8 +260,8 @@ var Playlist = Vue.extend({
return { return {
// if true, use this playlist as user's default playlist // if true, use this playlist as user's default playlist
default: false, default: false,
// single mode enabled // read all mode enabled
single_mode: false, read_all: false,
// playlist can be modified by user // playlist can be modified by user
modifiable: false, modifiable: false,
// if set, save items into localstorage using this root key // if set, save items into localstorage using this root key
@ -261,6 +271,13 @@ var Playlist = Vue.extend({
}; };
}, },
computed: {
// id of the read all mode checkbox switch
read_all_id() {
return this.id + "_read_all";
}
},
mounted() { mounted() {
// set default // set default
if(this.default) { if(this.default) {
@ -283,8 +300,8 @@ var Playlist = Vue.extend({
// ensure sound is stopped (beforeDestroy()) // ensure sound is stopped (beforeDestroy())
sound.stop(); sound.stop();
// next only when single mode // next only when read all mode
if(this.single_mode) if(!this.read_all)
return; return;
var sounds = this.$refs.sounds; var sounds = this.$refs.sounds;

View File

@ -9,16 +9,21 @@
<a-playlist class="playlist" id="{{ playlist_id }}"> <a-playlist class="playlist" id="{{ playlist_id }}">
<noscript> <noscript>
{% for track in tracks %} {% for track in tracks %}
<li class="item"> <div class="item">
<span class="name">{{ track.name }} ({{ track.duration|date:"H\"i's" }}): </span> <span class="name">
{{ track.data.name }}
{% if track.data.duration %}
({{ track.data.duration_str }})
{% endif %}
</span>
<span class="podcast"> <span class="podcast">
{% if not track.embed %} {% if not track.data.embed %}
<audio src="{{ track.url|escape }}" controls> <audio src="{{ track.url|escape }}" controls>
{% else %} {% else %}
{{ track.embed|safe }} {{ track.embed|safe }}
{% endif %} {% endif %}
</span> </span>
</li> </div>
{% endfor %} {% endfor %}
</noscript> </noscript>
<script> <script>

View File

@ -2,7 +2,9 @@
{% load i18n %} {% load i18n %}
<script type="text/x-template" id="template-sound"> <script type="text/x-template" id="template-sound">
<div class="sound"> <div class="component sound flex_row"
:state="state"
>
<audio preload="metadata" ref="audio" <audio preload="metadata" ref="audio"
@pause="state = State.Stop" @pause="state = State.Stop"
@playing="state = State.Play" @playing="state = State.Play"
@ -11,8 +13,9 @@
> >
<source v-for="source in track.sources" :src="source"> <source v-for="source in track.sources" :src="source">
</audio> </audio>
<img :src="track.cover" v-if="track.cover" class="icon cover"> <div class="cover button">
<button class="button" @click="play_stop"> <img :src="track.cover" v-if="track.cover">
<button @click="play_stop">
<img class="icon pause" <img class="icon pause"
src="{% static "aircox/images/pause.png" %}" src="{% static "aircox/images/pause.png" %}"
title="{% trans "Click to pause" %}" title="{% trans "Click to pause" %}"
@ -26,14 +29,25 @@
title="{% trans "Click to play" %}" title="{% trans "Click to play" %}"
v-else > v-else >
</button> </button>
<div> </div>
<h3> <div class="content flex_item">
<h3 class="flex_item">
<a :href="detail_url">[[ track.name ]]</a> <a :href="detail_url">[[ track.name ]]</a>
</h3> </h3>
<span v-if="track.duration" class="info"> <div v-if="track.duration" class="info">
[[ (track.duration[0] && track.duration[0] + '"') || '' ]] <span v-if="seek_position !== null">
[[ track.duration[1] + "'" + track.duration[2] ]] [[ format_time(seek_position) ]] /
</span> </span>
<span v-else-if="state == State.Play">[[ format_time(position) ]] /</span>
[[ format_time(track.duration) ]]
</div>
<progress ref="progress"
v-show="state == State.Play && track.duration"
v-on:click.prevent="progress_clicked"
v-on:mousemove = "progress_mouse_move"
v-on:mouseout = "progress_mouse_out"
:value="seek_position" :max="duration"
></progress>
</div> </div>
<div class="actions"> <div class="actions">
<a class="action remove" <a class="action remove"
@ -47,35 +61,28 @@
v-else v-else
>+</a> >+</a>
</div> </div>
<div class="content flex_row" v-show="track.duration != null">
<span v-if="seek_position !== null">[[ seek_position ]]</span>
<span v-else>[[ position ]]</span>
<progress class="flex_item progress" ref="progress"
v-show="track.duration"
v-on:click.prevent="progress_clicked"
v-on:mousemove = "progress_mouse_move"
v-on:mouseout = "progress_mouse_out"
:value="position" :max="duration"
></progress>
</div>
</div> </div>
</script> </script>
<script type="text/x-template" id="template-playlist"> <script type="text/x-template" id="template-playlist">
<div class="playlist"> <div class="component playlist">
<a-sound v-for="track in tracks" ref="sounds" <a-sound v-for="track in tracks" ref="sounds"
:id="track.id" :track="track" :id="track.id" :track="track"
@ended="sound_ended" @ended="sound_ended"
@beforeDestroy="sound_ended" @beforeDestroy="sound_ended"
/> />
<div v-show="tracks.length > 1" class="playlist_footer"> <footer v-show="tracks.length > 1" class="info">
<input type="checkbox" class="single" id="[[ playlist ]]_single_mode" <span v-show="read_all">{% trans "read all" %}</span>
value="true" v-model="single_mode"> <input type="checkbox" class="read_all"
<label for="[[ playlist ]]_single_mode" class="info" :id="read_all_id"
title="{% trans "Enable and disable single mode" %}">↻</label> value="true" v-model="read_all">
</div> <label :for="read_all_id"
title="{% trans "Read all the playlist" %}">
<img src="{% static "aircox/images/list.png" %}" class="small icon">
</label>
</footer>
</div> </div>
</script> </script>