Commit 4e8e8627 authored by yoshiki@chromium.org's avatar yoshiki@chromium.org

[Files.app] Initial implementation of new audio player

Implements the new audio player of Files.app. See the issue for detail. Although it has still room to implement and improve, but basically it works.

BUG=273308
TEST=manually tested
R=dgozman@chromium.org, mtomasz@chromium.org

Review URL: https://codereview.chromium.org/144883002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@247833 0039d316-1c4b-4281-b951-d872f2087c98
parent c50082b8
......@@ -112,6 +112,12 @@ Press any key to continue exploring.
<message name="IDS_FLAGS_FILE_MANAGER_SHOW_CHECKBOXES_DESCRIPTION" desc="Description of the about:flag option to show the selecting checkboxes to the Files app.">
Show selecting checkboxes in the Files.app.
</message>
<message name="IDS_FLAGS_FILE_MANAGER_ENABLE_NEW_AUDIO_PLAYER_NAME" desc="Name of the about:flag option to enable the new audio player app.">
Enable the new audio player
</message>
<message name="IDS_FLAGS_FILE_MANAGER_ENABLE_NEW_AUDIO_PLAYER_DESCRIPTION" desc="Description of the about:flag option to the enable new audio player app.">
Use the experimental new audio player instead of the stable one.
</message>
<message name="IDS_FLAGS_FILE_MANAGER_ENABLE_WEBSTORE_INTEGRATION" desc="Name of the about:flag option to enable the webstore integration feature in Files app.">
Enable the integration of Webstore and Files.app.
</message>
......
......@@ -1150,6 +1150,13 @@ const Experiment kExperiments[] = {
kOsCrOS,
SINGLE_VALUE_TYPE(chromeos::switches::kFileManagerShowCheckboxes)
},
{
"enable-new-audio-player",
IDS_FLAGS_FILE_MANAGER_ENABLE_NEW_AUDIO_PLAYER_NAME,
IDS_FLAGS_FILE_MANAGER_ENABLE_NEW_AUDIO_PLAYER_DESCRIPTION,
kOsCrOS,
SINGLE_VALUE_TYPE(chromeos::switches::kFileManagerEnableNewAudioPlayer)
},
{
"file-manager-enable-webstore-integration",
IDS_FLAGS_FILE_MANAGER_ENABLE_WEBSTORE_INTEGRATION,
......
......@@ -91,6 +91,9 @@
<include name="IDR_FILE_MANAGER_MEDIAPLAYER" file="file_manager/mediaplayer.html" allowexternalscript="true" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_MEDIAPLAYER_JS" file="file_manager/foreground/js/media/mediaplayer_scripts.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_AUDIO_PLAYER" file="file_manager/audio_player.html" allowexternalscript="true" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_AUDIO_PLAYER_SCRIPTS_JS" file="file_manager/audio_player/js/audio_player_scripts.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_VIDEO_PLAYER" file="file_manager/video_player.html" allowexternalscript="true" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_VIDEO_PLAYER_JS" file="file_manager/foreground/js/media/video_player_scripts.js" flattenhtml="true" type="BINDATA" />
......
<!--
-- Copyright 2014 The Chromium Authors. All rights reserved.
-- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file.
-->
<!DOCTYPE HTML>
<html>
<head>
<!-- We have to set some default title, or chrome will use the page name.
-- As soon as the i18n'd strings are loaded we replace it with the correct
-- string. Until then, use an invisible non-whitespace character.
-->
<title>&#xFEFF;</title>
<link rel="stylesheet" type="text/css" href="audio_player/css/audio_player.css">
<!-- Don't load mediaplayer_scripts.js when flattening is disabled -->
<if expr="0"><!-- </if>
<script src="audio_player/js/audio_player_scripts.js"></script>
<if expr="0"> --></if>
<if expr="0">
<!-- This section is used when the file manager is loaded with
'filemgr-ext-path' command-line flag. -->
<!-- Keep the list in sync with audio_player_scripts.js. -->
<script src="../../../../ui/webui//resources/js/cr.js"></script>
<script src="../../../../ui/webui/resources/js/cr/event_target.js"></script>
<script src="../../../../ui/webui/resources/js/cr/ui/array_data_model.js">
</script>
<script src="../../../../third_party/polymer/platform/platform.js"></script>
<script src="../../../../third_party/polymer/polymer/polymer.js"></script>
<script src="common/js/async_util.js"></script>
<script src="common/js/util.js"></script>
<script src="common/js/path_util.js"></script>
<script src="foreground/js/file_type.js"></script>
<script src="foreground/js/volume_manager_wrapper.js"></script>
<script src="foreground/js/metadata/metadata_cache.js"></script>
<script src="audio_player/js/audio_player.js"></script>
<script src="foreground/js/media/player_testapi.js"></script>
<script src="audio_player/elements/track_list.js"></script>
<script src="audio_player/elements/control_panel.js"></script>
<script src="audio_player/elements/volume_controller.js"></script>
<script src="audio_player/elements/audio_player.js"></script>
</if>
</head>
<body>
<!-- Definition of <track-list> tag. -->
<polymer-element name="track-list" attributes="tracks expanded">
<template>
<link rel="stylesheet" href="audio_player/elements/track_list.css"></link>
<template id="tracks" repeat="{{tracks}}">
<div class="track" active="{{active}}" on-click="{{trackClicked}}">
<div class="data">
<div class="data-title">{{title}}</div>
<div class="data-artist">{{artist}}</div>
</div>
</div>
</template>
</template>
</polymer-element>
<!-- Definition of <control-panel> tag. -->
<polymer-element name="control-panel" attributes="expanded">
<template>
<link rel="stylesheet"
href="audio_player/elements/control_panel.css"></link>
<div class="controls">
<div class="upper-controls time-controls">
<div class="time media-control">
<div class="current">{{timeString_}}</div>
</div>
<div class="progress media-control custom-slider">
<input name="timeInput"
type="range"
min="0" max="{{duration}}" value="{{time}}">
<div class="bar">
<div class="filled" style="width: {{time/duration*100}}%;"></div>
<div class="cap left"></div>
<div class="cap right"></div>
</div>
</div>
<div class="time media-control">
<div class="duration">{{durationString_}}</div>
</div>
</div>
<div class="lower-controls audio-controls">
<!-- Shuffle toggle button in the bottom line. -->
<div class="shuffle-mode media-button toggle" state="default">
<label>
<input id="shuffleCheckbox"
type="checkbox"
checked="{{shuffle}}"></input>
<div class="icon"></div>
</label>
</div>
<!-- Repeat toggle button in the bottom line. -->
<div class="repeat media-button toggle" state="default">
<label>
<input id="repeatCheckbox"
type="checkbox"
checked="{{repeat}}"></input>
<div class="icon"></div>
</label>
</div>
<!-- Prev button in the bottom line. -->
<div class="previous media-button"
state="default"
on-click="{{previousClick}}">
<div class="normal default"></div>
<div class="disabled"></div>
</div>
<!-- Play button in the bottom line. -->
<div class="play media-control media-button"
state='{{playing ? "playing" : "ended"}}'
on-click="{{playClick}}">
<div class="normal playing"></div>
<div class="normal ended"></div>
<div class="disabled"></div>
</div>
<!-- Next button in the bottom line. -->
<div class="next media-button"
state="default"
on-click="{{nextClick}}">
<div class="normal default"></div>
<div class="disabled"></div>
</div>
<div id="volumeContainer"
class="default-hidden"
anchor-point="bottom center">
<volume-controller id="volumeSlider"
width="32" height="85" value="50"
on-changed="{{volumeSliderChanged}}">
</volume-controller>
<polymer-anchor-point id="anchorHelper"></polymer-anchor-point>
</div>
<!-- Volume button in the bottom line. -->
<div id="volumeButton"
class="volume media-button toggle"
state="default"
on-click="{{volumeButtonClick}}"
anchor-point="bottom center">
<label>
<input type="checkbox" checked="{{volumeSliderShown}}"></input>
<div class="icon"></div>
</label>
</div>
<!-- Playlist button in the bottom line. -->
<div id="playlistButton"
class="playlist media-button toggle"
state="default">
<label>
<input type="checkbox" checked="{{playlistExpanded}}"></input>
<div class="icon"></div>
</label>
</div>
</div>
</div>
</template>
</polymer-element>
<!-- Definition of <volume-controller> tag. -->
<polymer-element name="volume-controller" attributes="width height value">
<template>
<link rel="stylesheet"
href="audio_player/elements/volume_controller.css"></link>
<div id="background"></div>
<input name="rawValueInput" id="rawValueInput"
type="range" min="0" max="100" value="{{rawValue}}">
<div id="bar">
<div class="filled" style="height: {{rawValue}}%;"></div>
<div class="cap left"></div>
<div class="cap right"></div>
</div>
</template>
</polymer-element>
<!-- Definition of <audio-player> tag. -->
<polymer-element name="audio-player" attributes="tracks">
<template>
<track-list id="trackList" expanded="true"></track-list>
<control-panel id="audioController"
on-next-clicked="{{onControllerNextClicked}}"
on-previous-clicked="{{onControllerPreviousClicked}}">
</control-panel>
<audio id="audio"></audio>
</template>
</polymer-element>
<div class="audio-player">
<!-- Place the audio player. -->
<audio-player></audio-player>
</div>
</body>
</html>
/* Copyright 2013 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
html {
height: 100%;
}
body {
-webkit-user-select: none;
display: flex;
flex-direction: column;
height: 100%;
margin: 0;
overflow: hidden;
padding: 0;
}
.header {
background: #fff;
border-bottom: 1px solid #ddd;
height: 32px;
}
.audio-player {
color: #3d3d3d;
cursor: default;
flex: 1 1 auto;
font-family: Open Sans, Droid Sans Fallback, sans-serif;
font-size: 10pt;
position: relative;
}
.audio-player:not(.collapsed):not(.single-track) > .title-button {
background-color: #1f1f1f;
}
/* Customized scrollbar for the playlist. */
::-webkit-scrollbar {
height: 16px;
width: 16px;
}
::-webkit-scrollbar-button {
height: 0;
width: 0;
}
::-webkit-scrollbar-thumb {
background-clip: padding-box;
background-color: rgba(255, 255, 255, 0.15);
box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.10),
inset 0 -1px 0 rgba(0, 0, 0, 0.07);
min-height: 28px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(255, 255, 255, 0.20);
box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
}
::-webkit-scrollbar-thumb:active {
background-color: rgba(255, 255, 255, 0.25);
box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
}
::-webkit-scrollbar-thumb:vertical {
border-bottom: 0 solid transparent;
border-left: 5px solid transparent;
border-right: 0 solid transparent;
border-top: 0 solid transparent;
}
::-webkit-scrollbar-track:hover {
background-color: rgba(0, 0, 0, 0.05);
box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.10);
}
::-webkit-scrollbar-track:active {
background-color: rgba(0, 0, 0, 0.05);
box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.14),
inset -1px -1px 0 rgba(0, 0, 0, 0.07);
}
::-webkit-scrollbar-track:vertical {
background-clip: padding-box;
background-color: transparent;
border-left: 5px solid transparent;
border-right: 0 solid transparent;
}
control-panel {
border-top: 1px solid rgba(0, 0, 0, 0.1);
bottom: 0;
left: 0;
margin-bottom: 8px;
position: fixed;
right: 0;
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
Polymer('audio-player', {
/**
* Child Elements
*/
audioController: null,
audioElement: null,
trackList: null,
/**
* Initializes an element. This method is called automatically when the
* element is ready.
*/
ready: function() {
this.audioController = this.$.audioController;
this.audioElement = this.$.audio;
this.trackList = this.$.trackList;
this.audioElement.volume = this.audioController.volume / 100;
this.audioElement.addEventListener('ended', this.onAudioEnded.bind(this));
this.audioElement.addEventListener('error', this.onAudioError.bind(this));
var onAudioStatusUpdatedBound = this.onAudioStatusUpdate_.bind(this);
this.audioElement.addEventListener('timeupdate', onAudioStatusUpdatedBound);
this.audioElement.addEventListener('ended', onAudioStatusUpdatedBound);
this.audioElement.addEventListener('play', onAudioStatusUpdatedBound);
this.audioElement.addEventListener('pause', onAudioStatusUpdatedBound);
this.audioElement.addEventListener('suspend', onAudioStatusUpdatedBound);
this.audioElement.addEventListener('abort', onAudioStatusUpdatedBound);
this.audioElement.addEventListener('error', onAudioStatusUpdatedBound);
this.audioElement.addEventListener('emptied', onAudioStatusUpdatedBound);
this.audioElement.addEventListener('stalled', onAudioStatusUpdatedBound);
},
/**
* Registers handlers for changing of external variables
*/
observe: {
'trackList.currentTrackIndex': 'onCurrentTrackIndexChanged',
'audioController.playlistExpanded': 'onPlayerExpandedChanged',
'audioController.playing': 'onControllerPlayingChanged',
'audioController.volume': 'onControllerVolumeChanged',
'audioController.time': 'onControllerTimeChanged',
'audioController.shuffle': 'onControllerShuffleChanged',
'audioController.repeat': 'onControllerRepeatChanged',
},
/**
* Invoked when trackList.currentTrackIndex is changed.
* @param {number} oldValue old value.
* @param {number} newValue new value.
*/
onCurrentTrackIndexChanged: function(oldValue, newValue) {
if (oldValue != newValue) {
var currentTrack = this.trackList.getCurrentTrack();
if (currentTrack && currentTrack.url != this.audioElement.src) {
this.audioElement.src = currentTrack.url;
this.audioElement.play();
}
}
},
/**
* Invoked when audioController.playlistExpanded is changed.
* @param {boolean} oldValue old value.
* @param {boolean} newValue new value.
*/
onPlayerExpandedChanged: function(oldValue, newValue) {
if (oldValue != newValue) {
this.trackList.expanded = newValue;
if (AudioPlayer.instance)
AudioPlayer.instance.syncExpanded();
}
},
/**
* Invoked when audioController.playing is changed.
* @param {boolean} oldValue old value.
* @param {boolean} newValue new value.
*/
onControllerPlayingChanged: function(oldValue, newValue) {
if (newValue) {
if (!this.audioElement.src) {
var currentTrack = this.trackList.getCurrentTrack();
if (currentTrack && currentTrack.url != this.audioElement.src)
this.audioElement.src = currentTrack.url;
}
if (this.audioElement.src) {
this.audioElement.play();
return;
}
}
this.audioController.playing = false;
this.audioElement.pause();
},
/**
* Invoked when audioController.volume is changed.
* @param {number} oldValue old value.
* @param {number} newValue new value.
*/
onControllerVolumeChanged: function(oldValue, newValue) {
this.audioElement.volume = newValue / 100;
},
/**
* Invoked when audioController.time is changed.
* @param {number} oldValue old time (in ms).
* @param {number} newValue new time (in ms).
*/
onControllerTimeChanged: function(oldValue, newValue) {
// Ignore periodical updates and small amount change.
if (Math.abs(oldValue - newValue) <= 500)
return;
if (this.audioElement.readyState !== 0)
this.audioElement.currentTime = this.audioController.time / 1000;
},
/**
* Invoked when audioController.shuffle is changed.
* @param {boolean} oldValue old value.
* @param {boolean} newValue new value.
*/
onControllerShuffleChanged: function(oldValue, newValue) {
// TODO(yoshiki): Implement shuffle mode.
},
/**
* Invoked when audioController.repeat is changed.
* @param {boolean} oldValue old value.
* @param {boolean} newValue new value.
*/
onControllerRepeatChanged: function(oldValue, newValue) {
this.trackList.repeat = newValue;
},
/**
* Invoked when the next button in the controller is clicked.
* This handler is registered in the 'on-click' attribute of the element.
*/
onControllerNextClicked: function() {
this.advance_(true /* forward */, true /* repeat */);
},
/**
* Invoked when the previous button in the controller is clicked.
* This handler is registered in the 'on-click' attribute of the element.
*/
onControllerPreviousClicked: function() {
this.advance_(false /* forward */, true /* repeat */);
},
/**
* Invoked when the playback in the audio element is ended.
* This handler is registered in this.ready().
*/
onAudioEnded: function() {
this.advance_(true /* forward */, this.audioController.repeat);
},
/**
* Invoked when the playback in the audio element gets error.
* This handler is registered in this.ready().
*/
onAudioError: function() {
this.scheduleAutoAdvance_(true /* forward */, this.audioController.repeat);
},
/**
* Invoked when the time of playback in the audio element is updated.
* This handler is registered in this.ready().
* @private
*/
onAudioStatusUpdate_: function() {
this.audioController.time = this.audioElement.currentTime * 1000;
this.audioController.duration = this.audioElement.duration * 1000;
this.audioController.playing = !this.audioElement.paused;
},
/**
* Goes to the previous or the next track.
* @param {boolean} forward True if next, false if previous.
* @param {boolean} repeat True if repeat-mode is enabled. False otherwise.
* @private
*/
advance_: function(forward, repeat) {
this.cancelAutoAdvance_();
var nextTrackIndex = this.trackList.getNextTrackIndex(forward);
var nextTrack = this.trackList.tracks[nextTrackIndex];
var isNextTrackAvailable = this.trackList.isNextTrackAvailable(forward);
this.trackList.currentTrackIndex = nextTrackIndex;
if (isNextTrackAvailable || repeat && nextTrack) {
this.audioElement.src = nextTrack.url;
this.audioElement.play();
} else {
this.audioElement.pause();
}
},
/**
* Timeout ID of auto advance. Used internally in scheduleAutoAdvance_() and
* cancelAutoAdvance_().
* @type {number}
* @private
*/
autoAdvanceTimer_: null,
/**
* Schedules automatic advance to the next track after a timeout.
* @param {boolean} forward True if next, false if previous.
* @param {boolean} repeat True if repeat-mode is enabled. False otherwise.
* @private
*/
scheduleAutoAdvance_: function(forward, repeat) {
this.cancelAutoAdvance_();
this.autoAdvanceTimer_ = setTimeout(
function() {
this.autoAdvanceTimer_ = null;
// We are advancing only if the next track is not known to be invalid.
// This prevents an endless auto-advancing in the case when all tracks
// are invalid (we will only visit each track once).
this.advance_(forward, repeat, true /* only if valid */);
}.bind(this),
3000);
},
/**
* Cancels the scheduled auto advance.
* @private
*/
cancelAutoAdvance_: function() {
if (this.autoAdvanceTimer_) {
clearTimeout(this.autoAdvanceTimer_);
this.autoAdvanceTimer_ = null;
}
},
/**
* The index of the current track.
* If the list has no tracks, the value must be -1.
*
* @type {number}
*/
get currentTrackIndex() {
return this.trackList.currentTrackIndex;
},
set currentTrackIndex(value) {
this.trackList.currentTrackIndex = value;
},
/**
* The list of the tracks in the playlist.
*
* When it changed, current operation including playback is stopped and
* restarts playback with new tracks if necessary.
*
* @type {Array.<AudioPlayer.TrackInfo>}
*/
get tracks() {
return this.trackList ? this.trackList.tracks : null;
},
set tracks(tracks) {
if (this.trackList.tracks === tracks)
return;
this.cancelAutoAdvance_();
this.trackList.tracks = tracks;
var currentTrack = this.trackList.getCurrentTrack();
if (currentTrack && currentTrack.url != this.audioElement.src) {
this.audioElement.src = currentTrack.url;
this.audioElement.play();
}
},
/**
* Returns whether the track list is expanded or not.
* @return {boolean} True if the list is expanded. False, otherwise.
*/
isExpanded: function() {
return this.audioController.playlistExpanded;
},
/**
* Expands or collapse the track list.
* @param {boolean} True to expand the list, false to collapse.
*/
expand: function(expand) {
this.audioController.playlistExpanded = !!expand;
},
/**
* Invoked when the audio player is being unloaded.
*/
onPageUnload: function() {
this.audioElement.src = ''; // Hack to prevent crashing.
},
});
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
/**
* Moves |target| element above |anchor| element, in order to match the
* bottom lines.
* @param {HTMLElement} target Target element.
* @param {HTMLElement} anchor Anchor element.
*/
function matchBottomLine(target, anchor) {
var targetRect = target.getBoundingClientRect();
var anchorRect = anchor.getBoundingClientRect();
var pos = {
left: anchorRect.left + anchorRect.width / 2 - targetRect.width / 2,
bottom: window.innerHeight - anchorRect.bottom,
};
target.style.position = 'fixed';
target.style.left = pos.left + 'px';
target.style.bottom = pos.bottom + 'px';
}
/**
* Converts the time into human friendly string.
* @param {number} time Time to be converted.
* @return {string} String representation of the given time
*/
function time2string(time) {
return ~~(time / 60000) + ':' + ('0' + ~~(time / 1000 % 60)).slice(-2);
}
Polymer('control-panel', {
/**
* Initialize an element. This method is called automatically when the
* element is ready.
*/
ready: function() {
this.$.volumeSlider.value = this.volume || 50;
this.$.playlistButton.querySelector('input').checked =
this.playlistExpanded;
},
/**
* Current elapsed time in the current music in millisecond.
* @type {number}
*/
time: 0,
/**
* String representation of 'time'.
* @type {number}
* @private
*/
get timeString_() {
return time2string(this.time);
},
/**
* Total length of the current music in millisecond.
* @type {number}
*/
duration: 0,
/**
* String representation of 'duration'.
* @type {string}
* @private
*/
get durationString_() {
return time2string(this.duration);
},
/**
* Current volume. Must be between 0 to 100.
* @type {number}
*/
volume: 50,
/**
* Flag whether the playlist is expanded or not.
* @type {boolean}
*/
playlistExpanded: true,
/**
* Flag whether the volume slider is expanded or not.
* @type {boolean}
*/
volumeSliderShown: false,
/**
* Flag to enable shuffle mode.
* @type {boolean}
*/
shuffle: false,
/*
* Flag to enable repeat mode.
* @type {boolean}
*/
repeat: false,
/*
* Flag if the audio is playing or paused. True if playing, or false paused.
* @type {boolean}
*/
playing: false,
/**
* Invoked when the 'playlistExpanded' property is changed.
* @param {boolean} oldValue old value.
* @param {boolean} newValue new value.
*/
playlistExpandedChanged: function(oldValue, newValue) {
this.$.playlistButton.querySelector('input').checked = !!newValue;
},
/**
* Invoked when the 'duration' property is changed.
* @param {number} oldValue old value.
* @param {number} newValue new value.
*/
durationChanged: function(oldValue, newValue) {
// Reset the current playback position.
this.time = 0;
},
/**
* Invoked when the next button is clicked.
*/
nextClick: function() {
this.fire('next-clicked');
},
/**
* Invoked when the play button is clicked.
*/
playClick: function() {
this.playing = !this.playing;
},
/**
* Invoked when the previous button is clicked.
*/
previousClick: function() {
this.fire('next-clicked');
},
/**
* Invoked the volume button is clicked.
* @type {Event} event The event.
*/
volumeButtonClick: function(event) {
if (this.volumeSliderShown) {
matchBottomLine(this.$.volumeContainer, this.$.volumeButton);
this.$.volumeContainer.style.visibility = 'visible';
} else {
this.$.volumeContainer.style.visibility = 'hidden';
}
event.stopPropagation();
},
/**
* Invoked the value of the volume slider is changed.
* @type {number}
*/
volumeSliderChanged: function() {
this.volume = this.$.volumeSlider.value;
this.fire('volume-changed');
},
});
})(); // Anonymous closure
/* Copyright 2014 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
:host {
align-items: center;
background: #f5f5f5;
bottom: 72px; /* Room for the controls bar. */
color: #3d3d3d;
cursor: default;
display: flex;
flex-direction: column;
font-family: Open Sans, Droid Sans Fallback, sans-serif;
font-size: 10pt;
justify-content: flex-start;
left: 0;
overflow-x: hidden;
overflow-y: auto;
position: absolute;
right: 0;
top: 0;
}
/* Track item. */
.track {
align-items: center;
display: flex;
flex: 0 0 auto;
flex-direction: row;
height: 44px;
justify-content: flex-start;
padding-left: 20px;
width: 100%;
}
track-list:not([expanded]) > .track:not([active='true']) {
display: none;
}
/* In the expanded mode the selected track is highlighted. */
.track[active='true'] {
background-color: rgb(66, 129, 244);
}
/* Track data. */
.track .data {
display: flex;
flex: 1 1 auto;
flex-direction: column;
justify-content: center;
margin-left: 0;
margin-right: 4px;
}
.track .data .data-title,
.track .data .data-artist {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.track .data .data-title {
color: #343434;
font-weight: bold;
}
.track .data .data-artist {
color: #969696;
}
.track[active='true'] .data .data-title,
.track[active='true'] .data .data-artist {
color: #fff;
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
Polymer('track-list', {
/**
* Initializes an element. This method is called automatically when the
* element is ready.
*/
ready: function() {
this.tracksObserver_ = new ArrayObserver(
this.tracks,
this.tracksValueChanged_.bind(this));
},
/**
* List of tracks.
* @type {Array.<AudioPlayer.TrackInfo>}
*/
tracks: [],
/**
* Track index of the current track.
* If the tracks propertye is empty, it should be -1. Otherwise, be a valid
* track number.
*
* @type {number}
*/
currentTrackIndex: -1,
/**
* Invoked when the current track index is changed.
* @param {number} oldValue old value.
* @param {number} newValue new value.
*/
currentTrackIndexChanged: function(oldValue, newValue) {
if (oldValue === newValue)
return;
if (oldValue !== -1)
this.tracks[oldValue].active = false;
if (newValue < 0 || this.tracks.length <= newValue) {
if (this.tracks.length === 0)
this.currentTrackIndex = -1;
else
this.currentTrackIndex = 0;
} else {
this.tracks[newValue].active = true;
}
},
/**
* Invoked when 'tracks' property is clicked.
* @param {Event} event Click event.
*/
tracksChanged: function(oldValue, newValue) {
if (oldValue !== newValue) {
this.tracksObserver_.close();
this.tracksObserver_ = new ArrayObserver(
this.tracks,
this.tracksValueChanged_.bind(this));
if (this.tracks.length !== 0)
this.currentTrackIndex = 0;
}
if (this.tracks.length === 0)
this.currentTrackIndex = -1;
},
/**
* Invoked when the value in the 'tracks' is changed.
* @param {Array.<Object>} splices The detail of the change.
*/
tracksValueChanged_: function(splices) {
if (this.tracks.length === 0)
this.currentTrackIndex = -1;
else
this.tracks[this.currentTrackIndex].active = true;
},
/**
* Invoked when the track element is clicked.
* @param {Event} event Click event.
*/
trackClicked: function(event) {
var track = event.target.templateInstance.model;
this.selectTrack(track);
},
/**
* Sets the current track.
* @param {AudioPlayer.TrackInfo} track TrackInfo to be set as the current
* track.
*/
selectTrack: function(track) {
var index = -1;
for (var i = 0; i < this.tracks.length; i++) {
if (this.tracks[i].url === track.url) {
index = i;
break;
}
}
if (index >= 0)
this.currentTrackIndex = index;
},
/**
* Returns the current track.
* @param {AudioPlayer.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.
*
* @param {boolean} forward Specify direction: forward or previous mode.
* True: forward mode, false: previous mode.
* @return {AudioPlayer.TrackInfo} TrackInfo of the next track. If there is
* no track, the return value is null.
*/
getNextTrackIndex: function(forward) {
var defaultTrack = forward ? 0 : (this.tracks.length - 1);
var tentativeNewTrackIndex = this.currentTrackIndex + (forward ? +1 : -1);
var newTrackIndex;
if (this.tracks.length === 0) {
newTrackIndex = -1;
} else {
if (this.currentTrackIndex === -1) {
newTrackIndex = defaultTrack;
} else if (0 <= tentativeNewTrackIndex &&
tentativeNewTrackIndex < this.tracks.length) {
newTrackIndex = tentativeNewTrackIndex;
} else {
newTrackIndex = defaultTrack;
}
}
return newTrackIndex;
},
/**
* Returns if the next (or previous) track in the track list is available.
*
* @param {boolean} forward Specify direction: forward or previous mode.
* True: forward mode, false: previous mode.
* @return {true} True if the next (or previous) track available. False
* otherwise.
*/
isNextTrackAvailable: function(forward) {
if (this.tracks.length === 0) {
return false;
} else {
var tentativeNewTrackIndex =
this.currentTrackIndex + (forward ? +1 : -1);
if (this.currentTrackIndex === -1) {
return false;
} else if (0 <= tentativeNewTrackIndex &&
tentativeNewTrackIndex < this.tracks.length) {
return true;
} else {
return false;
}
}
}
}); // Polymer('track-list') block
})(); // Anonymous closure
/* Copyright 2014 The Chromium Authors. All rights reserved.
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
:host {
background: white;
display: block;
height: 100px;
position: relative;
width: 32px;
}
#background {
height: 100%; /* will be overridden by javascript */
left: 0;
position: absolute;
top: 0;
width: 100%;
}
input[type='range'] {
-webkit-appearance: none !important;
-webkit-transform: rotate(90deg);
background: transparent;
outline: none;
position: absolute;
z-index: 1;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
-webkit-transform: rotate(-90deg);
background-image: -webkit-image-set(
url(../assets/100/player_timeline_handler.png) 1x,
url(../assets/200/player_timeline_handler.png) 2x);
background-position: 50% 50%;
background-repeat: no-repeat no-repeat;
height: 24px;
position: relative;
width: 24px;
}
#bar {
background: #000;
bottom: 14px;
position: absolute;
top: 14px;
}
#bar .filled {
background: #aaa;
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function() {
'use strict';
Polymer('volume-controller', {
/**
* Initializes an element. This method is called automatically when the
* element is ready.
*/
ready: function() {
this.style.width = this.width + 'px';
this.style.height = this.height + 'px';
this.$.rawValueInput.style.width = this.height + 'px';
this.$.rawValueInput.style.height = this.width + 'px';
this.$.rawValueInput.style.webkitTransformOrigin =
(this.width / 2) + 'px ' +
(this.width / 2 - 2) + 'px';
var barLeft = (this.width / 2 - 1);
this.$.bar.style.left = barLeft + 'px';
this.$.bar.style.right = barLeft + 'px';
},
/**
* Volume. 0 is silent, and 100 is maximum.
* @type {number}
*/
value: 50,
/**
* Volume. 1000 is silent, and 0 is maximum.
* @type {number}
*/
rawValue: 0,
/**
* Height of the element in pixels. Must be specified before ready() is
* called. Dynamic change is not supprted.
* @type {number}
*/
height: 100,
/**
* Width of the element in pixels. Must be specified before ready() is
* called. Dynamic change is not supported.
* @type {number}
*/
width: 32,
/**
* Invoked the 'value' property is changed.
* @param {number} oldValue Old value.
* @param {number} newValue New value.
*/
valueChanged: function(oldValue, newValue) {
if (oldValue != newValue)
this.rawValue = 100 - newValue;
this.fire('changed');
},
/**
* Invoked the 'rawValue' property is changed.
* @param {number} oldValue Old value.
* @param {number} newValue New value.
*/
rawValueChanged: function(oldValue, newValue) {
if (oldValue != newValue)
this.value = 100 - newValue;
},
});
})(); // Anonymous closure
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The include directives are put into Javascript-style comments to prevent
// parsing errors in non-flattened mode. The flattener still sees them.
// Note that this makes the flattener to comment out the first line of the
// included file but that's all right since any javascript file should start
// with a copyright comment anyway.
//<include src="../../../../../../ui/webui/resources/js/cr.js"/>
//<include src="../../../../../../ui/webui/resources/js/cr/event_target.js"/>
//<include src="../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js"/>
// Hack for polymer, notifying that CSP is enabled here.
// TODO(yoshiki): Find a way to remove the hack.
if (!('securityPolicy' in document))
document['securityPolicy'] = {};
if (!('allowsEval' in document.securityPolicy))
document.securityPolicy['allowsEval'] = false;
//<include src="../../../../../../third_party/polymer/platform/platform.js">
//<include src="../../../../../../third_party/polymer/polymer/polymer.js">
(function() {
// 'strict mode' is invoked for this scope.
'use strict';
//<include src="../../common/js/async_util.js"/>
//<include src="../../common/js/util.js"/>
//<include src="../../common/js/path_util.js"/>
//<include src="../../foreground/js/file_type.js"/>
//<include src="../../foreground/js/volume_manager_wrapper.js">
//<include src="../../foreground/js/metadata/metadata_cache.js"/>
//<include src="audio_player.js"/>
//<include src="../../foreground/js/media/player_testapi.js"/>
//<include src="../elements/track_list.js"/>
//<include src="../elements/control_panel.js"/>
//<include src="../elements/volume_controller.js"/>
//<include src="../elements/audio_player.js"/>
window.reload = reload;
window.unload = unload;
window.AudioPlayer = AudioPlayer;
})();
......@@ -683,29 +683,48 @@ Background.prototype.onExecute_ = function(action, details) {
}
};
/**
* Audio player window create options.
* @type {Object}
* @const
*/
var AUDIO_PLAYER_CREATE_OPTIONS = Object.freeze({
type: 'panel',
hidden: true,
minHeight: 35 + 58,
minWidth: 280,
height: 35 + 58,
width: 280
});
// The instance of audio player. Until it's ready, this is null.
var audioPlayer = null;
// Queue to serializes the initialization, launching and reloading of the audio
// player, so races won't happen.
var audioPlayerInitializationQueue = new AsyncUtil.Queue();
audioPlayerInitializationQueue.run(function(callback) {
chrome.commandLinePrivate.hasSwitch(
'file-manager-enable-new-audio-player',
function(newAudioPlayerEnabled) {
var audioPlayerHTML =
newAudioPlayerEnabled ? 'audio_player.html' : 'mediaplayer.html';
/**
* Audio player window create options.
* @type {Object}
*/
var audioPlayerCreateOptions = Object.freeze({
type: 'panel',
hidden: true,
minHeight: newAudioPlayerEnabled ? 116 : (35 + 58),
minWidth: newAudioPlayerEnabled ? 292 : 280,
height: newAudioPlayerEnabled ? 356 : (35 + 58),
width: newAudioPlayerEnabled ? 292 : 280,
});
var audioPlayer = new SingletonAppWindowWrapper('mediaplayer.html',
AUDIO_PLAYER_CREATE_OPTIONS);
audioPlayer = new SingletonAppWindowWrapper(audioPlayerHTML,
audioPlayerCreateOptions);
callback();
});
});
/**
* Launch the audio player.
* @param {Object} playlist Playlist.
*/
function launchAudioPlayer(playlist) {
audioPlayer.launch(playlist);
audioPlayerInitializationQueue.run(function(callback) {
audioPlayer.launch(playlist);
callback();
});
}
var videoPlayer = new SingletonAppWindowWrapper('video_player.html',
......@@ -762,8 +781,13 @@ Background.prototype.onRestarted_ = function() {
}
});
// Reopen sub-applications.
audioPlayer.reopen();
// Reopen audio player.
audioPlayerInitializationQueue.run(function(callback) {
audioPlayer.reopen();
callback();
});
// Reopen video player.
videoPlayer.reopen();
};
......
......@@ -127,6 +127,10 @@ const char kEnterpriseEnrollmentSkipRobotAuth[] =
// Shows the selecting checkboxes in the Files.app.
const char kFileManagerShowCheckboxes[] = "file-manager-show-checkboxes";
// Enables the new audio player in the Files.app.
const char kFileManagerEnableNewAudioPlayer[] =
"file-manager-enable-new-audio-player";
// Enables the webstore integration feature in the Files.app.
const char kFileManagerEnableWebstoreIntegration[] =
"file-manager-enable-webstore-integration";
......
......@@ -52,6 +52,7 @@ CHROMEOS_EXPORT extern const char kEnterpriseEnrollmentInitialModulus[];
CHROMEOS_EXPORT extern const char kEnterpriseEnrollmentModulusLimit[];
CHROMEOS_EXPORT extern const char kEnterpriseEnrollmentSkipRobotAuth[];
CHROMEOS_EXPORT extern const char kFileManagerShowCheckboxes[];
CHROMEOS_EXPORT extern const char kFileManagerEnableNewAudioPlayer[];
CHROMEOS_EXPORT extern const char kFileManagerEnableWebstoreIntegration[];
CHROMEOS_EXPORT extern const char kFileManagerEnableMultiProfile[];
CHROMEOS_EXPORT extern const char kFirstExecAfterBoot[];
......
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