Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / file_manager / audio_player / js / audio_player.js
blob216bb44802d251bb32ca67aba672303abb9acdeb
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 /**
6  * Overrided metadata worker's path.
7  * @type {string}
8  */
9 ContentMetadataProvider.WORKER_SCRIPT = '/js/metadata_worker.js';
11 /**
12  * @param {Element} container Container element.
13  * @constructor
14  */
15 function AudioPlayer(container) {
16   this.container_ = container;
17   this.volumeManager_ = new VolumeManagerWrapper(
18       VolumeManagerWrapper.NonNativeVolumeStatus.ENABLED);
19   this.metadataModel_ = MetadataModel.create(this.volumeManager_);
20   this.selectedEntry_ = null;
21   this.invalidTracks_ = {};
23   this.model_ = new AudioPlayerModel();
24   Object.observe(this.model_, function(changes) {
25     for (var i = 0; i < changes.length; i++) {
26       var change = changes[i];
27       if (change.name == 'expanded' &&
28           (change.type == 'add' || change.type == 'update')) {
29         this.onModelExpandedChanged(change.oldValue, change.object.expanded);
30         break;
31       }
32     }
33   }.bind(this));
35   this.entries_ = [];
36   this.currentTrackIndex_ = -1;
37   this.playlistGeneration_ = 0;
39   /**
40    * Whether if the playlist is expanded or not. This value is changed by
41    * this.syncExpanded().
42    * True: expanded, false: collapsed, null: unset.
43    *
44    * @type {?boolean}
45    * @private
46    */
47   this.isExpanded_ = null;  // Initial value is null. It'll be set in load().
49   this.player_ =
50     /** @type {AudioPlayerElement} */ (document.querySelector('audio-player'));
51   // TODO(yoshiki): Move tracks into the model.
52   this.player_.tracks = [];
53   this.model_.initialize(function() {
54     this.player_.model = this.model_;
55   }.bind(this));
57   // Run asynchronously after an event of model change is delivered.
58   setTimeout(function() {
59     this.errorString_ = '';
60     this.offlineString_ = '';
61     chrome.fileManagerPrivate.getStrings(function(strings) {
62       container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE'];
63       this.errorString_ = strings['AUDIO_ERROR'];
64       this.offlineString_ = strings['AUDIO_OFFLINE'];
65       AudioPlayer.TrackInfo.DEFAULT_ARTIST =
66           strings['AUDIO_PLAYER_DEFAULT_ARTIST'];
67     }.bind(this));
69     this.volumeManager_.addEventListener('externally-unmounted',
70         this.onExternallyUnmounted_.bind(this));
72     window.addEventListener('resize', this.onResize_.bind(this));
73     document.addEventListener('keydown', this.onKeyDown_.bind(this));
75     // Show the window after DOM is processed.
76     var currentWindow = chrome.app.window.current();
77     if (currentWindow)
78       setTimeout(currentWindow.show.bind(currentWindow), 0);
79   }.bind(this), 0);
82 /**
83  * Initial load method (static).
84  */
85 AudioPlayer.load = function() {
86   document.ondragstart = function(e) { e.preventDefault(); };
88   AudioPlayer.instance =
89       new AudioPlayer(document.querySelector('.audio-player'));
91   reload();
94 /**
95  * Unloads the player.
96  */
97 function unload() {
98   if (AudioPlayer.instance)
99     AudioPlayer.instance.onUnload();
103  * Reloads the player.
104  */
105 function reload() {
106   AudioPlayer.instance.load(/** @type {Playlist} */ (window.appState));
110  * Loads a new playlist.
111  * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
112  */
113 AudioPlayer.prototype.load = function(playlist) {
114   this.playlistGeneration_++;
115   this.currentTrackIndex_ = -1;
117   // Save the app state, in case of restart. Make a copy of the object, so the
118   // playlist member is not changed after entries are resolved.
119   window.appState = /** @type {Playlist} */ (
120       JSON.parse(JSON.stringify(playlist)));  // cloning
121   util.saveAppState();
123   this.isExpanded_ = this.player_.expanded;
125   // Resolving entries has to be done after the volume manager is initialized.
126   this.volumeManager_.ensureInitialized(function() {
127     util.URLsToEntries(playlist.items || [], function(entries) {
128       this.entries_ = entries;
130       var position = playlist.position || 0;
131       var time = playlist.time || 0;
133       if (this.entries_.length == 0)
134         return;
136       var newTracks = [];
137       var currentTracks = this.player_.tracks;
138       var unchanged = (currentTracks.length === this.entries_.length);
140       for (var i = 0; i != this.entries_.length; i++) {
141         var entry = this.entries_[i];
142         newTracks.push(new AudioPlayer.TrackInfo(entry));
144         if (unchanged && entry.toURL() !== currentTracks[i].url)
145           unchanged = false;
146       }
148       if (!unchanged)
149         this.player_.tracks = newTracks;
151       // Run asynchronously, to makes it sure that the handler of the track list
152       // is called, before the handler of the track index.
153       setTimeout(function() {
154         this.select_(position);
156         // Load the selected track metadata first, then load the rest.
157         this.loadMetadata_(position);
158         for (i = 0; i != this.entries_.length; i++) {
159           if (i != position)
160             this.loadMetadata_(i);
161         }
162       }.bind(this), 0);
163     }.bind(this));
164   }.bind(this));
168  * Loads metadata for a track.
169  * @param {number} track Track number.
170  * @private
171  */
172 AudioPlayer.prototype.loadMetadata_ = function(track) {
173   this.fetchMetadata_(
174       this.entries_[track], this.displayMetadata_.bind(this, track));
178  * Displays track's metadata.
179  * @param {number} track Track number.
180  * @param {Object} metadata Metadata object.
181  * @param {string=} opt_error Error message.
182  * @private
183  */
184 AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) {
185   this.player_.tracks[track].setMetadata(metadata, opt_error);
189  * Closes audio player when a volume containing the selected item is unmounted.
190  * @param {Event} event The unmount event.
191  * @private
192  */
193 AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
194   if (!this.selectedEntry_)
195     return;
197   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
198       event.volumeInfo)
199     window.close();
203  * Called on window is being unloaded.
204  */
205 AudioPlayer.prototype.onUnload = function() {
206   if (this.player_)
207     this.player_.onPageUnload();
209   if (this.volumeManager_)
210     this.volumeManager_.dispose();
214  * Selects a new track to play.
215  * @param {number} newTrack New track number.
216  * @private
217  */
218 AudioPlayer.prototype.select_ = function(newTrack) {
219   if (this.currentTrackIndex_ == newTrack) return;
221   this.currentTrackIndex_ = newTrack;
222   this.player_.currentTrackIndex = this.currentTrackIndex_;
223   this.player_.time = 0;
225   // Run asynchronously after an event of current track change is delivered.
226   setTimeout(function() {
227     if (!window.appReopen)
228       this.player_.$.audio.play();
230     window.appState.position = this.currentTrackIndex_;
231     window.appState.time = 0;
232     util.saveAppState();
234     var entry = this.entries_[this.currentTrackIndex_];
236     this.fetchMetadata_(entry, function(metadata) {
237       if (this.currentTrackIndex_ != newTrack)
238         return;
240       this.selectedEntry_ = entry;
241     }.bind(this));
242   }.bind(this), 0);
246  * @param {FileEntry} entry Track file entry.
247  * @param {function(Object)} callback Callback.
248  * @private
249  */
250 AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) {
251   this.metadataModel_.get(
252       [entry], ['mediaTitle', 'mediaArtist', 'present']).then(
253       function(generation, metadata) {
254         // Do nothing if another load happened since the metadata request.
255         if (this.playlistGeneration_ == generation)
256           callback(metadata[0]);
257       }.bind(this, this.playlistGeneration_));
261  * Media error handler.
262  * @private
263  */
264 AudioPlayer.prototype.onError_ = function() {
265   var track = this.currentTrackIndex_;
267   this.invalidTracks_[track] = true;
269   this.fetchMetadata_(
270       this.entries_[track],
271       function(metadata) {
272         var error = (!navigator.onLine && !metadata.present) ?
273             this.offlineString_ : this.errorString_;
274         this.displayMetadata_(track, metadata, error);
275         this.player_.onAudioError();
276       }.bind(this));
280  * Toggles the expanded mode when resizing.
282  * @param {Event} event Resize event.
283  * @private
284  */
285 AudioPlayer.prototype.onResize_ = function(event) {
286   if (!this.isExpanded_ &&
287       window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
288     this.isExpanded_ = true;
289     this.player_.expanded = true;
290   } else if (this.isExpanded_ &&
291              window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
292     this.isExpanded_ = false;
293     this.player_.expanded = false;
294   }
298  * Handles keydown event to open inspector with shortcut keys.
300  * @param {Event} event KeyDown event.
301  * @private
302  */
303 AudioPlayer.prototype.onKeyDown_ = function(event) {
304   switch (util.getKeyModifiers(event) + event.keyIdentifier) {
305     // Handle debug shortcut keys.
306     case 'Ctrl-Shift-U+0049': // Ctrl+Shift+I
307       chrome.fileManagerPrivate.openInspector('normal');
308       break;
309     case 'Ctrl-Shift-U+004A': // Ctrl+Shift+J
310       chrome.fileManagerPrivate.openInspector('console');
311       break;
312     case 'Ctrl-Shift-U+0043': // Ctrl+Shift+C
313       chrome.fileManagerPrivate.openInspector('element');
314       break;
315     case 'Ctrl-Shift-U+0042': // Ctrl+Shift+B
316       chrome.fileManagerPrivate.openInspector('background');
317       break;
318   }
321 /* Keep the below constants in sync with the CSS. */
324  * Window header size in pixels.
325  * @type {number}
326  * @const
327  */
328 AudioPlayer.HEADER_HEIGHT = 33;  // 32px + border 1px
331  * Track height in pixels.
332  * @type {number}
333  * @const
334  */
335 AudioPlayer.TRACK_HEIGHT = 44;
338  * Controls bar height in pixels.
339  * @type {number}
340  * @const
341  */
342 AudioPlayer.CONTROLS_HEIGHT = 73;  // 72px + border 1px
345  * Default number of items in the expanded mode.
346  * @type {number}
347  * @const
348  */
349 AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
352  * Minimum size of the window in the expanded mode in pixels.
353  * @type {number}
354  * @const
355  */
356 AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
357                                        AudioPlayer.TRACK_HEIGHT * 2;
360  * Invoked when the 'expanded' property in the model is changed.
361  * @param {boolean} oldValue Old value.
362  * @param {boolean} newValue New value.
363  */
364 AudioPlayer.prototype.onModelExpandedChanged = function(oldValue, newValue) {
365   if (this.isExpanded_ !== null &&
366       this.isExpanded_ === newValue)
367     return;
369   if (this.isExpanded_ && !newValue)
370     this.lastExpandedHeight_ = window.innerHeight;
372   if (this.isExpanded_ !== newValue) {
373     this.isExpanded_ = newValue;
374     this.syncHeight_();
376     // Saves new state.
377     window.appState.expanded = newValue;
378     util.saveAppState();
379   }
383  * @private
384  */
385 AudioPlayer.prototype.syncHeight_ = function() {
386   var targetHeight;
388   if (this.player_.expanded) {
389     // Expanded.
390     if (!this.lastExpandedHeight_ ||
391         this.lastExpandedHeight_ < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
392       var expandedListHeight =
393           Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
394               AudioPlayer.TRACK_HEIGHT;
395       targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
396       this.lastExpandedHeight_ = targetHeight;
397     } else {
398       targetHeight = this.lastExpandedHeight_;
399     }
400   } else {
401     // Not expanded.
402     targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
403   }
405   window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
409  * Create a TrackInfo object encapsulating the information about one track.
411  * @param {FileEntry} entry FileEntry to be retrieved the track info from.
412  * @constructor
413  */
414 AudioPlayer.TrackInfo = function(entry) {
415   this.url = entry.toURL();
416   this.title = this.getDefaultTitle();
417   this.artist = this.getDefaultArtist();
419   // TODO(yoshiki): implement artwork.
420   this.artwork = null;
421   this.active = false;
425  * @return {string} Default track title (file name extracted from the url).
426  */
427 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
428   var title = this.url.split('/').pop();
429   var dotIndex = title.lastIndexOf('.');
430   if (dotIndex >= 0) title = title.substr(0, dotIndex);
431   title = decodeURIComponent(title);
432   return title;
436  * TODO(kaznacheev): Localize.
437  */
438 AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
441  * @return {string} 'Unknown artist' string.
442  */
443 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
444   return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
448  * @param {Object} metadata The metadata object.
449  * @param {string} error Error string.
450  */
451 AudioPlayer.TrackInfo.prototype.setMetadata = function(
452     metadata, error) {
453   // TODO(yoshiki): Handle error in better way.
454   // TODO(yoshiki): implement artwork (metadata.thumbnail)
455   this.title = metadata.mediaTitle || this.getDefaultTitle();
456   this.artist = error || metadata.mediaArtist || this.getDefaultArtist();
459 // Starts loading the audio player.
460 window.addEventListener('DOMContentLoaded', function(e) {
461   AudioPlayer.load();