Commit 67f51b30 authored by Luciano Pacheco's avatar Luciano Pacheco Committed by Commit Bot

Audio Player: Polymer 3 JS module for the remaining Audio elements

Elements: <track-info-panel>, <track-list>, <control-panel> and
<audio-player>.

Move the <link> tag inside the <template> because this is enforced by
polymer_modulizer() this requires changing from rel="import" to normal
rel="stylesheet" which in turn requires to changed the URL to request
the CSS.

Fix all closure errors.

Bug: 1133186
Change-Id: I633ef710307afa8b1237a188a7b4d8932cbc0cdb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2526004Reviewed-by: default avatarJeremie Boulic <jboulic@chromium.org>
Commit-Queue: Jeremie Boulic <jboulic@chromium.org>
Auto-Submit: Luciano Pacheco <lucmult@chromium.org>
Cr-Commit-Position: refs/heads/master@{#825690}
parent 71481f0a
......@@ -30,6 +30,32 @@ js_library("audio_player") {
]
}
polymer_modulizer("audio_player") {
js_file = "audio_player.js"
html_file = "audio_player.html"
html_type = "dom-module"
preserve_url_scheme = true
auto_imports = [
"ui/file_manager/audio_player/elements/control_panel.html|AriaLabels",
"ui/file_manager/audio_player/elements/track_list.html|TrackInfo",
"third_party/polymer/v1_0/components-chromium/iron-behaviors/iron-control-state.html|IronControlState",
]
}
js_library("audio_player.m") {
sources = [
"$root_gen_dir/ui/file_manager/audio_player/elements/audio_player.m.js",
]
deps = [
":control_panel.m",
":track_info_panel.m",
":track_list.m",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
extra_deps = [ ":audio_player_module" ]
}
js_library("control_panel") {
deps = [
":repeat_button",
......@@ -38,6 +64,31 @@ js_library("control_panel") {
]
}
polymer_modulizer("control_panel") {
js_file = "control_panel.js"
html_file = "control_panel.html"
html_type = "dom-module"
preserve_url_scheme = true
auto_imports = [ "ui/webui/resources/html/assert.html|assert" ]
}
js_library("control_panel.m") {
sources = [
"$root_gen_dir/ui/file_manager/audio_player/elements/control_panel.m.js",
]
deps = [
":repeat_button.m",
"//third_party/polymer/v3_0/components-chromium/font-roboto:roboto",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/cr_elements/cr_slider:cr_slider.m",
"//ui/webui/resources/js:assert.m",
#TODO: Add dependency on //ui/file_manager/file_manager/elements/files_icon_button when it has Polymer3.
]
extra_deps = [ ":control_panel_module" ]
}
js_library("repeat_button") {
deps = [
"//third_party/polymer/v1_0/components-chromium/iron-behaviors:iron-button-state-extracted",
......@@ -48,9 +99,46 @@ js_library("repeat_button") {
js_library("track_info_panel") {
}
polymer_modulizer("track_info_panel") {
js_file = "track_info_panel.js"
html_file = "track_info_panel.html"
html_type = "dom-module"
preserve_url_scheme = true
}
js_library("track_info_panel.m") {
sources = [
"$root_gen_dir/ui/file_manager/audio_player/elements/track_info_panel.m.js",
]
deps = [
"//third_party/polymer/v3_0/components-chromium/font-roboto:roboto",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
"//ui/webui/resources/cr_elements/cr_button:cr_button.m",
]
extra_deps = [ ":track_info_panel_module" ]
}
js_library("track_list") {
}
polymer_modulizer("track_list") {
js_file = "track_list.js"
html_file = "track_list.html"
html_type = "dom-module"
preserve_url_scheme = true
}
js_library("track_list.m") {
sources =
[ "$root_gen_dir/ui/file_manager/audio_player/elements/track_list.m.js" ]
deps = [
"//third_party/polymer/v3_0/components-chromium/font-roboto:roboto",
"//third_party/polymer/v3_0/components-chromium/paper-ripple:paper-ripple",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
]
extra_deps = [ ":track_list_module" ]
}
polymer_modulizer("repeat_button") {
js_file = "repeat_button.js"
html_file = "repeat_button.html"
......@@ -76,11 +164,19 @@ js_library("repeat_button.m") {
"//third_party/polymer/v3_0/components-chromium/iron-behaviors:iron-button-state",
"//third_party/polymer/v3_0/components-chromium/iron-behaviors:iron-control-state",
"//third_party/polymer/v3_0/components-chromium/polymer:polymer_bundled",
#TODO: Add dependency on //ui/file_manager/file_manager/elements/files_toggle_ripple when it has Polymer3.
]
extra_deps = [ ":repeat_button_module" ]
}
js_type_check("closure_compile_jsmodules") {
is_polymer3 = true
deps = [ ":repeat_button.m" ]
deps = [
":audio_player.m",
":control_panel.m",
":repeat_button.m",
":track_info_panel.m",
":track_list.m",
]
}
......@@ -10,8 +10,8 @@
<link rel="import" href="control_panel.html">
<dom-module id="audio-player">
<link rel="import" type="css" href="audio_player.css">
<template>
<link rel="stylesheet" href="elements/audio_player.css">
<track-info-panel id="trackInfo" expanded="{{trackInfoExpanded}}"
aria-expand-artwork-label="[[ariaExpandArtworkLabel]]">
</track-info-panel>
......@@ -36,6 +36,5 @@
<audio id="audio"
volume="[[computeAudioVolume_(volume)]]"></audio>
</template>
<script src="audio_player.js"></script>
</dom-module>
......@@ -422,10 +422,11 @@ Polymer({
/**
* Invoked when dragging state of seek bar on control panel is changed.
* During the user is dragging it, audio playback is paused temporalily.
* @param {!CustomEvent<{value: boolean}>} e
* During the user is dragging it, audio playback is paused temporarily.
* @param {!Event} e
*/
onSeekingChanged_: function(e) {
e = /** @type {!CustomEvent<{value: boolean}>} */ (e);
if (e.detail.value && this.playing) {
this.$.audio.pause();
this.wasPlayingOnDragStart_ = true;
......
......@@ -6,13 +6,14 @@
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/polymer/v1_0/font-roboto/roboto.html">
<link rel="import" href="chrome://resources/html/assert.html">
<link rel="import" href="chrome://resources/cr_elements/cr_slider/cr_slider.html">
<link rel="import" href="chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/foreground/elements/files_icon_button.html">
<link rel="import" href="repeat_button.html">
<dom-module id="control-panel">
<link rel="import" type="css" href="control_panel.css">
<template>
<link rel="stylesheet" href="elements/control_panel.css">
<div class="controls">
<div class="upper-controls audio-controls">
<!-- Shuffle toggle button in the bottom line. -->
......@@ -75,6 +76,5 @@
</div>
</div>
</template>
<script src="control_panel.js"></script>
</dom-module>
......@@ -17,258 +17,259 @@
* volumeSlider: string,
* }}
*/
let AriaLabels;
/* #export */ let AriaLabels;
(function() {
'use strict';
/* #ignore */ 'use strict';
Polymer({
is: 'control-panel',
properties: {
/**
* Flag whether the audio is playing or paused. True if playing, or false
* paused.
*/
playing: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true,
observer: 'playingChanged_'
},
/**
* Current elapsed time in the current music in millisecond.
*/
time: {
type: Number,
value: 0,
},
/**
* Total length of the current music in millisecond.
*/
duration: {
type: Number,
value: 0,
},
/**
* Whether the shuffle button is ON.
*/
shuffle: {
type: Boolean,
value: false,
notify: true,
},
/**
* What mode the repeat button idicates.
* repeat-modes can be "no-repeat", "repeat-all", "repeat-one".
*/
repeatMode: {
type: String,
value: 'no-repeat',
notify: true,
},
/**
* The audio volume. 0 is silent, and 100 is maximum loud.
*/
volume: {
type: Number,
value: 50,
notify: true,
reflectToAttribute: true,
observer: 'volumeChanged_',
},
/**
* Whether the playlist is expanded or not.
*/
playlistExpanded: {
type: Boolean,
value: false,
notify: true,
},
/**
* Whether the knob of time slider is being dragged.
*/
dragging: {
type: Boolean,
value: false,
notify: true,
},
/**
* Dictionary which contains aria-labels for each controls.
* @type {AriaLabels}
*/
ariaLabels: {
type: Object,
observer: 'ariaLabelsChanged_',
},
},
Polymer({
is: 'control-panel',
properties: {
/**
* Initializes an element. This method is called automatically when the
* element is ready.
* Flag whether the audio is playing or paused. True if playing, or false
* paused.
*/
ready: function() {
const timeSlider = /** @type {!CrSliderElement} */ (this.$.timeSlider);
timeSlider.addEventListener('cr-slider-value-changed', () => {
this.fire('update-time', timeSlider.value);
});
const volumeSlider =
/** @type {!CrSliderElement} */ (this.$.volumeSlider);
volumeSlider.addEventListener('cr-slider-value-changed', () => {
this.volume = volumeSlider.value;
});
playing: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true,
observer: 'playingChanged_'
},
/**
* Invoked when the next button is clicked.
* Current elapsed time in the current music in millisecond.
*/
nextClick: function() {
this.fire('next-clicked');
time: {
type: Number,
value: 0,
},
/**
* Invoked when the play button is clicked.
* Total length of the current music in millisecond.
*/
playClick: function() {
this.playing = !this.playing;
duration: {
type: Number,
value: 0,
},
/**
* Invoked when the previous button is clicked.
* Whether the shuffle button is ON.
*/
previousClick: function() {
this.fire('previous-clicked');
shuffle: {
type: Boolean,
value: false,
notify: true,
},
/**
* Invoked when the volume button is clicked.
* What mode the repeat button indicates.
* repeat-modes can be "no-repeat", "repeat-all", "repeat-one".
*/
volumeClick: function() {
if (this.volume !== 0) {
this.savedVolume_ = this.volume;
this.volume = 0;
} else {
this.volume = this.savedVolume_ || 50;
}
repeatMode: {
type: String,
value: 'no-repeat',
notify: true,
},
/**
* @param {boolean} forward Whether to skip forward/backword.
* The audio volume. 0 is silent, and 100 is maximum loud.
*/
smallSkip: function(forward) {
this.skip_(true /* small */, forward);
volume: {
type: Number,
value: 50,
notify: true,
reflectToAttribute: true,
observer: 'volumeChanged_',
},
/**
* @param {boolean} forward Whether to skip forward/backword.
* Whether the playlist is expanded or not.
*/
bigSkip: function(forward) {
this.skip_(false /* small */, forward);
playlistExpanded: {
type: Boolean,
value: false,
notify: true,
},
/**
* Skips small min(5 seconds, 10% of duration) or large
* min(10 seconds, 20% of duration).
* @param {boolean} small Whether to skip small/large interval.
* @param {boolean} forward Whether to skip forward/backword.
* @private
* Whether the knob of time slider is being dragged.
*/
skip_: function(small, forward) {
const maxSkip = small ? 5000 : 10000;
const percentOfDuration = (small ? .1 : .2) * this.duration;
const update = (forward ? 1 : -1) * Math.min(maxSkip, percentOfDuration);
if (this.duration > 0) {
this.fire(
'update-time',
Math.max(Math.min(this.time + update, this.duration), 0));
}
dragging: {
type: Boolean,
value: false,
notify: true,
},
/**
* Converts the time into human friendly string.
* @param {number} time Time to be converted.
* @return {string} String representation of the given time
* Dictionary which contains aria-labels for each controls.
* @type {AriaLabels}
*/
time2string_: function(time) {
return ~~(time / 60000) + ':' + ('0' + ~~(time / 1000 % 60)).slice(-2);
ariaLabels: {
type: Object,
observer: 'ariaLabelsChanged_',
},
},
/**
* Converts the time and duration into human friendly string.
* @param {number} time Time to be converted.
* @param {number} duration Duration to be converted.
* @return {string} String representation of the given time
*/
computeTimeString_: function(time, duration) {
return this.time2string_(time) + ' / ' + this.time2string_(duration);
},
/**
* Initializes an element. This method is called automatically when the
* element is ready.
*/
ready: function() {
const timeSlider = /** @type {!CrSliderElement} */ (this.$.timeSlider);
timeSlider.addEventListener('cr-slider-value-changed', () => {
this.fire('update-time', timeSlider.value);
});
/**
* Invoked when the playing property is changed.
* @param {boolean} playing
* @private
*/
playingChanged_: function(playing) {
if (this.ariaLabels) {
this.$.play.setAttribute('aria-label',
playing ? this.ariaLabels.pause : this.ariaLabels.play);
}
},
const volumeSlider =
/** @type {!CrSliderElement} */ (this.$.volumeSlider);
volumeSlider.addEventListener('cr-slider-value-changed', () => {
this.volume = volumeSlider.value;
});
},
/**
* Invoked when the volume property is changed.
* @param {number} volume
* @private
*/
volumeChanged_: function(volume) {
if (!this.$.volumeSlider.dragging) {
this.$.volumeSlider.value = volume;
}
/**
* Invoked when the next button is clicked.
*/
nextClick: function() {
this.fire('next-clicked');
},
if (this.ariaLabels) {
this.$.volumeButton.setAttribute('aria-label',
volume !== 0 ? this.ariaLabels.mute : this.ariaLabels.unmute);
}
},
/**
* Invoked when the play button is clicked.
*/
playClick: function() {
this.playing = !this.playing;
},
/**
* @param {!CustomEvent<{value: boolean}>} e
* @private
*/
onSeekingChanged_: function(e) {
this.fire('seeking-changed', e.detail);
},
/**
* Invoked when the previous button is clicked.
*/
previousClick: function() {
this.fire('previous-clicked');
},
/**
* Invoked when the ariaLabels property is changed.
* @param {Object} ariaLabels
* @private
*/
ariaLabelsChanged_: function(ariaLabels) {
assert(ariaLabels);
// TODO(fukino): Use data bindings.
this.$.volumeSlider.setAttribute('aria-label', ariaLabels.volumeSlider);
this.$.shuffle.setAttribute('aria-label', ariaLabels.shuffle);
this.$.repeat.setAttribute('aria-label', ariaLabels.repeat);
this.$.previous.setAttribute('aria-label', ariaLabels.previous);
this.$.play.setAttribute('aria-label',
this.playing ? ariaLabels.pause : ariaLabels.play);
this.$.next.setAttribute('aria-label', ariaLabels.next);
this.$.playList.setAttribute('aria-label', ariaLabels.playList);
this.$.timeSlider.setAttribute('aria-label', ariaLabels.seekSlider);
this.$.volumeButton.setAttribute('aria-label',
this.volume !== 0 ? ariaLabels.mute : ariaLabels.unmute);
this.$.volumeSlider.setAttribute('aria-label', ariaLabels.volumeSlider);
},
});
/**
* Invoked when the volume button is clicked.
*/
volumeClick: function() {
if (this.volume !== 0) {
this.savedVolume_ = this.volume;
this.volume = 0;
} else {
this.volume = this.savedVolume_ || 50;
}
},
/**
* @param {boolean} forward Whether to skip forward/backword.
*/
smallSkip: function(forward) {
this.skip_(true /* small */, forward);
},
/**
* @param {boolean} forward Whether to skip forward/backword.
*/
bigSkip: function(forward) {
this.skip_(false /* small */, forward);
},
/**
* Skips small min(5 seconds, 10% of duration) or large
* min(10 seconds, 20% of duration).
* @param {boolean} small Whether to skip small/large interval.
* @param {boolean} forward Whether to skip forward/backword.
* @private
*/
skip_: function(small, forward) {
const maxSkip = small ? 5000 : 10000;
const percentOfDuration = (small ? .1 : .2) * this.duration;
const update = (forward ? 1 : -1) * Math.min(maxSkip, percentOfDuration);
if (this.duration > 0) {
this.fire(
'update-time',
Math.max(Math.min(this.time + update, this.duration), 0));
}
},
/**
* Converts the time into human friendly string.
* @param {number} time Time to be converted.
* @return {string} String representation of the given time
*/
time2string_: function(time) {
return ~~(time / 60000) + ':' + ('0' + ~~(time / 1000 % 60)).slice(-2);
},
/**
* Converts the time and duration into human friendly string.
* @param {number} time Time to be converted.
* @param {number} duration Duration to be converted.
* @return {string} String representation of the given time
*/
computeTimeString_: function(time, duration) {
return this.time2string_(time) + ' / ' + this.time2string_(duration);
},
/**
* Invoked when the playing property is changed.
* @param {boolean} playing
* @private
*/
playingChanged_: function(playing) {
if (this.ariaLabels) {
this.$.play.setAttribute(
'aria-label', playing ? this.ariaLabels.pause : this.ariaLabels.play);
}
},
/**
* Invoked when the volume property is changed.
* @param {number} volume
* @private
*/
volumeChanged_: function(volume) {
if (!this.$.volumeSlider.dragging) {
this.$.volumeSlider.value = volume;
}
if (this.ariaLabels) {
this.$.volumeButton.setAttribute(
'aria-label',
volume !== 0 ? this.ariaLabels.mute : this.ariaLabels.unmute);
}
},
/**
* @param {!CustomEvent<{value: boolean}>} e
* @private
*/
onSeekingChanged_: function(e) {
this.fire('seeking-changed', e.detail);
},
/**
* Invoked when the ariaLabels property is changed.
* @param {Object} ariaLabels
* @private
*/
ariaLabelsChanged_: function(ariaLabels) {
assert(ariaLabels);
// TODO(fukino): Use data bindings.
this.$.volumeSlider.setAttribute('aria-label', ariaLabels.volumeSlider);
this.$.shuffle.setAttribute('aria-label', ariaLabels.shuffle);
this.$.repeat.setAttribute('aria-label', ariaLabels.repeat);
this.$.previous.setAttribute('aria-label', ariaLabels.previous);
this.$.play.setAttribute(
'aria-label', this.playing ? ariaLabels.pause : ariaLabels.play);
this.$.next.setAttribute('aria-label', ariaLabels.next);
this.$.playList.setAttribute('aria-label', ariaLabels.playList);
this.$.timeSlider.setAttribute('aria-label', ariaLabels.seekSlider);
this.$.volumeButton.setAttribute(
'aria-label', this.volume !== 0 ? ariaLabels.mute : ariaLabels.unmute);
this.$.volumeSlider.setAttribute('aria-label', ariaLabels.volumeSlider);
},
});
})(); // Anonymous closure
......@@ -9,8 +9,8 @@
<link rel="import" href="chrome://resources/polymer/v1_0/font-roboto/roboto.html">
<dom-module id="track-info-panel">
<link rel="import" type="css" href="track_info_panel.css">
<template>
<link rel="stylesheet" href="elements/track_info_panel.css">
<div class="track-wrapper">
<div class="icon-unavailable-expanded"></div>
<div class="icon-expanded" style="background-image: url([[track.artworkUrl]]);"></div>
......@@ -28,7 +28,6 @@
</div>
</div>
</template>
<script src="track_info_panel.js"></script>
</dom-module>
......@@ -3,42 +3,42 @@
// found in the LICENSE file.
(function() {
'use strict';
Polymer({
is: 'track-info-panel',
properties: {
track: {
type: Object,
value: null,
},
expanded: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true,
observer: 'onExpandedChanged_',
},
artworkAvailable: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
ariaExpandArtworkLabel: String,
/* #ignore */ 'use strict';
Polymer({
is: 'track-info-panel',
properties: {
track: {
type: Object,
value: null,
},
/** @private */
onExpandClick_: function() {
this.expanded = !this.expanded;
expanded: {
type: Boolean,
value: false,
notify: true,
reflectToAttribute: true,
observer: 'onExpandedChanged_',
},
/** @private */
onExpandedChanged_: function() {
this.$.expand.setAttribute('aria-expanded', Boolean(this.expanded));
artworkAvailable: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
});
ariaExpandArtworkLabel: String,
},
/** @private */
onExpandClick_: function() {
this.expanded = !this.expanded;
},
/** @private */
onExpandedChanged_: function() {
this.$.expand.setAttribute('aria-expanded', Boolean(this.expanded));
},
});
})(); // Anonymous closure
......@@ -9,8 +9,8 @@
<link rel="import" href="chrome://resources/polymer/v1_0/paper-ripple/paper-ripple.html">
<dom-module id="track-list">
<link rel="import" type="css" href="track_list.css">
<template>
<link rel="stylesheet" href="elements/track_list.css">
<template is="dom-repeat" id="tracks" items="[[tracks]]">
<div class="track" active$="[[item.active]]" index$="[[index]]" on-click="trackClicked">
<div class="icon"></div>
......@@ -22,6 +22,5 @@
</div>
</template>
</template>
<script src="track_list.js"></script>
</dom-module>
......@@ -11,291 +11,290 @@
* active: boolean
* }}
*/
let TrackInfo;
/* #export */ let TrackInfo;
(function() {
'use strict';
Polymer({
is: 'track-list',
properties: {
/**
* List of tracks.
*/
tracks: {
type: Array,
value: [],
observer: 'tracksChanged',
},
/**
* Track index of the current track.
* If the tracks property is empty, it should be -1. Otherwise, be a valid
* track number.
*/
currentTrackIndex: {
type: Number,
value: -1,
observer: 'currentTrackIndexChanged',
notify: true,
},
/**
* Whether shuffling play order is enabled or not.
*/
shuffle: {
type: Boolean,
value: false,
observer: 'shuffleChanged',
},
/**
* Whether playlist is expanded or not.
*/
expanded: {
type: Boolean,
value: false,
observer: 'expandedChanged',
}
},
/**
* Play order of the tracks. Each value is the index of 'this.tracks'.
* @type {Array<number>}
*/
playOrder: [],
/**
* Invoked when 'expanded' property is changed.
* @param {boolean} newValue New value.
* @param {boolean} oldValue Old value.
*/
expandedChanged: function(newValue, oldValue) {
this.ensureTrackInViewport_(this.currentTrackIndex);
},
/* #ignore */ 'use strict';
/**
* Invoked when 'shuffle' property is changed.
* @param {boolean} newValue New value.
* @param {boolean} oldValue Old value.
*/
shuffleChanged: function(newValue, oldValue) {
this.generatePlayOrder(true /* keep the current track */);
},
Polymer({
is: 'track-list',
properties: {
/**
* Invoked when the current track index is changed.
* @param {number} newValue new value.
* @param {number} oldValue old value.
* List of tracks.
*/
currentTrackIndexChanged: function(newValue, oldValue) {
if (oldValue === newValue) {
return;
}
if (!isNaN(oldValue) && 0 <= oldValue && oldValue < this.tracks.length) {
this.set('tracks.' + oldValue + '.active', false);
}
if (0 <= newValue && newValue < this.tracks.length) {
const currentPlayOrder = this.playOrder.indexOf(newValue);
if (currentPlayOrder !== -1) {
// Success
this.set('tracks.' + newValue + '.active', true);
this.ensureTrackInViewport_(newValue /* trackIndex */);
return;
}
}
// Invalid index
if (this.tracks.length === 0) {
this.currentTrackIndex = -1;
} else {
this.generatePlayOrder(false /* no need to keep the current track */);
}
tracks: {
type: Array,
value: [],
observer: 'tracksChanged',
},
/**
* Invoked when 'tracks' property is changed.
* @param {Array<!TrackInfo>} newValue New value.
* @param {Array<!TrackInfo>} oldValue Old value.
* Track index of the current track.
* If the tracks property is empty, it should be -1. Otherwise, be a valid
* track number.
*/
tracksChanged: function(newValue, oldValue) {
// Note: Sometimes both oldValue and newValue are null though the actual
// values are not null. Maybe it's a bug of Polymer.
if (this.tracks.length !== 0) {
// Restore the active track.
if (this.currentTrackIndex !== -1 &&
this.currentTrackIndex < this.tracks.length) {
this.set('tracks.' + this.currentTrackIndex + '.active', true);
}
// Reset play order and current index.
this.generatePlayOrder(false /* no need to keep the current track */);
} else {
this.playOrder = [];
this.currentTrackIndex = -1;
}
currentTrackIndex: {
type: Number,
value: -1,
observer: 'currentTrackIndexChanged',
notify: true,
},
/**
* Invoked when the track element is clicked.
* @param {Event} event Click event.
* Whether shuffling play order is enabled or not.
*/
trackClicked: function(event) {
const index = ~~event.currentTarget.getAttribute('index');
const track = this.tracks[index];
if (track) {
this.selectTrack(track);
}
shuffle: {
type: Boolean,
value: false,
observer: 'shuffleChanged',
},
/**
* Scrolls the track list to ensure the given track in the viewport.
* @param {number} trackIndex The index of the track to be in the viewport.
* @private
* Whether playlist is expanded or not.
*/
ensureTrackInViewport_: function(trackIndex) {
const trackElement = this.$$('.track[index="' + trackIndex + '"]');
if (trackElement) {
const viewTop = this.scrollTop;
const viewHeight = this.clientHeight;
const elementTop = trackElement.offsetTop - this.offsetTop;
const elementHeight = trackElement.offsetHeight;
if (elementTop <= viewTop) {
// Adjust the tops.
this.scrollTop = elementTop;
} else if (elementTop + elementHeight >= viewTop + viewHeight) {
// Adjust the bottoms.
this.scrollTop = Math.max(0,
(elementTop + elementHeight - viewHeight));
} else {
// The entire element is in the viewport. Do nothing.
}
}
},
/**
* Invoked when the track element is clicked.
* @param {boolean} keepCurrentTrack Keep the current track or not.
*/
generatePlayOrder: function(keepCurrentTrack) {
console.assert((keepCurrentTrack !== undefined),
'The argument "forward" is undefined');
if (this.tracks.length === 0) {
this.playOrder = [];
expanded: {
type: Boolean,
value: false,
observer: 'expandedChanged',
}
},
/**
* Play order of the tracks. Each value is the index of 'this.tracks'.
* @type {Array<number>}
*/
playOrder: [],
/**
* Invoked when 'expanded' property is changed.
* @param {boolean} newValue New value.
* @param {boolean} oldValue Old value.
*/
expandedChanged: function(newValue, oldValue) {
this.ensureTrackInViewport_(this.currentTrackIndex);
},
/**
* Invoked when 'shuffle' property is changed.
* @param {boolean} newValue New value.
* @param {boolean} oldValue Old value.
*/
shuffleChanged: function(newValue, oldValue) {
this.generatePlayOrder(true /* keep the current track */);
},
/**
* Invoked when the current track index is changed.
* @param {number} newValue new value.
* @param {number} oldValue old value.
*/
currentTrackIndexChanged: function(newValue, oldValue) {
if (oldValue === newValue) {
return;
}
if (!isNaN(oldValue) && 0 <= oldValue && oldValue < this.tracks.length) {
this.set('tracks.' + oldValue + '.active', false);
}
if (0 <= newValue && newValue < this.tracks.length) {
const currentPlayOrder = this.playOrder.indexOf(newValue);
if (currentPlayOrder !== -1) {
// Success
this.set('tracks.' + newValue + '.active', true);
this.ensureTrackInViewport_(newValue /* trackIndex */);
return;
}
// Creates sequenced array.
this.playOrder = this.tracks.map(function(unused, index) {
return index;
});
if (this.shuffle) {
// Randomizes the play order array (Schwarzian-transform algorithm).
this.playOrder = this.playOrder
.map(function(a) {
return {weight: Math.random(), index: a};
})
.sort(function(a, b) {
return a.weight - b.weight;
})
.map(function(a) {
return a.index;
});
if (keepCurrentTrack) {
// Puts the current track at the beginning of the play order.
this.playOrder = this.playOrder
.filter(function(value) {
return this.currentTrackIndex !== value;
}, this);
this.playOrder.splice(0, 0, this.currentTrackIndex);
}
}
// Invalid index
if (this.tracks.length === 0) {
this.currentTrackIndex = -1;
} else {
this.generatePlayOrder(false /* no need to keep the current track */);
}
},
/**
* Invoked when 'tracks' property is changed.
* @param {Array<!TrackInfo>} newValue New value.
* @param {Array<!TrackInfo>} oldValue Old value.
*/
tracksChanged: function(newValue, oldValue) {
// Note: Sometimes both oldValue and newValue are null though the actual
// values are not null. Maybe it's a bug of Polymer.
if (this.tracks.length !== 0) {
// Restore the active track.
if (this.currentTrackIndex !== -1 &&
this.currentTrackIndex < this.tracks.length) {
this.set('tracks.' + this.currentTrackIndex + '.active', true);
}
if (!keepCurrentTrack) {
this.currentTrackIndex = this.playOrder[0];
}
},
/**
* Sets the current track.
* @param {!TrackInfo} track TrackInfo to be set as the current
* track.
*/
selectTrack: function(track) {
let index = -1;
for (let i = 0; i < this.tracks.length; i++) {
if (this.tracks[i].url === track.url) {
index = i;
break;
}
// Reset play order and current index.
this.generatePlayOrder(false /* no need to keep the current track */);
} else {
this.playOrder = [];
this.currentTrackIndex = -1;
}
},
/**
* Invoked when the track element is clicked.
* @param {Event} event Click event.
*/
trackClicked: function(event) {
const index = ~~event.currentTarget.getAttribute('index');
const track = this.tracks[index];
if (track) {
this.selectTrack(track);
}
},
/**
* Scrolls the track list to ensure the given track in the viewport.
* @param {number} trackIndex The index of the track to be in the viewport.
* @private
*/
ensureTrackInViewport_: function(trackIndex) {
const trackElement = this.$$('.track[index="' + trackIndex + '"]');
if (trackElement) {
const viewTop = this.scrollTop;
const viewHeight = this.clientHeight;
const elementTop = trackElement.offsetTop - this.offsetTop;
const elementHeight = trackElement.offsetHeight;
if (elementTop <= viewTop) {
// Adjust the tops.
this.scrollTop = elementTop;
} else if (elementTop + elementHeight >= viewTop + viewHeight) {
// Adjust the bottoms.
this.scrollTop = Math.max(0, (elementTop + elementHeight - viewHeight));
} else {
// The entire element is in the viewport. Do nothing.
}
if (index >= 0) {
if (this.currentTrackIndex === index) {
this.fire('replay');
} else {
this.currentTrackIndex = index;
this.fire('play');
}
}
},
/**
* Invoked when the track element is clicked.
* @param {boolean} keepCurrentTrack Keep the current track or not.
*/
generatePlayOrder: function(keepCurrentTrack) {
console.assert(
(keepCurrentTrack !== undefined),
'The argument "forward" is undefined');
if (this.tracks.length === 0) {
this.playOrder = [];
return;
}
// Creates sequenced array.
this.playOrder = this.tracks.map(function(unused, index) {
return index;
});
if (this.shuffle) {
// Randomizes the play order array (Schwarzian-transform algorithm).
this.playOrder = this.playOrder
.map(function(a) {
return {weight: Math.random(), index: a};
})
.sort(function(a, b) {
return a.weight - b.weight;
})
.map(function(a) {
return a.index;
});
if (keepCurrentTrack) {
// Puts the current track at the beginning of the play order.
this.playOrder = this.playOrder.filter(function(value) {
return this.currentTrackIndex !== value;
}, this);
this.playOrder.splice(0, 0, this.currentTrackIndex);
}
},
/**
* Returns the current track.
* @return {TrackInfo} track TrackInfo of the current track.
*/
getCurrentTrack: function() {
if (this.tracks.length === 0) {
return null;
}
if (!keepCurrentTrack) {
this.currentTrackIndex = this.playOrder[0];
}
},
/**
* Sets the current track.
* @param {!TrackInfo} track TrackInfo to be set as the current
* track.
*/
selectTrack: function(track) {
let index = -1;
for (let i = 0; i < this.tracks.length; i++) {
if (this.tracks[i].url === track.url) {
index = i;
break;
}
return this.tracks[this.currentTrackIndex];
},
/**
* Returns the next (or previous) track in the track list. If there is no
* next track, returns -1.
*
* @param {boolean} forward Specify direction: forward or previous mode.
* True: forward mode, false: previous mode.
* @param {boolean} cyclic Specify if cyclically or not: It true, the first
* track is succeeding to the last track, otherwise no track after the
* last.
* @return {number} The next track index.
*/
getNextTrackIndex: function(forward, cyclic) {
if (this.tracks.length === 0) {
return -1;
}
const defaultTrackIndex =
forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1];
const currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex);
console.assert(
(0 <= currentPlayOrder && currentPlayOrder < this.tracks.length),
'Insufficient TrackList.playOrder. The current track is not on the ' +
'track list.');
const newPlayOrder = currentPlayOrder + (forward ? +1 : -1);
if (newPlayOrder === -1 || newPlayOrder === this.tracks.length) {
return cyclic ? defaultTrackIndex : -1;
}
if (index >= 0) {
if (this.currentTrackIndex === index) {
this.fire('replay');
} else {
this.currentTrackIndex = index;
this.fire('play');
}
const newTrackIndex = this.playOrder[newPlayOrder];
console.assert(
(0 <= newTrackIndex && newTrackIndex < this.tracks.length),
'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder);
return newTrackIndex;
},
});
}
},
/**
* Returns the current track.
* @return {TrackInfo} track TrackInfo of the current track.
*/
getCurrentTrack: function() {
if (this.tracks.length === 0) {
return null;
}
return this.tracks[this.currentTrackIndex];
},
/**
* Returns the next (or previous) track in the track list. If there is no
* next track, returns -1.
*
* @param {boolean} forward Specify direction: forward or previous mode.
* True: forward mode, false: previous mode.
* @param {boolean} cyclic Specify if cyclically or not: It true, the first
* track is succeeding to the last track, otherwise no track after the
* last.
* @return {number} The next track index.
*/
getNextTrackIndex: function(forward, cyclic) {
if (this.tracks.length === 0) {
return -1;
}
const defaultTrackIndex =
forward ? this.playOrder[0] : this.playOrder[this.tracks.length - 1];
const currentPlayOrder = this.playOrder.indexOf(this.currentTrackIndex);
console.assert(
(0 <= currentPlayOrder && currentPlayOrder < this.tracks.length),
'Insufficient TrackList.playOrder. The current track is not on the ' +
'track list.');
const newPlayOrder = currentPlayOrder + (forward ? +1 : -1);
if (newPlayOrder === -1 || newPlayOrder === this.tracks.length) {
return cyclic ? defaultTrackIndex : -1;
}
const newTrackIndex = this.playOrder[newPlayOrder];
console.assert(
(0 <= newTrackIndex && newTrackIndex < this.tracks.length),
'Insufficient TrackList.playOrder. New Play Order: ' + newPlayOrder);
return newTrackIndex;
},
});
})(); // Anonymous closure
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment