OLD | NEW |
| (Empty) |
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 }); | |
OLD | NEW |