From 8624927fc4d2c3519d42f68f913456e25eca8747 Mon Sep 17 00:00:00 2001 From: Prem Sichanugrist Date: Tue, 1 May 2012 17:54:11 -0400 Subject: [PATCH] Rewritten video module --- djangoapps/courseware/modules/video_module.py | 23 +-- static/coffee/src/courseware.coffee | 2 + static/coffee/src/main.coffee | 4 + static/js/jquery.scrollTo-1.4.2-min.js | 11 ++ static/sass/courseware/_video.scss | 114 +++++++------- templates/coffee/src/modules/video.coffee | 41 +++++ .../src/modules/video/video_caption.coffee | 116 ++++++++++++++ .../src/modules/video/video_player.coffee | 145 ++++++++++++++++++ .../video/video_progress_slider.coffee | 54 +++++++ .../modules/video/video_speed_control.coffee | 38 +++++ templates/coffee/src/time.coffee | 21 +++ templates/courseware.html | 2 +- templates/main.html | 5 + templates/video.html | 142 ++++------------- 14 files changed, 530 insertions(+), 188 deletions(-) create mode 100644 static/js/jquery.scrollTo-1.4.2-min.js create mode 100644 templates/coffee/src/modules/video.coffee create mode 100644 templates/coffee/src/modules/video/video_caption.coffee create mode 100644 templates/coffee/src/modules/video/video_player.coffee create mode 100644 templates/coffee/src/modules/video/video_progress_slider.coffee create mode 100644 templates/coffee/src/modules/video/video_speed_control.coffee create mode 100644 templates/coffee/src/time.coffee diff --git a/djangoapps/courseware/modules/video_module.py b/djangoapps/courseware/modules/video_module.py index c678838f2b..1cf2c71435 100644 --- a/djangoapps/courseware/modules/video_module.py +++ b/djangoapps/courseware/modules/video_module.py @@ -30,32 +30,19 @@ class Module(XModule): def get_xml_tags(c): '''Tags in the courseware file guaranteed to correspond to the module''' return ["video"] - + def video_list(self): l = self.youtube.split(',') l = [i.split(":") for i in l] return json.dumps(dict(l)) - + def get_html(self): return render_to_string('video.html',{'streams':self.video_list(), 'id':self.item_id, - 'position':self.position, - 'name':self.name, + 'position':self.position, + 'name':self.name, 'annotations':self.annotations}) - def get_init_js(self): - '''JavaScript code to be run when problem is shown. Be aware - that this may happen several times on the same page - (e.g. student switching tabs). Common functions should be put - in the main course .js files for now. ''' - log.debug(u"INIT POSITION {0}".format(self.position)) - return render_to_string('video_init.js',{'streams':self.video_list(), - 'id':self.item_id, - 'position':self.position})+self.annotations_init - - def get_destroy_js(self): - return "videoDestroy(\"{0}\");".format(self.item_id)+self.annotations_destroy - def __init__(self, system, xml, item_id, state=None): XModule.__init__(self, system, xml, item_id, state) xmltree=etree.fromstring(xml) @@ -69,5 +56,3 @@ class Module(XModule): self.annotations=[(e.get("name"),self.render_function(e)) \ for e in xmltree] - self.annotations_init="".join([e[1]['init_js'] for e in self.annotations if 'init_js' in e[1]]) - self.annotations_destroy="".join([e[1]['destroy_js'] for e in self.annotations if 'destroy_js' in e[1]]) diff --git a/static/coffee/src/courseware.coffee b/static/coffee/src/courseware.coffee index a4c93ec0b4..73b7938b46 100644 --- a/static/coffee/src/courseware.coffee +++ b/static/coffee/src/courseware.coffee @@ -13,6 +13,8 @@ class window.Courseware autoHeight: false $('#open_close_accordion a').click navigation.toggle + $('#accordion').show() + log: (event, ui) -> log_event 'accordion', newheader: ui.newHeader.text() diff --git a/static/coffee/src/main.coffee b/static/coffee/src/main.coffee index 8a9a892f94..39f84228a6 100644 --- a/static/coffee/src/main.coffee +++ b/static/coffee/src/main.coffee @@ -2,7 +2,11 @@ $ -> $.ajaxSetup headers : { 'X-CSRFToken': $.cookie 'csrftoken' } + window.onTouchBasedDevice = -> + navigator.userAgent.match /iPhone|iPod|iPad/i + Calculator.bind() Courseware.bind() FeedbackForm.bind() $("a[rel*=leanModal]").leanModal() + diff --git a/static/js/jquery.scrollTo-1.4.2-min.js b/static/js/jquery.scrollTo-1.4.2-min.js new file mode 100644 index 0000000000..73a334184e --- /dev/null +++ b/static/js/jquery.scrollTo-1.4.2-min.js @@ -0,0 +1,11 @@ +/** + * jQuery.ScrollTo - Easy element scrolling using jQuery. + * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com + * Dual licensed under MIT and GPL. + * Date: 5/25/2009 + * @author Ariel Flesler + * @version 1.4.2 + * + * http://flesler.blogspot.com/2007/10/jqueryscrollto.html + */ +;(function(d){var k=d.scrollTo=function(a,i,e){d(window).scrollTo(a,i,e)};k.defaults={axis:'xy',duration:parseFloat(d.fn.jquery)>=1.3?0:1};k.window=function(a){return d(window)._scrollable()};d.fn._scrollable=function(){return this.map(function(){var a=this,i=!a.nodeName||d.inArray(a.nodeName.toLowerCase(),['iframe','#document','html','body'])!=-1;if(!i)return a;var e=(a.contentWindow||a).document||a.ownerDocument||a;return d.browser.safari||e.compatMode=='BackCompat'?e.body:e.documentElement})};d.fn.scrollTo=function(n,j,b){if(typeof j=='object'){b=j;j=0}if(typeof b=='function')b={onAfter:b};if(n=='max')n=9e9;b=d.extend({},k.defaults,b);j=j||b.speed||b.duration;b.queue=b.queue&&b.axis.length>1;if(b.queue)j/=2;b.offset=p(b.offset);b.over=p(b.over);return this._scrollable().each(function(){var q=this,r=d(q),f=n,s,g={},u=r.is('html,body');switch(typeof f){case'number':case'string':if(/^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(f)){f=p(f);break}f=d(f,this);case'object':if(f.is||f.style)s=(f=d(f)).offset()}d.each(b.axis.split(''),function(a,i){var e=i=='x'?'Left':'Top',h=e.toLowerCase(),c='scroll'+e,l=q[c],m=k.max(q,i);if(s){g[c]=s[h]+(u?0:l-r.offset()[h]);if(b.margin){g[c]-=parseInt(f.css('margin'+e))||0;g[c]-=parseInt(f.css('border'+e+'Width'))||0}g[c]+=b.offset[h]||0;if(b.over[h])g[c]+=f[i=='x'?'width':'height']()*b.over[h]}else{var o=f[h];g[c]=o.slice&&o.slice(-1)=='%'?parseFloat(o)/100*m:o}if(/^\d+$/.test(g[c]))g[c]=g[c]<=0?0:Math.min(g[c],m);if(!a&&b.queue){if(l!=g[c])t(b.onAfterFirst);delete g[c]}});t(b.onAfter);function t(a){r.animate(g,j,b.easing,a&&function(){a.call(this,n,b)})}}).end()};k.max=function(a,i){var e=i=='x'?'Width':'Height',h='scroll'+e;if(!d(a).is('html,body'))return a[h]-d(a)[e.toLowerCase()]();var c='client'+e,l=a.ownerDocument.documentElement,m=a.ownerDocument.body;return Math.max(l[h],m[h])-Math.min(l[c],m[c])};function p(a){return typeof a=='object'?a:{top:a,left:a}}})(jQuery); \ No newline at end of file diff --git a/static/sass/courseware/_video.scss b/static/sass/courseware/_video.scss index 54c1b9f600..76da33d652 100644 --- a/static/sass/courseware/_video.scss +++ b/static/sass/courseware/_video.scss @@ -14,38 +14,29 @@ section.course-content { } } - div.video-subtitles { + div.video { + @include clearfix(); background: #f3f3f3; border-bottom: 1px solid #e1e1e1; border-top: 1px solid #e1e1e1; - @include clearfix(); display: block; margin: 0 (-(lh())); padding: 6px lh(); - div.video-wrapper { + article.video-wrapper { float: left; margin-right: flex-gutter(9); width: flex-grid(6, 9); - div.video-player { + section.video-player { height: 0; overflow: hidden; padding-bottom: 56.25%; padding-top: 30px; position: relative; - object { - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; - } - - iframe#html5_player { + object, iframe { border: none; - display: none; height: 100%; left: 0; position: absolute; @@ -68,7 +59,7 @@ section.course-content { } } - div#slider { + div.slider { @extend .clearfix; background: #c2c2c2; border: none; @@ -175,7 +166,8 @@ section.course-content { } } - div#vidtime { + div.vidtime { + padding-left: lh(.75); font-weight: bold; line-height: 46px; //height of play pause buttons padding-left: lh(.75); @@ -190,6 +182,7 @@ section.course-content { div.speeds { float: left; + position: relative; a { background: url('../images/closed-arrow.png') 10px center no-repeat; @@ -211,7 +204,7 @@ section.course-content { &.open { background: url('../images/open-arrow.png') 10px center no-repeat; - ol#video_speeds { + ol.video_speeds { display: block; opacity: 1; } @@ -234,47 +227,52 @@ section.course-content { padding: 0 lh(.5) 0 0; } - // fix for now - ol#video_speeds { + &:hover, &:active, &:focus { + opacity: 1; background-color: #444; - border: 1px solid #000; - @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); - display: none; - left: -1px; - opacity: 0; - position: absolute; - top:0; - @include transition(); - width: 100%; - z-index: 10; + } + } - li { - border-bottom: 1px solid #000; - @include box-shadow( 0 1px 0 #555); + // fix for now + ol.video_speeds { + @include box-shadow(inset 1px 0 0 #555, 0 3px 0 #444); + @include transition(); + background-color: #444; + border: 1px solid #000; + bottom: 46px; + display: none; + opacity: 0; + position: absolute; + width: 125px; + z-index: 10; + + li { + @include box-shadow( 0 1px 0 #555); + border-bottom: 1px solid #000; + color: #fff; + cursor: pointer; + + a { + border: 0; color: #fff; - cursor: pointer; - padding: 0 lh(.5); - - &.active { - font-weight: bold; - } - - &:last-child { - border-bottom: 0; - @include box-shadow(none); - margin-top: 0; - } + display: block; + padding: lh(.5); &:hover { background-color: #666; color: #aaa; } } - } - &:hover { - background-color: #444; - opacity: 1; + &.active { + font-weight: bold; + } + + &:last-child { + @include box-shadow(none); + border-bottom: 0; + margin-top: 0; + } } } } @@ -334,7 +332,7 @@ section.course-content { opacity: 1; } - div#slider { + div.slider { height: 14px; margin-top: -7px; @@ -352,7 +350,7 @@ section.course-content { ol.subtitles { float: left; max-height: 460px; - overflow: hidden; + overflow: auto; padding-top: 10px; width: flex-grid(3, 9); @@ -360,7 +358,7 @@ section.course-content { border: 0; color: #666; cursor: pointer; - margin-bottom: 0px; + margin-bottom: 8px; padding: 0; @include transition(all, .5s, ease-in); @@ -373,11 +371,7 @@ section.course-content { color: $mit-red; } - div { - margin-bottom: 8px; - } - - div:empty { + &:empty { margin-bottom: 0px; } } @@ -386,7 +380,7 @@ section.course-content { &.closed { @extend .trans; - div.video-wrapper { + article.video-wrapper { width: flex-grid(9,9); } @@ -441,11 +435,11 @@ section.course-content { } div.tc-wrapper { - div.video-wrapper { + article.video-wrapper { width: 100%; } - object#myytplayer, iframe { + object, iframe { bottom: 0; height: 100%; left: 0; @@ -487,7 +481,7 @@ section.course-content { } } -div.course-wrapper.closed section.course-content div.video-subtitles { +div.course-wrapper.closed section.course-content div.video { ol.subtitles { max-height: 577px; } diff --git a/templates/coffee/src/modules/video.coffee b/templates/coffee/src/modules/video.coffee new file mode 100644 index 0000000000..01aad7c334 --- /dev/null +++ b/templates/coffee/src/modules/video.coffee @@ -0,0 +1,41 @@ +class window.Video + constructor: (@id, videos) -> + @element = $("#video_#{@id}") + @parseVideos videos + @fetchMetadata() + @parseSpeed() + $("#video_#{@id}").data('video', this) + window.onYouTubePlayerAPIReady = => + $('.course-content .video').each -> + $(this).data('video').embed() + + youtubeId: (speed)-> + @videos[speed || @speed] + + parseVideos: (videos) -> + @videos = {} + $.each videos, (speed, url) => + speed = parseFloat(speed).toFixed(2).replace /\.00$/, '.0' + @videos[speed] = url + + parseSpeed: -> + @setSpeed($.cookie('video_speed')) + @speeds = ($.map @videos, (url, speed) -> speed).sort() + + setSpeed: (newSpeed) -> + if @videos[newSpeed] != undefined + @speed = newSpeed + $.cookie('video_speed', "#{newSpeed}", expires: 3650, path: '/') + else + @speed = '1.0' + + embed: -> + @player = new VideoPlayer(this) + + fetchMetadata: (url) -> + @metadata = {} + $.each @videos, (speed, url) => + $.get "http://gdata.youtube.com/feeds/api/videos/#{url}?v=2&alt=jsonc", ((data) => @metadata[data.data.id] = data.data) , 'jsonp' + + getDuration: -> + @metadata[@youtubeId()].duration diff --git a/templates/coffee/src/modules/video/video_caption.coffee b/templates/coffee/src/modules/video/video_caption.coffee new file mode 100644 index 0000000000..8742c2031a --- /dev/null +++ b/templates/coffee/src/modules/video/video_caption.coffee @@ -0,0 +1,116 @@ +class VideoCaption + constructor: (@player, @youtubeId) -> + @index = [] + @fetchCaption() + @bind() + + $: (selector) -> + @player.$(selector) + + bind: -> + $(window).bind('resize', @onWindowResize) + $(@player).bind('resize', @onWindowResize) + $(@player).bind('updatePlayTime', @onUpdatePlayTime) + @$('.hide-subtitles').click @toggle + + fetchCaption: -> + $.getJSON @captionURL(), (captions) => + @captions = captions.text + @start = captions.start + for index in [0...captions.start.length] + for time in [captions.start[index]..captions.end[index]] + @index[time] ||= [] + @index[time].push(index) + @render() + + captionURL: -> + "/static/subs/#{@youtubeId}.srt.sjson" + + render: -> + container = $('
    ') + container.css maxHeight: @$('.video-wrapper').height() - 5 + + $.each @captions, (index, text) => + container.append $('
  1. ').html(text).attr + 'data-index': index + 'data-start': @start[index] + + @$('.subtitles').replaceWith(container) + @$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave) + .mousemove(@onMovement).bind('mousewheel', @onMovement) + .bind('DOMMouseScroll', @onMovement) + @$('.subtitles li[data-index]').click @seekPlayer + + # prepend and append an empty
  2. for cosmatic reason + @$('.subtitles').prepend($('
  3. ').height(@topSpacingHeight())) + .append($('
  4. ').height(@bottomSpacingHeight())) + + onUpdatePlayTime: (event, time) => + # This 250ms offset is required to match the video speed + time = Math.round(Time.convert(time, @player.currentSpeed(), '1.0') * 1000 + 250) + newIndex = @index[time] + + if newIndex != undefined && @currentIndex != newIndex + if @currentIndex + for index in @currentIndex + @$(".subtitles li[data-index='#{index}']").removeClass('current') + + for index in newIndex + @$(".subtitles li[data-index='#{newIndex}']").addClass('current') + + @currentIndex = newIndex + @scrollCaption() + + onWindowResize: => + @$('.subtitles').css maxHeight: @captionHeight() + @$('.subtitles .spacing:first').height(@topSpacingHeight()) + @$('.subtitles .spacing:last').height(@bottomSpacingHeight()) + @scrollCaption() + + onMouseEnter: => + clearTimeout @frozen if @frozen + @frozen = setTimeout @onMouseLeave, 10000 + + onMovement: (event) => + @onMouseEnter() + + onMouseLeave: => + clearTimeout @frozen if @frozen + @frozen = null + @scrollCaption() if @player.isPlaying() + + scrollCaption: -> + if !@frozen && @$('.subtitles .current:first').length + @$('.subtitles').scrollTo @$('.subtitles .current:first'), + offset: - @calculateOffset(@$('.subtitles .current:first')) + + seekPlayer: (event) => + event.preventDefault() + time = Math.round(Time.convert($(event.target).data('start'), '1.0', @player.currentSpeed()) / 1000) + $(@player).trigger('seek', time) + + calculateOffset: (element) -> + @captionHeight() / 2 - element.height() / 2 + + topSpacingHeight: -> + @calculateOffset(@$('.subtitles li:not(.spacing).first')) + + bottomSpacingHeight: -> + @calculateOffset(@$('.subtitles li:not(.spacing).last')) + + toggle: (event) => + event.preventDefault() + if @player.element.hasClass('closed') + @$('.hide-subtitles').attr('title', 'Turn off captions') + @player.element.removeClass('closed') + else + @$('.hide-subtitles').attr('title', 'Turn on captions') + @player.element.addClass('closed') + @scrollCaption() + + captionHeight: -> + if @player.element.hasClass('fullscreen') + $(window).height() - @$('.video-controls').height() + else + @$('.video-wrapper').height() + diff --git a/templates/coffee/src/modules/video/video_player.coffee b/templates/coffee/src/modules/video/video_player.coffee new file mode 100644 index 0000000000..b6ae94aabb --- /dev/null +++ b/templates/coffee/src/modules/video/video_player.coffee @@ -0,0 +1,145 @@ +class VideoPlayer + constructor: (@video) -> + @currentTime = 0 + @element = $("#video_#{@video.id}") + @buildPlayer() + @bind() + + $: (selector) -> + $(selector, @element) + + bind: -> + $(@).bind('seek', @onSeek) + $(@).bind('updatePlayTime', @onUpdatePlayTime) + $(@).bind('speedChange', @onSpeedChange) + $(document).keyup @bindExitFullScreen + + @$('.video_control').click @togglePlayback + @$('.add-fullscreen').click @toggleFullScreen + @addToolTip unless onTouchBasedDevice() + + bindExitFullScreen: (event) => + if @element.hasClass('fullscreen') && event.keyCode == 27 + @toggleFullScreen(event) + + buildPlayer: -> + new VideoCaption(this, @video.youtubeId('1.0')) + new VideoSpeedControl(this, @video.speeds) + new VideoProgressSlider(this) + @player = new YT.Player @video.id, + playerVars: + controls: 0 + wmode: 'transparent' + rel: 0 + showinfo: 0 + enablejsapi: 1 + videoId: @video.youtubeId() + events: + onReady: @onReady + onStateChange: @onStateChange + + addToolTip: -> + @$('.add-fullscreen, .hide-subtitles').qtip + position: + my: 'top right' + at: 'top center' + + onReady: => + @setProgress(0, @duration()) + $(@).trigger('ready') + unless true || onTouchBasedDevice() + $('.course-content .video:first').data('video').player.play() + + onStateChange: (event) => + switch event.data + when YT.PlayerState.PLAYING + if window.player && window.player != @player + window.player.pauseVideo() + window.player = @player + @onPlay() + when YT.PlayerState.PAUSED + if window.player == @player + window.player = null + @onPause() + when YT.PlayerState.ENDED + if window.player == @player + window.player = null + @onPause() + + onPlay: -> + @$('.video_control').removeClass('play').addClass('pause').html('Pause') + unless @player.interval + @player.interval = setInterval(@update, 200) + + onPause: -> + @$('.video_control').removeClass('pause').addClass('play').html('Play') + clearInterval(@player.interval) + @player.interval = null + + onSeek: (event, time) -> + @player.seekTo(time, true) + if @isPlaying() + clearInterval(@player.interval) + @player.interval = setInterval(@update, 200) + else + @currentTime = time + $(@).trigger('updatePlayTime', time) + + onSpeedChange: (event, newSpeed) => + @currentTime = Time.convert(@currentTime, parseFloat(@currentSpeed()), newSpeed) + @video.setSpeed(parseFloat(newSpeed).toFixed(2).replace /\.00$/, '.0') + + if @isPlaying() + @player.loadVideoById(@video.youtubeId(), @currentTime) + else + @player.cueVideoById(@video.youtubeId(), @currentTime) + @setProgress(@currentTime, @duration()) + $(@).trigger('updatePlayTime', @currentTime) + + update: => + if @currentTime = @player.getCurrentTime() + $(@).trigger('updatePlayTime', @currentTime) + + onUpdatePlayTime: (event, time) => + @setProgress(@currentTime) if time + + setProgress: (time) => + progress = Time.format(time) + ' / ' + Time.format(@duration()) + if @progress != progress + @$(".vidtime").html(progress) + @progress = progress + + togglePlayback: (event) => + event.preventDefault() + if $(event.target).hasClass('play') + @play() + else + @pause() + + toggleFullScreen: (event) => + event.preventDefault() + if @element.hasClass('fullscreen') + @$('.exit').remove() + @$('.add-fullscreen').attr('title', 'Fill browser') + @element.removeClass('fullscreen') + else + @element.append('Exit').addClass('fullscreen') + @$('.add-fullscreen').attr('title', 'Exit fill browser') + @$('.exit').click @toggleFullScreen + $(@).trigger('resize') + + # Delegates + play: -> + @player.playVideo() if @player.playVideo + + isPlaying: -> + @player.getPlayerState() == YT.PlayerState.PLAYING + + pause: -> + @player.pauseVideo() + + duration: -> + @video.getDuration() + + currentSpeed: -> + @video.speed diff --git a/templates/coffee/src/modules/video/video_progress_slider.coffee b/templates/coffee/src/modules/video/video_progress_slider.coffee new file mode 100644 index 0000000000..4eaba392ba --- /dev/null +++ b/templates/coffee/src/modules/video/video_progress_slider.coffee @@ -0,0 +1,54 @@ +class VideoProgressSlider + constructor: (@player) -> + @buildSlider() + @buildHandle() + $(@player).bind('updatePlayTime', @onUpdatePlayTime) + $(@player).bind('ready', @onReady) + + $: (selector) -> + @player.$(selector) + + buildSlider: -> + @slider = @$('.slider').slider + range: 'min' + change: @onChange + slide: @onSlide + stop: @onStop + + buildHandle: -> + @handle = @$('.ui-slider-handle') + @handle.qtip + content: "#{Time.format(@slider.slider('value'))}" + position: + my: 'bottom center' + at: 'top center' + container: @handle + hide: + delay: 700 + style: + classes: 'ui-tooltip-slider' + widget: true + + onReady: => + @slider.slider('option', 'max', @player.duration()) + + onUpdatePlayTime: (event, currentTime) => + if !@frozen + @slider.slider('option', 'max', @player.duration()) + @slider.slider('value', currentTime) + + onSlide: (event, ui) => + @frozen = true + @updateTooltip(ui.value) + $(@player).trigger('seek', ui.value) + + onChange: (event, ui) => + @updateTooltip(ui.value) + + onStop: (event, ui) => + @frozen = true + $(@player).trigger('seek', ui.value) + setTimeout (=> @frozen = false), 200 + + updateTooltip: (value)-> + @handle.qtip('option', 'content.text', "#{Time.format(value)}") diff --git a/templates/coffee/src/modules/video/video_speed_control.coffee b/templates/coffee/src/modules/video/video_speed_control.coffee new file mode 100644 index 0000000000..1214053fd9 --- /dev/null +++ b/templates/coffee/src/modules/video/video_speed_control.coffee @@ -0,0 +1,38 @@ +class VideoSpeedControl + constructor: (@player, @speeds) -> + @build() + @bind() + + $: (selector) -> + @player.$(selector) + + bind: -> + $(@player).bind('speedChange', @onSpeedChange) + @$('.video_speeds a').click @changeVideoSpeed + if onTouchBasedDevice() + @$('.speeds').click -> $(this).toggleClass('open') + else + @$('.speeds').mouseover -> $(this).addClass('open') + .mouseout -> $(this).removeClass('open') + .click (event) -> + event.preventDefault() + $(this).removeClass('open') + + build: -> + $.each @speeds, (index, speed) => + link = $('').attr(href: "#").html("#{speed}x") + @$('.video_speeds').prepend($('
  5. ').attr('data-speed', speed).html(link)) + @setSpeed(@player.currentSpeed()) + + changeVideoSpeed: (event) => + event.preventDefault() + unless $(event.target).parent().hasClass('active') + $(@player).trigger 'speedChange', $(event.target).parent().data('speed') + + onSpeedChange: (event, speed) => + @setSpeed(parseFloat(speed).toFixed(2).replace /\.00$/, '.0') + + setSpeed: (speed) -> + @$('.video_speeds li').removeClass('active') + @$(".video_speeds li[data-speed='#{speed}']").addClass('active') + @$('.speeds p.active').html("#{speed}x") diff --git a/templates/coffee/src/time.coffee b/templates/coffee/src/time.coffee new file mode 100644 index 0000000000..3e39820866 --- /dev/null +++ b/templates/coffee/src/time.coffee @@ -0,0 +1,21 @@ +class @Time + @format: (time) -> + seconds = Math.floor time + minutes = Math.floor seconds / 60 + hours = Math.floor minutes / 60 + seconds = seconds % 60 + minutes = minutes % 60 + + if hours + "#{hours}:#{@pad(minutes)}:#{@pad(seconds % 60)}" + else + "#{minutes}:#{@pad(seconds % 60)}" + + @pad: (number) -> + if number < 10 + "0#{number}" + else + number + + @convert: (time, oldSpeed, newSpeed) -> + (time * oldSpeed / newSpeed).toFixed(3) diff --git a/templates/courseware.html b/templates/courseware.html index 404871e49c..ca74d843a8 100644 --- a/templates/courseware.html +++ b/templates/courseware.html @@ -26,7 +26,7 @@ close -
    +