Fix search results being clipped in app list.
[chromium-blink-merge.git] / ui / file_manager / audio_player / js / audio_player.js
bloba00929193a904480abad9b7a9b6579a9ed8576f7
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' && change.type == 'update') {
28         this.onModelExpandedChanged(change.oldValue, change.object.expanded);
29         break;
30       }
31     }
32   }.bind(this));
34   this.entries_ = [];
35   this.currentTrackIndex_ = -1;
36   this.playlistGeneration_ = 0;
38   /**
39    * Whether if the playlist is expanded or not. This value is changed by
40    * this.syncExpanded().
41    * True: expanded, false: collapsed, null: unset.
42    *
43    * @type {?boolean}
44    * @private
45    */
46   this.isExpanded_ = null;  // Initial value is null. It'll be set in load().
48   this.player_ =
49     /** @type {AudioPlayerElement} */ (document.querySelector('audio-player'));
50   // TODO(yoshiki): Move tracks into the model.
51   this.player_.tracks = [];
52   this.player_.model = this.model_;
54   // Run asynchronously after an event of model change is delivered.
55   setTimeout(function() {
56     this.errorString_ = '';
57     this.offlineString_ = '';
58     chrome.fileManagerPrivate.getStrings(function(strings) {
59       container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE'];
60       this.errorString_ = strings['AUDIO_ERROR'];
61       this.offlineString_ = strings['AUDIO_OFFLINE'];
62       AudioPlayer.TrackInfo.DEFAULT_ARTIST =
63           strings['AUDIO_PLAYER_DEFAULT_ARTIST'];
64     }.bind(this));
66     this.volumeManager_.addEventListener('externally-unmounted',
67         this.onExternallyUnmounted_.bind(this));
69     window.addEventListener('resize', this.onResize_.bind(this));
71     // Show the window after DOM is processed.
72     var currentWindow = chrome.app.window.current();
73     if (currentWindow)
74       setTimeout(currentWindow.show.bind(currentWindow), 0);
75   }.bind(this), 0);
78 /**
79  * Initial load method (static).
80  */
81 AudioPlayer.load = function() {
82   document.ondragstart = function(e) { e.preventDefault(); };
84   AudioPlayer.instance =
85       new AudioPlayer(document.querySelector('.audio-player'));
87   reload();
90 /**
91  * Unloads the player.
92  */
93 function unload() {
94   if (AudioPlayer.instance)
95     AudioPlayer.instance.onUnload();
98 /**
99  * Reloads the player.
100  */
101 function reload() {
102   AudioPlayer.instance.load(window.appState);
106  * Loads a new playlist.
107  * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
108  */
109 AudioPlayer.prototype.load = function(playlist) {
110   this.playlistGeneration_++;
111   this.currentTrackIndex_ = -1;
113   // Save the app state, in case of restart. Make a copy of the object, so the
114   // playlist member is not changed after entries are resolved.
115   window.appState = JSON.parse(JSON.stringify(playlist));  // cloning
116   util.saveAppState();
118   this.isExpanded_ = this.model_.expanded;
120   // Resolving entries has to be done after the volume manager is initialized.
121   this.volumeManager_.ensureInitialized(function() {
122     util.URLsToEntries(playlist.items, function(entries) {
123       this.entries_ = entries;
125       var position = playlist.position || 0;
126       var time = playlist.time || 0;
128       if (this.entries_.length == 0)
129         return;
131       var newTracks = [];
132       var currentTracks = this.player_.tracks;
133       var unchanged = (currentTracks.length === this.entries_.length);
135       for (var i = 0; i != this.entries_.length; i++) {
136         var entry = this.entries_[i];
137         var onClick = this.select_.bind(this, i);
138         newTracks.push(new AudioPlayer.TrackInfo(entry, onClick));
140         if (unchanged && entry.toURL() !== currentTracks[i].url)
141           unchanged = false;
142       }
144       if (!unchanged)
145         this.player_.tracks = newTracks;
147       // Run asynchronously, to makes it sure that the handler of the track list
148       // is called, before the handler of the track index.
149       setTimeout(function() {
150         this.select_(position, !!time);
152         // Load the selected track metadata first, then load the rest.
153         this.loadMetadata_(position);
154         for (i = 0; i != this.entries_.length; i++) {
155           if (i != position)
156             this.loadMetadata_(i);
157         }
158       }.bind(this), 0);
159     }.bind(this));
160   }.bind(this));
164  * Loads metadata for a track.
165  * @param {number} track Track number.
166  * @private
167  */
168 AudioPlayer.prototype.loadMetadata_ = function(track) {
169   this.fetchMetadata_(
170       this.entries_[track], this.displayMetadata_.bind(this, track));
174  * Displays track's metadata.
175  * @param {number} track Track number.
176  * @param {Object} metadata Metadata object.
177  * @param {string=} opt_error Error message.
178  * @private
179  */
180 AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) {
181   this.player_.tracks[track].setMetadata(metadata, opt_error);
185  * Closes audio player when a volume containing the selected item is unmounted.
186  * @param {Event} event The unmount event.
187  * @private
188  */
189 AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
190   if (!this.selectedEntry_)
191     return;
193   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
194       event.volumeInfo)
195     window.close();
199  * Called on window is being unloaded.
200  */
201 AudioPlayer.prototype.onUnload = function() {
202   if (this.player_)
203     this.player_.onPageUnload();
205   if (this.volumeManager_)
206     this.volumeManager_.dispose();
210  * Selects a new track to play.
211  * @param {number} newTrack New track number.
212  * @param {number} time New playback position (in second).
213  * @private
214  */
215 AudioPlayer.prototype.select_ = function(newTrack, time) {
216   if (this.currentTrackIndex_ == newTrack) return;
218   this.currentTrackIndex_ = newTrack;
219   this.player_.currentTrackIndex = this.currentTrackIndex_;
220   this.player_.audioController.time = time;
222   // Run asynchronously after an event of current track change is delivered.
223   setTimeout(function() {
224     if (!window.appReopen)
225       this.player_.audioElement.play();
227     window.appState.position = this.currentTrackIndex_;
228     window.appState.time = 0;
229     util.saveAppState();
231     var entry = this.entries_[this.currentTrackIndex_];
233     this.fetchMetadata_(entry, function(metadata) {
234       if (this.currentTrackIndex_ != newTrack)
235         return;
237       this.selectedEntry_ = entry;
238     }.bind(this));
239   }.bind(this), 0);
243  * @param {FileEntry} entry Track file entry.
244  * @param {function(Object)} callback Callback.
245  * @private
246  */
247 AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) {
248   this.metadataModel_.get(
249       [entry], ['mediaTitle', 'mediaArtist', 'present']).then(
250       function(generation, metadata) {
251         // Do nothing if another load happened since the metadata request.
252         if (this.playlistGeneration_ == generation)
253           callback(metadata[0]);
254       }.bind(this, this.playlistGeneration_));
258  * Media error handler.
259  * @private
260  */
261 AudioPlayer.prototype.onError_ = function() {
262   var track = this.currentTrackIndex_;
264   this.invalidTracks_[track] = true;
266   this.fetchMetadata_(
267       this.entries_[track],
268       function(metadata) {
269         var error = (!navigator.onLine && !metadata.present) ?
270             this.offlineString_ : this.errorString_;
271         this.displayMetadata_(track, metadata, error);
272         this.scheduleAutoAdvance_();
273       }.bind(this));
277  * Toggles the expanded mode when resizing.
279  * @param {Event} event Resize event.
280  * @private
281  */
282 AudioPlayer.prototype.onResize_ = function(event) {
283   if (!this.isExpanded_ &&
284       window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
285     this.isExpanded_ = true;
286     this.model_.expanded = true;
287   } else if (this.isExpanded_ &&
288              window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
289     this.isExpanded_ = false;
290     this.model_.expanded = false;
291   }
294 /* Keep the below constants in sync with the CSS. */
297  * Window header size in pixels.
298  * @type {number}
299  * @const
300  */
301 AudioPlayer.HEADER_HEIGHT = 33;  // 32px + border 1px
304  * Track height in pixels.
305  * @type {number}
306  * @const
307  */
308 AudioPlayer.TRACK_HEIGHT = 44;
311  * Controls bar height in pixels.
312  * @type {number}
313  * @const
314  */
315 AudioPlayer.CONTROLS_HEIGHT = 73;  // 72px + border 1px
318  * Default number of items in the expanded mode.
319  * @type {number}
320  * @const
321  */
322 AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
325  * Minimum size of the window in the expanded mode in pixels.
326  * @type {number}
327  * @const
328  */
329 AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
330                                        AudioPlayer.TRACK_HEIGHT * 2;
333  * Invoked when the 'expanded' property in the model is changed.
334  * @param {boolean} oldValue Old value.
335  * @param {boolean} newValue New value.
336  */
337 AudioPlayer.prototype.onModelExpandedChanged = function(oldValue, newValue) {
338   if (this.isExpanded_ !== null &&
339       this.isExpanded_ === newValue)
340     return;
342   if (this.isExpanded_ && !newValue)
343     this.lastExpandedHeight_ = window.innerHeight;
345   if (this.isExpanded_ !== newValue) {
346     this.isExpanded_ = newValue;
347     this.syncHeight_();
349     // Saves new state.
350     window.appState.expanded = newValue;
351     util.saveAppState();
352   }
356  * @private
357  */
358 AudioPlayer.prototype.syncHeight_ = function() {
359   var targetHeight;
361   if (this.model_.expanded) {
362     // Expanded.
363     if (!this.lastExpandedHeight_ ||
364         this.lastExpandedHeight_ < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
365       var expandedListHeight =
366           Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
367               AudioPlayer.TRACK_HEIGHT;
368       targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
369       this.lastExpandedHeight_ = targetHeight;
370     } else {
371       targetHeight = this.lastExpandedHeight_;
372     }
373   } else {
374     // Not expanded.
375     targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
376   }
378   window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
382  * Create a TrackInfo object encapsulating the information about one track.
384  * @param {FileEntry} entry FileEntry to be retrieved the track info from.
385  * @param {function(MouseEvent)} onClick Click handler.
386  * @constructor
387  */
388 AudioPlayer.TrackInfo = function(entry, onClick) {
389   this.url = entry.toURL();
390   this.title = this.getDefaultTitle();
391   this.artist = this.getDefaultArtist();
393   // TODO(yoshiki): implement artwork.
394   this.artwork = null;
395   this.active = false;
399  * @return {string} Default track title (file name extracted from the url).
400  */
401 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
402   var title = this.url.split('/').pop();
403   var dotIndex = title.lastIndexOf('.');
404   if (dotIndex >= 0) title = title.substr(0, dotIndex);
405   title = decodeURIComponent(title);
406   return title;
410  * TODO(kaznacheev): Localize.
411  */
412 AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
415  * @return {string} 'Unknown artist' string.
416  */
417 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
418   return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
422  * @param {Object} metadata The metadata object.
423  * @param {string} error Error string.
424  */
425 AudioPlayer.TrackInfo.prototype.setMetadata = function(
426     metadata, error) {
427   // TODO(yoshiki): Handle error in better way.
428   // TODO(yoshiki): implement artwork (metadata.thumbnail)
429   this.title = metadata.mediaTitle || this.getDefaultTitle();
430   this.artist = error || metadata.mediaArtist || this.getDefaultArtist();
433 // Starts loading the audio player.
434 window.addEventListener('polymer-ready', function(e) {
435   AudioPlayer.load();