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 = $('