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 document.addEventListener('DOMContentLoaded', function() { | |
6 // Test harness sets the search string to prevent the automatic load. | |
7 // It calls AudioPlayer.load() explicitly after initializing | |
8 // the |chrome| variable with an appropriate mock object. | |
9 if (!document.location.search) { | |
10 AudioPlayer.load(); | |
11 } | |
12 }); | |
13 | |
14 /** | |
15 * @param {HTMLElement} container | |
16 * @constructor | |
17 */ | |
18 function AudioPlayer(container) { | |
19 this.container_ = container; | |
20 this.metadataProvider_ = new MetadataProvider(); | |
21 this.currentTrack_ = -1; | |
22 this.playlistGeneration_ = 0; | |
23 | |
24 this.container_.classList.add('collapsed'); | |
25 | |
26 function createChild(opt_className, opt_tag) { | |
27 var child = container.ownerDocument.createElement(opt_tag || 'div'); | |
28 if (opt_className) | |
29 child.className = opt_className; | |
30 container.appendChild(child); | |
31 return child; | |
32 } | |
33 | |
34 // We create two separate containers (for expanded and compact view) and keep | |
35 // two sets of TrackInfo instances. We could fiddle with a single set instead | |
36 // but it would make keeping the list scroll position very tricky. | |
37 this.trackList_ = createChild('track-list'); | |
38 this.trackStack_ = createChild('track-stack'); | |
39 | |
40 createChild('title-button close').addEventListener( | |
41 'click', function() { chrome.mediaPlayerPrivate.closeWindow() }); | |
42 | |
43 createChild('title-button collapse').addEventListener( | |
44 'click', this.onExpandCollapse_.bind(this)); | |
45 | |
46 this.audioControls_ = new AudioControls( | |
47 createChild(), this.advance_.bind(this)); | |
48 | |
49 this.audioControls_.attachMedia(createChild('', 'audio')); | |
50 } | |
51 | |
52 AudioPlayer.load = function() { | |
53 document.ondragstart = function(e) { e.preventDefault() }; | |
54 document.oncontextmenu = function(e) { e.preventDefault(); }; | |
55 | |
56 var player = new AudioPlayer(document.querySelector('.audio-player')); | |
57 function getPlaylist() { | |
58 chrome.mediaPlayerPrivate.getPlaylist(player.load.bind(player)); | |
59 } | |
60 getPlaylist(); | |
61 chrome.mediaPlayerPrivate.onPlaylistChanged.addListener(getPlaylist); | |
62 }; | |
63 | |
64 AudioPlayer.prototype.load = function(playlist) { | |
65 this.playlistGeneration_++; | |
66 | |
67 this.audioControls_.pause(); | |
68 | |
69 this.currentTrack_ = -1; | |
70 | |
71 this.urls_ = playlist.items; | |
72 | |
73 if (this.urls_.length == 1) | |
74 this.container_.classList.add('single-track'); | |
75 else | |
76 this.container_.classList.remove('single-track'); | |
77 | |
78 this.syncHeight_(); | |
79 | |
80 this.trackList_.textContent = ''; | |
81 this.trackStack_.textContent = ''; | |
82 | |
83 this.trackListItems_ = []; | |
84 this.trackStackItems_ = []; | |
85 | |
86 for (var i = 0; i != this.urls_.length; i++) { | |
87 var url = this.urls_[i]; | |
88 var onClick = this.select_.bind(this, i); | |
89 this.trackListItems_.push( | |
90 new AudioPlayer.TrackInfo(this.trackList_, url, onClick)); | |
91 this.trackStackItems_.push( | |
92 new AudioPlayer.TrackInfo(this.trackStack_, url, onClick)); | |
93 } | |
94 | |
95 this.select_(playlist.position); | |
96 | |
97 // This class will be removed if at least one track has art. | |
98 this.container_.classList.add('noart'); | |
99 | |
100 // Load the selected track metadata first, then load the rest. | |
101 this.loadMetadata_(playlist.position); | |
102 for (i = 0; i != this.urls_.length; i++) { | |
103 if (i != playlist.position) | |
104 this.loadMetadata_(i); | |
105 } | |
106 }; | |
107 | |
108 AudioPlayer.prototype.loadMetadata_ = function(track) { | |
109 this.metadataProvider_.fetch( | |
110 this.urls_[track], | |
111 function(generation, metadata) { | |
112 // Do nothing if another load happened since the metadata request. | |
113 if (this.playlistGeneration_ != generation) | |
114 return; | |
115 | |
116 if (metadata.thumbnailURL) { | |
117 this.container_.classList.remove('noart'); | |
118 } | |
119 this.trackListItems_[track].setMetadata(metadata); | |
120 this.trackStackItems_[track].setMetadata(metadata); | |
121 }.bind(this, this.playlistGeneration_)); | |
122 }; | |
123 | |
124 AudioPlayer.prototype.select_ = function(newTrack) { | |
125 if (this.currentTrack_ == newTrack) return; | |
126 | |
127 this.changeSelectionInList_(this.currentTrack_, newTrack); | |
128 this.changeSelectionInStack_(this.currentTrack_, newTrack); | |
129 | |
130 this.currentTrack_ = newTrack; | |
131 this.scrollToCurrent_(false); | |
132 | |
133 var media = this.audioControls_.getMedia(); | |
134 media.src = this.urls_[this.currentTrack_]; | |
135 media.load(); | |
136 this.audioControls_.play(); | |
137 }; | |
138 | |
139 AudioPlayer.prototype.changeSelectionInList_ = function(oldTrack, newTrack) { | |
140 this.trackListItems_[newTrack].getBox().classList.add('selected'); | |
141 | |
142 if (oldTrack >= 0) { | |
143 this.trackListItems_[oldTrack].getBox().classList.remove('selected'); | |
144 } | |
145 }; | |
146 | |
147 AudioPlayer.prototype.changeSelectionInStack_ = function(oldTrack, newTrack) { | |
148 var newBox = this.trackStackItems_[newTrack].getBox(); | |
149 newBox.classList.add('selected'); // Put on top immediately. | |
150 newBox.classList.add('visible'); // Start fading in. | |
151 | |
152 if (oldTrack >= 0) { | |
153 var oldBox = this.trackStackItems_[oldTrack].getBox(); | |
154 oldBox.classList.remove('selected'); // Put under immediately. | |
155 setTimeout(function () { | |
156 if (!oldBox.classList.contains('selected')) { | |
157 // This will start fading out which is not really necessary because | |
158 // oldBox is already completely obscured by newBox. | |
159 oldBox.classList.remove('visible'); | |
160 } | |
161 }, 300); | |
162 } | |
163 }; | |
164 | |
165 /** | |
166 * Scrolls the current track into the viewport. | |
167 * | |
168 * @param {boolean} keepAtBottom If true, make the selected track the last | |
169 * of the visible (if possible). If false, perform minimal scrolling. | |
170 */ | |
171 AudioPlayer.prototype.scrollToCurrent_ = function(keepAtBottom) { | |
172 var box = this.trackListItems_[this.currentTrack_].getBox(); | |
173 this.trackList_.scrollTop = Math.max( | |
174 keepAtBottom ? 0 : Math.min(box.offsetTop, this.trackList_.scrollTop), | |
175 box.offsetTop + box.offsetHeight - this.trackList_.clientHeight); | |
176 }; | |
177 | |
178 AudioPlayer.prototype.isCompact_ = function() { | |
179 return this.container_.classList.contains('collapsed') || | |
180 this.container_.classList.contains('single-track'); | |
181 }; | |
182 | |
183 AudioPlayer.prototype.advance_ = function(forward) { | |
184 var newTrack = this.currentTrack_ + (forward ? 1 : -1); | |
185 if (newTrack < 0) newTrack = this.urls_.length - 1; | |
186 if (newTrack == this.urls_.length) newTrack = 0; | |
187 this.select_(newTrack); | |
188 }; | |
189 | |
190 AudioPlayer.prototype.onExpandCollapse_ = function() { | |
191 this.container_.classList.toggle('collapsed'); | |
192 this.syncHeight_(); | |
193 if (!this.isCompact_()) | |
194 this.scrollToCurrent_(true); | |
195 }; | |
196 | |
197 /* Keep the below constants in sync with the CSS. */ | |
198 AudioPlayer.HEADER_HEIGHT = 30; | |
199 AudioPlayer.TRACK_HEIGHT = 58; | |
200 AudioPlayer.CONTROLS_HEIGHT = 35; | |
201 | |
202 AudioPlayer.prototype.syncHeight_ = function() { | |
203 var expandedListHeight = | |
204 Math.min(this.urls_.length, 3) * AudioPlayer.TRACK_HEIGHT; | |
205 this.trackList_.style.height = expandedListHeight + 'px'; | |
206 | |
207 chrome.mediaPlayerPrivate.setWindowHeight( | |
208 (this.isCompact_() ? | |
209 AudioPlayer.TRACK_HEIGHT : | |
210 AudioPlayer.HEADER_HEIGHT + expandedListHeight) + | |
211 AudioPlayer.CONTROLS_HEIGHT); | |
212 }; | |
213 | |
214 | |
215 /** | |
216 * Create a TrackInfo object encapsulating the information about one track. | |
217 * | |
218 * @param {HTMLElement} container | |
219 * @param {string} url | |
220 * @param {function} onClick | |
221 * @constructor | |
222 */ | |
223 AudioPlayer.TrackInfo = function(container, url, onClick) { | |
224 this.url_ = url; | |
225 | |
226 var doc = container.ownerDocument; | |
227 | |
228 this.box_ = doc.createElement('div'); | |
229 this.box_.className = 'track'; | |
230 this.box_.addEventListener('click', onClick); | |
231 container.appendChild(this.box_); | |
232 | |
233 this.art_ = doc.createElement('div'); | |
234 this.art_.className = 'art blank'; | |
235 this.box_.appendChild(this.art_); | |
236 | |
237 this.img_ = doc.createElement('img'); | |
238 this.art_.appendChild(this.img_); | |
239 | |
240 this.data_ = doc.createElement('div'); | |
241 this.data_.className = 'data'; | |
242 this.box_.appendChild(this.data_); | |
243 | |
244 this.title_ = doc.createElement('div'); | |
245 this.title_.className = 'data-title'; | |
246 this.data_.appendChild(this.title_); | |
247 | |
248 this.artist_ = doc.createElement('div'); | |
249 this.artist_.className = 'data-artist'; | |
250 this.data_.appendChild(this.artist_); | |
251 }; | |
252 | |
253 AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ }; | |
254 | |
255 AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() { | |
256 var title = this.url_.split('/').pop(); | |
257 var dotIndex = title.lastIndexOf('.'); | |
258 if (dotIndex >= 0) title = title.substr(0, dotIndex); | |
259 return title; | |
260 }; | |
261 | |
262 AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() { | |
263 return 'Unknown Artist'; // TODO(kaznacheev): i18n | |
264 }; | |
265 | |
266 AudioPlayer.TrackInfo.prototype.setMetadata = function(metadata) { | |
267 if (metadata.thumbnailURL) { | |
268 this.art_.classList.remove('blank'); | |
269 this.img_.src = metadata.thumbnailURL; | |
270 } | |
271 this.title_.textContent = metadata.title || this.getDefaultTitle(); | |
272 this.artist_.textContent = metadata.artist || this.getDefaultArtist(); | |
273 }; | |
OLD | NEW |