Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / file_manager / audio_player / js / 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  * @param {HTMLElement} container Container element.
9  * @constructor
10  */
11 function AudioPlayer(container) {
12   this.container_ = container;
13   this.volumeManager_ = new VolumeManagerWrapper(
14       VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
15   this.metadataCache_ = MetadataCache.createFull(this.volumeManager_);
16   this.selectedEntry_ = null;
17
18   this.model_ = new AudioPlayerModel();
19   var observer = new PathObserver(this.model_, 'expanded');
20   observer.open(function(newValue, oldValue) {
21     // Inverse arguments intentionally to match the Polymer way.
22     this.onModelExpandedChanged(oldValue, newValue);
23   }.bind(this));
24
25   this.entries_ = [];
26   this.currentTrackIndex_ = -1;
27   this.playlistGeneration_ = 0;
28
29   /**
30    * Whether if the playlist is expanded or not. This value is changed by
31    * this.syncExpanded().
32    * True: expanded, false: collapsed, null: unset.
33    *
34    * @type {?boolean}
35    * @private
36    */
37   this.isExpanded_ = null;  // Initial value is null. It'll be set in load().
38
39   this.player_ = document.querySelector('audio-player');
40   // TODO(yoshiki): Move tracks into the model.
41   this.player_.tracks = [];
42   this.player_.model = this.model_;
43   Platform.performMicrotaskCheckpoint();
44
45   this.errorString_ = '';
46   this.offlineString_ = '';
47   chrome.fileBrowserPrivate.getStrings(function(strings) {
48     container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE'];
49     this.errorString_ = strings['AUDIO_ERROR'];
50     this.offlineString_ = strings['AUDIO_OFFLINE'];
51     AudioPlayer.TrackInfo.DEFAULT_ARTIST =
52         strings['AUDIO_PLAYER_DEFAULT_ARTIST'];
53   }.bind(this));
54
55   this.volumeManager_.addEventListener('externally-unmounted',
56       this.onExternallyUnmounted_.bind(this));
57
58   window.addEventListener('resize', this.onResize_.bind(this));
59
60   // Show the window after DOM is processed.
61   var currentWindow = chrome.app.window.current();
62   if (currentWindow)
63     setTimeout(currentWindow.show.bind(currentWindow), 0);
64 }
65
66 /**
67  * Initial load method (static).
68  */
69 AudioPlayer.load = function() {
70   document.ondragstart = function(e) { e.preventDefault() };
71
72   AudioPlayer.instance =
73       new AudioPlayer(document.querySelector('.audio-player'));
74
75   reload();
76 };
77
78 /**
79  * Unloads the player.
80  */
81 function unload() {
82   if (AudioPlayer.instance)
83     AudioPlayer.instance.onUnload();
84 }
85
86 /**
87  * Reloads the player.
88  */
89 function reload() {
90   AudioPlayer.instance.load(window.appState);
91 }
92
93 /**
94  * Loads a new playlist.
95  * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
96  */
97 AudioPlayer.prototype.load = function(playlist) {
98   this.playlistGeneration_++;
99   this.currentTrackIndex_ = -1;
100
101   // Save the app state, in case of restart. Make a copy of the object, so the
102   // playlist member is not changed after entries are resolved.
103   window.appState = JSON.parse(JSON.stringify(playlist));  // cloning
104   util.saveAppState();
105
106   this.isExpanded_ = this.model_.expanded;
107
108   // Resolving entries has to be done after the volume manager is initialized.
109   this.volumeManager_.ensureInitialized(function() {
110     util.URLsToEntries(playlist.items, function(entries) {
111       this.entries_ = entries;
112
113       var position = playlist.position || 0;
114       var time = playlist.time || 0;
115
116       if (this.entries_.length == 0)
117         return;
118
119       var newTracks = [];
120
121       for (var i = 0; i != this.entries_.length; i++) {
122         var entry = this.entries_[i];
123         var onClick = this.select_.bind(this, i, false /* no restore */);
124         newTracks.push(new AudioPlayer.TrackInfo(entry, onClick));
125       }
126
127       this.player_.tracks = newTracks;
128
129       // Makes it sure that the handler of the track list is called, before the
130       // handler of the track index.
131       Platform.performMicrotaskCheckpoint();
132
133       this.select_(position, !!time);
134
135       // Load the selected track metadata first, then load the rest.
136       this.loadMetadata_(position);
137       for (i = 0; i != this.entries_.length; i++) {
138         if (i != position)
139           this.loadMetadata_(i);
140       }
141     }.bind(this));
142   }.bind(this));
143 };
144
145 /**
146  * Loads metadata for a track.
147  * @param {number} track Track number.
148  * @private
149  */
150 AudioPlayer.prototype.loadMetadata_ = function(track) {
151   this.fetchMetadata_(
152       this.entries_[track], this.displayMetadata_.bind(this, track));
153 };
154
155 /**
156  * Displays track's metadata.
157  * @param {number} track Track number.
158  * @param {Object} metadata Metadata object.
159  * @param {string=} opt_error Error message.
160  * @private
161  */
162 AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) {
163   this.player_.tracks[track].setMetadata(metadata, opt_error);
164 };
165
166 /**
167  * Closes audio player when a volume containing the selected item is unmounted.
168  * @param {Event} event The unmount event.
169  * @private
170  */
171 AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
172   if (!this.selectedEntry_)
173     return;
174
175   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
176       event.volumeInfo)
177     window.close();
178 };
179
180 /**
181  * Called on window is being unloaded.
182  */
183 AudioPlayer.prototype.onUnload = function() {
184   if (this.player_)
185     this.player_.onPageUnload();
186
187   if (this.volumeManager_)
188     this.volumeManager_.dispose();
189 };
190
191 /**
192  * Selects a new track to play.
193  * @param {number} newTrack New track number.
194  * @private
195  */
196 AudioPlayer.prototype.select_ = function(newTrack) {
197   if (this.currentTrackIndex_ == newTrack) return;
198
199   this.currentTrackIndex_ = newTrack;
200   this.player_.currentTrackIndex = this.currentTrackIndex_;
201   Platform.performMicrotaskCheckpoint();
202
203   if (!window.appReopen)
204     this.player_.audioElement.play();
205
206   window.appState.position = this.currentTrackIndex_;
207   window.appState.time = 0;
208   util.saveAppState();
209
210   var entry = this.entries_[this.currentTrackIndex_];
211
212   this.fetchMetadata_(entry, function(metadata) {
213     if (this.currentTrackIndex_ != newTrack)
214       return;
215
216     this.selectedEntry_ = entry;
217   }.bind(this));
218 };
219
220 /**
221  * @param {FileEntry} entry Track file entry.
222  * @param {function(object)} callback Callback.
223  * @private
224  */
225 AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) {
226   this.metadataCache_.get(entry, 'thumbnail|media|streaming',
227       function(generation, metadata) {
228         // Do nothing if another load happened since the metadata request.
229         if (this.playlistGeneration_ == generation)
230           callback(metadata);
231       }.bind(this, this.playlistGeneration_));
232 };
233
234 /**
235  * Media error handler.
236  * @private
237  */
238 AudioPlayer.prototype.onError_ = function() {
239   var track = this.currentTrackIndex_;
240
241   this.invalidTracks_[track] = true;
242
243   this.fetchMetadata_(
244       this.entries_[track],
245       function(metadata) {
246         var error = (!navigator.onLine && metadata.streaming) ?
247             this.offlineString_ : this.errorString_;
248         this.displayMetadata_(track, metadata, error);
249         this.scheduleAutoAdvance_();
250       }.bind(this));
251 };
252
253 /**
254  * Toggles the expanded mode when resizing.
255  *
256  * @param {Event} event Resize event.
257  * @private
258  */
259 AudioPlayer.prototype.onResize_ = function(event) {
260   if (!this.isExpanded_ &&
261       window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
262     this.isExpanded_ = true;
263     this.model_.expanded = true;
264   } else if (this.isExpanded_ &&
265              window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
266     this.isExpanded_ = false;
267     this.model_.expanded = false;
268   }
269 };
270
271 /* Keep the below constants in sync with the CSS. */
272
273 /**
274  * Window header size in pixels.
275  * @type {number}
276  * @const
277  */
278 AudioPlayer.HEADER_HEIGHT = 33;  // 32px + border 1px
279
280 /**
281  * Track height in pixels.
282  * @type {number}
283  * @const
284  */
285 AudioPlayer.TRACK_HEIGHT = 44;
286
287 /**
288  * Controls bar height in pixels.
289  * @type {number}
290  * @const
291  */
292 AudioPlayer.CONTROLS_HEIGHT = 73;  // 72px + border 1px
293
294 /**
295  * Default number of items in the expanded mode.
296  * @type {number}
297  * @const
298  */
299 AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
300
301 /**
302  * Minimum size of the window in the expanded mode in pixels.
303  * @type {number}
304  * @const
305  */
306 AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
307                                        AudioPlayer.TRACK_HEIGHT * 2;
308
309 /**
310  * Invoked when the 'expanded' property in the model is changed.
311  * @param {boolean} oldValue Old value.
312  * @param {boolean} newValue New value.
313  */
314 AudioPlayer.prototype.onModelExpandedChanged = function(oldValue, newValue) {
315   if (this.isExpanded_ !== null &&
316       this.isExpanded_ === newValue)
317     return;
318
319   if (this.isExpanded_ && !newValue)
320     this.lastExpandedHeight_ = window.innerHeight;
321
322   if (this.isExpanded_ !== newValue) {
323     this.isExpanded_ = newValue;
324     this.syncHeight_();
325
326     // Saves new state.
327     window.appState.expanded = newValue;
328     util.saveAppState();
329   }
330 };
331
332 /**
333  * @private
334  */
335 AudioPlayer.prototype.syncHeight_ = function() {
336   var targetHeight;
337
338   if (this.model_.expanded) {
339     // Expanded.
340     if (!this.lastExpandedHeight_ ||
341         this.lastExpandedHeight_ < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
342       var expandedListHeight =
343         Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
344                                        AudioPlayer.TRACK_HEIGHT;
345       targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
346       this.lastExpandedHeight_ = targetHeight;
347     } else {
348       targetHeight = this.lastExpandedHeight_;
349     }
350   } else {
351     // Not expanded.
352     targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
353   }
354
355   window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
356 };
357
358 /**
359  * Create a TrackInfo object encapsulating the information about one track.
360  *
361  * @param {fileEntry} entry FileEntry to be retrieved the track info from.
362  * @param {function} onClick Click handler.
363  * @constructor
364  */
365 AudioPlayer.TrackInfo = function(entry, onClick) {
366   this.url = entry.toURL();
367   this.title = entry.name;
368   this.artist = this.getDefaultArtist();
369
370   // TODO(yoshiki): implement artwork.
371   this.artwork = null;
372   this.active = false;
373 };
374
375 /**
376  * @return {HTMLDivElement} The wrapper element for the track.
377  */
378 AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ };
379
380 /**
381  * @return {string} Default track title (file name extracted from the url).
382  */
383 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
384   var title = this.url.split('/').pop();
385   var dotIndex = title.lastIndexOf('.');
386   if (dotIndex >= 0) title = title.substr(0, dotIndex);
387   title = decodeURIComponent(title);
388   return title;
389 };
390
391 /**
392  * TODO(kaznacheev): Localize.
393  */
394 AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
395
396 /**
397  * @return {string} 'Unknown artist' string.
398  */
399 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
400   return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
401 };
402
403 /**
404  * @param {Object} metadata The metadata object.
405  * @param {string} error Error string.
406  */
407 AudioPlayer.TrackInfo.prototype.setMetadata = function(
408     metadata, error) {
409   if (error) {
410     // TODO(yoshiki): Handle error in better way.
411     this.title = entry.name;
412     this.artist = this.getDefaultArtist();
413   } else if (metadata.thumbnail && metadata.thumbnail.url) {
414     // TODO(yoshiki): implement artwork.
415   }
416   this.title = (metadata.media && metadata.media.title) ||
417       this.getDefaultTitle();
418   this.artist = error ||
419       (metadata.media && metadata.media.artist) || this.getDefaultArtist();
420 };
421
422 // Starts loading the audio player.
423 window.addEventListener('polymer-ready', function(e) {
424   AudioPlayer.load();
425 });