• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2014 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(function() {
6  'use strict';
7
8  Polymer('track-list', {
9    /**
10     * Initializes an element. This method is called automatically when the
11     * element is ready.
12     */
13    ready: function() {
14      this.tracksObserver_ = new ArrayObserver(
15          this.tracks,
16          this.tracksValueChanged_.bind(this));
17
18      window.addEventListener('resize', this.onWindowResize_.bind(this));
19    },
20
21    /**
22     * Registers handlers for changing of external variables
23     */
24    observe: {
25      'model.shuffle': 'onShuffleChanged',
26    },
27
28    /**
29     * Model object of the Audio Player.
30     * @type {AudioPlayerModel}
31     */
32    model: null,
33
34    /**
35     * List of tracks.
36     * @type {Array.<AudioPlayer.TrackInfo>}
37     */
38    tracks: [],
39
40    /**
41     * Play order of the tracks. Each value is the index of 'this.tracks'.
42     * @type {Array.<number>}
43     */
44    playOrder: [],
45
46    /**
47     * Track index of the current track.
48     * If the tracks property is empty, it should be -1. Otherwise, be a valid
49     * track number.
50     *
51     * @type {number}
52     */
53    currentTrackIndex: -1,
54
55    /**
56     * Invoked when 'shuffle' property is changed.
57     * @param {boolean} oldValue Old value.
58     * @param {boolean} newValue New value.
59     */
60    onShuffleChanged: function(oldValue, newValue) {
61      this.generatePlayOrder(true /* keep the current track */);
62    },
63
64    /**
65     * Invoked when the current track index is changed.
66     * @param {number} oldValue old value.
67     * @param {number} newValue new value.
68     */
69    currentTrackIndexChanged: function(oldValue, newValue) {
70      if (oldValue === newValue)
71        return;
72
73      if (!isNaN(oldValue) && 0 <= oldValue && oldValue < this.tracks.length)
74        this.tracks[oldValue].active = false;
75
76      if (0 <= newValue && newValue < this.tracks.length) {
77        var currentPlayOrder = this.playOrder.indexOf(newValue);
78        if (currentPlayOrder !== -1) {
79          // Success
80          this.tracks[newValue].active = true;
81
82          this.ensureTrackInViewport_(newValue /* trackIndex */);
83          return;
84        }
85      }
86
87      // Invalid index
88      if (this.tracks.length === 0)
89        this.currentTrackIndex = -1;
90      else
91        this.generatePlayOrder(false /* no need to keep the current track */);
92    },
93
94    /**
95     * Invoked when 'tracks' property is changed.
96     * @param {Array.<TrackInfo>} oldValue Old value.
97     * @param {Array.<TrackInfo>} newValue New value.
98     */
99    tracksChanged: function(oldValue, newValue) {
100      // Note: Sometimes both oldValue and newValue are null though the actual
101      // values are not null. Maybe it's a bug of Polymer.
102
103      // Re-register the observer of 'this.tracks'.
104      this.tracksObserver_.close();
105      this.tracksObserver_ = new ArrayObserver(this.tracks);
106      this.tracksObserver_.open(this.tracksValueChanged_.bind(this));
107
108      if (this.tracks.length !== 0) {
109        // Restore the active track.
110        if (this.currentTrackIndex !== -1 &&
111            this.currentTrackIndex < this.tracks.length) {
112          this.tracks[this.currentTrackIndex].active = true;
113        }
114
115        // Reset play order and current index.
116        this.generatePlayOrder(false /* no need to keep the current track */);
117      } else {
118        this.playOrder = [];
119        this.currentTrackIndex = -1;
120      }
121    },
122
123    /**
124     * Invoked when the value in the 'tracks' is changed.
125     * @param {Array.<Object>} splices The detail of the change.
126     */
127    tracksValueChanged_: function(splices) {
128      if (this.tracks.length === 0)
129        this.currentTrackIndex = -1;
130      else
131        this.tracks[this.currentTrackIndex].active = true;
132    },
133
134    /**
135     * Invoked when the track element is clicked.
136     * @param {Event} event Click event.
137     */
138    trackClicked: function(event) {
139      var index = ~~event.currentTarget.getAttribute('index');
140      var track = this.tracks[index];
141      if (track)
142        this.selectTrack(track);
143    },
144
145    /**
146     * Invoked when the window is resized.
147     * @private
148     */
149    onWindowResize_: function() {
150      this.ensureTrackInViewport_(this.currentTrackIndex);
151    },
152
153    /**
154     * Scrolls the track list to ensure the given track in the viewport.
155     * @param {number} trackIndex The index of the track to be in the viewport.
156     * @private
157     */
158    ensureTrackInViewport_: function(trackIndex) {
159      var trackSelector = '.track[index="' + trackIndex + '"]';
160      var trackElement = this.querySelector(trackSelector);
161      if (trackElement) {
162        var viewTop = this.scrollTop;
163        var viewHeight = this.clientHeight;
164        var elementTop = trackElement.offsetTop;
165        var elementHeight = trackElement.offsetHeight;
166
167        if (elementTop < viewTop) {
168          // Adjust the tops.
169          this.scrollTop = elementTop;
170        } else if (elementTop + elementHeight <= viewTop + viewHeight) {
171          // The entire element is in the viewport. Do nothing.
172        } else {
173          // Adjust the bottoms.
174          this.scrollTop = Math.max(0,
175                                    (elementTop + elementHeight - viewHeight));
176        }
177      }
178    },
179
180    /**
181     * Invoked when the track element is clicked.
182     * @param {boolean} keepCurrentTrack Keep the current track or not.
183     */
184    generatePlayOrder: function(keepCurrentTrack) {
185      console.assert((keepCurrentTrack !== undefined),
186                     'The argument "forward" is undefined');
187
188      if (this.tracks.length === 0) {
189        this.playOrder = [];
190        return;
191      }
192
193      // Creates sequenced array.
194      this.playOrder =
195          this.tracks.
196          map(function(unused, index) { return index; });
197
198      if (this.model && this.model.shuffle) {
199        // Randomizes the play order array (Schwarzian-transform algorithm).
200        this.playOrder =
201            this.playOrder.
202            map(function(a) {
203              return {weight: Math.random(), index: a};
204            }).
205            sort(function(a, b) { return a.weight - b.weight }).
206            map(function(a) { return a.index });
207
208        if (keepCurrentTrack) {
209          // Puts the current track at the beginning of the play order.
210          this.playOrder =
211              this.playOrder.filter(function(value) {
212                return this.currentTrackIndex !== value;
213              }, this);
214          this.playOrder.splice(0, 0, this.currentTrackIndex);
215        }
216      }
217
218      if (!keepCurrentTrack)
219        this.currentTrackIndex = this.playOrder[0];
220    },
221
222    /**
223     * Sets the current track.
224     * @param {AudioPlayer.TrackInfo} track TrackInfo to be set as the current
225     *     track.
226     */
227    selectTrack: function(track) {
228      var index = -1;
229      for (var i = 0; i < this.tracks.length; i++) {
230        if (this.tracks[i].url === track.url) {
231          index = i;
232          break;
233        }
234      }
235      if (index >= 0) {
236        // TODO(yoshiki): Clean up the flow and the code around here.
237        if (this.currentTrackIndex == index)
238          this.replayCurrentTrack();
239        else
240          this.currentTrackIndex = index;
241      }
242    },
243
244    /**
245     * Request to replay the current music.
246     */
247    replayCurrentTrack: function() {
248      this.fire('replay');
249    },
250
251    /**
252     * Returns the current track.
253     * @param {AudioPlayer.TrackInfo} track TrackInfo of the current track.
254     */
255    getCurrentTrack: function() {
256      if (this.tracks.length === 0)
257        return null;
258
259      return this.tracks[this.currentTrackIndex];
260    },
261
262    /**
263     * Returns the next (or previous) track in the track list. If there is no
264     * next track, returns -1.
265     *
266     * @param {boolean} forward Specify direction: forward or previous mode.
267     *     True: forward mode, false: previous mode.
268     * @param {boolean} cyclic Specify if cyclically or not: It true, the first
269     *     track is succeeding to the last track, otherwise no track after the
270     *     last.
271     * @return {number} The next track index.
272     */
273    getNextTrackIndex: function(forward, cyclic)  {
274      if (this.tracks.length === 0)
275        return -1;
276
277      var defaultTrackIndex =
278          forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1];
279
280      var currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex);
281      console.assert(
282          (0 <= currentPlayOrder && currentPlayOrder < this.tracks.length),
283          'Insufficient TrackList.playOrder. The current track is not on the ' +
284            'track list.');
285
286      var newPlayOrder = currentPlayOrder + (forward ? +1 : -1);
287      if (newPlayOrder === -1 || newPlayOrder === this.tracks.length)
288        return cyclic ? defaultTrackIndex : -1;
289
290      var newTrackIndex = this.playOrder[newPlayOrder];
291      console.assert(
292          (0 <= newTrackIndex && newTrackIndex < this.tracks.length),
293          'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder);
294
295      return newTrackIndex;
296    },
297  });  // Polymer('track-list') block
298})();  // Anonymous closure
299