cros: Remove default pinned apps trial.
[chromium-blink-merge.git] / chrome / browser / resources / file_manager / foreground / js / media / audio_player.js
blob4791c31f92b9077364414c861a3e2c948efea043
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.
5 'use strict';
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.metadataCache_ = MetadataCache.createFull();
16   this.currentTrack_ = -1;
17   this.playlistGeneration_ = 0;
18   this.selectedEntry_ = null;
19   this.volumeManager_ = new VolumeManagerWrapper(
20       VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
22   this.container_.classList.add('collapsed');
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   }
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');
38   createChild('title-button collapse').addEventListener(
39       'click', this.onExpandCollapse_.bind(this));
41   this.audioControls_ = new FullWindowAudioControls(
42       createChild(), this.advance_.bind(this), this.onError_.bind(this));
44   this.audioControls_.attachMedia(createChild('', 'audio'));
46   chrome.fileBrowserPrivate.getStrings(function(strings) {
47     container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE'];
48     this.errorString_ = strings['AUDIO_ERROR'];
49     this.offlineString_ = strings['AUDIO_OFFLINE'];
50     AudioPlayer.TrackInfo.DEFAULT_ARTIST =
51         strings['AUDIO_PLAYER_DEFAULT_ARTIST'];
52   }.bind(this));
54   this.volumeManager_.addEventListener('externally-unmounted',
55       this.onExternallyUnmounted_.bind(this));
57   window.addEventListener('resize', this.onResize_.bind(this));
59   // Show the window after DOM is processed.
60   var currentWindow = chrome.app.window.current();
61   setTimeout(currentWindow.show.bind(currentWindow), 0);
64 /**
65  * Initial load method (static).
66  */
67 AudioPlayer.load = function() {
68   document.ondragstart = function(e) { e.preventDefault() };
70   // TODO(mtomasz): Consider providing an exact size icon, instead of relying
71   // on downsampling by ash.
72   chrome.app.window.current().setIcon(
73       'foreground/images/media/2x/audio_player.png');
75   AudioPlayer.instance =
76       new AudioPlayer(document.querySelector('.audio-player'));
77   reload();
80 util.addPageLoadHandler(AudioPlayer.load);
82 /**
83  * Unload the player.
84  */
85 function unload() {
86   if (AudioPlayer.instance)
87     AudioPlayer.instance.onUnload();
90 /**
91  * Reload the player.
92  */
93 function reload() {
94   if (window.appState) {
95     util.saveAppState();
96     AudioPlayer.instance.load(window.appState);
97     return;
98   }
102  * Load a new playlist.
103  * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
104  */
105 AudioPlayer.prototype.load = function(playlist) {
106   this.playlistGeneration_++;
107   this.audioControls_.pause();
108   this.currentTrack_ = -1;
110   // Save the app state, in case of restart.
111   window.appState = playlist;
112   util.saveAppState();
114   util.URLsToEntries(playlist.items, function(entries) {
115     this.entries_ = entries;
116     this.invalidTracks_ = {};
117     this.cancelAutoAdvance_();
119     if (this.entries_.length <= 1)
120       this.container_.classList.add('single-track');
121     else
122       this.container_.classList.remove('single-track');
124     this.syncHeight_();
126     this.trackList_.textContent = '';
127     this.trackStack_.textContent = '';
129     this.trackListItems_ = [];
130     this.trackStackItems_ = [];
132     if (this.entries_.length == 0)
133       return;
135     for (var i = 0; i != this.entries_.length; i++) {
136       var entry = this.entries_[i];
137       var onClick = this.select_.bind(this, i, false /* no restore */);
138       this.trackListItems_.push(
139           new AudioPlayer.TrackInfo(this.trackList_, entry, onClick));
140       this.trackStackItems_.push(
141           new AudioPlayer.TrackInfo(this.trackStack_, entry, onClick));
142     }
144     this.select_(playlist.position, !!playlist.time);
146     // This class will be removed if at least one track has art.
147     this.container_.classList.add('noart');
149     // Load the selected track metadata first, then load the rest.
150     this.loadMetadata_(playlist.position);
151     for (i = 0; i != this.entries_.length; i++) {
152       if (i != playlist.position)
153         this.loadMetadata_(i);
154     }
155   }.bind(this));
159  * Load metadata for a track.
160  * @param {number} track Track number.
161  * @private
162  */
163 AudioPlayer.prototype.loadMetadata_ = function(track) {
164   this.fetchMetadata_(
165       this.entries_[track], this.displayMetadata_.bind(this, track));
169  * Display track's metadata.
170  * @param {number} track Track number.
171  * @param {Object} metadata Metadata object.
172  * @param {string=} opt_error Error message.
173  * @private
174  */
175 AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) {
176   this.trackListItems_[track].
177       setMetadata(metadata, this.container_, opt_error);
178   this.trackStackItems_[track].
179       setMetadata(metadata, this.container_, opt_error);
183  * Closes audio player when a volume containing the selected item is unmounted.
184  * @param {Event} event The unmount event.
185  * @private
186  */
187 AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
188   if (!this.selectedEntry_)
189     return;
191   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
192       event.volumeInfo) {
193     window.close();
194   }
198  * Called on window is being unloaded.
199  */
200 AudioPlayer.prototype.onUnload = function() {
201   this.audioControls_.cleanup();
202   this.volumeManager_.dispose();
206  * Select a new track to play.
207  * @param {number} newTrack New track number.
208  * @param {boolean=} opt_restoreState True if restoring the play state from URL.
209  * @private
210  */
211 AudioPlayer.prototype.select_ = function(newTrack, opt_restoreState) {
212   if (this.currentTrack_ == newTrack) return;
214   this.changeSelectionInList_(this.currentTrack_, newTrack);
215   this.changeSelectionInStack_(this.currentTrack_, newTrack);
217   this.currentTrack_ = newTrack;
219   if (window.appState) {
220     window.appState.position = this.currentTrack_;
221     window.appState.time = 0;
222     util.saveAppState();
223   } else {
224     util.platform.setPreference(AudioPlayer.TRACK_KEY, this.currentTrack_);
225   }
227   this.scrollToCurrent_(false);
229   var currentTrack = this.currentTrack_;
230   var entry = this.entries_[currentTrack];
231   this.fetchMetadata_(entry, function(metadata) {
232     if (this.currentTrack_ != currentTrack)
233       return;
234     this.audioControls_.load(entry, opt_restoreState);
236     // Resolve real filesystem path of the current audio file.
237     this.selectedEntry_ = entry;
238   }.bind(this));
242  * @param {Entry} entry Track file entry.
243  * @param {function(object)} callback Callback.
244  * @private
245  */
246 AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) {
247   this.metadataCache_.get(entry, 'thumbnail|media|streaming',
248       function(generation, metadata) {
249         // Do nothing if another load happened since the metadata request.
250         if (this.playlistGeneration_ == generation)
251           callback(metadata);
252       }.bind(this, this.playlistGeneration_));
256  * @param {number} oldTrack Old track number.
257  * @param {number} newTrack New track number.
258  * @private
259  */
260 AudioPlayer.prototype.changeSelectionInList_ = function(oldTrack, newTrack) {
261   this.trackListItems_[newTrack].getBox().classList.add('selected');
263   if (oldTrack >= 0) {
264     this.trackListItems_[oldTrack].getBox().classList.remove('selected');
265   }
269  * @param {number} oldTrack Old track number.
270  * @param {number} newTrack New track number.
271  * @private
272  */
273 AudioPlayer.prototype.changeSelectionInStack_ = function(oldTrack, newTrack) {
274   var newBox = this.trackStackItems_[newTrack].getBox();
275   newBox.classList.add('selected');  // Put on top immediately.
276   newBox.classList.add('visible');  // Start fading in.
278   if (oldTrack >= 0) {
279     var oldBox = this.trackStackItems_[oldTrack].getBox();
280     oldBox.classList.remove('selected'); // Put under immediately.
281     setTimeout(function() {
282       if (!oldBox.classList.contains('selected')) {
283         // This will start fading out which is not really necessary because
284         // oldBox is already completely obscured by newBox.
285         oldBox.classList.remove('visible');
286       }
287     }, 300);
288   }
292  * Scrolls the current track into the viewport.
294  * @param {boolean} keepAtBottom If true, make the selected track the last
295  *   of the visible (if possible). If false, perform minimal scrolling.
296  * @private
297  */
298 AudioPlayer.prototype.scrollToCurrent_ = function(keepAtBottom) {
299   var box = this.trackListItems_[this.currentTrack_].getBox();
300   this.trackList_.scrollTop = Math.max(
301       keepAtBottom ? 0 : Math.min(box.offsetTop, this.trackList_.scrollTop),
302       box.offsetTop + box.offsetHeight - this.trackList_.clientHeight);
306  * @return {boolean} True if the player is be displayed in compact mode.
307  * @private
308  */
309 AudioPlayer.prototype.isCompact_ = function() {
310   return this.container_.classList.contains('collapsed') ||
311          this.container_.classList.contains('single-track');
315  * Go to the previous or the next track.
316  * @param {boolean} forward True if next, false if previous.
317  * @param {boolean=} opt_onlyIfValid True if invalid tracks should be selected.
318  * @private
319  */
320 AudioPlayer.prototype.advance_ = function(forward, opt_onlyIfValid) {
321   this.cancelAutoAdvance_();
323   var newTrack = this.currentTrack_ + (forward ? 1 : -1);
324   if (newTrack < 0) newTrack = this.entries_.length - 1;
325   if (newTrack == this.entries_.length) newTrack = 0;
326   if (opt_onlyIfValid && this.invalidTracks_[newTrack])
327     return;
328   this.select_(newTrack);
332  * Media error handler.
333  * @private
334  */
335 AudioPlayer.prototype.onError_ = function() {
336   var track = this.currentTrack_;
338   this.invalidTracks_[track] = true;
340   this.fetchMetadata_(
341       this.entries_[track],
342       function(metadata) {
343         var error = (!navigator.onLine && metadata.streaming) ?
344             this.offlineString_ : this.errorString_;
345         this.displayMetadata_(track, metadata, error);
346         this.scheduleAutoAdvance_();
347       }.bind(this));
351  * Schedule automatic advance to the next track after a timeout.
352  * @private
353  */
354 AudioPlayer.prototype.scheduleAutoAdvance_ = function() {
355   this.cancelAutoAdvance_();
356   this.autoAdvanceTimer_ = setTimeout(
357       function() {
358         this.autoAdvanceTimer_ = null;
359         // We are advancing only if the next track is not known to be invalid.
360         // This prevents an endless auto-advancing in the case when all tracks
361         // are invalid (we will only visit each track once).
362         this.advance_(true /* forward */, true /* only if valid */);
363       }.bind(this),
364       3000);
368  * Cancel the scheduled auto advance.
369  * @private
370  */
371 AudioPlayer.prototype.cancelAutoAdvance_ = function() {
372   if (this.autoAdvanceTimer_) {
373     clearTimeout(this.autoAdvanceTimer_);
374     this.autoAdvanceTimer_ = null;
375   }
379  * Expand/collapse button click handler. Toggles the mode and updates the
380  * height of the window.
382  * @private
383  */
384 AudioPlayer.prototype.onExpandCollapse_ = function() {
385   if (!this.isCompact_()) {
386     this.setExpanded_(false);
387     this.lastExpandedHeight_ = window.innerHeight;
388   } else {
389     this.setExpanded_(true);
390   }
391   this.syncHeight_();
395  * Toggles the current expand mode.
397  * @param {boolean} on True if on, false otherwise.
398  * @private
399  */
400 AudioPlayer.prototype.setExpanded_ = function(on) {
401   if (on) {
402     this.container_.classList.remove('collapsed');
403     this.scrollToCurrent_(true);
404   } else {
405     this.container_.classList.add('collapsed');
406   }
410  * Toggles the expanded mode when resizing.
412  * @param {Event} event Resize event.
413  * @private
414  */
415 AudioPlayer.prototype.onResize_ = function(event) {
416   if (this.isCompact_() &&
417       window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
418     this.setExpanded_(true);
419   } else if (!this.isCompact_() &&
420              window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
421     this.setExpanded_(false);
422   }
425 /* Keep the below constants in sync with the CSS. */
428  * Window header size in pixels.
429  * @type {number}
430  * @const
431  */
432 AudioPlayer.HEADER_HEIGHT = 28;
435  * Track height in pixels.
436  * @type {number}
437  * @const
438  */
439 AudioPlayer.TRACK_HEIGHT = 58;
442  * Controls bar height in pixels.
443  * @type {number}
444  * @const
445  */
446 AudioPlayer.CONTROLS_HEIGHT = 35;
449  * Default number of items in the expanded mode.
450  * @type {number}
451  * @const
452  */
453 AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
456  * Minimum size of the window in the expanded mode in pixels.
457  * @type {number}
458  * @const
459  */
460 AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
461                                        AudioPlayer.TRACK_HEIGHT * 2;
464  * Set the correct player window height.
465  * @private
466  */
467 AudioPlayer.prototype.syncHeight_ = function() {
468   var targetHeight;
470   if (!this.isCompact_()) {
471     // Expanded.
472     if (this.lastExpandedHeight_) {
473       targetHeight = this.lastExpandedHeight_;
474     } else {
475       var expandedListHeight =
476         Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
477                                     AudioPlayer.TRACK_HEIGHT;
478       targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
479     }
480   } else {
481     // Not expaned.
482     targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
483   }
485   window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
489  * Create a TrackInfo object encapsulating the information about one track.
491  * @param {HTMLElement} container Container element.
492  * @param {Entry} entry Track entry.
493  * @param {function} onClick Click handler.
494  * @constructor
495  */
496 AudioPlayer.TrackInfo = function(container, entry, onClick) {
497   this.entry_ = entry;
499   var doc = container.ownerDocument;
501   this.box_ = doc.createElement('div');
502   this.box_.className = 'track';
503   this.box_.addEventListener('click', onClick);
504   container.appendChild(this.box_);
506   this.art_ = doc.createElement('div');
507   this.art_.className = 'art blank';
508   this.box_.appendChild(this.art_);
510   this.img_ = doc.createElement('img');
511   this.art_.appendChild(this.img_);
513   this.data_ = doc.createElement('div');
514   this.data_.className = 'data';
515   this.box_.appendChild(this.data_);
517   this.title_ = doc.createElement('div');
518   this.title_.className = 'data-title';
519   this.data_.appendChild(this.title_);
521   this.artist_ = doc.createElement('div');
522   this.artist_.className = 'data-artist';
523   this.data_.appendChild(this.artist_);
527  * @return {HTMLDivElement} The wrapper element for the track.
528  */
529 AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ };
532  * @return {string} Default track title (file name extracted from the entry).
533  */
534 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
535   // TODO(mtomasz): Reuse ImageUtil.getDisplayNameFromName().
536   var name = this.entry_.name;
537   var dotIndex = name.lastIndexOf('.');
538   var title = dotIndex >= 0 ? name.substr(0, dotIndex) : name;
539   return title;
543  * TODO(kaznacheev): Localize.
544  */
545 AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
548  * @return {string} 'Unknown artist' string.
549  */
550 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
551   return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
555  * @param {Object} metadata The metadata object.
556  * @param {HTMLElement} container The container for the tracks.
557  * @param {string} error Error string.
558  */
559 AudioPlayer.TrackInfo.prototype.setMetadata = function(
560     metadata, container, error) {
561   if (error) {
562     this.art_.classList.add('blank');
563     this.art_.classList.add('error');
564     container.classList.remove('noart');
565   } else if (metadata.thumbnail && metadata.thumbnail.url) {
566     this.img_.onload = function() {
567       // Only display the image if the thumbnail loaded successfully.
568       this.art_.classList.remove('blank');
569       container.classList.remove('noart');
570     }.bind(this);
571     this.img_.src = metadata.thumbnail.url;
572   }
573   this.title_.textContent = (metadata.media && metadata.media.title) ||
574       this.getDefaultTitle();
575   this.artist_.textContent = error ||
576       (metadata.media && metadata.media.artist) || this.getDefaultArtist();
580  * Audio controls specific for the Audio Player.
582  * @param {HTMLElement} container Parent container.
583  * @param {function(boolean)} advanceTrack Parameter: true=forward.
584  * @param {function} onError Error handler.
585  * @constructor
586  */
587 function FullWindowAudioControls(container, advanceTrack, onError) {
588   AudioControls.apply(this, arguments);
590   document.addEventListener('keydown', function(e) {
591     if (e.keyIdentifier == 'U+0020') {
592       this.togglePlayState();
593       e.preventDefault();
594     }
595   }.bind(this));
598 FullWindowAudioControls.prototype = { __proto__: AudioControls.prototype };
601  * Enable play state restore from the location hash.
602  * @param {FileEntry} entry Source Entry.
603  * @param {boolean} restore True if need to restore the play state.
604  */
605 FullWindowAudioControls.prototype.load = function(entry, restore) {
606   this.media_.src = entry.toURL();
607   this.media_.load();
608   this.restoreWhenLoaded_ = restore;
612  * Save the current state so that it survives page/app reload.
613  */
614 FullWindowAudioControls.prototype.onPlayStateChanged = function() {
615   this.encodeState();
619  * Restore the state after page/app reload.
620  */
621 FullWindowAudioControls.prototype.restorePlayState = function() {
622   if (this.restoreWhenLoaded_) {
623     this.restoreWhenLoaded_ = false;  // This should only work once.
624     if (this.decodeState())
625       return;
626   }
627   this.play();