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