Upstream version 11.39.266.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.fileManagerPrivate.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       var currentTracks = this.player_.tracks;
121       var unchanged = (currentTracks.length === this.entries_.length);
122
123       for (var i = 0; i != this.entries_.length; i++) {
124         var entry = this.entries_[i];
125         var onClick = this.select_.bind(this, i);
126         newTracks.push(new AudioPlayer.TrackInfo(entry, onClick));
127
128         if (unchanged && entry.toURL() !== currentTracks[i].url)
129           unchanged = false;
130       }
131
132       if (!unchanged) {
133         this.player_.tracks = newTracks;
134
135         // Makes it sure that the handler of the track list is called, before
136         // the handler of the track index.
137         Platform.performMicrotaskCheckpoint();
138       }
139
140       this.select_(position, !!time);
141
142       // Load the selected track metadata first, then load the rest.
143       this.loadMetadata_(position);
144       for (i = 0; i != this.entries_.length; i++) {
145         if (i != position)
146           this.loadMetadata_(i);
147       }
148     }.bind(this));
149   }.bind(this));
150 };
151
152 /**
153  * Loads 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  * Displays 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.player_.tracks[track].setMetadata(metadata, opt_error);
171 };
172
173 /**
174  * Closes audio player when a volume containing the selected item is unmounted.
175  * @param {Event} event The unmount event.
176  * @private
177  */
178 AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
179   if (!this.selectedEntry_)
180     return;
181
182   if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
183       event.volumeInfo)
184     window.close();
185 };
186
187 /**
188  * Called on window is being unloaded.
189  */
190 AudioPlayer.prototype.onUnload = function() {
191   if (this.player_)
192     this.player_.onPageUnload();
193
194   if (this.volumeManager_)
195     this.volumeManager_.dispose();
196 };
197
198 /**
199  * Selects a new track to play.
200  * @param {number} newTrack New track number.
201  * @param {number} time New playback position (in second).
202  * @private
203  */
204 AudioPlayer.prototype.select_ = function(newTrack, time) {
205   if (this.currentTrackIndex_ == newTrack) return;
206
207   this.currentTrackIndex_ = newTrack;
208   this.player_.currentTrackIndex = this.currentTrackIndex_;
209   this.player_.audioController.time = time;
210   Platform.performMicrotaskCheckpoint();
211
212   if (!window.appReopen)
213     this.player_.audioElement.play();
214
215   window.appState.position = this.currentTrackIndex_;
216   window.appState.time = 0;
217   util.saveAppState();
218
219   var entry = this.entries_[this.currentTrackIndex_];
220
221   this.fetchMetadata_(entry, function(metadata) {
222     if (this.currentTrackIndex_ != newTrack)
223       return;
224
225     this.selectedEntry_ = entry;
226   }.bind(this));
227 };
228
229 /**
230  * @param {FileEntry} entry Track file entry.
231  * @param {function(object)} callback Callback.
232  * @private
233  */
234 AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) {
235   this.metadataCache_.getOne(entry, 'thumbnail|media|external',
236       function(generation, metadata) {
237         // Do nothing if another load happened since the metadata request.
238         if (this.playlistGeneration_ == generation)
239           callback(metadata);
240       }.bind(this, this.playlistGeneration_));
241 };
242
243 /**
244  * Media error handler.
245  * @private
246  */
247 AudioPlayer.prototype.onError_ = function() {
248   var track = this.currentTrackIndex_;
249
250   this.invalidTracks_[track] = true;
251
252   this.fetchMetadata_(
253       this.entries_[track],
254       function(metadata) {
255         var error = (!navigator.onLine && !metadata.external.present) ?
256             this.offlineString_ : this.errorString_;
257         this.displayMetadata_(track, metadata, error);
258         this.scheduleAutoAdvance_();
259       }.bind(this));
260 };
261
262 /**
263  * Toggles the expanded mode when resizing.
264  *
265  * @param {Event} event Resize event.
266  * @private
267  */
268 AudioPlayer.prototype.onResize_ = function(event) {
269   if (!this.isExpanded_ &&
270       window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
271     this.isExpanded_ = true;
272     this.model_.expanded = true;
273   } else if (this.isExpanded_ &&
274              window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
275     this.isExpanded_ = false;
276     this.model_.expanded = false;
277   }
278 };
279
280 /* Keep the below constants in sync with the CSS. */
281
282 /**
283  * Window header size in pixels.
284  * @type {number}
285  * @const
286  */
287 AudioPlayer.HEADER_HEIGHT = 33;  // 32px + border 1px
288
289 /**
290  * Track height in pixels.
291  * @type {number}
292  * @const
293  */
294 AudioPlayer.TRACK_HEIGHT = 44;
295
296 /**
297  * Controls bar height in pixels.
298  * @type {number}
299  * @const
300  */
301 AudioPlayer.CONTROLS_HEIGHT = 73;  // 72px + border 1px
302
303 /**
304  * Default number of items in the expanded mode.
305  * @type {number}
306  * @const
307  */
308 AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
309
310 /**
311  * Minimum size of the window in the expanded mode in pixels.
312  * @type {number}
313  * @const
314  */
315 AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
316                                        AudioPlayer.TRACK_HEIGHT * 2;
317
318 /**
319  * Invoked when the 'expanded' property in the model is changed.
320  * @param {boolean} oldValue Old value.
321  * @param {boolean} newValue New value.
322  */
323 AudioPlayer.prototype.onModelExpandedChanged = function(oldValue, newValue) {
324   if (this.isExpanded_ !== null &&
325       this.isExpanded_ === newValue)
326     return;
327
328   if (this.isExpanded_ && !newValue)
329     this.lastExpandedHeight_ = window.innerHeight;
330
331   if (this.isExpanded_ !== newValue) {
332     this.isExpanded_ = newValue;
333     this.syncHeight_();
334
335     // Saves new state.
336     window.appState.expanded = newValue;
337     util.saveAppState();
338   }
339 };
340
341 /**
342  * @private
343  */
344 AudioPlayer.prototype.syncHeight_ = function() {
345   var targetHeight;
346
347   if (this.model_.expanded) {
348     // Expanded.
349     if (!this.lastExpandedHeight_ ||
350         this.lastExpandedHeight_ < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
351       var expandedListHeight =
352           Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
353               AudioPlayer.TRACK_HEIGHT;
354       targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
355       this.lastExpandedHeight_ = targetHeight;
356     } else {
357       targetHeight = this.lastExpandedHeight_;
358     }
359   } else {
360     // Not expanded.
361     targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
362   }
363
364   window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
365 };
366
367 /**
368  * Create a TrackInfo object encapsulating the information about one track.
369  *
370  * @param {fileEntry} entry FileEntry to be retrieved the track info from.
371  * @param {function} onClick Click handler.
372  * @constructor
373  */
374 AudioPlayer.TrackInfo = function(entry, onClick) {
375   this.url = entry.toURL();
376   this.title = this.getDefaultTitle();
377   this.artist = this.getDefaultArtist();
378
379   // TODO(yoshiki): implement artwork.
380   this.artwork = null;
381   this.active = false;
382 };
383
384 /**
385  * @return {HTMLDivElement} The wrapper element for the track.
386  */
387 AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ };
388
389 /**
390  * @return {string} Default track title (file name extracted from the url).
391  */
392 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
393   var title = this.url.split('/').pop();
394   var dotIndex = title.lastIndexOf('.');
395   if (dotIndex >= 0) title = title.substr(0, dotIndex);
396   title = decodeURIComponent(title);
397   return title;
398 };
399
400 /**
401  * TODO(kaznacheev): Localize.
402  */
403 AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
404
405 /**
406  * @return {string} 'Unknown artist' string.
407  */
408 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
409   return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
410 };
411
412 /**
413  * @param {Object} metadata The metadata object.
414  * @param {string} error Error string.
415  */
416 AudioPlayer.TrackInfo.prototype.setMetadata = function(
417     metadata, error) {
418   // TODO(yoshiki): Handle error in better way.
419   // TODO(yoshiki): implement artwork (metadata.thumbnail)
420   this.title = (metadata.media && metadata.media.title) ||
421       this.getDefaultTitle();
422   this.artist = error ||
423       (metadata.media && metadata.media.artist) || this.getDefaultArtist();
424 };
425
426 // Starts loading the audio player.
427 window.addEventListener('polymer-ready', function(e) {
428   AudioPlayer.load();
429 });