Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(7)

Side by Side Diff: chrome/browser/resources/file_manager/js/media/audio_player.js

Issue 39123003: [Files.app] Split the JavaScript files into subdirectories: common, background, and foreground (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fixed test failure. Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 * TODO(mtomasz): Rewrite the entire audio player.
9 *
10 * @param {HTMLElement} container Container element.
11 * @constructor
12 */
13 function AudioPlayer(container) {
14 this.container_ = container;
15 this.metadataCache_ = MetadataCache.createFull();
16 this.currentTrack_ = -1;
17 this.playlistGeneration_ = 0;
18 this.volumeManager_ = new VolumeManagerWrapper(
19 VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
20
21 this.container_.classList.add('collapsed');
22
23 function createChild(opt_className, opt_tag) {
24 var child = container.ownerDocument.createElement(opt_tag || 'div');
25 if (opt_className)
26 child.className = opt_className;
27 container.appendChild(child);
28 return child;
29 }
30
31 // We create two separate containers (for expanded and compact view) and keep
32 // two sets of TrackInfo instances. We could fiddle with a single set instead
33 // but it would make keeping the list scroll position very tricky.
34 this.trackList_ = createChild('track-list');
35 this.trackStack_ = createChild('track-stack');
36
37 createChild('title-button collapse').addEventListener(
38 'click', this.onExpandCollapse_.bind(this));
39
40 this.audioControls_ = new FullWindowAudioControls(
41 createChild(), this.advance_.bind(this), this.onError_.bind(this));
42
43 this.audioControls_.attachMedia(createChild('', 'audio'));
44
45 chrome.fileBrowserPrivate.getStrings(function(strings) {
46 container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE'];
47 this.errorString_ = strings['AUDIO_ERROR'];
48 this.offlineString_ = strings['AUDIO_OFFLINE'];
49 AudioPlayer.TrackInfo.DEFAULT_ARTIST =
50 strings['AUDIO_PLAYER_DEFAULT_ARTIST'];
51 }.bind(this));
52
53 this.volumeManager_.addEventListener('externally-unmounted',
54 this.onExternallyUnmounted_.bind(this));
55
56 window.addEventListener('resize', this.onResize_.bind(this));
57
58 // Show the window after DOM is processed.
59 var currentWindow = chrome.app.window.current();
60 setTimeout(currentWindow.show.bind(currentWindow), 0);
61 }
62
63 /**
64 * Initial load method (static).
65 */
66 AudioPlayer.load = function() {
67 document.ondragstart = function(e) { e.preventDefault() };
68
69 // TODO(mtomasz): Consider providing an exact size icon, instead of relying
70 // on downsampling by ash.
71 chrome.app.window.current().setIcon('images/media/2x/audio_player.png');
72
73 AudioPlayer.instance =
74 new AudioPlayer(document.querySelector('.audio-player'));
75 reload();
76 };
77
78 util.addPageLoadHandler(AudioPlayer.load);
79
80 /**
81 * Unload the player.
82 */
83 function unload() {
84 if (AudioPlayer.instance)
85 AudioPlayer.instance.onUnload();
86 }
87
88 /**
89 * Reload the player.
90 */
91 function reload() {
92 if (window.appState) {
93 util.saveAppState();
94 AudioPlayer.instance.load(window.appState);
95 return;
96 }
97 }
98
99 /**
100 * Load a new playlist.
101 * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
102 */
103 AudioPlayer.prototype.load = function(playlist) {
104 this.playlistGeneration_++;
105 this.audioControls_.pause();
106 this.currentTrack_ = -1;
107 this.urls_ = playlist.items;
108
109 this.invalidTracks_ = {};
110 this.cancelAutoAdvance_();
111
112 if (this.urls_.length <= 1)
113 this.container_.classList.add('single-track');
114 else
115 this.container_.classList.remove('single-track');
116
117 this.syncHeight_();
118
119 this.trackList_.textContent = '';
120 this.trackStack_.textContent = '';
121
122 this.trackListItems_ = [];
123 this.trackStackItems_ = [];
124
125 if (this.urls_.length == 0)
126 return;
127
128 for (var i = 0; i != this.urls_.length; i++) {
129 var url = this.urls_[i];
130 var onClick = this.select_.bind(this, i, false /* no restore */);
131 this.trackListItems_.push(
132 new AudioPlayer.TrackInfo(this.trackList_, url, onClick));
133 this.trackStackItems_.push(
134 new AudioPlayer.TrackInfo(this.trackStack_, url, onClick));
135 }
136
137 this.select_(playlist.position, !!playlist.time);
138
139 // This class will be removed if at least one track has art.
140 this.container_.classList.add('noart');
141
142 // Load the selected track metadata first, then load the rest.
143 this.loadMetadata_(playlist.position);
144 for (i = 0; i != this.urls_.length; i++) {
145 if (i != playlist.position)
146 this.loadMetadata_(i);
147 }
148 };
149
150 /**
151 * Load metadata for a track.
152 * @param {number} track Track number.
153 * @private
154 */
155 AudioPlayer.prototype.loadMetadata_ = function(track) {
156 this.fetchMetadata_(
157 this.urls_[track], this.displayMetadata_.bind(this, track));
158 };
159
160 /**
161 * Display track's metadata.
162 * @param {number} track Track number.
163 * @param {Object} metadata Metadata object.
164 * @param {string=} opt_error Error message.
165 * @private
166 */
167 AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) {
168 this.trackListItems_[track].
169 setMetadata(metadata, this.container_, opt_error);
170 this.trackStackItems_[track].
171 setMetadata(metadata, this.container_, opt_error);
172 };
173
174 /**
175 * Closes audio player when a volume containing the selected item is unmounted.
176 * @param {Event} event The unmount event.
177 * @private
178 */
179 AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
180 if (!this.selectedItemFilesystemPath_)
181 return;
182 if (this.selectedItemFilesystemPath_.indexOf(event.mountPath) == 0)
183 close();
184 };
185
186 /**
187 * Called on window is being unloaded.
188 */
189 AudioPlayer.prototype.onUnload = function() {
190 this.audioControls_.cleanup();
191 this.volumeManager_.dispose();
192 };
193
194 /**
195 * Select a new track to play.
196 * @param {number} newTrack New track number.
197 * @param {boolean=} opt_restoreState True if restoring the play state from URL.
198 * @private
199 */
200 AudioPlayer.prototype.select_ = function(newTrack, opt_restoreState) {
201 if (this.currentTrack_ == newTrack) return;
202
203 this.changeSelectionInList_(this.currentTrack_, newTrack);
204 this.changeSelectionInStack_(this.currentTrack_, newTrack);
205
206 this.currentTrack_ = newTrack;
207
208 if (window.appState) {
209 window.appState.position = this.currentTrack_;
210 window.appState.time = 0;
211 util.saveAppState();
212 } else {
213 util.platform.setPreference(AudioPlayer.TRACK_KEY, this.currentTrack_);
214 }
215
216 this.scrollToCurrent_(false);
217
218 var currentTrack = this.currentTrack_;
219 var url = this.urls_[currentTrack];
220 this.fetchMetadata_(url, function(metadata) {
221 if (this.currentTrack_ != currentTrack)
222 return;
223 var src = url;
224 this.audioControls_.load(src, opt_restoreState);
225
226 // Resolve real filesystem path of the current audio file.
227 this.selectedItemFilesystemPath_ = null;
228 webkitResolveLocalFileSystemURL(src,
229 function(entry) {
230 if (this.currentTrack_ != currentTrack)
231 return;
232 this.selectedItemFilesystemPath_ = entry.fullPath;
233 }.bind(this));
234 }.bind(this));
235 };
236
237 /**
238 * @param {string} url Track file url.
239 * @param {function(object)} callback Callback.
240 * @private
241 */
242 AudioPlayer.prototype.fetchMetadata_ = function(url, callback) {
243 this.metadataCache_.get(url, 'thumbnail|media|streaming',
244 function(generation, metadata) {
245 // Do nothing if another load happened since the metadata request.
246 if (this.playlistGeneration_ == generation)
247 callback(metadata);
248 }.bind(this, this.playlistGeneration_));
249 };
250
251 /**
252 * @param {number} oldTrack Old track number.
253 * @param {number} newTrack New track number.
254 * @private
255 */
256 AudioPlayer.prototype.changeSelectionInList_ = function(oldTrack, newTrack) {
257 this.trackListItems_[newTrack].getBox().classList.add('selected');
258
259 if (oldTrack >= 0) {
260 this.trackListItems_[oldTrack].getBox().classList.remove('selected');
261 }
262 };
263
264 /**
265 * @param {number} oldTrack Old track number.
266 * @param {number} newTrack New track number.
267 * @private
268 */
269 AudioPlayer.prototype.changeSelectionInStack_ = function(oldTrack, newTrack) {
270 var newBox = this.trackStackItems_[newTrack].getBox();
271 newBox.classList.add('selected'); // Put on top immediately.
272 newBox.classList.add('visible'); // Start fading in.
273
274 if (oldTrack >= 0) {
275 var oldBox = this.trackStackItems_[oldTrack].getBox();
276 oldBox.classList.remove('selected'); // Put under immediately.
277 setTimeout(function() {
278 if (!oldBox.classList.contains('selected')) {
279 // This will start fading out which is not really necessary because
280 // oldBox is already completely obscured by newBox.
281 oldBox.classList.remove('visible');
282 }
283 }, 300);
284 }
285 };
286
287 /**
288 * Scrolls the current track into the viewport.
289 *
290 * @param {boolean} keepAtBottom If true, make the selected track the last
291 * of the visible (if possible). If false, perform minimal scrolling.
292 * @private
293 */
294 AudioPlayer.prototype.scrollToCurrent_ = function(keepAtBottom) {
295 var box = this.trackListItems_[this.currentTrack_].getBox();
296 this.trackList_.scrollTop = Math.max(
297 keepAtBottom ? 0 : Math.min(box.offsetTop, this.trackList_.scrollTop),
298 box.offsetTop + box.offsetHeight - this.trackList_.clientHeight);
299 };
300
301 /**
302 * @return {boolean} True if the player is be displayed in compact mode.
303 * @private
304 */
305 AudioPlayer.prototype.isCompact_ = function() {
306 return this.container_.classList.contains('collapsed') ||
307 this.container_.classList.contains('single-track');
308 };
309
310 /**
311 * Go to the previous or the next track.
312 * @param {boolean} forward True if next, false if previous.
313 * @param {boolean=} opt_onlyIfValid True if invalid tracks should be selected.
314 * @private
315 */
316 AudioPlayer.prototype.advance_ = function(forward, opt_onlyIfValid) {
317 this.cancelAutoAdvance_();
318
319 var newTrack = this.currentTrack_ + (forward ? 1 : -1);
320 if (newTrack < 0) newTrack = this.urls_.length - 1;
321 if (newTrack == this.urls_.length) newTrack = 0;
322 if (opt_onlyIfValid && this.invalidTracks_[newTrack])
323 return;
324 this.select_(newTrack);
325 };
326
327 /**
328 * Media error handler.
329 * @private
330 */
331 AudioPlayer.prototype.onError_ = function() {
332 var track = this.currentTrack_;
333
334 this.invalidTracks_[track] = true;
335
336 this.fetchMetadata_(
337 this.urls_[track],
338 function(metadata) {
339 var error = (!navigator.onLine && metadata.streaming) ?
340 this.offlineString_ : this.errorString_;
341 this.displayMetadata_(track, metadata, error);
342 this.scheduleAutoAdvance_();
343 }.bind(this));
344 };
345
346 /**
347 * Schedule automatic advance to the next track after a timeout.
348 * @private
349 */
350 AudioPlayer.prototype.scheduleAutoAdvance_ = function() {
351 this.cancelAutoAdvance_();
352 this.autoAdvanceTimer_ = setTimeout(
353 function() {
354 this.autoAdvanceTimer_ = null;
355 // We are advancing only if the next track is not known to be invalid.
356 // This prevents an endless auto-advancing in the case when all tracks
357 // are invalid (we will only visit each track once).
358 this.advance_(true /* forward */, true /* only if valid */);
359 }.bind(this),
360 3000);
361 };
362
363 /**
364 * Cancel the scheduled auto advance.
365 * @private
366 */
367 AudioPlayer.prototype.cancelAutoAdvance_ = function() {
368 if (this.autoAdvanceTimer_) {
369 clearTimeout(this.autoAdvanceTimer_);
370 this.autoAdvanceTimer_ = null;
371 }
372 };
373
374 /**
375 * Expand/collapse button click handler. Toggles the mode and updates the
376 * height of the window.
377 *
378 * @private
379 */
380 AudioPlayer.prototype.onExpandCollapse_ = function() {
381 if (!this.isCompact_()) {
382 this.setExpanded_(false);
383 this.lastExpandedHeight_ = window.innerHeight;
384 } else {
385 this.setExpanded_(true);
386 }
387 this.syncHeight_();
388 };
389
390 /**
391 * Toggles the current expand mode.
392 *
393 * @param {boolean} on True if on, false otherwise.
394 * @private
395 */
396 AudioPlayer.prototype.setExpanded_ = function(on) {
397 if (on) {
398 this.container_.classList.remove('collapsed');
399 this.scrollToCurrent_(true);
400 } else {
401 this.container_.classList.add('collapsed');
402 }
403 };
404
405 /**
406 * Toggles the expanded mode when resizing.
407 *
408 * @param {Event} event Resize event.
409 * @private
410 */
411 AudioPlayer.prototype.onResize_ = function(event) {
412 if (this.isCompact_() &&
413 window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
414 this.setExpanded_(true);
415 } else if (!this.isCompact_() &&
416 window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
417 this.setExpanded_(false);
418 }
419 };
420
421 /* Keep the below constants in sync with the CSS. */
422
423 /**
424 * Window header size in pixels.
425 * @type {number}
426 * @const
427 */
428 AudioPlayer.HEADER_HEIGHT = 28;
429
430 /**
431 * Track height in pixels.
432 * @type {number}
433 * @const
434 */
435 AudioPlayer.TRACK_HEIGHT = 58;
436
437 /**
438 * Controls bar height in pixels.
439 * @type {number}
440 * @const
441 */
442 AudioPlayer.CONTROLS_HEIGHT = 35;
443
444 /**
445 * Default number of items in the expanded mode.
446 * @type {number}
447 * @const
448 */
449 AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
450
451 /**
452 * Minimum size of the window in the expanded mode in pixels.
453 * @type {number}
454 * @const
455 */
456 AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
457 AudioPlayer.TRACK_HEIGHT * 2;
458
459 /**
460 * Set the correct player window height.
461 * @private
462 */
463 AudioPlayer.prototype.syncHeight_ = function() {
464 var targetHeight;
465
466 if (!this.isCompact_()) {
467 // Expanded.
468 if (this.lastExpandedHeight_) {
469 targetHeight = this.lastExpandedHeight_;
470 } else {
471 var expandedListHeight =
472 Math.min(this.urls_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
473 AudioPlayer.TRACK_HEIGHT;
474 targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
475 }
476 } else {
477 // Not expaned.
478 targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
479 }
480
481 window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
482 };
483
484 /**
485 * Create a TrackInfo object encapsulating the information about one track.
486 *
487 * @param {HTMLElement} container Container element.
488 * @param {string} url Track url.
489 * @param {function} onClick Click handler.
490 * @constructor
491 */
492 AudioPlayer.TrackInfo = function(container, url, onClick) {
493 this.url_ = url;
494
495 var doc = container.ownerDocument;
496
497 this.box_ = doc.createElement('div');
498 this.box_.className = 'track';
499 this.box_.addEventListener('click', onClick);
500 container.appendChild(this.box_);
501
502 this.art_ = doc.createElement('div');
503 this.art_.className = 'art blank';
504 this.box_.appendChild(this.art_);
505
506 this.img_ = doc.createElement('img');
507 this.art_.appendChild(this.img_);
508
509 this.data_ = doc.createElement('div');
510 this.data_.className = 'data';
511 this.box_.appendChild(this.data_);
512
513 this.title_ = doc.createElement('div');
514 this.title_.className = 'data-title';
515 this.data_.appendChild(this.title_);
516
517 this.artist_ = doc.createElement('div');
518 this.artist_.className = 'data-artist';
519 this.data_.appendChild(this.artist_);
520 };
521
522 /**
523 * @return {HTMLDivElement} The wrapper element for the track.
524 */
525 AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ };
526
527 /**
528 * @return {string} Default track title (file name extracted from the url).
529 */
530 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
531 var title = this.url_.split('/').pop();
532 var dotIndex = title.lastIndexOf('.');
533 if (dotIndex >= 0) title = title.substr(0, dotIndex);
534 title = decodeURIComponent(title);
535 return title;
536 };
537
538 /**
539 * TODO(kaznacheev): Localize.
540 */
541 AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
542
543 /**
544 * @return {string} 'Unknown artist' string.
545 */
546 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
547 return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
548 };
549
550 /**
551 * @param {Object} metadata The metadata object.
552 * @param {HTMLElement} container The container for the tracks.
553 * @param {string} error Error string.
554 */
555 AudioPlayer.TrackInfo.prototype.setMetadata = function(
556 metadata, container, error) {
557 if (error) {
558 this.art_.classList.add('blank');
559 this.art_.classList.add('error');
560 container.classList.remove('noart');
561 } else if (metadata.thumbnail && metadata.thumbnail.url) {
562 this.img_.onload = function() {
563 // Only display the image if the thumbnail loaded successfully.
564 this.art_.classList.remove('blank');
565 container.classList.remove('noart');
566 }.bind(this);
567 this.img_.src = metadata.thumbnail.url;
568 }
569 this.title_.textContent = (metadata.media && metadata.media.title) ||
570 this.getDefaultTitle();
571 this.artist_.textContent = error ||
572 (metadata.media && metadata.media.artist) || this.getDefaultArtist();
573 };
574
575 /**
576 * Audio controls specific for the Audio Player.
577 *
578 * @param {HTMLElement} container Parent container.
579 * @param {function(boolean)} advanceTrack Parameter: true=forward.
580 * @param {function} onError Error handler.
581 * @constructor
582 */
583 function FullWindowAudioControls(container, advanceTrack, onError) {
584 AudioControls.apply(this, arguments);
585
586 document.addEventListener('keydown', function(e) {
587 if (e.keyIdentifier == 'U+0020') {
588 this.togglePlayState();
589 e.preventDefault();
590 }
591 }.bind(this));
592 }
593
594 FullWindowAudioControls.prototype = { __proto__: AudioControls.prototype };
595
596 /**
597 * Enable play state restore from the location hash.
598 * @param {string} src Source URL.
599 * @param {boolean} restore True if need to restore the play state.
600 */
601 FullWindowAudioControls.prototype.load = function(src, restore) {
602 this.media_.src = src;
603 this.media_.load();
604 this.restoreWhenLoaded_ = restore;
605 };
606
607 /**
608 * Save the current state so that it survives page/app reload.
609 */
610 FullWindowAudioControls.prototype.onPlayStateChanged = function() {
611 this.encodeState();
612 };
613
614 /**
615 * Restore the state after page/app reload.
616 */
617 FullWindowAudioControls.prototype.restorePlayState = function() {
618 if (this.restoreWhenLoaded_) {
619 this.restoreWhenLoaded_ = false; // This should only work once.
620 if (this.decodeState())
621 return;
622 }
623 this.play();
624 };
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698