Ext.namespace('Fabs.boombox.ui');
/*
* Fab's BoomBox Version 1.0
* Copyright(c) 2009, Owl Watch Consulting Serivces, LLC
* support@owlwatch.com
*
* MIT License
*/
/**
* @class Fabs.boombox.ui.FullPlayer
* @extends Ext.util.Observable
* A graphic display of the music player
* @constructor
* @param {Object} config The config object
*/
Fabs.boombox.ui.FullPlayer = Ext.extend( Ext.util.Observable, {
/**
* @cfg {Number/String} width
* The width of the player in pixels, or 'auto' to fill the containing element. Defaults to 'auto'.
*/
width : 'auto',
/**
* @cfg {Player} player The player reference for this ui. This is required.
*/
player : null,
/**
* @cfg {Number} scrollIncrement The pixel increment that the track label should scroll.
*/
scrollIncrement : 2,
/**
* @cfg {Number} scrollHold The number of update intervals to wait before scrolling the opposite direction after the label has reached an end.
*/
scrollHold : 20,
/**
* @cfg {Number} opacity The opacity of the player. A number between 0 and 1. Best to keep at 1 for now.
*/
opacity: 1,
/**
* @cfg {Number} zIndex The zIndex of the boombox element
*/
zIndex : 100,
/**
* @cfg {Number} updateInterval Then time in milliseconds to run the update tasks (label scrolling and position updating)
*/
updateInterval : 80,
/**
* @cfg {Number} maxListHeight The maximum size of the playlist
*/
maxListHeight: 100,
/**
* @cfg {String} emptyText The text to display in the info bar when no track is playing or active
*/
emptyText : 'Fab\'s BoomBox!',
/**
* @cfg {Object} lang The language strings to use for the player. Check the source to see the values that would need to be replaced
*/
lang : {
prev :'Previous Song',
play :'Play Song',
pause :'Pause Song',
next :'Next Song',
stop :'Stop Song',
openPlaylist :'Open Playlist',
closePlaylist :'Close Playlist',
shuffle :'Shuffle',
volume :'Adjust Volume'
},
/**
* @cfg {String} loadingText The text to display when loading track information
*/
loadingText : 'Loading Track Information...',
/**
* @cfg {String} trackTpl The string template to display for tracks with song information
*/
trackTpl : '{playlistIndex}. {title}',
/**
* @cfg {String} unknownTrackTpl The string template to display for tracks without song information
*/
unknownTrackTpl : '{playlistIndex}. {filename}',
/**
* @cfg {Boolean} draggable True to allow this element to be dragged. The element will
* remain in its initial position in the DOM. Defaults to false.
*/
draggable : false,
/**
* @cfg {Boolean} resizeable Allow the player to be resized. The right side will act as the handle.
* Defaults to false
*/
resizeable : false,
/**
* @cfg {Number} minWidth The minimum allowed width for the player. Only applicable if resizeable is enabled.
*/
minWidth : 200,
/**
* @cfg {HTMLElement/Element/String} renderTo The element, or id of the element, to render this to upon creation
*/
// private
labelScrollDirection : -1, // false for right, true for left
btns : {},
constructor : function(config){
Ext.apply(this, config);
Fabs.boombox.ui.FullPlayer.superclass.constructor.call(this);
if( !this.tpl ){
this.tpl = [
'',
'
',
'
',
'
',
'
',
'
',
'
',
'
',
'
',
'
',
'
',
'
',
'
',
'
',
'
'
];
}
this.tpl = new Ext.Template(this.tpl);
this.trackTpl = new Ext.Template(this.trackTpl);
this.unknownTrackTpl = new Ext.Template(this.unknownTrackTpl);
if( this.renderTo ){
Ext.onReady(function(){
this.render(this.renderTo);
}, this);
}
this.player.on({
scope :this,
statechange :this.onPlayerStateChange,
trackchange :this.onPlayerTrackChange
});
this.updateTask = {
run :this.update.createDelegate(this),
interval :this.updateInterval
};
this.scrollHoldIndex=0;
},
/**
* Render this function to an element. This can be called multiple times so long as it was unrendered first
* @param {Element/HtmlElement/String} el Element or id of element
*/
render : function(el){
this.el = Ext.get(el);
// lets get a reference to the ownerDocument
this.doc = this.el.dom.ownerDocument;
this.el.update(this.tpl.applyTemplate({}));
// now lets get refs to our elements
this.ct = this.el.child('.bb-ct');
if( this.pos ){
this.ct.setXY(this.pos);
}
this.ct.setStyle('z-index',this.zIndex);
if( this.resizeable ){
this.resizeHandle = this.ct.child('.bb-player-r');
//this.resizeHandle.on('mousedown', this.onResizeHandleMouseDown, this);
this.ct.addClass('resizeable');
this.createDragEvent(
this.resizeHandle,
this.resizeInit,
this.resizeMove,
this
);
}
if( this.draggable ){
this.dragHandle = this.ct.child('.bb-player-l');
this.ct.addClass('draggable');
this.createDragEvent(
this.ct,
this.dragInit,
this.dragMove,
this
);
}
this.playerCenter = this.ct.child('.bb-player-c');
// player and buttons
this.playerCt = this.ct.child('.bb-player-ct');
this.createButton( this.playerCt, 'prev');
this.createButton( this.playerCt, 'play');
this.createButton( this.playerCt, 'next');
this.createButton( this.playerCt, 'stop');
this.createButton( this.playerCt, 'playlist');
// this.btns['playlist'].un('click', this.btnHandlers['playlist']);
// volume (lazy slider...)
this.volumeBtn = this.playerCt.child('.bb-button-volume');
this.volumeOverlay = this.volumeBtn.child('.bb-button-volume-overlay');
this.createDragEvent(
this.volumeBtn,
this.onVolumeButtonMouseDown,
this.onVolumeButtonMouseEvent,
this
);
this.volumeBtn.on('keydown', this.onVolumeKeyDown, this);
//this.volumeOverlay.on('click', this.onVolumeButtonClick, this);
// shuffle
this.shuffleBtn = this.playerCt.child('.bb-button-shuffle');
this.shuffleBtn.on('click', function(){
this.player.toggleShuffle();
}, this);
// track container
this.trackCt = this.playerCt.child('.bb-track-ct');
this.trackScroller = this.trackCt.child('.bb-track-name-scroller');
this.trackLabel = this.trackCt.child('.bb-track-name');
this.createDragEvent(
this.trackLabel,
this.onTrackLabelMouseDown,
this.onTrackLabelMouseMove,
this,
function(){ this.trackLabelMouseOrigin=null; }
);
this.trackProgress = this.trackCt.child('.bb-track-progress');
this.trackProgressOverlay = this.trackProgress.child('.bb-track-progress-overlay');
this.createDragEvent(
this.trackProgress,
this.onTrackProgressMouseDown,
this.onTrackProgressMouseEvent,
this
);
// playlist container
this.playlistCt = this.ct.child('.bb-playlist-ct');
this.playlistCt.setOpacity(0, false);
this.playlistScroller = this.playlistCt.child('.bb-playlist-scroller');
this.playlistCt.on('scroll', function(e){ if(e&&e.stopPropogation) e.stopPropogation(); } );
this.onPlayerStateChange();
this.setWidth(this.width);
Ext.TaskMgr.start(this.updateTask);
this.resetTrackScroll.defer(1,this);
this.updateVolumeOverlay();
this.ct.setStyle('opacity', this.opacity);
if( this.player.currentTrack ){
this.currentTrack = this.player.currentTrack;
this.updateTrackInfo();
}
},
/**
* Safely unrender the UI and remove all events
*/
unrender : function(){
Ext.TaskMgr.stop(this.updateTask);
Ext.each([this.shuffleBtn, this.playlistCt, this.trackProgress, this.trackLabel, this.volumeBtn], function(o){
o.removeAllListeners();
// a little housecleaning to keep our EventManager from getting too big.
delete Ext.EventManager.elHash[o.dom.id];
});
for(var name in this.btns){
this.btns[name].removeAllListeners();
// a little housecleaning to keep our EventManager from getting too big.
delete Ext.EventManager.elHash[this.btns[name].dom.id];
}
this.ct.remove();
},
// private
resizeInit : function(e){
this.resizeOrigin = {x: e.getPageX(), width: this.playerCt.getWidth()};
},
// private
resizeMove : function(e){
var w = Math.max( this.resizeOrigin.width+e.getPageX()-this.resizeOrigin.x, this.minWidth );
this.setWidth(w);
e.preventDefault();
},
// private
dragInit : function(e){
if( !e.within(this.playerCt.dom)){ return false; }
this.dragOffset = {x: e.getPageX()-this.ct.getX(), y: e.getPageY()-this.ct.getY() };
return true;
},
// private
dragMove : function(e){
var pos = [e.getPageX()-this.dragOffset.x, e.getPageY()-this.dragOffset.y];
if( pos[0] < 0 ){ pos[0] = 0; }
if( pos[1] < 0 ){ pos[1] = 0; }
this.pos = pos;
this.ct.setXY(this.pos);
e.preventDefault();
},
/**
* Dynamically set the width of the player
* @param {Number/String} width A pixel value or 'auto' to fill the containing element
*/
setWidth : function(w){
this.width = w;
if( this.ct ){
if( this.width == 'auto' ){
this.ct.setStyle({'width':'auto'});
}else{
this.ct.setStyle('width', this.width+'px');
}
this.playlistCt.setStyle( 'width', this.playerCenter.getWidth()+'px' );
}
},
// private
createButton : function(parent, name, fn){
this.btns[name] = parent.child('.bb-button-'+name);
this.btns[name].on('click', this.onButtonClick.createDelegate(this,[name],0) );
},
// private
onButtonClick : function(name,e){
e.stopPropagation();
switch(name){
case 'prev':
case 'next':
case 'stop':
this.player[name]();
break;
case 'play':
this.player.togglePlay();
break;
case 'playlist':
this.togglePlaylist();
break;
}
},
createDragEvent : function(el, onMouseDown, onMouseMove, scope, onMouseUp ){
var doc = this.doc;
var E = Ext.EventManager;
var mouseMove = function(e){
e.stopPropagation();
e.preventDefault();
onMouseMove.apply( scope, arguments );
};
var mouseUp = function(e){
e.preventDefault();
if( onMouseUp ){
onMouseUp.apply(scope,arguments);
}
E.un(doc,'mousemove', mouseMove, scope );
E.un(doc,'mouseup', mouseUp, scope );
};
var mouseDown = function(e){
e.stopPropagation();
e.preventDefault();
if( onMouseDown.apply(scope, arguments) !== false ){
E.on(doc,'mousemove', mouseMove, scope );
E.on(doc,'mouseup', mouseUp, scope );
}
};
el.on('mousedown', mouseDown, scope);
},
// private
onVolumeButtonMouseDown : function(e){
this.onVolumeButtonMouseEvent(e);
},
// private
onVolumeButtonMouseEvent : function(e){
var mx = e.getPageX();
var vx = this.volumeBtn.getX();
var w = this.volumeBtn.getWidth();
var p = Math.max( Math.min((mx-vx)/w * 100, 100), 0 );
this.player.setVolume(p);
this.updateVolumeOverlay(p);
e.preventDefault();
},
// private
onTrackProgressMouseDown : function(e){
this.onTrackProgressMouseEvent(e);
},
// private
onTrackProgressMouseEvent : function(e){
var mx = e.getPageX();
var tx = this.trackProgress.getX();
var w = this.trackProgress.getWidth();
var p = Math.max( Math.min((mx-tx)/w * 100, 100), 0 );
this.player.seek(p);
e.preventDefault();
},
// private
onVolumeKeyDown : function(e){
var v = this.player.volume;
if( e.getKey() == 38 ){
v = Math.min(v+5,100);
}
else if( e.getKey() == 40 ){
v = Math.max(v-5,0);
}
this.player.setVolume(v);
this.updateVolumeOverlay(v);
},
// private
onTrackLabelMouseDown : function(e){
this.trackLabelMouseOrigin = {x: e.getPageX(), scrollLeft: this.trackScroller.getScroll().left};
},
// private
onTrackLabelMouseMove : function(e){
var d = this.trackLabelMouseOrigin.x - e.getPageX();
var lw = this.trackLabel.getWidth();
var lc = this.trackScroller.getWidth(true);
if( lw > lc ){
var o = lw - lc;
var left = this.trackLabelMouseOrigin.scrollLeft;
this.trackScroller.scrollTo('left', Math.max( Math.min(o, d+left), 0) );
}
e.preventDefault();
},
// private
onPlayerStateChange : function(){
this.playerCt[this.player.isPlaying()?'addClass':'removeClass']('bb-playing');
this.playerCt[this.player.shuffle?'addClass':'removeClass']('shuffle');
this.btns.play.dom.title = this.lang[ this.player.isPlaying() ? 'pause' : 'play' ];
},
// private
onPlayerTrackChange : function(player, track){
if( this.currentTrack && this.currentTrack == track ){
return;
}
if( this.currentTrack && this.currentTrack != track ){
track.un('infochange', this.updateTrackInfo, this);
}
this.currentTrack = track;
if( track.hasSongInfo('title') ){
this.updateTrackInfo();
}
else{
this.resetTrackScroll();
if( this.trackLabel ){
this.trackLabel.update(this.loadingText);
}
}
track.on('infochange', this.updateTrackInfo, this );
// track.on('positionchange', this.updateTrackPosition, this);
},
// private
resetTrackScroll : function(){
if( this.trackScroller ){
this.trackScroller.scrollTo('left',0);
}
this.labelScrollDirection=-1;
this.scrollHoldIndex=0;
},
// private
updateTrackInfo : function(){
this.resetTrackScroll();
this.trackLabel.update((this.player.getPlaylist().tracks.indexOf(this.currentTrack)+1)+'. '+this.currentTrack.title);
},
// private
updateTrackPosition : function(){
if( !this.currentTrack ){
return;
}
try{
this.trackProgressOverlay.setStyle( 'width', this.currentTrack.getProgressPercent()+'%');
}
catch(e){
// something weird with IE
}
},
// private
updateVolumeOverlay : function(p){
this.volumeOverlay.setWidth(parseInt((p||this.player.volume), 10)+'%');
},
// private
updateTrackLabelPosition : function(){
if( this.trackLabelMouseOrigin ){
return;
}
var lw = this.trackLabel.getWidth();
var lc = this.trackScroller.getWidth(true);
if( lw > lc ){
var o = lw - lc;
var left = parseInt(this.trackScroller.getScroll().left,10);
if( left <= 0 || left >= o){
if( this.scrollHoldIndex < this.scrollHold ){
this.scrollHoldIndex++;
return;
}
else{
this.scrollHoldIndex=0;
}
this.labelScrollDirection *= -1;
}
var d = this.scrollIncrement * this.labelScrollDirection;
this.trackScroller.scrollTo('left',left+d);
}
else{
this.trackScroller.scrollTo('left',0);
}
},
// private
update : function(){
this.updateTrackPosition();
this.updateTrackLabelPosition();
},
// private
playlistClickTest : function(e){
if( !e.within( this.ct.dom ) ){
this.togglePlaylist();
}
},
/**
* Toggle the playlist visibility
*/
togglePlaylist : function(){
var E = Ext.EventManager;
if( this.playlistCt.getHeight() > 0 || this.playlistCt.getStyle('opacity') > 0){
this.btns.playlist.dom.title = this.lang.openPlaylist;
this.ct.removeClass('playlist-open');
E.un(this.doc,'click', this.playlistClickTest, this);
this.playlistCt.un('click', this.onPlaylistCtClick, this);
this.playlistCt.setHeight(0, true);
this.playlistCt.setOpacity(0, true);
return false;
}
this.btns.playlist.dom.title = this.lang.closePlaylist;
this.ct.addClass('playlist-open');
E.on(this.doc,'click', this.playlistClickTest, this);
this.playlistCt.setWidth( this.playerCenter.getWidth() );
// lets neatly remove all old elements if they exist...
if( this.trackMap ){
for( var i in this.trackMap ){
/*
Weird... if i use Ext.fly(i).remove();
I get some strange behavior regarding clicks and mousedown events
*/
var el = this.doc.getElementById(i);
if( el ){
el.parentNode.removeChild(el);
}
}
}
this.trackMap = {};
// we don't use this yet, but would be cool to update tracks
// when there is an info change (id3 events)
this.trackMapR = {};
this.playlistScroller.update('');
// this just ensures that the playlist is empty.
this.player.getPlaylist().tracks.each( function(track, key, index){
track.playlistIndex = index+1;
try{
track.filename = /\/([^\/]*)$/.exec( decodeURIComponent( track.url))[1].replace(/\.mp3$/, '');
}catch(e){
track.filename = track.url;
}
var el = this.playlistScroller.createChild({
tag :'a',
href :'javascript:;',
cls :'bb-track',
html :this[track.hasSongInfo('title')?'trackTpl':'unknownTrackTpl'].apply(track)
});
this.trackMap[el.dom.id] = track;
this.trackMapR[track.id] = el;
}, this);
var h = this.playlistScroller.getHeight(true);
this.playlistCt.setHeight( this.maxListHeight ? Math.min( this.maxListHeight,h) : h, true );
this.playlistCt.setOpacity(0);
this.playlistCt.setOpacity(1, true);
this.playlistCt.on('click', this.onPlaylistCtClick, this);
return false;
},
onPlaylistCtClick : function(e){
var t = e.getTarget();
this.player.play(this.trackMap[t.id]);
}
});