From e05cdb343df518e3ea9d012cd9f43bbd0d23de73 Mon Sep 17 00:00:00 2001 From: bkfox Date: Fri, 9 Feb 2018 18:11:04 +0100 Subject: [PATCH] work on player ui --- aircox_cms/models/sections.py | 12 +- aircox_cms/static/aircox_cms/css/cms.css | 22 -- aircox_cms/static/aircox_cms/css/layout.css | 206 +++++++----------- aircox_cms/static/aircox_cms/css/theme.css | 28 --- aircox_cms/static/aircox_cms/js/player.js | 81 ++++--- .../aircox_cms/sections/playlist.html | 13 +- .../templates/aircox_cms/vues/player.html | 87 ++++---- 7 files changed, 196 insertions(+), 253 deletions(-) delete mode 100755 aircox_cms/static/aircox_cms/css/cms.css diff --git a/aircox_cms/models/sections.py b/aircox_cms/models/sections.py index 55800cd..e12ded9 100644 --- a/aircox_cms/models/sections.py +++ b/aircox_cms/models/sections.py @@ -543,9 +543,15 @@ class SectionPlaylist(Section): """ fields = { 'name': 'name', - 'duration': lambda e, o: ( - o.duration.hour, o.duration.minute, o.duration.second - ), + 'embed': 'embed', + '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() ], 'detail_url': lambda e, o: o.diffusion and hasattr(o.diffusion, 'page') \ diff --git a/aircox_cms/static/aircox_cms/css/cms.css b/aircox_cms/static/aircox_cms/css/cms.css deleted file mode 100755 index ba7f797..0000000 --- a/aircox_cms/static/aircox_cms/css/cms.css +++ /dev/null @@ -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; -} - - diff --git a/aircox_cms/static/aircox_cms/css/layout.css b/aircox_cms/static/aircox_cms/css/layout.css index 80dcedd..efd6381 100755 --- a/aircox_cms/static/aircox_cms/css/layout.css +++ b/aircox_cms/static/aircox_cms/css/layout.css @@ -408,7 +408,7 @@ ul.list, .list > ul { .date_list_item.now { - padding: 0.4em; + padding: 0.4em; } .date_list_item img.now { @@ -486,153 +486,111 @@ ul.list, .list > ul { } -/** content: player **/ -.player { +/** component: sound **/ +.component.sound { + display: flex; + flex-direction: row; + margin: 0.2em; + width: 100%; } - .player:not([seekable]) > .controls > .progress { - display: none; + .component.sound[state="play"] button { + animation-name: sound-blink; + animation-duration: 4s; + animation-iteration-count: infinite; + animation-direction: alternate; + } + + @keyframes sound-blink { + from { background-color: rgba(255, 255, 255, 0); } + to { background-color: rgba(255, 255, 255, 0.6); } } -.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; +.component.sound .button { + width: 4em; + height: 4em; cursor: pointer; + position: relative; + margin-right: 0.4em; } - .player .playlist .item:hover { - color: #007EDF; - } - - .player .item > * { - vertical-align: middle; - } - - .player .playlist .item .actions { - display: none; + .component.sound .button > img { + width: 100%; height: 100%; - max-width: 2.9em; + } + + .component.sound button { + transition: background-color 0.5s; + background-color: rgba(255,255,255,0.1); position: absolute; - right: 0px; - font-size: 1.2em; + cursor: pointer; + left: 0; + top: 0; + width: 100%; + height: 100%; + border: 0; + } + + .component.sound button:hover { + 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; } - .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 { + .component.sound progress { width: 100%; - } - - -.player .item[selected] { - border-left: 1px #007EDF solid; - font-size: 1.0em; -} - -.player .item:not([selected]) { -} - -.player .button { - display: inline-block; - cursor: pointer; - height: 2.0em; - background: none; - border: none; - font-size: 1.4em; -} - - .player .button > img { - max-height: 2.0em; - } - - .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 .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; - transition: opacity 0.2s; + bottom: 0; + height: 0.4em; } - .player .item:hover .button > img.cover { - opacity: 0.2; + .component.sound progress:hover { + height: 1em; } +/** component: playlist **/ +.component.playlist footer { + text-align: right; + display: block; +} -main .player .actions .action:not(.add), -.section_player .actions .action.add, -.player .list_item.live:hover .actions { +.component.playlist .read_all { 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 **/ main .body ~ section:not(.comments) { diff --git a/aircox_cms/static/aircox_cms/css/theme.css b/aircox_cms/static/aircox_cms/css/theme.css index 8345bba..4cad9a8 100755 --- a/aircox_cms/static/aircox_cms/css/theme.css +++ b/aircox_cms/static/aircox_cms/css/theme.css @@ -19,36 +19,8 @@ /** 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 { from { transform: rotate(0deg); diff --git a/aircox_cms/static/aircox_cms/js/player.js b/aircox_cms/static/aircox_cms/js/player.js index 78fb905..d1f7ee0 100644 --- a/aircox_cms/static/aircox_cms/js/player.js +++ b/aircox_cms/static/aircox_cms/js/player.js @@ -1,21 +1,13 @@ /* 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 * - mini-button integration in lists (list of diffusion articles) */ var State = Object.freeze({ - Stop: Symbol('Stop'), - Loading: Symbol('Loading'), - Play: Symbol('Play'), + Stop: 'stop', + Loading: 'loading', + Play: 'play', }); @@ -110,28 +102,29 @@ var Sound = Vue.extend({ state: State.Stop, // current position in playing sound position: 0, - // estimated position when user mouse over progress bar - seek_position: null, // url to the page related to the sound detail_url: '', + // estimated position when user mouse over progress bar + user_seek: null, }; }, computed: { // sound can be seeked - seekable: function() { + seekable() { // seekable: for the moment only when we have a podcast file // note: need mounted because $refs is not reactive return this.mounted && this.duration && this.$refs.audio.seekable; }, // sound duration in seconds - duration: function() { - if(this.track.duration) - return this.track.duration[0] * 3600 + - this.track.duration[1] * 60 + - this.track.duration[2]; - return null; + duration() { + return this.track.duration; + }, + + seek_position() { + return (this.user_seek === null && this.position) || + this.user_seek; }, }, @@ -196,6 +189,29 @@ var Sound = Vue.extend({ 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 // @@ -214,21 +230,15 @@ var Sound = Vue.extend({ 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) { - this.seek_position = null; + this.user_seek = null; }, progress_mouse_move(event) { if(this.$refs.audio.duration == Infinity || isNaN(this.$refs.audio.duration)) return; - this.seek_position = this._as_progress_time(event); + this.user_seek = this._as_progress_time(event); }, progress_clicked(event) { @@ -250,8 +260,8 @@ var Playlist = Vue.extend({ return { // if true, use this playlist as user's default playlist default: false, - // single mode enabled - single_mode: false, + // read all mode enabled + read_all: false, // playlist can be modified by user modifiable: false, // 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() { // set default if(this.default) { @@ -283,8 +300,8 @@ var Playlist = Vue.extend({ // ensure sound is stopped (beforeDestroy()) sound.stop(); - // next only when single mode - if(this.single_mode) + // next only when read all mode + if(!this.read_all) return; var sounds = this.$refs.sounds; diff --git a/aircox_cms/templates/aircox_cms/sections/playlist.html b/aircox_cms/templates/aircox_cms/sections/playlist.html index 387fef3..362996b 100755 --- a/aircox_cms/templates/aircox_cms/sections/playlist.html +++ b/aircox_cms/templates/aircox_cms/sections/playlist.html @@ -9,16 +9,21 @@