Upstream version 7.35.144.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / file_manager / foreground / js / media / audio_player.js
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8  * TODO(mtomasz): Rewrite the entire audio player.
9  *
10  * @param {HTMLElement} container Container element.
11  * @constructor
12  */
13 function AudioPlayer(container) {
14   this.container_ = container;
15   this.currentTrack_ = -1;
16   this.playlistGeneration_ = 0;
17   this.selectedEntry_ = null;
18   this.volumeManager_ = new VolumeManagerWrapper(
19       VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
20   this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
21
22   this.container_.classList.add('collapsed');
23
24   function createChild(opt_className, opt_tag) {
25     var child = container.ownerDocument.createElement(opt_tag || 'div');
26     if (opt_className)
27       child.className = opt_className;
28     container.appendChild(child);
29     return child;
30   }
31
32   // We create two separate containers (for expanded and compact view) and keep
33   // two sets of TrackInfo instances. We could fiddle with a single set instead
34   // but it would make keeping the list scroll position very tricky.
35   this.trackList_ = createChild('track-list');
36   this.trackStack_ = createChild('track-stack');
37
38   createChild('title-button collapse').addEventListener(
39       'click', this.onExpandCollapse_.bind(this));
40
41   this.audioControls_ = new FullWindowAudioControls(
42       createChild(), this.advance_.bind(this), this.onError_.bind(this));
43   this.audioControls_.attachMedia(createChild('', 'audio'));
44
45   chrome.fileBrowserPrivate.getStrings(function(strings) {
46     container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE'];
47     this.errorString_ = strings['AUDIO_ERROR'];
48     this.offlineString_ = strings['AUDIO_OFFLINE'];
49     AudioPlayer.TrackInfo.DEFAULT_ARTIST =
50         strings['AUDIO_PLAYER_DEFAULT_ARTIST'];
51   }.bind(this));
52
53   this.volumeManager_.addEventListener('externally-unmounted',
54       this.onExternallyUnmounted_.bind(this));
55
56   window.addEventListener('resize', this.onResize_.bind(this));
57
58   // Show the window after DOM is processed.
59   var currentWindow = chrome.app.window.current();
60   setTimeout(currentWindow.show.bind(currentWindow), 0);
61 }
62
63 /**
64  * Initial load method (static).
65  */
66 AudioPlayer.load = function() {
67   document.ondragstart = function(e) { e.preventDefault() };
68
69   AudioPlayer.instance =
70       new AudioPlayer(document.querySelector('.audio-player'));
71   AudioPlayer.instance.load(window.appState);
72 };
73
74 util.addPageLoadHandler(AudioPlayer.load);
75
76 /**
77  * Unload the player.
78  */
79 function unload() {
80   if (AudioPlayer.instance)
81     AudioPlayer.instance.onUnload();
82 }
83
84 /**
85  * Reload the player.
86  */
87 function reload() {
88   AudioPlayer.instance.load(window.appState);
89 }
90
91 /**
92  * Load a new playlist.
93  * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
94  */
95 AudioPlayer.prototype.load = function(playlist) {
96   this.playlistGeneration_++;
97   this.audioControls_.pause();
98   this.currentTrack_ = -1;
99
100   // Save the app state, in case of restart. Make a copy of the object, so the
101   // playlist member is not changed after entries are resolved.
102   window.appState = JSON.parse(JSON.stringify(playlist));
103   util.saveAppState();
104
105   // Resolving entries has to be done after the volume manager is initialized.
106   this.volumeManager_.ensureInitialized(function() {
107     util.URLsToEntries(playlist.items, function(entries) {
108       this.entries_ = entries;
109       this.invalidTracks_ = {};
110       this.cancelAutoAdvance_();
111
112       if (this.entries_.length <= 1)
113         this.container_.classList.add('single-track');
114       else
115         this.container_.classList.remove('single-track');
116
117       this.syncHeight_();
118
119       this.trackList_.textContent = '';
120       this.trackStack_.textContent = '';
121
122       this.trackListItems_ = [];
123       this.trackStackItems_ = [];
124
125       if (this.entries_.length == 0)
126         return;
127
128       for (var i = 0; i != this.entries_.length; i++) {
129         var entry = this.entries_[i];
130         var onClick = this.select_.bind(this, i, false /* no restore */);
131         this.trackListItems_.push(
132             new AudioPlayer.TrackInfo(this.trackList_, entry, onClick));
133         this.trackStackItems_.push(
134             new AudioPlayer.TrackInfo(this.trackStack_, entry, onClick));
135       }
136
137       this.select_(playlist.position, !!playlist.time);
138
139       // This class will be removed if at least one track has art.
140       this.container_.classList.add('noart');
141
142       // Load the selected track metadata first, then load the rest.
143       this.loadMetadata_(playlist.position);
144       for (i = 0; i != this.entries_.length; i++) {
145         if (i != playlist.position)
146           this.loadMetadata_(i);
147       }
148     }.bind(this));
149   }.bind(this));
150 };
151
152 /**
153  * Load metadata for a track.
154  * @param {number} track Track number.
155  * @private
156  */
157 AudioPlayer.prototype.loadMetadata_ = function(track) {
158   this.fetchMetadata_(
159       this.entries_[track], this.displayMetadata_.bind(this, track));
160 };
161
162 /**
163  * Display track's metadata.
164  * @param {number} track Track number.
165  * @param {Object} metadata Metadata object.
166  * @param {string=} opt_error Error message.
167  * @private
168  */
169 AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) {
170   this.trackListItems_[track].
171       setMetadata(metadata, this.container_, opt_error);
172   this.trackStackItems_[track].
173       setMetadata(metadata, this.container_, opt_error);
174 };
175
176 /**
177  * Closes audio player when a volume containing the selected item is unmounted.
178  * @param {Event} event The unmount event.
179  * @private
180  */
181 AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
182   if (!this.selectedEntry_)
183     return;
184
185   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
186       event.volumeInfo) {
187     window.close();
188   }
189 };
190
191 /**
192  * Called on window is being unloaded.
193  */
194 AudioPlayer.prototype.onUnload = function() {
195   this.audioControls_.cleanup();
196   this.volumeManager_.dispose();
197 };
198
199 /**
200  * Select a new track to play.
201  * @param {number} newTrack New track number.
202  * @param {boolean=} opt_restoreState True if restoring the play state from URL.
203  * @private
204  */
205 AudioPlayer.prototype.select_ = function(newTrack, opt_restoreState) {
206   if (this.currentTrack_ == newTrack) return;
207
208   this.changeSelectionInList_(this.currentTrack_, newTrack);
209   this.changeSelectionInStack_(this.currentTrack_, newTrack);
210
211   this.currentTrack_ = newTrack;
212
213   window.appState.position = this.currentTrack_;
214   window.appState.time = 0;
215   util.saveAppState();
216
217   this.scrollToCurrent_(false);
218
219   var currentTrack = this.currentTrack_;
220   var entry = this.entries_[currentTrack];
221   this.fetchMetadata_(entry, function(metadata) {
222     if (this.currentTrack_ != currentTrack)
223       return;
224     this.audioControls_.load(entry, opt_restoreState);
225
226     // Resolve real filesystem path of the current audio file.
227     this.selectedEntry_ = entry;
228   }.bind(this));
229 };
230
231 /**
232  * @param {Entry} entry Track file entry.
233  * @param {function(object)} callback Callback.
234  * @private
235  */
236 AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) {
237   this.metadataCache_.get(entry, 'thumbnail|media|streaming',
238       function(generation, metadata) {
239         // Do nothing if another load happened since the metadata request.
240         if (this.playlistGeneration_ == generation)
241           callback(metadata);
242       }.bind(this, this.playlistGeneration_));
243 };
244
245 /**
246  * @param {number} oldTrack Old track number.
247  * @param {number} newTrack New track number.
248  * @private
249  */
250 AudioPlayer.prototype.changeSelectionInList_ = function(oldTrack, newTrack) {
251   this.trackListItems_[newTrack].getBox().classList.add('selected');
252
253   if (oldTrack >= 0) {
254     this.trackListItems_[oldTrack].getBox().classList.remove('selected');
255   }
256 };
257
258 /**
259  * @param {number} oldTrack Old track number.
260  * @param {number} newTrack New track number.
261  * @private
262  */
263 AudioPlayer.prototype.changeSelectionInStack_ = function(oldTrack, newTrack) {
264   var newBox = this.trackStackItems_[newTrack].getBox();
265   newBox.classList.add('selected');  // Put on top immediately.
266   newBox.classList.add('visible');  // Start fading in.
267
268   if (oldTrack >= 0) {
269     var oldBox = this.trackStackItems_[oldTrack].getBox();
270     oldBox.classList.remove('selected'); // Put under immediately.
271     setTimeout(function() {
272       if (!oldBox.classList.contains('selected')) {
273         // This will start fading out which is not really necessary because
274         // oldBox is already completely obscured by newBox.
275         oldBox.classList.remove('visible');
276       }
277     }, 300);
278   }
279 };
280
281 /**
282  * Scrolls the current track into the viewport.
283  *
284  * @param {boolean} keepAtBottom If true, make the selected track the last
285  *   of the visible (if possible). If false, perform minimal scrolling.
286  * @private
287  */
288 AudioPlayer.prototype.scrollToCurrent_ = function(keepAtBottom) {
289   var box = this.trackListItems_[this.currentTrack_].getBox();
290   this.trackList_.scrollTop = Math.max(
291       keepAtBottom ? 0 : Math.min(box.offsetTop, this.trackList_.scrollTop),
292       box.offsetTop + box.offsetHeight - this.trackList_.clientHeight);
293 };
294
295 /**
296  * @return {boolean} True if the player is be displayed in compact mode.
297  * @private
298  */
299 AudioPlayer.prototype.isCompact_ = function() {
300   return this.container_.classList.contains('collapsed') ||
301          this.container_.classList.contains('single-track');
302 };
303
304 /**
305  * Go to the previous or the next track.
306  * @param {boolean} forward True if next, false if previous.
307  * @param {boolean=} opt_onlyIfValid True if invalid tracks should be selected.
308  * @private
309  */
310 AudioPlayer.prototype.advance_ = function(forward, opt_onlyIfValid) {
311   this.cancelAutoAdvance_();
312
313   var newTrack = this.currentTrack_ + (forward ? 1 : -1);
314   if (newTrack < 0) newTrack = this.entries_.length - 1;
315   if (newTrack == this.entries_.length) newTrack = 0;
316   if (opt_onlyIfValid && this.invalidTracks_[newTrack])
317     return;
318   this.select_(newTrack);
319 };
320
321 /**
322  * Media error handler.
323  * @private
324  */
325 AudioPlayer.prototype.onError_ = function() {
326   var track = this.currentTrack_;
327
328   this.invalidTracks_[track] = true;
329
330   this.fetchMetadata_(
331       this.entries_[track],
332       function(metadata) {
333         var error = (!navigator.onLine && metadata.streaming) ?
334             this.offlineString_ : this.errorString_;
335         this.displayMetadata_(track, metadata, error);
336         this.scheduleAutoAdvance_();
337       }.bind(this));
338 };
339
340 /**
341  * Schedule automatic advance to the next track after a timeout.
342  * @private
343  */
344 AudioPlayer.prototype.scheduleAutoAdvance_ = function() {
345   this.cancelAutoAdvance_();
346   this.autoAdvanceTimer_ = setTimeout(
347       function() {
348         this.autoAdvanceTimer_ = null;
349         // We are advancing only if the next track is not known to be invalid.
350         // This prevents an endless auto-advancing in the case when all tracks
351         // are invalid (we will only visit each track once).
352         this.advance_(true /* forward */, true /* only if valid */);
353       }.bind(this),
354       3000);
355 };
356
357 /**
358  * Cancel the scheduled auto advance.
359  * @private
360  */
361 AudioPlayer.prototype.cancelAutoAdvance_ = function() {
362   if (this.autoAdvanceTimer_) {
363     clearTimeout(this.autoAdvanceTimer_);
364     this.autoAdvanceTimer_ = null;
365   }
366 };
367
368 /**
369  * Expand/collapse button click handler. Toggles the mode and updates the
370  * height of the window.
371  *
372  * @private
373  */
374 AudioPlayer.prototype.onExpandCollapse_ = function() {
375   if (!this.isCompact_()) {
376     this.setExpanded_(false);
377     this.lastExpandedHeight_ = window.innerHeight;
378   } else {
379     this.setExpanded_(true);
380   }
381   this.syncHeight_();
382 };
383
384 /**
385  * Toggles the current expand mode.
386  *
387  * @param {boolean} on True if on, false otherwise.
388  * @private
389  */
390 AudioPlayer.prototype.setExpanded_ = function(on) {
391   if (on) {
392     this.container_.classList.remove('collapsed');
393     this.scrollToCurrent_(true);
394   } else {
395     this.container_.classList.add('collapsed');
396   }
397 };
398
399 /**
400  * Toggles the expanded mode when resizing.
401  *
402  * @param {Event} event Resize event.
403  * @private
404  */
405 AudioPlayer.prototype.onResize_ = function(event) {
406   if (this.isCompact_() &&
407       window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
408     this.setExpanded_(true);
409   } else if (!this.isCompact_() &&
410              window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
411     this.setExpanded_(false);
412   }
413 };
414
415 /* Keep the below constants in sync with the CSS. */
416
417 /**
418  * Window header size in pixels.
419  * @type {number}
420  * @const
421  */
422 AudioPlayer.HEADER_HEIGHT = 28;
423
424 /**
425  * Track height in pixels.
426  * @type {number}
427  * @const
428  */
429 AudioPlayer.TRACK_HEIGHT = 58;
430
431 /**
432  * Controls bar height in pixels.
433  * @type {number}
434  * @const
435  */
436 AudioPlayer.CONTROLS_HEIGHT = 35;
437
438 /**
439  * Default number of items in the expanded mode.
440  * @type {number}
441  * @const
442  */
443 AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
444
445 /**
446  * Minimum size of the window in the expanded mode in pixels.
447  * @type {number}
448  * @const
449  */
450 AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
451                                        AudioPlayer.TRACK_HEIGHT * 2;
452
453 /**
454  * Set the correct player window height.
455  * @private
456  */
457 AudioPlayer.prototype.syncHeight_ = function() {
458   var targetHeight;
459
460   if (!this.isCompact_()) {
461     // Expanded.
462     if (this.lastExpandedHeight_) {
463       targetHeight = this.lastExpandedHeight_;
464     } else {
465       var expandedListHeight =
466         Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
467                                     AudioPlayer.TRACK_HEIGHT;
468       targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
469     }
470   } else {
471     // Not expaned.
472     targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
473   }
474
475   window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
476 };
477
478 /**
479  * Create a TrackInfo object encapsulating the information about one track.
480  *
481  * @param {HTMLElement} container Container element.
482  * @param {Entry} entry Track entry.
483  * @param {function} onClick Click handler.
484  * @constructor
485  */
486 AudioPlayer.TrackInfo = function(container, entry, onClick) {
487   this.entry_ = entry;
488
489   var doc = container.ownerDocument;
490
491   this.box_ = doc.createElement('div');
492   this.box_.className = 'track';
493   this.box_.addEventListener('click', onClick);
494   container.appendChild(this.box_);
495
496   this.art_ = doc.createElement('div');
497   this.art_.className = 'art blank';
498   this.box_.appendChild(this.art_);
499
500   this.img_ = doc.createElement('img');
501   this.art_.appendChild(this.img_);
502
503   this.data_ = doc.createElement('div');
504   this.data_.className = 'data';
505   this.box_.appendChild(this.data_);
506
507   this.title_ = doc.createElement('div');
508   this.title_.className = 'data-title';
509   this.data_.appendChild(this.title_);
510
511   this.artist_ = doc.createElement('div');
512   this.artist_.className = 'data-artist';
513   this.data_.appendChild(this.artist_);
514 };
515
516 /**
517  * @return {HTMLDivElement} The wrapper element for the track.
518  */
519 AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ };
520
521 /**
522  * @return {string} Default track title (file name extracted from the entry).
523  */
524 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
525   // TODO(mtomasz): Reuse ImageUtil.getDisplayNameFromName().
526   var name = this.entry_.name;
527   var dotIndex = name.lastIndexOf('.');
528   var title = dotIndex >= 0 ? name.substr(0, dotIndex) : name;
529   return title;
530 };
531
532 /**
533  * TODO(kaznacheev): Localize.
534  */
535 AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
536
537 /**
538  * @return {string} 'Unknown artist' string.
539  */
540 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
541   return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
542 };
543
544 /**
545  * @param {Object} metadata The metadata object.
546  * @param {HTMLElement} container The container for the tracks.
547  * @param {string} error Error string.
548  */
549 AudioPlayer.TrackInfo.prototype.setMetadata = function(
550     metadata, container, error) {
551   if (error) {
552     this.art_.classList.add('blank');
553     this.art_.classList.add('error');
554     container.classList.remove('noart');
555   } else if (metadata.thumbnail && metadata.thumbnail.url) {
556     this.img_.onload = function() {
557       // Only display the image if the thumbnail loaded successfully.
558       this.art_.classList.remove('blank');
559       container.classList.remove('noart');
560     }.bind(this);
561     this.img_.src = metadata.thumbnail.url;
562   }
563   this.title_.textContent = (metadata.media && metadata.media.title) ||
564       this.getDefaultTitle();
565   this.artist_.textContent = error ||
566       (metadata.media && metadata.media.artist) || this.getDefaultArtist();
567 };
568
569 /**
570  * Audio controls specific for the Audio Player.
571  *
572  * @param {HTMLElement} container Parent container.
573  * @param {function(boolean)} advanceTrack Parameter: true=forward.
574  * @param {function} onError Error handler.
575  * @constructor
576  */
577 function FullWindowAudioControls(container, advanceTrack, onError) {
578   AudioControls.apply(this, arguments);
579
580   document.addEventListener('keydown', function(e) {
581     if (e.keyIdentifier == 'U+0020') {
582       this.togglePlayState();
583       e.preventDefault();
584     }
585   }.bind(this));
586 }
587
588 FullWindowAudioControls.prototype = { __proto__: AudioControls.prototype };
589
590 /**
591  * Enable play state restore from the location hash.
592  * @param {FileEntry} entry Source Entry.
593  * @param {boolean} restore True if need to restore the play state.
594  */
595 FullWindowAudioControls.prototype.load = function(entry, restore) {
596   this.media_.src = entry.toURL();
597   this.media_.load();
598   this.restoreWhenLoaded_ = restore;
599 };
600
601 /**
602  * Save the current state so that it survives page/app reload.
603  */
604 FullWindowAudioControls.prototype.onPlayStateChanged = function() {
605   this.encodeState();
606 };
607
608 /**
609  * Restore the state after page/app reload.
610  */
611 FullWindowAudioControls.prototype.restorePlayState = function() {
612   if (this.restoreWhenLoaded_) {
613     this.restoreWhenLoaded_ = false;  // This should only work once.
614     if (this.decodeState())
615       return;
616   }
617   this.play();
618 };