Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/TimedText.config.php |
— | — | @@ -20,6 +20,12 @@ |
21 | 21 | // If the link to request a transcript should be shown on video files |
22 | 22 | 'TimedText.ShowRequestTranscript' => false, |
23 | 23 | |
24 | | - // The category for listing videos that need transcription: |
25 | | - 'TimedText.NeedsTranscriptCategory' => 'Videos needing subtitles' |
| 24 | + // Category for listing videos that need transcription: |
| 25 | + 'TimedText.NeedsTranscriptCategory' => 'Videos needing subtitles', |
| 26 | + |
| 27 | + // Default bottom text padding |
| 28 | + 'TimedText.BottomPadding' => 10, |
| 29 | + |
| 30 | + // Height of black box below video. |
| 31 | + 'TimedText.BelowVideoBlackBoxHeight' => 60, |
26 | 32 | ); |
\ No newline at end of file |
Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedTextEdit.js |
— | — | @@ -1,455 +0,0 @@ |
2 | | -/** |
3 | | -* Timed text edit interface based off of participatory culture foundation timed text mockups. |
4 | | -*/ |
5 | | -( function( mw, $ ) { |
6 | | - |
7 | | -mw.TimedTextEdit = function( parentTimedText ) { |
8 | | - return this.init( parentTimedText ); |
9 | | -}; |
10 | | -mw.TimedTextEdit.prototype = { |
11 | | - // The target container for the interface: |
12 | | - target_container: null, |
13 | | - |
14 | | - // Interface steps can be "transcribe", "sync", "translate" |
15 | | - textEditStages:{ |
16 | | - 'upload':{ |
17 | | - 'icon' : 'folder-open' |
18 | | - } |
19 | | - /* |
20 | | - 'transcribe':{ |
21 | | - 'icon' : 'comment' |
22 | | - }, |
23 | | - 'sync':{ |
24 | | - 'icon' : 'clock' |
25 | | - }, |
26 | | - 'translate':{ |
27 | | - 'icon' : 'flag' |
28 | | - } |
29 | | - */ |
30 | | - }, |
31 | | - |
32 | | - /** |
33 | | - * @constructor |
34 | | - * @param {Object} parentTimedText The parent TimedText object that called the editor |
35 | | - */ |
36 | | - init: function( parentTimedText ) { |
37 | | - this.parentTimedText = parentTimedText; |
38 | | - }, |
39 | | - |
40 | | - /** |
41 | | - * Show the editor UI |
42 | | - */ |
43 | | - showUI: function() { |
44 | | - // Setup the parent container: |
45 | | - this.createDialogContainer(); |
46 | | - |
47 | | - // Setup the timedText editor interface |
48 | | - this.initDialog(); |
49 | | - }, |
50 | | - |
51 | | - /** |
52 | | - * Setup the dialog layout: s |
53 | | - */ |
54 | | - initDialog: function() { |
55 | | - var _this =this; |
56 | | - _this.createTabs(); |
57 | | - }, |
58 | | - |
59 | | - /** |
60 | | - * Creates interface tabs from the textEditStages |
61 | | - */ |
62 | | - createTabs: function() { |
63 | | - var _this = this; |
64 | | - $tabContainer = $( '<div />' ) |
65 | | - .attr( 'id', "TimedTextEdit-tabs" ) |
66 | | - .append( '<ul />' ); |
67 | | - for(var edit_stage_id in this.textEditStages) { |
68 | | - var editStage = this.textEditStages[ edit_stage_id ]; |
69 | | - // Append the menu item: |
70 | | - $tabContainer.find('ul').append( |
71 | | - $('<li>').append( |
72 | | - $('<a>') |
73 | | - .attr( 'href', '#tab-' + edit_stage_id ) |
74 | | - .append( |
75 | | - $('<span />') |
76 | | - .css( "float","left" ) |
77 | | - .addClass( 'ui-icon ui-icon-' + editStage.icon ) |
78 | | - , |
79 | | - $('<span>') |
80 | | - .text( gM('mwe-timedtext-stage-' + edit_stage_id) ) |
81 | | - ) |
82 | | - ) |
83 | | - ); |
84 | | - // Append the menu item content container |
85 | | - $tabContainer.append( |
86 | | - $('<div>') |
87 | | - .attr( 'id', 'tab-' + edit_stage_id ) |
88 | | - .css({ |
89 | | - 'height': $( window ).height() - 270, |
90 | | - 'position': 'relative' |
91 | | - }) |
92 | | - ); |
93 | | - } |
94 | | - //debugger |
95 | | - // Add the tags to the target: |
96 | | - $( _this.target_container ).append( $tabContainer ); |
97 | | - |
98 | | - //Create all the "interfaces" |
99 | | - for(var edit_stage_id in this.textEditStages) { |
100 | | - _this.createInterface( edit_stage_id ); |
101 | | - } |
102 | | - |
103 | | - //Add tabs interface |
104 | | - $('#TimedTextEdit-tabs').tabs( { |
105 | | - select: function( event, ui ) { |
106 | | - _this.selectTab( $( ui.tab ).attr( 'href' ).replace('#','') ); |
107 | | - } |
108 | | - }); |
109 | | - |
110 | | - }, |
111 | | - selectTab: function( tab_id ) { |
112 | | - mw.log('sel: ' + tab_id); |
113 | | - }, |
114 | | - |
115 | | - /** |
116 | | - * Creates an interface for a given stage id |
117 | | - * @return {Object} the jquery interface |
118 | | - */ |
119 | | - createInterface: function( edit_stage_id) { |
120 | | - $target = $('#tab-' + edit_stage_id); |
121 | | - if( this[edit_stage_id + 'Interface']) { |
122 | | - this[ edit_stage_id + 'Interface']( $target ); |
123 | | - }else{ |
124 | | - $target.append( ' interface under development' ); |
125 | | - } |
126 | | - }, |
127 | | - /** |
128 | | - * Builds out and binds the upload interface to a given target |
129 | | - * @param {Object} $target jQuery target for the upload interface |
130 | | - */ |
131 | | - uploadInterface: function( $target ) { |
132 | | - var _this = this; |
133 | | - // Check if user has XHR file upload support & we are on the target wiki |
134 | | - |
135 | | - $target.append( |
136 | | - $('<div />') |
137 | | - .addClass( "leftcolumn" ) |
138 | | - .append('<h4>') |
139 | | - .text( gM('mwe-timedtext-upload-text') ), |
140 | | - $('<div />') |
141 | | - .addClass( 'rightcolumn' ) |
142 | | - .append( |
143 | | - $( '<span />' ) |
144 | | - .attr('id', "timed-text-rightcolum-desc") |
145 | | - .append( |
146 | | - $('<h4>') |
147 | | - .text( gM('mwe-timedtext-upload-text-desc-title') ), |
148 | | - $('<i>').text ( gM( 'mwe-timedtext-upload-text-desc-help' ) ), |
149 | | - $('<ul>').append( |
150 | | - $('<li>').text( gM('mwe-timedtext-upload-text-desc-help-browse') ), |
151 | | - $('<li>').text( gM('mwe-timedtext-upload-text-desc-help-select') ), |
152 | | - $('<li>').text( gM('mwe-timedtext-upload-text-desc-help-review') ) |
153 | | - ) |
154 | | - ), |
155 | | - //The text preview |
156 | | - $('<h3>') |
157 | | - .text( gM( 'mwe-timedtext-upload-text-preview' ) ), |
158 | | - $('<textarea id="timed-text-file-preview"></textarea>') |
159 | | - ) |
160 | | - ); |
161 | | - |
162 | | - // Adjust the height of the text preview: |
163 | | - $('#timed-text-file-preview') |
164 | | - .css({ |
165 | | - 'width':'100%', |
166 | | - 'height': '300px' |
167 | | - }); |
168 | | - |
169 | | - // Add Select file: |
170 | | - $target.append( |
171 | | - $('<div>').css({ |
172 | | - 'width':'300px', |
173 | | - 'float': 'left' |
174 | | - }).append( |
175 | | - $('<input />') |
176 | | - .attr( { |
177 | | - 'type': "file", |
178 | | - 'id' : "timed-text-file-upload" |
179 | | - }), |
180 | | - $('<br />') |
181 | | - ) |
182 | | - ); |
183 | | - |
184 | | - |
185 | | - $target.append( |
186 | | - //Get a little helper input field to update the language |
187 | | - $('<input />') |
188 | | - .attr( { |
189 | | - 'id' : "timed-text-langKey-input", |
190 | | - 'type' : "text", |
191 | | - 'maxlength' : "10", |
192 | | - 'size' :"3" |
193 | | - } ) |
194 | | - .change(function() { |
195 | | - var langKey = $(this).val(); |
196 | | - if( mw.Language.names[ langKey ] ) { |
197 | | - $buttonTarget.find('.btnText').text( |
198 | | - mw.Language.names[ langKey ] |
199 | | - ); |
200 | | - } |
201 | | - }), |
202 | | - // Get a jQuery button object with language menu: |
203 | | - $.button( { |
204 | | - 'style': { 'float' : 'left' }, |
205 | | - 'class': 'language-select-btn', |
206 | | - 'text': gM('mwe-timedtext-select-language'), |
207 | | - 'icon': 'triangle-1-e' |
208 | | - } ) |
209 | | - .attr('id', 'language-select') |
210 | | - ) |
211 | | - |
212 | | - |
213 | | - var $buttonTarget = $target.find('.language-select-btn'); |
214 | | - |
215 | | - // Add menu container: |
216 | | - var loc = $buttonTarget.position(); |
217 | | - $target.append( |
218 | | - $('<div>') |
219 | | - .addClass('ui-widget ui-widget-content ui-corner-all') |
220 | | - .attr( 'id', 'upload-language-select' ) |
221 | | - .loadingSpinner() |
222 | | - .css( { |
223 | | - 'position' : 'relative', |
224 | | - 'z-index' : 10, |
225 | | - 'height' : '180px', |
226 | | - 'width' : '180px', |
227 | | - 'overflow' : 'auto', |
228 | | - 'font-size' : '12px', |
229 | | - 'z-index' : 1005 |
230 | | - } ) |
231 | | - .hide() |
232 | | - ); |
233 | | - // Add menu binding to button target |
234 | | - setTimeout(function(){ |
235 | | - $buttonTarget.menu( { |
236 | | - 'content' : _this.getLanguageList(), |
237 | | - 'backLinkText' : gM( 'mwe-timedtext-back-btn' ), |
238 | | - 'targetMenuContainer': '#upload-language-select', |
239 | | - 'keepPosition' : true |
240 | | - } ); |
241 | | - // force the layout ( menu binding does strange things ) |
242 | | - $('#upload-language-select').css( {'left': '315px', 'top' : '87px', 'position' : 'absolute'}); |
243 | | - },10); |
244 | | - |
245 | | - |
246 | | - //Add upload input bindings: |
247 | | - $( '#timed-text-file-upload' ).change( function( ev ) { |
248 | | - if ( $(this).val() ) { |
249 | | - |
250 | | - // Update the preview text area: |
251 | | - var file = $( '#timed-text-file-upload' ).get(0).files[0]; |
252 | | - if( file.fileSize > 1048576 ) { |
253 | | - $( '#timed-text-file-preview' ).text( 'Error the file you selected is too lage'); |
254 | | - return ; |
255 | | - } |
256 | | - var srtData = file.getAsBinary(); |
257 | | - srtData = srtData.replace( '\r\n', '\n' ); |
258 | | - $( '#timed-text-file-preview' ).text( srtData ); |
259 | | - |
260 | | - // Update the selected language |
261 | | - var langKey = $(this).val().split( '.' ); |
262 | | - var extension = langKey.pop(); |
263 | | - langKey = langKey.pop(); |
264 | | - if( mw.Language.names[ langKey ] ) { |
265 | | - $buttonTarget.find('.btnText').text( |
266 | | - mw.Language.names[ langKey ] |
267 | | - ); |
268 | | - // Update the key code |
269 | | - $('#timed-text-langKey-input').val( langKey ); |
270 | | - } |
271 | | - } |
272 | | - }); |
273 | | - |
274 | | - //Add an upload button: |
275 | | - $target.append( |
276 | | - $('<div />') |
277 | | - .css('clear', 'both'), |
278 | | - $('<br />'), |
279 | | - $('<br />'), |
280 | | - $.button( { |
281 | | - 'style': { 'float' : 'left' }, |
282 | | - 'text': gM('mwe-timedtext-upload-text'), |
283 | | - 'icon': 'disk' |
284 | | - } ) |
285 | | - .click( function() { |
286 | | - _this.uploadTextFile(); |
287 | | - }) |
288 | | - ); |
289 | | - |
290 | | - }, |
291 | | - /** |
292 | | - * Uploads the text content |
293 | | - */ |
294 | | - uploadTextFile: function() { |
295 | | - // Put a dialog ontop |
296 | | - mw.addLoaderDialog( gM( 'mwe-timedtext-uploading-text') ); |
297 | | - |
298 | | - // Get timed text target title |
299 | | - // NOTE: this should be cleaned up with accessors |
300 | | - var targetTitleKey = this.parentTimedText.embedPlayer.apiTitleKey; |
301 | | - |
302 | | - // Add TimedText NS and language key and ".srt" |
303 | | - targetTitleKey = 'TimedText:' + targetTitleKey + '.' + $('#timed-text-langKey-input').val() + '.srt'; |
304 | | - |
305 | | - // Get a token |
306 | | - mw.getToken( targetTitleKey, function( token ) { |
307 | | - mw.log("got token: " + token); |
308 | | - var request = { |
309 | | - 'action' : 'edit', |
310 | | - 'title' : targetTitleKey, |
311 | | - 'text' : $('#timed-text-file-preview').val(), |
312 | | - 'token': token |
313 | | - }; |
314 | | - mw.getJSON( request, function( data ) { |
315 | | - //Close the loader dialog: |
316 | | - mw.closeLoaderDialog(); |
317 | | - |
318 | | - if( data.edit && data.edit.result == 'Success' ) { |
319 | | - var buttons = { }; |
320 | | - buttons[ gM("mwe-timedtext-upload-text-another")] = function() { |
321 | | - // just close the current dialog: |
322 | | - $( this ).dialog('close'); |
323 | | - }; |
324 | | - buttons[ gM( "mwe-timedtext-upload-text-done-uploading" ) ] = function() { |
325 | | - window.location.reload(); |
326 | | - }; |
327 | | - //Edit success |
328 | | - setTimeout(function(){ |
329 | | - mw.addDialog( { |
330 | | - 'width' : '400px', |
331 | | - 'title' : gM( "mwe-timedtext-upload-text-done"), |
332 | | - 'content' : gM("mwe-timedtext-upload-text-success"), |
333 | | - 'buttons' : buttons |
334 | | - }); |
335 | | - }, 10 ); |
336 | | - }else{ |
337 | | - //Edit fail |
338 | | - setTimeout(function(){ |
339 | | - mw.addDialog({ |
340 | | - 'width' : '400px', |
341 | | - 'title' : gM( "mwe-timedtext-upload-text-fail-title"), |
342 | | - 'content' :gM( "mwe-timedtext-upload-text-fail-desc"), |
343 | | - 'buttons' : gM( 'mwe-ok' ) |
344 | | - }); |
345 | | - },10 ); |
346 | | - } |
347 | | - }); |
348 | | - }) |
349 | | - }, |
350 | | - |
351 | | - /** |
352 | | - * Gets the language set. |
353 | | - * |
354 | | - * Checks off languages that area already "loaded" according to parentTimedText |
355 | | - * |
356 | | - * This is cpu intensive function |
357 | | - * Optimize: switch to html string building, insert and bind |
358 | | - * (instead of building html with jquery calls ) |
359 | | - * Optimize: pre-sort both language lists and continue checks where we left off |
360 | | - * |
361 | | - * ~ what really a lot of time is putting this ~into~ the dom ~ |
362 | | - */ |
363 | | - getLanguageList: function() { |
364 | | - var _this = this; |
365 | | - var $langMenu = $( '<ul>' ); |
366 | | - // Loop through all supported languages: |
367 | | - for ( var langKey in mw.Language.names ) { |
368 | | - var language = mw.Language.names [ langKey ]; |
369 | | - var source_icon = 'radio-on'; |
370 | | - //check if the key is in the _this.parentTimedText source array |
371 | | - for( var i in _this.parentTimedText.textSources ) { |
372 | | - var pSource = _this.parentTimedText.textSources[i]; |
373 | | - if( pSource.lang == langKey) { |
374 | | - source_icon = 'bullet'; |
375 | | - } |
376 | | - } |
377 | | - // call out to "anonymous" function to variable scope the langKey |
378 | | - $langMenu.append( |
379 | | - _this.getLangMenuItem( langKey , source_icon) |
380 | | - ); |
381 | | - } |
382 | | - return $langMenu; |
383 | | - }, |
384 | | - |
385 | | - getLangMenuItem: function( langKey , source_icon) { |
386 | | - return $.getLineItem( |
387 | | - langKey + ' - ' + mw.Language.names[ langKey ], |
388 | | - source_icon, |
389 | | - function() { |
390 | | - mw.log( "Selected: " + langKey ); |
391 | | - // Update the input box text |
392 | | - $('#timed-text-langKey-input').val( langKey ); |
393 | | - // Update the menu item: |
394 | | - $('#language-select').find('.btnText').text( mw.Language.names[ langKey ] ) |
395 | | - } |
396 | | - ); |
397 | | - }, |
398 | | - /** |
399 | | - * Creates the interface dialog container |
400 | | - */ |
401 | | - createDialogContainer: function() { |
402 | | - var _this = this; |
403 | | - //Setup the target container: |
404 | | - _this.target_container = '#timedTextEdit_target'; |
405 | | - $( _this.target_container ).remove(); |
406 | | - $( 'body' ).append( |
407 | | - $('<div>') |
408 | | - .attr({ |
409 | | - 'id' : 'timedTextEdit_target', |
410 | | - 'title' : gM( 'mwe-timedtext-editor' ) |
411 | | - }) |
412 | | - .addClass('TimedTextEdit') |
413 | | - ); |
414 | | - |
415 | | - // Build cancel button |
416 | | - var cancelButton = {}; |
417 | | - var cancelText = gM( 'mwe-cancel' ); |
418 | | - cancelButton[ cancelText ] = function() { |
419 | | - _this.onCancelClipEdit(); |
420 | | - }; |
421 | | - |
422 | | - $( _this.target_container ).dialog( { |
423 | | - bgiframe: true, |
424 | | - autoOpen: true, |
425 | | - width: $(window).width()-50, |
426 | | - height: $(window).height()-50, |
427 | | - position : 'center', |
428 | | - modal: true, |
429 | | - draggable: false, |
430 | | - resizable: false, |
431 | | - buttons: cancelButton, |
432 | | - close: function() { |
433 | | - // @@TODO if we are 'editing' we should confirm they want to exist: |
434 | | - $( this ).parents( '.ui-dialog' ).fadeOut( 'slow' ); |
435 | | - } |
436 | | - } ); |
437 | | - // set a non-blocking fit window request |
438 | | - setTimeout(function(){ |
439 | | - $( _this.target_container ).dialogFitWindow(); |
440 | | - },10); |
441 | | - |
442 | | - // Add the window resize hook to keep dialog layout |
443 | | - $( window ).resize( function() { |
444 | | - $( _this.target_container ).dialogFitWindow(); |
445 | | - } ); |
446 | | - |
447 | | - }, |
448 | | - |
449 | | - onCancelClipEdit: function() { |
450 | | - var _this = this; |
451 | | - // Cancel edit |
452 | | - $( _this.target_container ).dialog( 'close' ); |
453 | | - } |
454 | | -}; |
455 | | - |
456 | | -} )( window.mediaWiki, window.jQuery ); |
\ No newline at end of file |
Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js |
— | — | @@ -0,0 +1,471 @@ |
| 2 | +/** |
| 3 | + * Base mw.TextSource object |
| 4 | + * |
| 5 | + * @param {Object} source Source object to extend |
| 6 | + * @param {Object} textProvider [Optional] The text provider interface ( to load source from api ) |
| 7 | + */ |
| 8 | +( function( mw, $ ) { |
| 9 | + |
| 10 | + mw.TextSource = function( source ) { |
| 11 | + return this.init( source ); |
| 12 | + }; |
| 13 | + mw.TextSource.prototype = { |
| 14 | + |
| 15 | + //The load state: |
| 16 | + loaded: false, |
| 17 | + |
| 18 | + // Container for the captions |
| 19 | + // captions include "start", "end" and "content" fields |
| 20 | + captions: [], |
| 21 | + |
| 22 | + // The css style for captions ( some file formats specify display types ) |
| 23 | + styleCss: {}, |
| 24 | + |
| 25 | + // The previous index of the timed text served |
| 26 | + // Avoids searching the entire array on time updates. |
| 27 | + prevIndex: 0, |
| 28 | + |
| 29 | + /** |
| 30 | + * @constructor Inherits mediaSource from embedPlayer |
| 31 | + * @param {source} Base source element |
| 32 | + * @param {Object} Pointer to the textProvider |
| 33 | + */ |
| 34 | + init: function( source , textProvider) { |
| 35 | + // Inherits mediaSource |
| 36 | + for( var i in source){ |
| 37 | + this[ i ] = source[ i ]; |
| 38 | + } |
| 39 | + |
| 40 | + // Set default category to subtitle if unset: |
| 41 | + if( ! this.kind ) { |
| 42 | + this.kind = 'subtitle'; |
| 43 | + } |
| 44 | + //Set the textProvider if provided |
| 45 | + if( textProvider ) { |
| 46 | + this.textProvider = textProvider; |
| 47 | + } |
| 48 | + return this; |
| 49 | + }, |
| 50 | + |
| 51 | + /** |
| 52 | + * Function to load and parse the source text |
| 53 | + * @param {Function} callback Function called once text source is loaded |
| 54 | + */ |
| 55 | + load: function( callback ) { |
| 56 | + var _this = this; |
| 57 | + // Setup up a callback ( in case it was not defined ) |
| 58 | + if( !callback ){ |
| 59 | + callback = function(){ return ; }; |
| 60 | + } |
| 61 | + |
| 62 | + // Check if the captions have already been loaded: |
| 63 | + if( this.loaded ){ |
| 64 | + return callback(); |
| 65 | + } |
| 66 | + |
| 67 | + // Try to load src via XHR source |
| 68 | + if( !this.getSrc() ) { |
| 69 | + mw.log( "Error: TextSource no source url for text track"); |
| 70 | + return callback(); |
| 71 | + } |
| 72 | + try { |
| 73 | + $.ajax({ |
| 74 | + url: _this.getSrc(), |
| 75 | + success: function( data ) { |
| 76 | + _this.captions = _this.getCaptions( data ); |
| 77 | + _this.loaded = true; |
| 78 | + mw.log("mw.TextSource :: loaded from " + _this.getSrc() + " Found: " + _this.captions.length + ' captions' ); |
| 79 | + callback(); |
| 80 | + }, |
| 81 | + error: function( jqXHR, textStatus, errorThrown ){ |
| 82 | + // try to load the file with the proxy: |
| 83 | + _this.loadViaProxy( function(){ |
| 84 | + callback(); |
| 85 | + _this.loaded = true; |
| 86 | + }); |
| 87 | + } |
| 88 | + }); |
| 89 | + } catch ( e ){ |
| 90 | + mw.log( "TimedText source:: first cross domain request failed, trying via proxy" ); |
| 91 | + } |
| 92 | + }, |
| 93 | + loadViaProxy: function( callback ){ |
| 94 | + var _this = this; |
| 95 | + // Load via proxy: |
| 96 | + var proxyUrl = mw.getConfig('Mw.XmlProxyUrl'); |
| 97 | + $.getJSON( proxyUrl + '?url=' + encodeURIComponent( this.getSrc() ) + '&callback=?', function( result ){ |
| 98 | + if( result['http_code'] == 'ERROR' || result['http_code'] == 0 ){ |
| 99 | + mw.log("Error: TextSource Error with http response"); |
| 100 | + return callback(); |
| 101 | + } |
| 102 | + // Parse and load captions: |
| 103 | + _this.captions = _this.getCaptions( result['contents'] ); |
| 104 | + mw.log("mw.TextSource :: loaded from proxy xml request: captions length: " + _this.captions.length + ' captions' ); |
| 105 | + callback(); |
| 106 | + }); |
| 107 | + }, |
| 108 | + /** |
| 109 | + * Returns the text content for requested time |
| 110 | + * |
| 111 | + * @param {Number} time Time in seconds |
| 112 | + */ |
| 113 | + getCaptionForTime: function ( time ) { |
| 114 | + var prevCaption = this.captions[ this.prevIndex ]; |
| 115 | + var captionSet = {}; |
| 116 | + |
| 117 | + // Setup the startIndex: |
| 118 | + if( prevCaption && time >= prevCaption.start ) { |
| 119 | + var startIndex = this.prevIndex; |
| 120 | + }else{ |
| 121 | + // If a backwards seek start searching at the start: |
| 122 | + var startIndex = 0; |
| 123 | + } |
| 124 | + var firstCapIndex = 0; |
| 125 | + // Start looking for the text via time, add all matches that are in range |
| 126 | + for( var i = startIndex ; i < this.captions.length; i++ ) { |
| 127 | + var caption = this.captions[ i ]; |
| 128 | + // Don't handle captions with 0 or -1 end time: |
| 129 | + if( caption.end == 0 || caption.end == -1) |
| 130 | + continue; |
| 131 | + |
| 132 | + if( time >= caption.start && |
| 133 | + time <= caption.end ) { |
| 134 | + // set the earliest valid time to the current start index: |
| 135 | + if( !firstCapIndex ){ |
| 136 | + firstCapIndex = caption.start; |
| 137 | + } |
| 138 | + |
| 139 | + //mw.log("Start cap time: " + caption.start + ' End time: ' + caption.end ); |
| 140 | + captionSet[i] = caption ; |
| 141 | + } |
| 142 | + // captions are stored in start order stop search if we get larger than time |
| 143 | + if( caption.start > time ){ |
| 144 | + break; |
| 145 | + } |
| 146 | + } |
| 147 | + // Update the prevIndex: |
| 148 | + this.prevIndex =firstCapIndex; |
| 149 | + //Return the set of captions in range: |
| 150 | + return captionSet; |
| 151 | + }, |
| 152 | + getCaptions: function( data ){ |
| 153 | + // Detect caption data type: |
| 154 | + switch( this.mimeType ){ |
| 155 | + case 'text/mw-srt': |
| 156 | + return this.getCaptiosnFromMediaWikiSrt( data ); |
| 157 | + break; |
| 158 | + case 'text/x-srt': |
| 159 | + return this.getCaptionsFromSrt( data); |
| 160 | + break; |
| 161 | + case 'text/xml': |
| 162 | + return this.getCaptionsFromTMML( data ); |
| 163 | + break; |
| 164 | + } |
| 165 | + }, |
| 166 | + |
| 167 | + getStyleCssById: function( styleId ){ |
| 168 | + if( this.styleCss[ styleId ] ){ |
| 169 | + return this.styleCss[ styleId ]; |
| 170 | + } |
| 171 | + return {}; |
| 172 | + }, |
| 173 | + /** |
| 174 | + * Grab timed text from TMML format |
| 175 | + * |
| 176 | + * @param data |
| 177 | + * @return |
| 178 | + */ |
| 179 | + getCaptionsFromTMML: function( data ){ |
| 180 | + var _this = this; |
| 181 | + mw.log("TextSource::getCaptionsFromTMML", data); |
| 182 | + // set up display information: |
| 183 | + var captions = []; |
| 184 | + var xml = ( $( data ).find("tt").length ) ? data : $.parseXML( data ); |
| 185 | + |
| 186 | + // Check for parse error: |
| 187 | + try { |
| 188 | + if( !xml || $( xml ).find('parsererror').length ){ |
| 189 | + mw.log("Error: close caption parse error: " + $( xml ).find('parsererror').text() ); |
| 190 | + return captions; |
| 191 | + } |
| 192 | + } catch ( e ) { |
| 193 | + mw.log( "Error: close caption parse error: " + e.toString() ); |
| 194 | + return captions; |
| 195 | + } |
| 196 | + |
| 197 | + // Set the body Style |
| 198 | + var bodyStyleId = $( xml ).find('body').attr('style'); |
| 199 | + |
| 200 | + // Set style translate ttml to css |
| 201 | + $( xml ).find( 'style').each( function( inx, style){ |
| 202 | + var cssObject = {}; |
| 203 | + // Map CamelCase css properties: |
| 204 | + $( style.attributes ).each(function(inx, attr){ |
| 205 | + var attrName = attr.name; |
| 206 | + if( attrName.substr(0, 4) !== 'tts:' ){ |
| 207 | + // skip |
| 208 | + return true; |
| 209 | + } |
| 210 | + var cssName = ''; |
| 211 | + for( var c = 4; c < attrName.length; c++){ |
| 212 | + if( attrName[c].toLowerCase() != attrName[c] ){ |
| 213 | + cssName += '-' + attrName[c].toLowerCase(); |
| 214 | + } else { |
| 215 | + cssName+= attrName[c] |
| 216 | + } |
| 217 | + } |
| 218 | + cssObject[ cssName ] = attr.nodeValue; |
| 219 | + }); |
| 220 | + //for(var i =0; i< style.length ) |
| 221 | + _this.styleCss[ $( style).attr('id') ] = cssObject; |
| 222 | + }); |
| 223 | + |
| 224 | + $( xml ).find( 'p' ).each( function( inx, p ){ |
| 225 | + |
| 226 | + // Get text content ( just a quick hack, we need more detailed spec or TTML parser ) |
| 227 | + var content = ''; |
| 228 | + $( p.childNodes ).each(function(inx,node){ |
| 229 | + if( node.nodeName != '#text' && node.nodeName != 'metadata' ){ |
| 230 | + // Add any html tags: |
| 231 | + content +='<' + node.nodeName + '/>'; |
| 232 | + } else { |
| 233 | + content += node.textContent; |
| 234 | + } |
| 235 | + }); |
| 236 | + |
| 237 | + // Get the end time: |
| 238 | + var end = null; |
| 239 | + if( $( p ).attr( 'end' ) ){ |
| 240 | + end = mw.npt2seconds( $( p ).attr( 'end' ) ); |
| 241 | + } |
| 242 | + // Look for dur |
| 243 | + if( !end && $( p ).attr( 'dur' )){ |
| 244 | + end = mw.npt2seconds( $( p ).attr( 'begin' ) ) + |
| 245 | + mw.npt2seconds( $( p ).attr( 'dur' ) ); |
| 246 | + } |
| 247 | + |
| 248 | + // Create the caption object : |
| 249 | + var captionObj ={ |
| 250 | + 'start': mw.npt2seconds( $( p ).attr( 'begin' ) ), |
| 251 | + 'end': end, |
| 252 | + 'content': content |
| 253 | + }; |
| 254 | + |
| 255 | + // See if we have custom metadata for position of this caption object |
| 256 | + // there are 35 columns across and 15 rows high |
| 257 | + var $meta = $(p).find( 'metadata' ); |
| 258 | + if( $meta.length ){ |
| 259 | + captionObj['css'] = { |
| 260 | + 'position': 'absolute' |
| 261 | + }; |
| 262 | + if( $meta.attr('cccol') ){ |
| 263 | + captionObj['css']['left'] = ( $meta.attr('cccol') / 35 ) * 100 +'%'; |
| 264 | + // also means the width has to be reduced: |
| 265 | + captionObj['css']['width'] = 100 - parseInt( captionObj['css']['left'] ) + '%'; |
| 266 | + } |
| 267 | + if( $meta.attr('ccrow') ){ |
| 268 | + captionObj['css']['top'] = ( $meta.attr('ccrow') / 15 ) * 100 +'%'; |
| 269 | + } |
| 270 | + } |
| 271 | + if( $(p).attr('tts:textAlign') ){ |
| 272 | + if( !captionObj['css'] ) |
| 273 | + captionObj['css'] = {}; |
| 274 | + captionObj['css']['text-align'] = $(p).attr('tts:textAlign'); |
| 275 | + |
| 276 | + // Remove text align is "right" flip the css left: |
| 277 | + if( captionObj['css']['text-align'] == 'right' && captionObj['css']['left'] ){ |
| 278 | + captionObj['css']['width'] = captionObj['css']['left']; |
| 279 | + captionObj['css']['left'] = null; |
| 280 | + } |
| 281 | + } |
| 282 | + |
| 283 | + // check if this p has any style else use the body parent |
| 284 | + if( $(p).attr('style') ){ |
| 285 | + captionObj['styleId'] = $(p).attr('style') ; |
| 286 | + } else { |
| 287 | + captionObj['styleId'] = bodyStyleId; |
| 288 | + } |
| 289 | + captions.push( captionObj); |
| 290 | + }); |
| 291 | + return captions; |
| 292 | + }, |
| 293 | + /** |
| 294 | + * srt timed text parse handle: |
| 295 | + * @param {String} data Srt string to be parsed |
| 296 | + */ |
| 297 | + getCaptionsFromSrt: function ( data ){ |
| 298 | + mw.log("TextSource::getCaptionsFromSrt"); |
| 299 | + var _this = this; |
| 300 | + // Check if the "srt" parses as an XML |
| 301 | + try{ |
| 302 | + var xml = $.parseXML( data ); |
| 303 | + if( xml && $( xml ).find('parsererror').length == 0 ){ |
| 304 | + return this.getCaptionsFromTMML( data ); |
| 305 | + } |
| 306 | + } catch ( e ){ |
| 307 | + // srt should not be xml |
| 308 | + } |
| 309 | + // Remove dos newlines |
| 310 | + var srt = data.replace(/\r+/g, ''); |
| 311 | + |
| 312 | + // Trim white space start and end |
| 313 | + srt = srt.replace(/^\s+|\s+$/g, ''); |
| 314 | + |
| 315 | + // Remove all html tags for security reasons |
| 316 | + srt = srt.replace(/<[a-zA-Z\/][^>]*>/g, ''); |
| 317 | + |
| 318 | + // Get captions |
| 319 | + var captions = []; |
| 320 | + var caplist = srt.split('\n\n'); |
| 321 | + for (var i = 0; i < caplist.length; i++) { |
| 322 | + var captionText = ""; |
| 323 | + var caption = false; |
| 324 | + captionText = caplist[i]; |
| 325 | + s = captionText.split(/\n/); |
| 326 | + if (s.length < 2) { |
| 327 | + // file format error or comment lines |
| 328 | + continue; |
| 329 | + } |
| 330 | + if (s[0].match(/^\d+$/) && s[1].match(/\d+:\d+:\d+/)) { |
| 331 | + // ignore caption number in s[0] |
| 332 | + // parse time string |
| 333 | + var m = s[1].match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/); |
| 334 | + if (m) { |
| 335 | + caption = _this.match2caption( m ); |
| 336 | + } else { |
| 337 | + // Unrecognized timestring |
| 338 | + continue; |
| 339 | + } |
| 340 | + if( caption ){ |
| 341 | + // concatenate text lines to html text |
| 342 | + caption['content'] = s.slice(2).join("<br>"); |
| 343 | + } |
| 344 | + } else { |
| 345 | + // file format error or comment lines |
| 346 | + continue; |
| 347 | + } |
| 348 | + // Add the current caption to the captions set: |
| 349 | + captions.push( caption ); |
| 350 | + } |
| 351 | + |
| 352 | + return captions; |
| 353 | + }, |
| 354 | + |
| 355 | + /** |
| 356 | + * Get srts from a mediawiki html / srt string |
| 357 | + * |
| 358 | + * Right now wiki -> html is not always friendly to our srt parsing. |
| 359 | + * The long term plan is to move the srt parsing to server side and have the api |
| 360 | + * server up the srt's times in JSON form |
| 361 | + * |
| 362 | + * Also see https://bugzilla.wikimedia.org/show_bug.cgi?id=29126 |
| 363 | + * |
| 364 | + * TODO move to mediaWiki specific module. |
| 365 | + */ |
| 366 | + getCaptiosnFromMediaWikiSrt: function( data ){ |
| 367 | + var _this = this; |
| 368 | + var captions = [ ]; |
| 369 | + var curentCap = { |
| 370 | + 'content': '' |
| 371 | + }; |
| 372 | + var parseNextAsTime = false; |
| 373 | + // Note this string concatenation and html error wrapping sometimes causes |
| 374 | + // parse issues where the wikitext includes many native <p /> tags without child |
| 375 | + // subtitles. In prating this is not a deal breakers because the wikitext for |
| 376 | + // TimedText namespace and associated srts already has a specific format. |
| 377 | + // Long term we will move to server side parsing. |
| 378 | + $( '<div>' + data + '</div>' ).find('p').each( function() { |
| 379 | + var currentPtext = $(this).html(); |
| 380 | + //mw.log( 'pText: ' + currentPtext ); |
| 381 | + |
| 382 | + // We translate raw wikitext gennerated html into a matched srt time sample. |
| 383 | + // The raw html looks like: |
| 384 | + // # |
| 385 | + // hh:mm:ss,ms --> hh:mm:ss,ms |
| 386 | + // text |
| 387 | + // |
| 388 | + // You can read more about the srt format here: |
| 389 | + // http://en.wikipedia.org/wiki/SubRip |
| 390 | + // |
| 391 | + // We attempt to be fairly robust in our regular expression to catch a few |
| 392 | + // srt variations such as omition of commas and empty text lines. |
| 393 | + var m = currentPtext |
| 394 | + .replace('-->', '-->') // restore --> with --> for easier srt parsing: |
| 395 | + .match(/\d+\s([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\s*--?>\s*([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\n?(.*)/); |
| 396 | + |
| 397 | + if (m) { |
| 398 | + captions.push( |
| 399 | + _this.match2caption( m ) |
| 400 | + ); |
| 401 | + return true; |
| 402 | + } |
| 403 | + |
| 404 | + /*** |
| 405 | + * Handle multi line sytle output |
| 406 | + * |
| 407 | + * Handles cases parse cases where an entire line can't be parsed in the single |
| 408 | + * regular expression above, Since the diffrent captions pars are outputed in |
| 409 | + * diffrent <p /> tags by the wikitext parser output. |
| 410 | + */ |
| 411 | + |
| 412 | + // Check if we have reached the end of a multi line match |
| 413 | + if( parseInt( currentPtext ) == currentPtext ) { |
| 414 | + if( curentCap.content != '' ) { |
| 415 | + captions.push( curentCap ); |
| 416 | + } |
| 417 | + // Clear out the current caption content |
| 418 | + curentCap = { |
| 419 | + 'content': '' |
| 420 | + }; |
| 421 | + return true; |
| 422 | + } |
| 423 | + // Check only for time match: |
| 424 | + var m = currentPtext |
| 425 | + .replace('-->', '-->') |
| 426 | + .match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/); |
| 427 | + if (m) { |
| 428 | + // Update the currentCap: |
| 429 | + curentCap = _this.match2caption( m ); |
| 430 | + return true; |
| 431 | + } |
| 432 | + // Else append contnet for the curentCap |
| 433 | + if( currentPtext != '<br>' ) { |
| 434 | + curentCap['content'] += currentPtext; |
| 435 | + } |
| 436 | + }); |
| 437 | + //Push last subtitle: |
| 438 | + if( curentCap.length != 0) { |
| 439 | + captions.push( curentCap ); |
| 440 | + } |
| 441 | + return captions; |
| 442 | + }, |
| 443 | + /** |
| 444 | + * Takes a regular expresion match and converts it to a caption object |
| 445 | + */ |
| 446 | + match2caption: function( m ){ |
| 447 | + var caption = {}; |
| 448 | + // Look for ms: |
| 449 | + var startMs = (m[4])? (parseInt(m[4], 10) / 1000): 0; |
| 450 | + var endMs = (m[8])? (parseInt(m[8], 10) / 1000) : 0; |
| 451 | + caption['start'] = this.timeParts2seconds( m[1], m[2], m[3], startMs ); |
| 452 | + caption['end'] = this.timeParts2seconds( m[5], m[6], m[7], endMs ); |
| 453 | + if( m[9] ){ |
| 454 | + caption['content'] = $.trim( m[9] ); |
| 455 | + } |
| 456 | + return caption; |
| 457 | + }, |
| 458 | + /** |
| 459 | + * Takes time parts in hours, min, seconds and milliseconds and coverts to float seconds. |
| 460 | + */ |
| 461 | + timeParts2seconds: function( hours, min, sec, ms ){ |
| 462 | + return mw.measurements2seconds({ |
| 463 | + 'hours': hours, |
| 464 | + 'minutes': min, |
| 465 | + 'seconds' : sec, |
| 466 | + 'milliseconds': ms |
| 467 | + }); |
| 468 | + } |
| 469 | + }; |
| 470 | + |
| 471 | + |
| 472 | +} )( window.mediaWiki, window.jQuery ); |
\ No newline at end of file |
Property changes on: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TextSource.js |
___________________________________________________________________ |
Added: svn:mime-type |
1 | 473 | + text/plain |
Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/resources/mw.TimedText.js |
— | — | @@ -10,16 +10,15 @@ |
11 | 11 | * @author: Michael Dale |
12 | 12 | * |
13 | 13 | */ |
| 14 | +mw.includeAllModuleMessages(); |
14 | 15 | |
15 | | -// Bind to mw ( for uncluttered global namespace ) |
16 | 16 | ( function( mw, $ ) { |
17 | 17 | |
18 | 18 | // Merge in timed text related attributes: |
19 | 19 | mw.mergeConfig( 'EmbedPlayer.SourceAttributes', [ |
20 | 20 | 'srclang', |
21 | | - 'category', |
22 | | - 'label', |
23 | | - 'data-mwtitle' |
| 21 | + 'kind', |
| 22 | + 'label' |
24 | 23 | ]); |
25 | 24 | |
26 | 25 | /** |
— | — | @@ -42,16 +41,21 @@ |
43 | 42 | 'layout' : 'ontop', |
44 | 43 | |
45 | 44 | //Set the default local ( should be grabbed from the browser ) |
46 | | - 'userLanugage' : 'en', |
| 45 | + 'userLanguage' : 'en', |
47 | 46 | |
48 | | - //Set the default category of timedText to display ( un-categorized timed-text is by default "SUB" ) |
49 | | - 'userCategory' : 'SUB' |
| 47 | + //Set the default kind of timedText to display ( un-categorized timed-text is by default "subtitles" ) |
| 48 | + 'userKind' : 'subtitles' |
50 | 49 | }, |
51 | | - |
| 50 | + // The bind prefix: |
| 51 | + bindPostFix: '.timedText', |
| 52 | + |
| 53 | + // Default options are empty |
| 54 | + options: {}, |
| 55 | + |
52 | 56 | /** |
53 | 57 | * The list of enabled sources |
54 | 58 | */ |
55 | | - enabledSources: null, |
| 59 | + enabledSources: [], |
56 | 60 | |
57 | 61 | /** |
58 | 62 | * The current language key |
— | — | @@ -59,10 +63,10 @@ |
60 | 64 | currentLangKey : null, |
61 | 65 | |
62 | 66 | /** |
63 | | - * Stores the last text string per category to avoid dom checks |
| 67 | + * Stores the last text string per kind to avoid dom checks |
64 | 68 | * for updated text |
65 | 69 | */ |
66 | | - prevText: null, |
| 70 | + prevText: [], |
67 | 71 | |
68 | 72 | /** |
69 | 73 | * Text sources ( a set of textSource objects ) |
— | — | @@ -70,17 +74,6 @@ |
71 | 75 | textSources: null, |
72 | 76 | |
73 | 77 | /** |
74 | | - * Text Source(s) Setup Flag |
75 | | - */ |
76 | | - textSourceSetupFlag: null, |
77 | | - |
78 | | - /* |
79 | | - * Hard coded to "commons" right now .. but we will want to support per-asset provider id's |
80 | | - * in addition to a standard "callback" system from cross domain grabbing of srt's |
81 | | - */ |
82 | | - textProviderId : 'commons', |
83 | | - |
84 | | - /** |
85 | 78 | * Valid "Track" categories |
86 | 79 | */ |
87 | 80 | validCategoriesKeys: [ |
— | — | @@ -99,113 +92,122 @@ |
100 | 93 | ], |
101 | 94 | |
102 | 95 | /** |
103 | | - * Timed text extension to mime map |
104 | | - */ |
105 | | - timedTextExtMime: { |
106 | | - 'srt': 'text/x-srt', |
107 | | - 'mw-srt': 'text/mw-srt', |
108 | | - 'cmml': 'text/cmml' |
109 | | - }, |
110 | | - |
111 | | - /** |
112 | 96 | * @constructor |
113 | 97 | * @param {Object} embedPlayer Host player for timedText interfaces |
114 | 98 | */ |
115 | 99 | init: function( embedPlayer, options ) { |
116 | 100 | var _this = this; |
117 | 101 | mw.log("TimedText: init() "); |
118 | | - this.embedPlayer = embedPlayer; |
119 | | - this.options = options; |
120 | | - |
121 | | - //Init internal variables: |
122 | | - this.enabledSources = []; |
123 | | - this.prevText = ''; |
124 | | - this.textSources = []; |
125 | | - this.textSourceSetupFlag = false; |
126 | | - // Set default language via wgUserLanguage if set |
127 | | - if( typeof wgUserLanguage == 'object' ) { |
128 | | - this.config.userLanugage = wgUserLanguage; |
| 102 | + this.embedPlayer = embedPlayer; |
| 103 | + if( options ){ |
| 104 | + this.options = options; |
129 | 105 | } |
130 | | - |
131 | 106 | // Load user preferences config: |
132 | 107 | var preferenceConfig = $.cookie( 'TimedText.Preferences' ); |
133 | | - if( preferenceConfig !== null ) { |
| 108 | + if( preferenceConfig !== "false" && preferenceConfig != null ) { |
134 | 109 | this.config = JSON.parse( preferenceConfig ); |
135 | 110 | } |
136 | | - // Set up embedPlayer hooks: |
| 111 | + // remove any old bindings on change media: |
| 112 | + $( this.embedPlayer ).bind('onChangeMedia', function(){ |
| 113 | + _this.destroy(); |
| 114 | + }); |
| 115 | + // Remove any old bindings before we add the current bindings: |
| 116 | + _this.destroy(); |
| 117 | + // Add player bindings |
| 118 | + this.addPlayerBindings(); |
| 119 | + }, |
| 120 | + destroy:function(){ |
| 121 | + // remove any old player bindings; |
| 122 | + $( this.embedPlayer ).unbind( this.bindPostFix ) |
| 123 | + }, |
| 124 | + /** |
| 125 | + * Add timed text related player bindings |
| 126 | + * @return |
| 127 | + */ |
| 128 | + addPlayerBindings: function(){ |
| 129 | + var _this = this; |
| 130 | + var embedPlayer = this.embedPlayer; |
137 | 131 | |
138 | 132 | // Check for timed text support: |
139 | | - $( embedPlayer ).bind( 'addControlBarComponent', function(event, controlBar ){ |
140 | | - if( mw.isTimedTextSupported( embedPlayer ) ){ |
| 133 | + $( embedPlayer ).bind( 'addControlBarComponent' + this.bindPostFix, function(event, controlBar ){ |
| 134 | + if( embedPlayer.hasTextTracks() ){ |
141 | 135 | controlBar.supportedComponents['timedText'] = true; |
142 | 136 | controlBar.components['timedText'] = _this.getTimedTextButton(); |
143 | 137 | } |
144 | 138 | }); |
145 | 139 | |
146 | | - |
147 | | - $( embedPlayer ).bind( 'monitorEvent', function() { |
| 140 | + $( embedPlayer ).bind( 'monitorEvent'+ this.bindPostFix, function() { |
148 | 141 | _this.monitor(); |
149 | 142 | } ); |
150 | 143 | |
151 | | - $( embedPlayer ).bind( 'onplay', function() { |
| 144 | + $( embedPlayer ).bind( 'onplay'+ this.bindPostFix, function() { |
152 | 145 | // Will load and setup timedText sources (if not loaded already loaded ) |
153 | 146 | _this.setupTextSources(); |
| 147 | + // Hide the caption menu if presently displayed |
| 148 | + $( '#textMenuContainer_' + embedPlayer.id ).parent().remove(); |
154 | 149 | } ); |
155 | 150 | |
156 | 151 | // Resize the timed text font size per window width |
157 | | - $( embedPlayer ).bind( 'onCloseFullScreen onOpenFullScreen', function() { |
158 | | - var textOffset = _this.embedPlayer.controlBuilder.fullscreenMode ? 30 : 10; |
159 | | - |
160 | | - mw.log( 'TimedText::set text size for: : ' + embedPlayer.$interface.width() + ' = ' + _this.getInterfaceSizeTextCss({ |
| 152 | + $( embedPlayer ).bind( 'onCloseFullScreen'+ this.bindPostFix + ' onOpenFullScreen'+ this.bindPostFix, function() { |
| 153 | + // Check if we are in fullscreen or not, if so add an additional bottom offset of |
| 154 | + // double the default bottom padding. |
| 155 | + var textOffset = _this.embedPlayer.controlBuilder.fullscreenMode ? |
| 156 | + mw.getConfig("TimedText.BottomPadding") *2 : |
| 157 | + mw.getConfig("TimedText.BottomPadding"); |
| 158 | + |
| 159 | + var textCss = _this.getInterfaceSizeTextCss({ |
161 | 160 | 'width' : embedPlayer.$interface.width(), |
162 | 161 | 'height' : embedPlayer.$interface.height() |
163 | | - })['font-size'] ); |
| 162 | + }); |
164 | 163 | |
165 | | - embedPlayer.$interface.find( '.track' ).css( _this.getInterfaceSizeTextCss({ |
166 | | - 'width' : embedPlayer.$interface.width(), |
167 | | - 'height' : embedPlayer.$interface.height() |
168 | | - }) ).css({ |
169 | | - // Get the text size scale then set it to control bar height + 10 px; |
| 164 | + mw.log( 'TimedText::set text size for: : ' + embedPlayer.$interface.width() + ' = ' + textCss['font-size'] ); |
| 165 | + |
| 166 | + embedPlayer.$interface.find( '.track' ) |
| 167 | + .css( textCss ) |
| 168 | + .css({ |
| 169 | + // Get the text size scale then set it to control bar height + TimedText.BottomPadding; |
170 | 170 | 'bottom': ( _this.embedPlayer.controlBuilder.getHeight() + textOffset ) + 'px' |
171 | 171 | }); |
172 | | - |
173 | 172 | }); |
174 | 173 | |
175 | 174 | // Update the timed text size |
176 | | - $( embedPlayer ).bind( 'onResizePlayer', function(e, size, animate) { |
177 | | - mw.log( 'TimedText::onResizePlayer: ' + _this.getInterfaceSizeTextCss(size)['font-size'] ); |
178 | | - if (animate) { |
179 | | - embedPlayer.$interface.find( '.track' ).animate( _this.getInterfaceSizeTextCss( size ) ); |
| 175 | + $( embedPlayer ).bind( 'onResizePlayer'+ this.bindPostFix, function(event, size, animate) { |
| 176 | + // If the the player resize action is an animation, animate text resize, |
| 177 | + // else instantly adjust the css. |
| 178 | + var textCss = _this.getInterfaceSizeTextCss( size ); |
| 179 | + mw.log( 'TimedText::onResizePlayer: ' + textCss['font-size']); |
| 180 | + if ( animate ) { |
| 181 | + embedPlayer.$interface.find( '.track' ).animate( textCss); |
180 | 182 | } else { |
181 | | - embedPlayer.$interface.find( '.track' ).css( _this.getInterfaceSizeTextCss( size ) ); |
| 183 | + embedPlayer.$interface.find( '.track' ).css( textCss ); |
182 | 184 | } |
183 | 185 | }); |
184 | 186 | |
185 | 187 | // Setup display binding |
186 | | - $( embedPlayer ).bind( 'onShowControlBar', function(event, layout ){ |
| 188 | + $( embedPlayer ).bind( 'onShowControlBar'+ this.bindPostFix, function(event, layout ){ |
187 | 189 | // Move the text track if present |
188 | 190 | embedPlayer.$interface.find( '.track' ) |
189 | 191 | .stop() |
190 | 192 | .animate( layout, 'fast' ); |
191 | 193 | }); |
192 | 194 | |
193 | | - $( embedPlayer ).bind( 'onHideControlBar', function(event, layout ){ |
| 195 | + $( embedPlayer ).bind( 'onHideControlBar'+ this.bindPostFix, function(event, layout ){ |
194 | 196 | // Move the text track down if present |
195 | 197 | embedPlayer.$interface.find( '.track' ) |
196 | 198 | .stop() |
197 | 199 | .animate( layout, 'fast' ); |
198 | 200 | }); |
199 | | - |
200 | 201 | }, |
| 202 | + |
201 | 203 | /** |
202 | 204 | * Get the current language key |
203 | | - * |
204 | 205 | * @return |
205 | 206 | * @type {string} |
206 | 207 | */ |
207 | 208 | getCurrentLangKey: function(){ |
208 | 209 | return this.currentLangKey; |
209 | 210 | }, |
| 211 | + |
210 | 212 | /** |
211 | 213 | * The timed text button to be added to the interface |
212 | 214 | */ |
— | — | @@ -249,6 +251,7 @@ |
250 | 252 | 'font-size' : this.getInterfaceSizePercent( size ) + '%' |
251 | 253 | }; |
252 | 254 | }, |
| 255 | + |
253 | 256 | /** |
254 | 257 | * Show the text interface library and show the text interface near the player. |
255 | 258 | */ |
— | — | @@ -258,7 +261,6 @@ |
259 | 262 | mw.log('showTextInterface::' + embedPlayer.id + ' t' + loc.top + ' r' + loc.right); |
260 | 263 | |
261 | 264 | var $menu = $( '#timedTextMenu_' + embedPlayer.id ); |
262 | | - //This may be unnecessary .. we just need to show a spinner somewhere |
263 | 265 | if ( $menu.length != 0 ) { |
264 | 266 | // Hide show the menu: |
265 | 267 | if( $menu.is( ':visible' ) ) { |
— | — | @@ -267,12 +269,19 @@ |
268 | 270 | // move the menu to proper location |
269 | 271 | $menu.show("fast"); |
270 | 272 | } |
271 | | - }else{ |
| 273 | + }else{ |
| 274 | + // Bind the text menu: |
| 275 | + this.bindMenu( true ); |
| 276 | + } |
| 277 | + }, |
| 278 | + getTextMenuContainer: function(){ |
| 279 | + var textMenuId = 'textMenuContainer_' + this.embedPlayer.id; |
| 280 | + if( !$( '#' + textMenuId ).length ){ |
272 | 281 | //Setup the menu: |
273 | 282 | $('body').append( |
274 | 283 | $('<div>') |
275 | 284 | .addClass('ui-widget ui-widget-content ui-corner-all') |
276 | | - .attr( 'id', 'timedTextMenu_' + embedPlayer.id ) |
| 285 | + .attr( 'id', textMenuId ) |
277 | 286 | .css( { |
278 | 287 | 'position' : 'absolute', |
279 | 288 | 'z-index' : 10, |
— | — | @@ -281,17 +290,25 @@ |
282 | 291 | 'font-size' : '12px', |
283 | 292 | 'display' : 'none' |
284 | 293 | } ) |
285 | | - |
| 294 | + |
286 | 295 | ); |
287 | | - // Load text interface ( if not already loaded ) |
288 | | - $( '#' + embedPlayer.id ).timedText( 'showMenu', '#timedTextMenu_' + embedPlayer.id ); |
289 | 296 | } |
| 297 | + return $( '#' + textMenuId ); |
290 | 298 | }, |
| 299 | + /** |
| 300 | + * Gets a text size percent relative to about 30 columns of text for 400 |
| 301 | + * pixel wide player, at 100% text size. |
| 302 | + * |
| 303 | + * @param size {object} The size of the target player area width and height |
| 304 | + */ |
291 | 305 | getInterfaceSizePercent: function( size ) { |
292 | | - // Some arbitrary scale relative to window size ( 400px wide is text size 105% ) |
293 | | - var textSize = size.width / 3.8; |
294 | | - if( textSize < 95 ) textSize = 95; |
295 | | - if( textSize > 200 ) textSize = 200; |
| 306 | + var textSize = size.width / 4; |
| 307 | + if( textSize < 95 ){ |
| 308 | + textSize = 95; |
| 309 | + } |
| 310 | + if( textSize > 200 ){ |
| 311 | + textSize = 200; |
| 312 | + } |
296 | 313 | return textSize; |
297 | 314 | }, |
298 | 315 | |
— | — | @@ -304,17 +321,8 @@ |
305 | 322 | setupTextSources: function( callback ) { |
306 | 323 | mw.log( 'mw.TimedText::setupTextSources'); |
307 | 324 | var _this = this; |
308 | | - if( this.textSourceSetupFlag ) { |
309 | | - if( callback ) { |
310 | | - callback(); |
311 | | - } |
312 | | - return ; |
313 | | - } |
314 | | - this.textSourceSetupFlag = true; |
315 | | - |
316 | 325 | // Load textSources |
317 | 326 | _this.loadTextSources( function() { |
318 | | - |
319 | 327 | // Enable a default source and issue a request to "load it" |
320 | 328 | _this.autoSelectSource(); |
321 | 329 | |
— | — | @@ -334,10 +342,8 @@ |
335 | 343 | * @param {Object} target to display the menu |
336 | 344 | * @param {Boolean} autoShow If the menu should be displayed |
337 | 345 | */ |
338 | | - bindMenu: function( target , autoShow) { |
| 346 | + bindMenu: function( autoShow) { |
339 | 347 | var _this = this; |
340 | | - mw.log( "TimedText:bindMenu:" + target ); |
341 | | - _this.menuTarget = target; |
342 | 348 | var $menuButton = this.embedPlayer.$interface.find( '.timed-text' ); |
343 | 349 | |
344 | 350 | var positionOpts = { }; |
— | — | @@ -360,7 +366,7 @@ |
361 | 367 | 'zindex' : mw.getConfig( 'EmbedPlayer.FullScreenZIndex' ) + 2, |
362 | 368 | 'crumbDefaultText' : ' ', |
363 | 369 | 'autoShow': autoShow, |
364 | | - 'targetMenuContainer' : _this.menuTarget, |
| 370 | + 'targetMenuContainer' : _this.getTextMenuContainer(), |
365 | 371 | 'positionOpts' : positionOpts, |
366 | 372 | 'backLinkText' : gM( 'mwe-timedtext-back-btn' ), |
367 | 373 | 'createMenuCallback' : function(){ |
— | — | @@ -382,107 +388,43 @@ |
383 | 389 | // Setup local reference to currentTime: |
384 | 390 | var currentTime = embedPlayer.currentTime; |
385 | 391 | |
386 | | - // Get the text per category |
| 392 | + // Get the text per kind |
387 | 393 | var textCategories = [ ]; |
388 | 394 | |
389 | | - for( var i = 0; i < this.enabledSources.length ; i++ ) { |
390 | | - var source = this.enabledSources[ i ]; |
391 | | - this.updateSourceDisplay( source, currentTime ); |
392 | | - } |
| 395 | + //for( var i = 0; i < this.enabledSources.length ; i++ ) { |
| 396 | + //var source = this.enabledSources[ i ]; |
| 397 | + var source = this.enabledSources[ 0 ]; |
| 398 | + if( source ) { |
| 399 | + this.updateSourceDisplay( source, currentTime ); |
| 400 | + } |
| 401 | + //} |
393 | 402 | }, |
394 | 403 | |
395 | 404 | /** |
396 | 405 | * Load all the available text sources from the inline embed |
397 | | - * or from a apiProvider |
398 | 406 | * @param {Function} callback Function to call once text sources are loaded |
399 | 407 | */ |
400 | 408 | loadTextSources: function( callback ) { |
401 | 409 | var _this = this; |
402 | | - this.textSources = []; |
403 | | - |
404 | | - // Setup the provider id ( set to local if not found ) |
405 | | - var providerId = $( this.embedPlayer ).attr('data-mwprovider') ? |
406 | | - $( this.embedPlayer ).attr('data-mwprovider') : |
407 | | - 'local'; |
408 | | - var apiUrl = mw.getApiProviderURL( providerId ); |
409 | | - var apiTitleKey = this.embedPlayer.apiTitleKey; |
410 | | - if( !apiUrl || !apiTitleKey ) { |
411 | | - mw.log("Error: loading source without apiProvider or apiTitleKey"); |
| 410 | + // check if text sources are already loaded ( not null ) |
| 411 | + if( this.textSources !== null ){ |
| 412 | + callback( this.textSources ); |
412 | 413 | return ; |
413 | 414 | } |
414 | | - |
415 | | - //For now only support mediaWikTrack provider library |
416 | | - this.textProvider = new mw.MediaWikTrackProvider( { |
417 | | - 'providerId' : providerId, |
418 | | - 'apiUrl': apiUrl, |
419 | | - 'embedPlayer': this.embedPlayer |
420 | | - } ); |
421 | | - |
422 | | - // Get local reference to all timed text sources: ( text/xml, text/x-srt etc ) |
423 | | - var inlineSources = this.embedPlayer.mediaElement.getSources( 'text' ); |
424 | | - |
425 | | - // Add all the sources to textSources |
426 | | - for( var i = 0 ; i < inlineSources.length ; i++ ) { |
427 | | - // Check if the inline source has a text provider: |
428 | | - var textSourceProvider = $( this.embedPlayer ).attr('data-mwprovider') ? |
429 | | - $( this.embedPlayer ).attr('data-mwprovider') : |
430 | | - this.textProvider; |
431 | | - |
432 | | - // Make a new textSource: |
433 | | - var source = new TextSource( inlineSources[i] , this.textProvider); |
434 | | - this.textSources.push( source); |
435 | | - } |
436 | | - |
437 | | - // If there are inline sources don't check the api ) |
438 | | - if( this.textSources.length != 0 ){ |
439 | | - if( callback ) |
440 | | - callback(); |
441 | | - return ; |
442 | | - } |
443 | | - |
444 | | - // Load the textProvider sources |
445 | | - this.textProvider.loadSources( apiTitleKey, function( textSources ) { |
446 | | - for( var i=0; i < textSources.length; i++ ) { |
447 | | - var textSource = textSources[ i ]; |
448 | | - // Try to insert the track source: |
449 | | - var textElm = document.createElement( 'track' ); |
450 | | - $( textElm ).attr({ |
451 | | - 'category' : 'SUB', |
452 | | - 'srclang' : textSource.srclang, |
453 | | - 'type' : _this.timedTextExtMime[ textSource.extension ], |
454 | | - 'titleKey' : textSource.titleKey |
455 | | - }); |
456 | | - |
457 | | - // Build the url for downloading the text: |
458 | | - $( textElm ).attr('src', |
459 | | - _this.textProvider.apiUrl.replace('api.php', 'index.php?title=') + |
460 | | - encodeURIComponent( textSource.titleKey ) + '&action=raw&ctype=text/x-srt' |
461 | | - ); |
462 | | - |
463 | | - // Add a title |
464 | | - $( textElm ).attr('title', |
465 | | - gM('mwe-timedtext-key-language', textSource.srclang, mw.Language.names[ textSource.srclang ] ) |
466 | | - ); |
467 | | - |
468 | | - // Add the sources to the parent embedPlayer |
469 | | - // ( in case other interfaces want to access them ) |
470 | | - var embedSource = _this.embedPlayer.mediaElement.tryAddSource( textElm ); |
471 | | - |
472 | | - // Get a "textSource" object: |
473 | | - var source = new TextSource( embedSource, _this.textProvider ); |
474 | | - _this.textSources.push( source ); |
475 | | - } |
476 | | - // All sources loaded run callback: |
477 | | - if( callback ) |
478 | | - callback(); |
479 | | - } ); |
| 415 | + this.textSources = []; |
| 416 | + // load inline text sources: |
| 417 | + $.each( this.embedPlayer.getTextTracks(), function( inx, textSource ){ |
| 418 | + _this.textSources.push( new mw.TextSource( textSource ) ); |
| 419 | + }); |
| 420 | + // return the callback with sources |
| 421 | + callback( _this.textSources ); |
480 | 422 | }, |
481 | 423 | |
482 | 424 | /** |
483 | 425 | * Get the layout mode |
484 | 426 | * |
485 | 427 | * Takes into consideration: |
486 | | - * Playback method overlays support ( have to put subtitles bellow video ) |
| 428 | + * Playback method overlays support ( have to put subtitles below video ) |
487 | 429 | * |
488 | 430 | */ |
489 | 431 | getLayoutMode: function() { |
— | — | @@ -500,23 +442,32 @@ |
501 | 443 | * In the future we could support multiple "enabled sources" |
502 | 444 | */ |
503 | 445 | autoSelectSource: function() { |
| 446 | + var _this = this; |
504 | 447 | this.enabledSources = []; |
505 | | - // Check if any source matches our "local" |
506 | | - for( var i=0; i < this.textSources.length; i++ ) { |
507 | | - var source = this.textSources[ i ]; |
508 | | - if( this.config.userLanugage && |
509 | | - this.config.userLanugage == source.srclang.toLowerCase() ) { |
510 | | - // Check for category if available |
511 | | - this.enableSource( source ); |
| 448 | + // Check if any source matches our "local" pref |
| 449 | + $.each( this.textSources, function(inx, source){ |
| 450 | + if( _this.config.userLanguage == source.srclang.toLowerCase() |
| 451 | + && |
| 452 | + _this.config.userKind == source.kind |
| 453 | + ) { |
| 454 | + _this.enableSource( source ); |
512 | 455 | return ; |
513 | 456 | } |
514 | | - } |
| 457 | + }); |
| 458 | + // Check if any source is marked default: |
| 459 | + $.each( this.textSources, function(inx, source){ |
| 460 | + if( source['default'] ){ |
| 461 | + _this.enableSource( source ); |
| 462 | + return ; |
| 463 | + } |
| 464 | + }); |
| 465 | + |
515 | 466 | // If no userLang, source try enabling English: |
516 | 467 | if( this.enabledSources.length == 0 ) { |
517 | 468 | for( var i=0; i < this.textSources.length; i++ ) { |
518 | 469 | var source = this.textSources[ i ]; |
519 | 470 | if( source.srclang.toLowerCase() == 'en' ) { |
520 | | - this.enableSource( source ); |
| 471 | + _this.enableSource( source ); |
521 | 472 | return ; |
522 | 473 | } |
523 | 474 | } |
— | — | @@ -525,14 +476,14 @@ |
526 | 477 | if( this.enabledSources.length == 0 ) { |
527 | 478 | for( var i=0; i < this.textSources.length; i++ ) { |
528 | 479 | var source = this.textSources[ i ]; |
529 | | - this.enableSource( source ); |
| 480 | + _this.enableSource( source ); |
530 | 481 | return ; |
531 | 482 | } |
532 | 483 | } |
533 | 484 | }, |
534 | 485 | /** |
535 | 486 | * Enable a source and update the currentLangKey |
536 | | - * @param source |
| 487 | + * @param {object} source |
537 | 488 | * @return |
538 | 489 | */ |
539 | 490 | enableSource: function( source ){ |
— | — | @@ -540,12 +491,15 @@ |
541 | 492 | this.currentLangKey = source.srclang; |
542 | 493 | }, |
543 | 494 | |
544 | | - // Get the current source sub captions |
545 | | - loadCurrentSubSrouce: function( callback ){ |
546 | | - mw.log("loadCurrentSubSrouce:: enabled source:" + this.enabledSources.length); |
| 495 | + /** |
| 496 | + * Get the current source sub captions |
| 497 | + * @param {function} callback function called once source is loaded |
| 498 | + */ |
| 499 | + loadCurrentSubSource: function( callback ){ |
| 500 | + mw.log("loadCurrentSubSource:: enabled source:" + this.enabledSources.length); |
547 | 501 | for( var i =0; i < this.enabledSources.length; i++ ){ |
548 | 502 | var source = this.enabledSources[i]; |
549 | | - if( source.category == 'SUB' ){ |
| 503 | + if( source.kind == 'SUB' ){ |
550 | 504 | source.load( function(){ |
551 | 505 | callback( source); |
552 | 506 | return ; |
— | — | @@ -555,11 +509,16 @@ |
556 | 510 | return false; |
557 | 511 | }, |
558 | 512 | |
559 | | - // Get sub captions by language key: |
| 513 | + /** |
| 514 | + * Get sub captions by language key: |
| 515 | + * |
| 516 | + * @param {string} langKey Key of captions to load |
| 517 | + * @pram {function} callback function called once language key is loaded |
| 518 | + */ |
560 | 519 | getSubCaptions: function( langKey, callback ){ |
561 | 520 | for( var i=0; i < this.textSources.length; i++ ) { |
562 | 521 | var source = this.textSources[ i ]; |
563 | | - if( source.srclang.toLowerCase() == langKey ) { |
| 522 | + if( source.srclang.toLowerCase() === langKey ) { |
564 | 523 | var source = this.textSources[ i ]; |
565 | 524 | source.load( function(){ |
566 | 525 | callback( source.captions ); |
— | — | @@ -573,11 +532,9 @@ |
574 | 533 | * Should be called anytime enabled Source list is updated |
575 | 534 | */ |
576 | 535 | loadEnabledSources: function() { |
577 | | - for(var i=0; i < this.enabledSources.length; i++ ) { |
578 | | - var enabledSource = this.enabledSources[ i ]; |
579 | | - if( ! enabledSource.loaded ) |
580 | | - enabledSource.load(); |
581 | | - } |
| 536 | + $.each( this.enabledSources, function( inx, enabledSource ) { |
| 537 | + enabledSource.load(); |
| 538 | + }); |
582 | 539 | }, |
583 | 540 | |
584 | 541 | /** |
— | — | @@ -587,6 +544,7 @@ |
588 | 545 | */ |
589 | 546 | selectMenuItem: function( item ) { |
590 | 547 | mw.log("selectMenuItem: " + $( item ).find('a').attr('class') ); |
| 548 | + //this.currentLangKey = '' |
591 | 549 | }, |
592 | 550 | |
593 | 551 | /** |
— | — | @@ -596,28 +554,31 @@ |
597 | 555 | * false if source is off |
598 | 556 | */ |
599 | 557 | isSourceEnabled: function( source ) { |
600 | | - for(var i=0; i < this.enabledSources.length; i++ ) { |
601 | | - var enabledSource = this.enabledSources[i]; |
| 558 | + $.each( this.enabledSources, function( inx, enabledSource ) { |
602 | 559 | if( source.id ) { |
603 | | - if( source.id == enabledSource.id ) |
| 560 | + if( source.id === enabledSource.id ){ |
604 | 561 | return true; |
| 562 | + } |
605 | 563 | } |
606 | 564 | if( source.srclang ) { |
607 | | - if( source.srclang == enabledSource.srclang ) |
| 565 | + if( source.srclang === enabledSource.srclang ){ |
608 | 566 | return true; |
| 567 | + } |
609 | 568 | } |
610 | | - } |
| 569 | + }); |
611 | 570 | return false; |
612 | 571 | }, |
613 | 572 | |
614 | 573 | /** |
615 | 574 | * Get a source object by language, returns "false" if not found |
| 575 | + * @param {string} langKey The language key filter for selected source |
616 | 576 | */ |
617 | 577 | getSourceByLanguage: function ( langKey ) { |
618 | 578 | for(var i=0; i < this.textSources.length; i++) { |
619 | 579 | var source = this.textSources[ i ]; |
620 | | - if( source.srclang == langKey ) |
| 580 | + if( source.srclang == langKey ){ |
621 | 581 | return source; |
| 582 | + } |
622 | 583 | } |
623 | 584 | return false; |
624 | 585 | }, |
— | — | @@ -646,30 +607,28 @@ |
647 | 608 | |
648 | 609 | // Build the source list menu item: |
649 | 610 | var $menu = $( '<ul>' ); |
650 | | - // Show text menu item ( if there are sources) |
651 | | - if( _this.textSources.length != 0 ) { |
| 611 | + |
| 612 | + // Show text menu item with layout option (if not fullscren ) |
| 613 | + if( _this.textSources.length !== 0 ) { |
652 | 614 | $menu.append( |
653 | 615 | $.getLineItem( gM( 'mwe-timedtext-choose-text'), 'comment' ).append( |
654 | 616 | _this.getLanguageMenu() |
655 | | - ), |
656 | | - // Layout Menu option |
657 | | - $.getLineItem( gM( 'mwe-timedtext-layout' ), 'image' ).append( |
658 | | - _this.getLayoutMenu() |
659 | | - ) |
| 617 | + ) |
660 | 618 | ); |
661 | | - } else { |
662 | | - // Add a link to request timed text for this clip: |
663 | | - if( mw.getConfig( 'TimedText.ShowRequestTranscript' ) ){ |
664 | | - $menu.append( |
665 | | - $.getLineItem( gM( 'mwe-timedtext-request-subs'), 'comment', function(){ |
666 | | - _this.getAddSubRequest(); |
667 | | - }) |
668 | | - ); |
669 | | - } else { |
670 | | - $menu.append( |
671 | | - $.getLineItem( gM( 'mwe-timedtext-no-subs'), 'close' ) |
672 | | - ) |
673 | | - } |
| 619 | + } |
| 620 | + |
| 621 | + // Layout Menu option if not in an iframe and we can expand video size: |
| 622 | + |
| 623 | + $menu.append( |
| 624 | + $.getLineItem( gM( 'mwe-timedtext-layout' ), 'image' ).append( |
| 625 | + _this.getLayoutMenu() |
| 626 | + ) |
| 627 | + ); |
| 628 | + |
| 629 | + if( _this.textSources.length == 0 ){ |
| 630 | + $menu.append( |
| 631 | + $.getLineItem( gM( 'mwe-timedtext-no-subs'), 'close' ) |
| 632 | + ); |
674 | 633 | } |
675 | 634 | |
676 | 635 | // Put in the "Make Transcript" link if config enabled and we have an api key |
— | — | @@ -682,117 +641,16 @@ |
683 | 642 | // Allow other modules to add to the timed text menu: |
684 | 643 | $( _this.embedPlayer ).trigger( 'TimedText.BuildCCMenu', $menu ) ; |
685 | 644 | |
| 645 | + // Test if only one menu item move its children to the top level |
| 646 | + if( $menu.children('li').length == 1 ){ |
| 647 | + $menu.find('li > ul > li').detach().appendTo( $menu ); |
| 648 | + $menu.find('li').eq(0).remove(); |
| 649 | + } |
| 650 | + |
686 | 651 | return $menu; |
687 | 652 | }, |
688 | 653 | |
689 | | - // Simple interface to add a transcription request |
690 | | - // TODO this should probably be moved to a gadget |
691 | | - getAddSubRequest: function(){ |
692 | | - var _this = this; |
693 | | - var buttons = {}; |
694 | | - buttons[ gM('mwe-timedtext-request-subs') ] = function(){ |
695 | | - var apiUrl = _this.textProvider.apiUrl; |
696 | | - var videoTitle = 'File:' + _this.embedPlayer.apiTitleKey.replace('File:|Image:', ''); |
697 | | - var catName = mw.getConfig( 'TimedText.NeedsTranscriptCategory' ); |
698 | | - var $dialog = $(this); |
699 | | - |
700 | | - var subRequestCategoryUrl = apiUrl.replace('api.php', 'index.php') + |
701 | | - '?title=Category:' + catName.replace(/ /g, '_'); |
702 | | - |
703 | | - var buttonOk= {}; |
704 | | - buttonOk[gM('mwe-ok')] =function(){ |
705 | | - $(this).dialog('close'); |
706 | | - }; |
707 | | - // Set the loadingSpinner: |
708 | | - $( this ).loadingSpinner(); |
709 | | - // Turn off buttons while loading |
710 | | - $dialog.dialog( 'option', 'buttons', null ); |
711 | | - |
712 | | - // Check if the category does not already exist: |
713 | | - mw.getJSON( apiUrl, { 'titles': videoTitle, 'prop': 'categories' }, function( data ){ |
714 | | - if( data && data.query && data.query.pages ){ |
715 | | - for( var i in data.query.pages ){ |
716 | | - // we only request a single page: |
717 | | - if( data.query.pages[i].categories ){ |
718 | | - var categories = data.query.pages[i].categories; |
719 | | - for(var j =0; j < categories.length; j++){ |
720 | | - if( categories[j].title.indexOf( catName ) != -1 ){ |
721 | | - $dialog.html( gM('mwe-timedtext-request-already-done', subRequestCategoryUrl ) ); |
722 | | - $dialog.dialog( 'option', 'buttons', buttonOk); |
723 | | - return ; |
724 | | - } |
725 | | - } |
726 | | - } |
727 | | - } |
728 | | - } |
729 | | - |
730 | | - // Else category not found add to category: |
731 | | - // check if the user is logged in: |
732 | | - mw.getUserName( apiUrl, function( userName ){ |
733 | | - if( !userName ){ |
734 | | - $dialog.html( gM('mwe-timedtext-request-subs-fail') ); |
735 | | - return ; |
736 | | - } |
737 | | - // Get an edit token: |
738 | | - mw.getToken( apiUrl, videoTitle, function( token ) { |
739 | | - if( !token ){ |
740 | | - $dialog.html( gM('mwe-timedtext-request-subs-fail') ); |
741 | | - return ; |
742 | | - } |
743 | | - var request = { |
744 | | - 'action' : 'edit', |
745 | | - 'summary' : 'Added request for subtitles using [[Commons:MwEmbed|MwEmbed]]', |
746 | | - 'title' : videoTitle, |
747 | | - 'appendtext' : "\n[[Category:" + catName + "]]", |
748 | | - 'token': token |
749 | | - }; |
750 | | - // Do the edit request: |
751 | | - mw.getJSON( apiUrl, request, function(data){ |
752 | | - if( data.edit && data.edit.newrevid){ |
753 | | - |
754 | | - $dialog.html( gM('mwe-timedtext-request-subs-done', subRequestCategoryUrl ) |
755 | | - ); |
756 | | - } else { |
757 | | - $dialog.html( gM('mwe-timedtext-request-subs-fail') ); |
758 | | - } |
759 | | - $dialog.dialog( 'option', 'buttons', buttonOk ); |
760 | | - }); |
761 | | - }); |
762 | | - }); |
763 | | - }); |
764 | | - }; |
765 | | - buttons[ gM('mwe-cancel') ] = function(){ |
766 | | - $(this).dialog('close'); |
767 | | - }; |
768 | | - mw.addDialog({ |
769 | | - 'title' : gM( 'mwe-timedtext-request-subs'), |
770 | | - 'width' : 450, |
771 | | - 'content' : gM('mwe-timedtext-request-subs-desc'), |
772 | | - 'buttons' : buttons |
773 | | - }); |
774 | | - }, |
775 | 654 | /** |
776 | | - * Shows the timed text edit ui |
777 | | - * |
778 | | - * @param {String} mode Mode or page to display ( to differentiate between edit vs new transcript) |
779 | | - */ |
780 | | - showTimedTextEditUI: function( mode ) { |
781 | | - var _this = this; |
782 | | - // Show a loader: |
783 | | - mw.addLoaderDialog( gM( 'mwe-timedtext-loading-text-edit' ) ); |
784 | | - // Load the timedText edit interface |
785 | | - mw.load( 'mw.TimedTextEdit', function() { |
786 | | - if( ! _this.editText ) { |
787 | | - _this.editText = new mw.TimedTextEdit( _this ); |
788 | | - } |
789 | | - // Close the loader: |
790 | | - mw.closeLoaderDialog(); |
791 | | - // Show the upload text ui: |
792 | | - _this.editText.showUI(); |
793 | | - }); |
794 | | - }, |
795 | | - |
796 | | - /** |
797 | 655 | * Utility function to assist in menu build out: |
798 | 656 | * Get menu line item (li) html: <li><a> msgKey </a></li> |
799 | 657 | * |
— | — | @@ -805,8 +663,8 @@ |
806 | 664 | getLiAddText: function() { |
807 | 665 | var _this = this; |
808 | 666 | return $.getLineItem( gM( 'mwe-timedtext-upload-timed-text'), 'script', function() { |
809 | | - _this.showTimedTextEditUI( 'add' ); |
810 | | - } ); |
| 667 | + _this.showTimedTextEditUI( 'add' ); |
| 668 | + }); |
811 | 669 | }, |
812 | 670 | |
813 | 671 | /** |
— | — | @@ -822,10 +680,9 @@ |
823 | 681 | return $.getLineItem( source.title, source_icon, function() { |
824 | 682 | _this.selectTextSource( source ); |
825 | 683 | }); |
826 | | - } |
| 684 | + } |
827 | 685 | if( source.srclang ) { |
828 | 686 | var langKey = source.srclang.toLowerCase(); |
829 | | - var cat = gM('mwe-timedtext-key-language', langKey, _this.getLanguageName ( langKey ) ); |
830 | 687 | return $.getLineItem( |
831 | 688 | gM('mwe-timedtext-key-language', langKey, _this.getLanguageName ( langKey ) ), |
832 | 689 | source_icon, |
— | — | @@ -857,11 +714,14 @@ |
858 | 715 | var layoutOptions = [ ]; |
859 | 716 | |
860 | 717 | //Only display the "ontop" option if the player supports it: |
861 | | - if( this.embedPlayer.supports[ 'overlays' ] ) |
| 718 | + if( this.embedPlayer.supports[ 'overlays' ] ){ |
862 | 719 | layoutOptions.push( 'ontop' ); |
| 720 | + } |
863 | 721 | |
864 | 722 | //Add below and "off" options: |
865 | | - layoutOptions.push( 'below' ); |
| 723 | + if( ! mw.getConfig('EmbedPlayer.IsIframeServer') ){ |
| 724 | + layoutOptions.push( 'below' ); |
| 725 | + } |
866 | 726 | layoutOptions.push( 'off' ); |
867 | 727 | |
868 | 728 | $ul = $('<ul>'); |
— | — | @@ -893,7 +753,13 @@ |
894 | 754 | _this.updateLayout(); |
895 | 755 | } |
896 | 756 | }, |
897 | | - |
| 757 | + toggleCaptions: function(){ |
| 758 | + if( this.config.layout == 'off' ){ |
| 759 | + this.selectLayout( 'ontop' ); |
| 760 | + } else { |
| 761 | + this.selectLayout( 'off' ); |
| 762 | + } |
| 763 | + }, |
898 | 764 | /** |
899 | 765 | * Updates the timed text layout ( should be called when config.layout changes ) |
900 | 766 | */ |
— | — | @@ -911,22 +777,22 @@ |
912 | 778 | selectTextSource: function( source ) { |
913 | 779 | var _this = this; |
914 | 780 | mw.log("mw.TimedText:: selectTextSource: select lang: " + source.srclang ); |
| 781 | + |
915 | 782 | // For some reason we lose binding for the menu ~sometimes~ re-bind |
916 | 783 | this.bindTextButton( this.embedPlayer.$interface.find('timed-text') ); |
917 | 784 | |
918 | | - |
919 | 785 | this.currentLangKey = source.srclang; |
920 | 786 | |
921 | 787 | // Update the config language if the source includes language |
922 | 788 | if( source.srclang ) |
923 | | - this.config.userLanugage = source.srclang; |
| 789 | + this.config.userLanguage = source.srclang; |
924 | 790 | |
925 | | - if( source.category ) |
926 | | - this.config.userCategory = source.category; |
| 791 | + if( source.kind ) |
| 792 | + this.config.userKind = source.kind; |
927 | 793 | |
928 | | - // (@@todo update category & setup category language buckets? ) |
| 794 | + // (@@todo update kind & setup kind language buckets? ) |
929 | 795 | |
930 | | - // Remove any other sources selected in sources category |
| 796 | + // Remove any other sources selected in sources kind |
931 | 797 | this.enabledSources = []; |
932 | 798 | |
933 | 799 | this.enabledSources.push( source ); |
— | — | @@ -943,6 +809,9 @@ |
944 | 810 | } else { |
945 | 811 | _this.refreshDisplay(); |
946 | 812 | } |
| 813 | + |
| 814 | + // Trigger the event |
| 815 | + $( this.embedPlayer ).trigger( 'TimedText_ChangeSource' ); |
947 | 816 | }, |
948 | 817 | |
949 | 818 | /** |
— | — | @@ -970,30 +839,34 @@ |
971 | 840 | |
972 | 841 | /** |
973 | 842 | * Builds the language source list menu |
974 | | - * checks all text sources for category and language key attribute |
| 843 | + * Cehck if the "track" tags had the "kind" attribute. |
| 844 | + * |
| 845 | + * The kind attribute forms "categories" of text tracks like "subtitles", |
| 846 | + * "audio description", "chapter names". We check for these categories |
| 847 | + * when building out the language menu. |
975 | 848 | */ |
976 | 849 | getLanguageMenu: function() { |
977 | 850 | var _this = this; |
978 | 851 | |
979 | 852 | // See if we have categories to worry about |
980 | | - // associative array of SUB etc categories. Each category contains an array of textSources. |
981 | | - var catSourceList = {}; |
982 | | - var catSourceCount = 0; |
| 853 | + // associative array of SUB etc categories. Each kind contains an array of textSources. |
| 854 | + var categorySourceList = {}; |
| 855 | + var sourcesWithCategoryCount = 0; |
983 | 856 | |
984 | | - // ( All sources should have a category (depreciate ) |
| 857 | + // ( All sources should have a kind (depreciate ) |
985 | 858 | var sourcesWithoutCategory = [ ]; |
986 | 859 | for( var i=0; i < this.textSources.length; i++ ) { |
987 | 860 | var source = this.textSources[ i ]; |
988 | | - if( source.category ) { |
989 | | - var catKey = source.category ; |
| 861 | + if( source.kind ) { |
| 862 | + var categoryKey = source.kind ; |
990 | 863 | // Init Category menu item if it does not already exist: |
991 | | - if( !catSourceList[ catKey ] ) { |
| 864 | + if( !categorySourceList[ categoryKey ] ) { |
992 | 865 | // Set up catList pointer: |
993 | | - catSourceList[ catKey ] = [ ]; |
994 | | - catSourceCount++; |
| 866 | + categorySourceList[ categoryKey ] = [ ]; |
| 867 | + sourcesWithCategoryCount++; |
995 | 868 | } |
996 | | - // Append to the source category key menu item: |
997 | | - catSourceList[ catKey ].push( |
| 869 | + // Append to the source kind key menu item: |
| 870 | + categorySourceList[ categoryKey ].push( |
998 | 871 | _this.getLiSource( source ) |
999 | 872 | ); |
1000 | 873 | }else{ |
— | — | @@ -1002,31 +875,31 @@ |
1003 | 876 | } |
1004 | 877 | var $langMenu = $('<ul>'); |
1005 | 878 | // Check if we have multiple categories ( if not just list them under the parent menu item) |
1006 | | - if( catSourceCount > 1 ) { |
1007 | | - for(var catKey in catSourceList) { |
| 879 | + if( sourcesWithCategoryCount > 1 ) { |
| 880 | + for(var categoryKey in categorySourceList) { |
1008 | 881 | var $catChildren = $('<ul>'); |
1009 | | - for(var i=0; i < catSourceList[ catKey ].length; i++) { |
| 882 | + for(var i=0; i < categorySourceList[ categoryKey ].length; i++) { |
1010 | 883 | $catChildren.append( |
1011 | | - catSourceList[ catKey ][i] |
| 884 | + categorySourceList[ categoryKey ][i] |
1012 | 885 | ); |
1013 | 886 | } |
1014 | | - // Append a cat menu item for each category list |
| 887 | + // Append a cat menu item for each kind list |
1015 | 888 | $langMenu.append( |
1016 | | - $.getLineItem( gM( 'mwe-timedtext-textcat-' + catKey.toLowerCase() ) ).append( |
| 889 | + $.getLineItem( gM( 'mwe-timedtext-textcat-' + categoryKey.toLowerCase() ) ).append( |
1017 | 890 | $catChildren |
1018 | 891 | ) |
1019 | 892 | ); |
1020 | 893 | } |
1021 | 894 | } else { |
1022 | | - for(var catKey in catSourceList) { |
1023 | | - for(var i=0; i < catSourceList[ catKey ].length; i++) { |
| 895 | + for(var categoryKey in categorySourceList) { |
| 896 | + for(var i=0; i < categorySourceList[ categoryKey ].length; i++) { |
1024 | 897 | $langMenu.append( |
1025 | | - catSourceList[ catKey ][i] |
| 898 | + categorySourceList[ categoryKey ][i] |
1026 | 899 | ); |
1027 | 900 | } |
1028 | 901 | } |
1029 | 902 | } |
1030 | | - |
| 903 | + // Add any remaning sources that did nto have a category |
1031 | 904 | for(var i=0; i < sourcesWithoutCategory.length; i++) { |
1032 | 905 | $langMenu.append( sourcesWithoutCategory[i] ); |
1033 | 906 | } |
— | — | @@ -1037,706 +910,235 @@ |
1038 | 911 | _this.getLiAddText() |
1039 | 912 | ); |
1040 | 913 | } |
1041 | | - |
| 914 | + |
1042 | 915 | return $langMenu; |
1043 | 916 | }, |
1044 | 917 | |
1045 | 918 | /** |
1046 | 919 | * Updates a source display in the interface for a given time |
1047 | | - * @param {Object} source Source to update |
| 920 | + * @param {object} source Source to update |
| 921 | + * @param {number} time Caption time used to add and remove active captions. |
1048 | 922 | */ |
1049 | 923 | updateSourceDisplay: function ( source, time ) { |
1050 | | - // Get the source text for the requested time: |
1051 | | - var text = source.getTimedText( time ); |
1052 | | - |
1053 | | - // We do a type comparison so that "undefined" != "false" |
1054 | | - // ( check if we are updating the text ) |
1055 | | - if( text === this.prevText[ source.category ] ){ |
1056 | | - return ; |
| 924 | + var _this = this; |
| 925 | + if( this.timeOffset ){ |
| 926 | + time = time + parseInt( this.timeOffset ); |
1057 | 927 | } |
1058 | | - |
1059 | | - //mw.log( 'mw.TimedText:: updateTextDisplay: ' + text ); |
1060 | | - |
1061 | | - var $playerTarget = this.embedPlayer.$interface; |
1062 | | - var $textTarget = $playerTarget.find( '.track_' + source.category + ' span' ); |
1063 | | - // If we are missing the target add it: |
1064 | | - if( $textTarget.length == 0 ) { |
1065 | | - this.addItextDiv( source.category ); |
1066 | | - // Re-grab the textTarget: |
1067 | | - $textTarget = $playerTarget.find( '.track_' + source.category + ' span' ); |
1068 | | - } |
1069 | | - |
1070 | | - // If text is "false" fade out the subtitle: |
1071 | | - if( text === false ) { |
1072 | | - $textTarget.fadeOut('fast'); |
1073 | | - }else{ |
1074 | | - // Fade in the target if not visible |
1075 | | - if( ! $textTarget.is(':visible') ) { |
1076 | | - $textTarget.fadeIn('fast'); |
| 928 | + |
| 929 | + // Get the source text for the requested time: |
| 930 | + var activeCaptions = source.getCaptionForTime( time ); |
| 931 | + var addedCaption = false; |
| 932 | + // Show captions that are on: |
| 933 | + $.each(activeCaptions, function( capId, caption){ |
| 934 | + if( _this.embedPlayer.$interface.find( '.track[data-capId="' + capId +'"]').length == 0){ |
| 935 | + _this.addCaption( source, capId, caption ); |
| 936 | + addedCaption = true; |
1077 | 937 | } |
1078 | | - // Update text ( use "html" instead of "text" so that subtitle format can |
1079 | | - // include html formating |
1080 | | - // TOOD we should scrub this for non-formating html |
1081 | | - $textTarget.html( text ); |
1082 | | - |
1083 | | - // Add/update the lang option |
1084 | | - $textTarget.attr( 'lang', source.srclang.toLowerCase() ); |
1085 | | - |
1086 | | - // Update any links to point to a new window |
1087 | | - $textTarget.find( 'a' ).attr( 'target', '_blank' ); |
| 938 | + }); |
| 939 | + |
| 940 | + // hide captions that are off: |
| 941 | + _this.embedPlayer.$interface.find( '.track' ).each(function( inx, caption){ |
| 942 | + if( !activeCaptions[ $( caption ).attr('data-capId') ] ){ |
| 943 | + if( addedCaption ){ |
| 944 | + $( caption ).remove(); |
| 945 | + } else { |
| 946 | + $( caption ).fadeOut( mw.getConfig('EmbedPlayer.MonitorRate'), function(){ $(this).remove();} ); |
| 947 | + } |
| 948 | + } |
| 949 | + }); |
| 950 | + }, |
| 951 | + getCaptionsTarget: function(){ |
| 952 | + var $capTarget = this.embedPlayer.$interface.find('.captionsLayoutTarget'); |
| 953 | + var layoutCss = { |
| 954 | + 'left' : 0, |
| 955 | + 'top' :0, |
| 956 | + 'right':0, |
| 957 | + 'position': 'absolute' |
| 958 | + }; |
| 959 | + if( this.embedPlayer.controlBuilder.isOverlayControls() || |
| 960 | + !mw.getConfig( 'EmbedPlayer.OverlayControls') ) |
| 961 | + { |
| 962 | + layoutCss['bottom'] = 0; |
| 963 | + } else { |
| 964 | + layoutCss['bottom'] = this.embedPlayer.controlBuilder.getHeight(); |
1088 | 965 | } |
1089 | | - // mw.log( ' len: ' + $textTarget.length + ' ' + $textTarget.html() ); |
1090 | | - // Update the prev text: |
1091 | | - this.prevText[ source.category ] = text; |
| 966 | + |
| 967 | + if( $capTarget.length == 0 ){ |
| 968 | + $capTarget = $( '<div />' ) |
| 969 | + .addClass( 'captionsLayoutTarget' ) |
| 970 | + .css( layoutCss ) |
| 971 | + this.embedPlayer.$interface.append( $capTarget ) |
| 972 | + } |
| 973 | + return $capTarget; |
1092 | 974 | }, |
| 975 | + addCaption: function( source, capId, caption ){ |
| 976 | + if( this.getLayoutMode() == 'off' ){ |
| 977 | + return ; |
| 978 | + } |
| 979 | + // use capId as a class instead of id for easy selections and no conflicts with |
| 980 | + // multiple players on page. |
| 981 | + var $textTarget = $('<div />') |
| 982 | + .addClass( 'track' ) |
| 983 | + .attr( 'data-capId', capId ) |
| 984 | + .hide(); |
| 985 | + |
| 986 | + // Update text ( use "html" instead of "text" so that subtitle format can |
| 987 | + // include html formating |
| 988 | + // TOOD we should scrub this for non-formating html |
| 989 | + $textTarget.append( |
| 990 | + $('<span />') |
| 991 | + .css( this.getCaptionCss() ) |
| 992 | + .html( caption.content ) |
| 993 | + ); |
1093 | 994 | |
1094 | 995 | |
1095 | | - /** |
1096 | | - * Add an track div to the embedPlayer |
1097 | | - */ |
1098 | | - addItextDiv: function( category ) { |
1099 | | - mw.log(" addItextDiv: " + category ); |
1100 | | - // Get the relative positioned player class from the controlBuilder: |
1101 | | - var $playerTarget = this.embedPlayer.$interface; |
1102 | | - //Remove any existing track divs for this player; |
1103 | | - $playerTarget.find('.track_' + category ).remove(); |
1104 | | - |
1105 | | - // Setup the display text div: |
1106 | | - var layoutMode = this.getLayoutMode(); |
1107 | | - if( layoutMode == 'ontop' ) { |
1108 | | - this.embedPlayer.controlBuilder.keepControlBarOnScreen = false; |
1109 | | - var $track = $('<div>') |
1110 | | - .addClass( 'track' + ' ' + 'track_' + category ) |
1111 | | - .css( { |
1112 | | - 'position':'absolute', |
1113 | | - 'bottom': ( this.embedPlayer.controlBuilder.getHeight() + 10 ), |
1114 | | - 'width': '100%', |
1115 | | - 'display': 'block', |
1116 | | - 'opacity': .8, |
1117 | | - 'text-align':'center' |
1118 | | - }) |
1119 | | - .append( |
1120 | | - $('<span \>') |
1121 | | - ); |
1122 | | - |
1123 | | - // Scale the text Relative to player size: |
1124 | | - $track.css( |
1125 | | - this.getInterfaceSizeTextCss({ |
1126 | | - 'width' : this.embedPlayer.getWidth(), |
1127 | | - 'height' : this.embedPlayer.getHeight() |
1128 | | - }) |
1129 | | - ); |
1130 | | - // Resize the interface for layoutMode == 'below' ( if not in full screen) |
1131 | | - if( ! this.embedPlayer.controlBuilder.fullscreenMode ){ |
1132 | | - this.embedPlayer.$interface.animate({ |
1133 | | - 'height': this.embedPlayer.getHeight() |
1134 | | - }); |
| 996 | + // Add/update the lang option |
| 997 | + $textTarget.attr( 'lang', source.srclang.toLowerCase() ); |
| 998 | + |
| 999 | + // Update any links to point to a new window |
| 1000 | + $textTarget.find( 'a' ).attr( 'target', '_blank' ); |
| 1001 | + |
| 1002 | + // Apply any custom style ( if we are ontop of the video ) |
| 1003 | + if( this.getLayoutMode() == 'ontop' ){ |
| 1004 | + if( caption.css ){ |
| 1005 | + $textTarget.css( caption.css ); |
| 1006 | + } else { |
| 1007 | + $textTarget.css( this.getDefaultStyle() ); |
1135 | 1008 | } |
1136 | | - $playerTarget.append( $track ); |
1137 | | - |
1138 | | - } else if ( layoutMode == 'below') { |
1139 | | - this.embedPlayer.controlBuilder.keepControlBarOnScreen = true; |
1140 | | - // Set the belowBar size to 60 pixels: |
1141 | | - var belowBarHeight = 60; |
1142 | | - // Append before controls: |
1143 | | - $playerTarget.find( '.control-bar' ).before( |
1144 | | - $('<div>').addClass( 'track' + ' ' + 'track_' + category ) |
1145 | | - .css({ |
1146 | | - 'position' : 'absolute', |
1147 | | - 'top' : this.embedPlayer.getHeight(), |
1148 | | - 'display' : 'block', |
1149 | | - 'width' : '100%', |
1150 | | - 'height' : belowBarHeight + 'px', |
1151 | | - 'background-color' : '#000', |
1152 | | - 'text-align' : 'center', |
1153 | | - 'padding-top' : '5px' |
1154 | | - } ).append( |
1155 | | - $('<span>').css( { |
1156 | | - 'color':'white' |
1157 | | - } ) |
1158 | | - ) |
| 1009 | + this.getCaptionsTarget().append( |
| 1010 | + $textTarget |
1159 | 1011 | ); |
1160 | | - // Add some height for the bar and interface |
1161 | | - var height = ( belowBarHeight + 8 ) + this.embedPlayer.getHeight() + this.embedPlayer.controlBuilder.getHeight(); |
1162 | | - // Resize the interface for layoutMode == 'below' ( if not in full screen) |
1163 | | - if( ! this.embedPlayer.controlBuilder.fullscreenMode ){ |
1164 | | - this.embedPlayer.$interface.animate({ |
1165 | | - 'height': height |
1166 | | - }); |
1167 | | - } |
1168 | | - mw.log( ' height of ' + this.embedPlayer.id + ' is now: ' + $( '#' + this.embedPlayer.id ).height() ); |
| 1012 | + } else { |
| 1013 | + // else apply the default layout system: |
| 1014 | + this.addTextToDefaultLocation( $textTarget ); |
1169 | 1015 | } |
1170 | | - mw.log( 'should have been appended: ' + $playerTarget.find('.track').length ); |
1171 | | - } |
1172 | | - }; |
1173 | | - |
1174 | | - /** |
1175 | | - * TextSource object extends a base mediaSource object |
1176 | | - * with some timedText features |
1177 | | - * |
1178 | | - * @param {Object} source Source object to extend |
1179 | | - * @param {Object} textProvider [Optional] The text provider interface ( to load source from api ) |
1180 | | - */ |
1181 | | - TextSource = function( source , textProvider) { |
1182 | | - return this.init( source, textProvider ); |
1183 | | - }; |
1184 | | - TextSource.prototype = { |
1185 | | - |
1186 | | - //The load state: |
1187 | | - loaded: false, |
1188 | | - |
1189 | | - // Container for the captions |
1190 | | - // captions include "start", "end" and "content" fields |
1191 | | - captions: [], |
1192 | | - |
1193 | | - // The previous index of the timed text served |
1194 | | - // Avoids searching the entire array on time updates. |
1195 | | - prevIndex: 0, |
1196 | | - |
1197 | | - /** |
1198 | | - * @constructor Inherits mediaSource from embedPlayer |
1199 | | - * @param {source} Base source element |
1200 | | - * @param {Object} Pointer to the textProvider |
1201 | | - */ |
1202 | | - init: function( source , textProvider) { |
1203 | | - // Inherits mediaSource |
1204 | | - for( var i in source){ |
1205 | | - this[ i ] = source[ i]; |
1206 | | - } |
| 1016 | + // apply any interface size adjustments: |
| 1017 | + $textTarget.css( this.getInterfaceSizeTextCss({ |
| 1018 | + 'width' : this.embedPlayer.$interface.width(), |
| 1019 | + 'height' : this.embedPlayer.$interface.height() |
| 1020 | + }) |
| 1021 | + ); |
1207 | 1022 | |
1208 | | - // Set default category to subtitle if unset: |
1209 | | - if( ! this.category ) { |
1210 | | - this.category = 'SUB'; |
| 1023 | + |
| 1024 | + // Update the style of the text object if set |
| 1025 | + if( caption.styleId ){ |
| 1026 | + var capCss = source.getStyleCssById( caption.styleId ); |
| 1027 | + $textTarget.find('span').css( |
| 1028 | + capCss |
| 1029 | + ); |
1211 | 1030 | } |
1212 | | - //Set the textProvider if provided |
1213 | | - if( textProvider ) { |
1214 | | - this.textProvider = textProvider; |
1215 | | - |
1216 | | - // switch type to mw-srt if we are going to load via api |
1217 | | - // ( this is need because we want to represent one thing to search engines / crawlers, |
1218 | | - // while representing the mw-srt type internally so that mediawiki parsed text |
1219 | | - // gets converted to html before going into the video |
1220 | | - if( this.mwtitle ){ |
1221 | | - this.mimeType = 'text/mw-srt'; |
1222 | | - } |
1223 | | - } |
1224 | | - return this; |
| 1031 | + |
| 1032 | + $textTarget.fadeIn('fast'); |
1225 | 1033 | }, |
1226 | | - |
| 1034 | + getDefaultStyle: function(){ |
| 1035 | + var baseCss = { |
| 1036 | + 'position':'absolute', |
| 1037 | + 'bottom': 10, |
| 1038 | + 'width': '100%', |
| 1039 | + 'display': 'block', |
| 1040 | + 'opacity': .8, |
| 1041 | + 'text-align': 'center', |
| 1042 | + 'z-index': 2 |
| 1043 | + }; |
| 1044 | + baseCss =$.extend( baseCss, this.getInterfaceSizeTextCss({ |
| 1045 | + 'width' : this.embedPlayer.$interface.width(), |
| 1046 | + 'height' : this.embedPlayer.$interface.height() |
| 1047 | + })); |
| 1048 | + return baseCss; |
| 1049 | + }, |
1227 | 1050 | /** |
1228 | | - * Function to load and parse the source text |
1229 | | - * @param {Function} callback Function called once text source is loaded |
| 1051 | + * Applies the default layout for a text target |
1230 | 1052 | */ |
1231 | | - load: function( callback ) { |
1232 | | - var _this = this; |
1233 | | - |
1234 | | - //check if its already loaded: |
1235 | | - if( _this.loaded ) { |
1236 | | - if( callback ) { |
1237 | | - callback(); |
1238 | | - return ; |
1239 | | - } |
1240 | | - }; |
1241 | | - _this.loaded = true; |
1242 | | - // Set parser handler: |
1243 | | - switch( this.getMIMEType() ) { |
1244 | | - //Special mediaWiki srt format ( support wiki-text in srt's ) |
1245 | | - case 'text/mw-srt': |
1246 | | - var handler = parseMwSrt; |
1247 | | - break; |
1248 | | - case 'text/x-srt': |
1249 | | - var handler = parseSrt; |
1250 | | - break; |
1251 | | - case 'text/cmml': |
1252 | | - var handler = parseCMML; |
1253 | | - break; |
1254 | | - default: |
1255 | | - var hanlder = null; |
1256 | | - break; |
1257 | | - } |
1258 | | - if( !handler ) { |
1259 | | - mw.log("Error: no handler for type: " + this.getMIMEType() ); |
1260 | | - return ; |
1261 | | - } |
1262 | | - // Try to load src via textProvider: |
1263 | | - if( this.textProvider && this.mwtitle) { |
1264 | | - this.textProvider.loadTitleKey( this.mwtitle, function( data ) { |
1265 | | - if( data ) { |
1266 | | - _this.captions = handler( data ); |
1267 | | - } |
1268 | | - mw.log("mw.TimedText:: loaded from titleKey: " + _this.captions.length + ' captions'); |
1269 | | - // Update the loaded state: |
1270 | | - _this.loaded = true; |
1271 | | - if( callback ) { |
1272 | | - callback(); |
1273 | | - } |
| 1053 | + addTextBelowVideo: function( $textTarget ) { |
| 1054 | + var $playerTarget = this.embedPlayer.$interface; |
| 1055 | + // Get the relative positioned player class from the controlBuilder: |
| 1056 | + this.embedPlayer.controlBuilder.keepControlBarOnScreen = true; |
| 1057 | + // Set the belowBar size to 60 pixels: |
| 1058 | + var belowBarHeight = mw.getConfig('TimedText.BelowVideoBlackBoxHeight'); |
| 1059 | + |
| 1060 | + // Append before controls: |
| 1061 | + $playerTarget.find( '.control-bar' ).before( |
| 1062 | + $('<div>').addClass( 'captionContainer' ) |
| 1063 | + .css({ |
| 1064 | + 'position' : 'absolute', |
| 1065 | + 'top' : this.embedPlayer.getHeight(), |
| 1066 | + 'display' : 'block', |
| 1067 | + 'width' : '100%', |
| 1068 | + 'height' : belowBarHeight + 'px', |
| 1069 | + 'background-color' : '#000', |
| 1070 | + 'text-align' : 'center', |
| 1071 | + 'padding-top' : '5px' |
| 1072 | + } ).append( |
| 1073 | + $textTarget.css( { |
| 1074 | + 'color':'white' |
| 1075 | + } ) |
| 1076 | + ) |
| 1077 | + ); |
| 1078 | + |
| 1079 | + // Add some height for the bar and interface |
| 1080 | + var height = ( belowBarHeight + 8 ) + this.embedPlayer.getHeight() + this.embedPlayer.controlBuilder.getHeight(); |
| 1081 | + |
| 1082 | + // Resize the interface for layoutMode == 'below' ( if not in full screen) |
| 1083 | + if( ! this.embedPlayer.controlBuilder.fullscreenMode ){ |
| 1084 | + this.embedPlayer.$interface.animate({ |
| 1085 | + 'height': height |
1274 | 1086 | }); |
1275 | | - return ; |
1276 | 1087 | } |
1277 | | - |
1278 | | - // Try to load src via XHR source |
1279 | | - if( this.getSrc() ) { |
1280 | | - // Issue the direct load request |
1281 | | - if ( !mw.isLocalDomain( this.getSrc() ) ) { |
1282 | | - mw.log("Error: cant load crossDomain src:" + this.getSrc() ); |
1283 | | - return ; |
1284 | | - } |
1285 | | - $.get( this.getSrc(), function( data ) { |
1286 | | - // Parse and load captions: |
1287 | | - _this.captions = handler( data ); |
1288 | | - mw.log("mw.TimedText:: loaded from srt file: " + _this.captions.length + ' captions'); |
1289 | | - // Update the loaded state: |
1290 | | - _this.loaded = true; |
1291 | | - if( callback ) { |
1292 | | - callback(); |
1293 | | - } |
1294 | | - }, 'text' ); |
1295 | | - return ; |
1296 | | - } |
1297 | | - |
1298 | | - |
| 1088 | + mw.log( 'TimedText:: height of ' + this.embedPlayer.id + ' is now: ' + $( '#' + this.embedPlayer.id ).height() ); |
1299 | 1089 | }, |
1300 | | - |
1301 | 1090 | /** |
1302 | | - * Returns the text content for requested time |
1303 | | - * |
1304 | | - * @param {String} time Time in seconds |
1305 | | - */ |
1306 | | - getTimedText: function ( time ) { |
1307 | | - var prevCaption = this.captions[ this.prevIndex ]; |
| 1091 | + * Build css for caption using this.options |
| 1092 | + */ |
| 1093 | + getCaptionCss: function() { |
| 1094 | + var options = this.options; |
| 1095 | + var style = {'display': 'inline'}; |
1308 | 1096 | |
1309 | | - // Setup the startIndex: |
1310 | | - if( prevCaption && time >= prevCaption.start ) { |
1311 | | - var startIndex = this.prevIndex; |
1312 | | - }else{ |
1313 | | - //If a backwards seek start searching at the start: |
1314 | | - var startIndex = 0; |
| 1097 | + if( options.bg ) { |
| 1098 | + style["background-color"] = this.getHexColor( options.bg ); |
1315 | 1099 | } |
1316 | | - // Start looking for the text via time, return first match: |
1317 | | - for( var i = startIndex ; i < this.captions.length; i++ ) { |
1318 | | - var caption = this.captions[ i ]; |
1319 | | - // Don't handle captions with 0 or -1 end time: |
1320 | | - if( caption.end == 0 || caption.end == -1) |
1321 | | - continue; |
1322 | | - |
1323 | | - if( time >= caption.start && |
1324 | | - time <= caption.end ) { |
1325 | | - this.prevIndex = i; |
1326 | | - //mw.log("Start cap time: " + caption.start + ' End time: ' + caption.end ); |
1327 | | - return caption.content; |
1328 | | - } |
| 1100 | + if( options.fontColor ) { |
| 1101 | + style["color"] = this.getHexColor( options.fontColor ); |
1329 | 1102 | } |
1330 | | - //No text found in range return false: |
1331 | | - return false; |
1332 | | - } |
1333 | | - }; |
1334 | | - |
1335 | | - /** |
1336 | | - * parse mediaWiki html srt |
1337 | | - * @param {Object} data XML data string to be parsed |
1338 | | - */ |
1339 | | - function parseMwSrt( data ) { |
1340 | | - var captions = [ ]; |
1341 | | - var curentCap = []; |
1342 | | - var parseNextAsTime = false; |
1343 | | - // Optimize: we could use javascript strings functions instead of jQuery XML parsing: |
1344 | | - $( '<div>' + data + '</div>' ).find('p').each( function() { |
1345 | | - var currentPtext = $(this).html(); |
1346 | | - //mw.log( 'pText: ' + currentPtext ); |
1347 | | - |
1348 | | - //Check if the p matches the "all in one line" match: |
1349 | | - var m = currentPtext |
1350 | | - .replace('-->', '-->') |
1351 | | - .match(/\d+\s([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\s*--?>\s*([\d\-]+):([\d\-]+):([\d\-]+)(?:,([\d\-]+))?\n?(.*)/); |
1352 | | - |
1353 | | - if (m) { |
1354 | | - var startMs = (m[4])? (parseInt(m[4], 10) / 1000):0; |
1355 | | - var endMs = (m[8])? (parseInt(m[8], 10) / 1000) : 0; |
1356 | | - captions.push({ |
1357 | | - 'start': |
1358 | | - (parseInt(m[1], 10) * 60 * 60) + |
1359 | | - (parseInt(m[2], 10) * 60) + |
1360 | | - (parseInt(m[3], 10)) + |
1361 | | - startMs , |
1362 | | - 'end': |
1363 | | - (parseInt(m[5], 10) * 60 * 60) + |
1364 | | - (parseInt(m[6], 10) * 60) + |
1365 | | - (parseInt(m[7], 10)) + |
1366 | | - endMs, |
1367 | | - 'content': $.trim( m[9] ) |
1368 | | - }); |
1369 | | - return true; |
| 1103 | + if( options.fontFamily ){ |
| 1104 | + style["font-family"] = options.fontFamily; |
1370 | 1105 | } |
1371 | | - // Else check for multi-line match: |
1372 | | - if( parseInt( currentPtext ) == currentPtext ) { |
1373 | | - if( curentCap.length != 0) { |
1374 | | - captions.push( curentCap ); |
1375 | | - } |
1376 | | - curentCap = { |
1377 | | - 'content': '' |
1378 | | - }; |
1379 | | - return true; |
| 1106 | + if( options.fontsize ) { |
| 1107 | + // Translate to em size so that font-size parent percentage |
| 1108 | + // base on http://pxtoem.com/ |
| 1109 | + var emFontMap = { '6': .375, '7': .438, '8' : .5, '9': .563, '10': .625, '11':.688, |
| 1110 | + '12':.75, '13': .813, '14': .875, '15':.938, '16':1, '17':1.063, '18': 1.125, '19': 1.888, |
| 1111 | + '20':1.25, '21':1.313, '22':1.375, '23':1.438, '24':1.5}; |
| 1112 | + // Make sure its an int: |
| 1113 | + options.fontsize = parseInt( options.fontsize ); |
| 1114 | + style[ "font-size" ] = ( emFontMap[ options.fontsize ] ) ? |
| 1115 | + emFontMap[ options.fontsize ] +'em' : |
| 1116 | + ( options.fontsize > 24 )? emFontMap[24]+'em' : emFontMap[6]; |
1380 | 1117 | } |
1381 | | - //Check only for time match: |
1382 | | - var m = currentPtext.replace('-->', '-->').match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/); |
1383 | | - if (m) { |
1384 | | - var startMs = (m[4])? (parseInt(m[4], 10) / 1000):0; |
1385 | | - var endMs = (m[8])? (parseInt(m[8], 10) / 1000) : 0; |
1386 | | - curentCap['start']= |
1387 | | - (parseInt(m[1], 10) * 60 * 60) + |
1388 | | - (parseInt(m[2], 10) * 60) + |
1389 | | - (parseInt(m[3], 10)) + |
1390 | | - startMs; |
1391 | | - curentCap['end']= |
1392 | | - (parseInt(m[5], 10) * 60 * 60) + |
1393 | | - (parseInt(m[6], 10) * 60) + |
1394 | | - (parseInt(m[7], 10)) + |
1395 | | - endMs; |
1396 | | - return true; |
| 1118 | + if( options.useGlow && options.glowBlur && options.glowColor ) { |
| 1119 | + style["text-shadow"] = '0 0 ' + options.glowBlur + 'px ' + this.getHexColor( options.glowColor ); |
1397 | 1120 | } |
1398 | | - //Else content for the curentCap |
1399 | | - if( currentPtext != '<br>' ) { |
1400 | | - curentCap['content'] += currentPtext; |
1401 | | - } |
1402 | | - }); |
1403 | | - //Push last subtitle: |
1404 | | - if( curentCap.length != 0) { |
1405 | | - captions.push( curentCap ); |
1406 | | - } |
1407 | | - return captions; |
1408 | | - } |
1409 | | - /** |
1410 | | - * srt timed text parse handle: |
1411 | | - * @param {String} data Srt string to be parsed |
1412 | | - */ |
1413 | | - function parseSrt( data ) { |
1414 | | - // Remove dos newlines |
1415 | | - var srt = data.replace(/\r+/g, ''); |
1416 | 1121 | |
1417 | | - // Trim white space start and end |
1418 | | - srt = srt.replace(/^\s+|\s+$/g, ''); |
| 1122 | + return style; |
| 1123 | + }, |
1419 | 1124 | |
1420 | | - // Remove all html tags for security reasons |
1421 | | - srt = srt.replace(/<[a-zA-Z\/][^>]*>/g, ''); |
1422 | | - |
1423 | | - // Get captions |
1424 | | - var captions = []; |
1425 | | - var caplist = srt.split('\n\n'); |
1426 | | - for (var i = 0; i < caplist.length; i++) { |
1427 | | - var caption = ""; |
1428 | | - var content, start, end, s; |
1429 | | - caption = caplist[i]; |
1430 | | - s = caption.split(/\n/); |
1431 | | - if (s.length < 2) { |
1432 | | - // file format error or comment lines |
1433 | | - continue; |
1434 | | - } |
1435 | | - if (s[0].match(/^\d+$/) && s[1].match(/\d+:\d+:\d+/)) { |
1436 | | - // ignore caption number in s[0] |
1437 | | - // parse time string |
1438 | | - var m = s[1].match(/(\d+):(\d+):(\d+)(?:,(\d+))?\s*--?>\s*(\d+):(\d+):(\d+)(?:,(\d+))?/); |
1439 | | - if (m) { |
1440 | | - start = |
1441 | | - (parseInt(m[1], 10) * 60 * 60) + |
1442 | | - (parseInt(m[2], 10) * 60) + |
1443 | | - (parseInt(m[3], 10)) + |
1444 | | - (parseInt(m[4], 10) / 1000); |
1445 | | - end = |
1446 | | - (parseInt(m[5], 10) * 60 * 60) + |
1447 | | - (parseInt(m[6], 10) * 60) + |
1448 | | - (parseInt(m[7], 10)) + |
1449 | | - (parseInt(m[8], 10) / 1000); |
1450 | | - } else { |
1451 | | - // Unrecognized timestring |
1452 | | - continue; |
1453 | | - } |
1454 | | - // concatenate text lines to html text |
1455 | | - content = s.slice(2).join("<br>"); |
| 1125 | + getHexColor: function(color) { |
| 1126 | + if( color.substr(0,2) == "0x" ) { |
| 1127 | + return color.replace('0x', '#'); |
1456 | 1128 | } else { |
1457 | | - // file format error or comment lines |
1458 | | - continue; |
| 1129 | + color = parseInt( color ); |
| 1130 | + color = color.toString(16); |
| 1131 | + var len = 6 - color.length; |
| 1132 | + if( len > 0 ) { |
| 1133 | + var pre = ''; |
| 1134 | + for( var i=0; i<len; i++) { |
| 1135 | + pre += '0'; |
| 1136 | + } |
| 1137 | + color = pre + color; |
| 1138 | + } |
| 1139 | + return '#' + color; |
1459 | 1140 | } |
1460 | | - captions.push({ |
1461 | | - 'start' : start, |
1462 | | - 'end' : end, |
1463 | | - 'content' : content |
1464 | | - } ); |
1465 | 1141 | } |
1466 | | - |
1467 | | - return captions; |
1468 | 1142 | }; |
1469 | | - /** |
1470 | | - * CMML parser handle |
1471 | | - * @param {Mixed} data String or XML tree of CMML data to be parsed |
1472 | | - */ |
1473 | | - function parseCMML( data ) { |
1474 | | - var captions = [ ]; |
1475 | | - $( data ).find( 'clip' ).each( function( inx, clip ) { |
1476 | | - var content, start, end; |
1477 | | - // mw.log(' on clip ' + clip.id); |
1478 | | - start = mw.npt2seconds( $( clip ).attr( 'start' ).replace( 'npt:', '' ) ); |
1479 | | - end = mw.npt2seconds( $( clip ).attr( 'end' ).replace( 'npt:', '' ) ); |
1480 | 1143 | |
1481 | | - $( clip ).find( 'body' ).each( function( binx, bn ) { |
1482 | | - if ( bn.textContent ) { |
1483 | | - content = bn.textContent; |
1484 | | - } else if ( bn.text ) { |
1485 | | - content = bn.text; |
1486 | | - } |
1487 | | - } ); |
1488 | | - captions.push ( { |
1489 | | - 'start' : start, |
1490 | | - 'end' : end, |
1491 | | - 'content' : content |
1492 | | - } ); |
1493 | | - } ); |
1494 | | - |
1495 | | - return captions; |
1496 | | - } |
1497 | | - |
1498 | | - /** |
1499 | | - * Text Providers |
1500 | | - * |
1501 | | - * text provider objects let you map your player to a timed text provider |
1502 | | - * can provide discovery, and contribution push back |
1503 | | - * |
1504 | | - |
1505 | | - // Will add a base class once we are serving more than just mediaWiki "commons" |
1506 | | - // also we should segment out these files |
1507 | | - mw.BaseTextProvider = function() { |
1508 | | - return this.init(); |
1509 | | - } |
1510 | | - mw.BaseTextProvider.prototype = { |
1511 | | - init: function() { |
1512 | | - |
1513 | | - } |
1514 | | - } |
1515 | | - |
1516 | | - */ |
1517 | | - var default_textProvider_attr = [ |
1518 | | - 'apiUrl', |
1519 | | - 'providerId', |
1520 | | - 'timedTextNS', |
1521 | | - 'embedPlayer' |
1522 | | - ]; |
1523 | | - |
1524 | | - mw.MediaWikTrackProvider = function( options ) { |
1525 | | - this.init( options ); |
1526 | | - }; |
1527 | | - mw.MediaWikTrackProvider.prototype = { |
1528 | | - |
1529 | | - // The api url: |
1530 | | - apiUrl: null, |
1531 | | - |
1532 | | - // The timed text namespace |
1533 | | - timedTextNS: null, |
1534 | | - |
1535 | | - /** |
1536 | | - * @constructor |
1537 | | - * @param {Object} options Set of options for the provider |
1538 | | - */ |
1539 | | - init: function( options ) { |
1540 | | - for(var i in default_textProvider_attr) { |
1541 | | - var attr = default_textProvider_attr[ i ]; |
1542 | | - if( options[ attr ] ) |
1543 | | - this[ attr ] = options[ attr ]; |
1544 | | - |
1545 | | - } |
1546 | | - }, |
1547 | | - |
1548 | | - /** |
1549 | | - * Loads a single text source by titleKey |
1550 | | - * @param {Object} titleKey |
1551 | | - */ |
1552 | | - loadTitleKey: function( titleKey, callback ) { |
1553 | | - var request = { |
1554 | | - 'action': 'parse', |
1555 | | - 'page': titleKey |
1556 | | - /** |
1557 | | - * For now we don't use cache helpers since the request is |
1558 | | - * going over jsonp we kill any outer cache anyway |
1559 | | - 'smaxage' : 300, |
1560 | | - 'maxage' : 300 |
1561 | | - */ |
1562 | | - }; |
1563 | | - mw.getJSON( this.apiUrl, request, function( data ) { |
1564 | | - if ( data && data.parse && data.parse.text['*'] ) { |
1565 | | - callback( data.parse.text['*'] ); |
1566 | | - return; |
1567 | | - } |
1568 | | - mw.log("Error: could not load:" + titleKey); |
1569 | | - callback( false ); |
1570 | | - } ); |
1571 | | - }, |
1572 | | - |
1573 | | - /** |
1574 | | - * Loads all available source for a given apiTitleKey |
1575 | | - * |
1576 | | - * @param {String} apiTitleKey For mediaWiki the apiTitleKey is the "wiki title" |
1577 | | - */ |
1578 | | - loadSources: function( apiTitleKey, callback ) { |
1579 | | - var request = {}; |
1580 | | - var _this = this; |
1581 | | - this.getSourcePages( apiTitleKey, function( sourcePages ) { |
1582 | | - if( ! sourcePages.query.allpages ) { |
1583 | | - //Check if a shared asset |
1584 | | - mw.log( 'no subtitle pages found'); |
1585 | | - if( callback ) |
1586 | | - callback(); |
1587 | | - return ; |
1588 | | - } |
1589 | | - // We have sources put them into the player |
1590 | | - if( callback ) |
1591 | | - callback( _this.getSources( sourcePages ) ); |
1592 | | - } ); |
1593 | | - }, |
1594 | | - |
1595 | | - /** |
1596 | | - * Get the subtitle pages |
1597 | | - * @param {String} titleKey Title to get subtitles for |
1598 | | - * @param {Function} callback Function to call once NS subs are grabbed |
1599 | | - */ |
1600 | | - getSourcePages: function( titleKey, callback ) { |
1601 | | - var _this = this; |
1602 | | - var request = { |
1603 | | - 'list' : 'allpages', |
1604 | | - 'apprefix' : unescape( titleKey ), |
1605 | | - 'apnamespace' : this.getTimedTextNS(), |
1606 | | - 'aplimit' : 200, |
1607 | | - 'prop':'revisions' |
1608 | | - /** |
1609 | | - * For now we don't use cache helpers since the request is |
1610 | | - * going over jsonp we kill any outer cache anyway |
1611 | | - 'smaxage' : 300, |
1612 | | - 'maxage' : 300 |
1613 | | - */ |
1614 | | - }; |
1615 | | - mw.getJSON( this.apiUrl, request, function( sourcePages ) { |
1616 | | - // If "timedText" is not a valid namespace try "just" with prefix: |
1617 | | - if ( sourcePages.error && sourcePages.error.code == 'apunknown_apnamespace' ) { |
1618 | | - var request = { |
1619 | | - 'list' : 'allpages', |
1620 | | - 'apprefix' : _this.getCanonicalTimedTextNS() + ':' + _this.embedPlayer.apiTitleKey |
1621 | | - }; |
1622 | | - mw.getJSON( _this.apiUrl, request, function( sourcePages ) { |
1623 | | - callback( sourcePages ); |
1624 | | - } ); |
1625 | | - } else { |
1626 | | - callback( sourcePages ); |
1627 | | - } |
1628 | | - } ); |
1629 | | - }, |
1630 | | - |
1631 | | - /** |
1632 | | - * Get the sources from sourcePages data object ( api result ) |
1633 | | - * @param {Object} sourcePages Source page result object |
1634 | | - */ |
1635 | | - getSources: function( sourcePages ) { |
1636 | | - var _this = this; |
1637 | | - // look for text tracks: |
1638 | | - var foundTextTracks = false; |
1639 | | - var sources = []; |
1640 | | - for ( var i=0; i < sourcePages.query.allpages.length; i++ ) { |
1641 | | - |
1642 | | - var subPage = sourcePages.query.allpages[i]; |
1643 | | - if( !subPage || !subPage.title ){ |
1644 | | - continue; |
1645 | | - } |
1646 | | - var langKey = subPage.title.split( '.' ); |
1647 | | - var extension = langKey.pop(); |
1648 | | - langKey = langKey.pop(); |
1649 | | - //NOTE: we hard code the mw-srt type |
1650 | | - // ( This is because mediaWiki srt files can have wiki-text and parsed as such ) |
1651 | | - if( extension == 'srt' ) { |
1652 | | - extension = 'mw-srt'; |
1653 | | - } |
1654 | | - |
1655 | | - if ( ! _this.isSuportedLang( langKey ) ) { |
1656 | | - mw.log( 'Error: langkey:' + langKey + ' not supported' ); |
1657 | | - } else { |
1658 | | - sources.push( { |
1659 | | - 'extension': extension, |
1660 | | - 'srclang': langKey, |
1661 | | - 'titleKey': subPage.title.replace( / /g, "_") |
1662 | | - } ); |
1663 | | - } |
1664 | | - } |
1665 | | - return sources; |
1666 | | - }, |
1667 | | - |
1668 | | - /** |
1669 | | - * Return the namespace ( if not encoded on the page return default 102 ) |
1670 | | - */ |
1671 | | - getTimedTextNS: function() { |
1672 | | - if( this.timedTextNS ) |
1673 | | - return this.timedTextNS; |
1674 | | - if ( typeof wgNamespaceIds != 'undefined' && wgNamespaceIds['timedtext'] ) { |
1675 | | - this.timedTextNS = wgNamespaceIds['timedtext']; |
1676 | | - }else{ |
1677 | | - //default value is 102 ( probably should store this elsewhere ) |
1678 | | - this.timedTextNS = 102; |
1679 | | - } |
1680 | | - return this.timedTextNS; |
1681 | | - }, |
1682 | | - |
1683 | | - /** |
1684 | | - * Get the Canonical timed text namespace text |
1685 | | - */ |
1686 | | - getCanonicalTimedTextNS: function() { |
1687 | | - return 'TimedText'; |
1688 | | - }, |
1689 | | - |
1690 | | - /** |
1691 | | - * Check if the language is supported |
1692 | | - */ |
1693 | | - isSuportedLang: function( lang_key ) { |
1694 | | - if( mw.Language.names[ lang_key ]) { |
1695 | | - return true; |
1696 | | - } |
1697 | | - return false; |
1698 | | - } |
1699 | | - }; |
1700 | | - |
1701 | | - /** |
1702 | | - * jquery timedText binding. |
1703 | | - * Calls mw.timedText on the given selector |
1704 | | - * |
1705 | | - * @param {Object} options Options for the timed text menu |
1706 | | - */ |
1707 | | - $.fn.timedText = function ( action, target ) { |
1708 | | - mw.log('fn.timedText:: ' + action + ' t: ' + target ); |
1709 | | - var options; |
1710 | | - if( !target ){ |
1711 | | - options = action; |
1712 | | - } |
1713 | | - if( typeof options == 'undefined' ) |
1714 | | - options = {}; |
1715 | 1144 | |
1716 | | - $( this.selector ).each(function() { |
1717 | | - var embedPlayer = $(this).get(0); |
1718 | | - |
1719 | | - // Setup timed text for the given player: |
1720 | | - if( ! embedPlayer.timedText ) { |
1721 | | - embedPlayer.timedText = new mw.TimedText( embedPlayer, options); |
1722 | | - } |
1723 | | - |
1724 | | - // Show the timedText menu |
1725 | | - if( action == 'showMenu' ) { |
1726 | | - // Bind the menu to the target with autoShow = true |
1727 | | - mw.log('bind menu fn.timedText'); |
1728 | | - embedPlayer.timedText.bindMenu( target, true ); |
1729 | | - } |
1730 | | - } ); |
1731 | | - }; |
1732 | | - |
1733 | | - |
1734 | | - // On new embed player check if we need to add timedText |
1735 | | - $( mw ).bind( 'EmbedPlayerNewPlayer', function( event, embedPlayer ){ |
1736 | | - if( mw.isTimedTextSupported( embedPlayer) ){ |
1737 | | - if( ! embedPlayer.timedText && mw.TimedText ) { |
1738 | | - embedPlayer.timedText = new mw.TimedText( embedPlayer ); |
1739 | | - } |
1740 | | - } |
1741 | | - }); |
1742 | | - |
1743 | 1145 | } )( window.mediaWiki, window.jQuery ); |
Index: trunk/extensions/TimedMediaHandler/MwEmbedModules/TimedText/TimedText.php |
— | — | @@ -3,22 +3,17 @@ |
4 | 4 | // Register all the timedText modules |
5 | 5 | return array( |
6 | 6 | "mw.TimedText" => array( |
7 | | - 'scripts' => "resources/mw.TimedText.js", |
| 7 | + 'scripts' => array( |
| 8 | + "resources/mw.TimedText.js", |
| 9 | + "resources/mw.TextSource.js", |
| 10 | + ), |
8 | 11 | 'styles' => "resources/mw.style.TimedText.css", |
9 | 12 | 'dependencies' => array( |
10 | 13 | 'mw.EmbedPlayer', |
11 | 14 | 'mw.Api', |
12 | 15 | 'mw.Language.names', |
13 | | - 'jquery.ui.dialog', |
| 16 | + 'jquery.ui.dialog', |
14 | 17 | ), |
15 | 18 | 'messageFile' => 'TimedText.i18n.php', |
16 | | - ), |
17 | | - "mw.TimedTextEdit" => array( |
18 | | - 'scripts' => "resources/mw.TimedTextEdit.js", |
19 | | - 'styles' => "resources/mw.style.TimedTextEdit.css", |
20 | | - 'dependencies' => array( |
21 | | - 'mw.TimedText', |
22 | | - 'jquery.ui.tabs' |
23 | | - ) |
24 | 19 | ) |
25 | 20 | ); |
\ No newline at end of file |