├── sdk ├── img │ ├── .gitkeep │ ├── sc-dark.png │ ├── td-dark.png │ ├── sc-light.png │ └── td-light.png ├── css │ ├── index.js │ ├── fonts.css │ ├── themes.css │ └── player.css └── js │ ├── constants.js │ ├── flux │ ├── stores │ │ ├── index.js │ │ ├── TrackQueueStore.js │ │ ├── TrackStore.js │ │ └── PlayerInstanceStore.js │ ├── events.js │ └── actions.js │ ├── player │ ├── components │ │ ├── Loader.jsx │ │ ├── Row.jsx │ │ ├── themes │ │ │ ├── common │ │ │ │ ├── RepeatButton.jsx │ │ │ │ ├── PlaybackButtons.jsx │ │ │ │ ├── Volume.jsx │ │ │ │ └── Scrubber.jsx │ │ │ ├── Empty.jsx │ │ │ ├── Default │ │ │ │ ├── Controls.jsx │ │ │ │ ├── NowPlaying.jsx │ │ │ │ ├── index.jsx │ │ │ │ ├── SocialInfo.jsx │ │ │ │ └── Playlist.jsx │ │ │ ├── Feed │ │ │ │ ├── SocialInfo.jsx │ │ │ │ └── index.jsx │ │ │ ├── Full.jsx │ │ │ ├── Mini.jsx │ │ │ └── Single.jsx │ │ ├── Columns.jsx │ │ ├── mixins │ │ │ └── PlayerControls.jsx │ │ └── Player.jsx │ ├── network │ │ └── soundcloud.js │ ├── index.js │ └── AudioInterface.js │ ├── analytics.js │ ├── helpers.js │ └── index.js ├── mockupv1.png ├── .gitignore ├── package.json ├── loader └── index.js ├── LICENSE ├── test ├── css │ └── style.css └── index.html ├── Gruntfile.js └── README.md /sdk/img/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mockupv1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToneDen/Player/HEAD/mockupv1.png -------------------------------------------------------------------------------- /sdk/img/sc-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToneDen/Player/HEAD/sdk/img/sc-dark.png -------------------------------------------------------------------------------- /sdk/img/td-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToneDen/Player/HEAD/sdk/img/td-dark.png -------------------------------------------------------------------------------- /sdk/css/index.js: -------------------------------------------------------------------------------- 1 | require('./fonts'); 2 | require('./player'); 3 | require('./themes'); 4 | -------------------------------------------------------------------------------- /sdk/img/sc-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToneDen/Player/HEAD/sdk/img/sc-light.png -------------------------------------------------------------------------------- /sdk/img/td-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ToneDen/Player/HEAD/sdk/img/td-light.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.DS_Store 4 | *node_modules* 5 | npm-debug.log 6 | webpack.json 7 | toneden.js 8 | toneden.loader.js 9 | -------------------------------------------------------------------------------- /sdk/js/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: env, 3 | protocol: window.location.protocol === 'file:' ? 'http:' : window.location.protocol 4 | }; 5 | -------------------------------------------------------------------------------- /sdk/js/flux/stores/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PlayerInstanceStore: new (require('./PlayerInstanceStore'))(), 3 | TrackQueueStore: new (require('./TrackQueueStore'))(), 4 | TrackStore: new (require('./TrackStore'))() 5 | }; 6 | -------------------------------------------------------------------------------- /sdk/js/player/components/Loader.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Loader = React.createClass({ 4 | render: function() { 5 | return ( 6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ); 14 | } 15 | }); 16 | 17 | module.exports = Loader; 18 | -------------------------------------------------------------------------------- /sdk/js/player/components/Row.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstraction for a row in Foundation's CSS framework. 3 | */ 4 | 5 | var React = require('react'); 6 | 7 | var Row = React.createClass({ 8 | render: function() { 9 | return ( 10 |
15 | {this.props.children} 16 |
17 | ); 18 | } 19 | }); 20 | 21 | module.exports = Row; 22 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/common/RepeatButton.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var React = require('react'); 3 | 4 | var RepeatButton = React.createClass({ 5 | mixins: [ 6 | Fluxxor.FluxMixin(React), 7 | require('../../mixins/PlayerControls') 8 | ], 9 | render: function() { 10 | return ( 11 | 15 | ); 16 | } 17 | }); 18 | 19 | module.exports = RepeatButton; 20 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Empty.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Columns = require('../Columns'); 4 | var Row = require('../Row'); 5 | 6 | var Empty = React.createClass({ 7 | render: function() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | There are no tracks to play. 15 | 16 | 17 | ); 18 | } 19 | }); 20 | 21 | module.exports = Empty; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "grunt": "0.4.4", 4 | "grunt-aws-s3": "~0.13.1", 5 | "grunt-cloudfront-clear": "0.0.1", 6 | "webpack-dev-server": "~1.9.0", 7 | "grunt-webpack": "~1.0.8", 8 | "jsx-loader": "~0.13.2", 9 | "file-loader": "~0.8.4", 10 | "url-loader": "~0.5.6", 11 | "compression-webpack-plugin": "~0.2.0", 12 | "css-loader": "~0.15.1", 13 | "grunt-contrib-compress": "~0.13.0", 14 | "grunt-contrib-clean": "~0.6.0" 15 | }, 16 | "dependencies": { 17 | "async": "~1.2.1", 18 | "d3": "~3.5.5", 19 | "fluxxor": "~1.7.3", 20 | "immutable": "~3.7.4", 21 | "jquery": "~2.1.4", 22 | "jsonp": "^0.2.0", 23 | "lodash": "~3.10.0", 24 | "node-libs-browser": "~0.5.2", 25 | "normalizr": "~0.1.3", 26 | "querystring": "^0.2.0", 27 | "react": "~0.13.3", 28 | "style-loader": "~0.12.3", 29 | "superagent": "~1.2.0", 30 | "webpack": "~1.12.9" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /loader/index.js: -------------------------------------------------------------------------------- 1 | // Web setup. Makes ToneDen a global variable. 2 | if(typeof window !== 'undefined') { 3 | exports = window; 4 | 5 | if(!window.ToneDen) { 6 | window.ToneDen = {}; 7 | } 8 | 9 | ToneDen = window.ToneDen; 10 | } 11 | 12 | if(env === 'local' || window.location.host === 'publisher.dev' || window.location.host === 'lvho.st') { 13 | __webpack_public_path__ = '//widget.dev/'; 14 | } else if(env === 'production') { 15 | __webpack_public_path__ = '//sd.toneden.io/production/v2/'; 16 | } else if(env === 'staging') { 17 | __webpack_public_path__ = '//sd.toneden.io/dev/v2/'; 18 | } 19 | 20 | require.ensure(['../sdk/js/index'], function(ToneDen) { 21 | window.ToneDen = require('../sdk/js/index'); 22 | window.ToneDen.parameters = window.ToneDen.parameters || {}; 23 | 24 | if(window.ToneDenReady && window.ToneDenReady.length > 0) { 25 | for(var i = 0; i < ToneDenReady.length; i++) { 26 | ToneDenReady[i](); 27 | } 28 | } 29 | }, 'toneden'); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 ToneDen, Inc 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Default/Controls.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor');; 2 | var React = require('react'); 3 | 4 | var Columns = require('../../Columns'); 5 | var PlaybackButtons = require('../common/PlaybackButtons'); 6 | var RepeatButton = require('../common/RepeatButton'); 7 | var Row = require('../../Row'); 8 | var Volume = require('../common/Volume'); 9 | 10 | var Controls = React.createClass({ 11 | mixins: [ 12 | Fluxxor.FluxMixin(React), 13 | require('../../mixins/PlayerControls') 14 | ], 15 | render: function() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | }); 31 | 32 | module.exports = Controls; 33 | -------------------------------------------------------------------------------- /sdk/js/analytics.js: -------------------------------------------------------------------------------- 1 | var constants = require('./constants'); 2 | var trackerName = 'ToneDenTracker'; 3 | 4 | if(!window.ga) { 5 | (function(i, s, o, g, r, a, m){ 6 | i['GoogleAnalyticsObject'] = r; // Acts as a pointer to support renaming. 7 | 8 | // Creates an initial ga() function. The queued commands will be executed once analytics.js loads. 9 | i[r] = i[r] || function() { 10 | (i[r].q = i[r].q || []).push(arguments) 11 | }, 12 | 13 | // Sets the time (as an integer) this tag was executed. Used for timing hits. 14 | i[r].l = 1 * new Date(); 15 | 16 | // Insert the script tag asynchronously. Inserts above current tag to prevent blocking in 17 | // addition to using the async attribute. 18 | a = s.createElement(o), 19 | m = s.getElementsByTagName(o)[0]; 20 | a.async = 1; 21 | a.src = g; 22 | m.parentNode.insertBefore(a, m) 23 | })(window, document, 'script', constants.protocol + '//www.google-analytics.com/analytics.js', 'ga'); 24 | } 25 | 26 | if(!ga.getByName || !ga.getByName(trackerName)) { 27 | ga('create', 'UA-55279667-1', 'auto', { 28 | cookieDomain: 'none', 29 | name: trackerName 30 | }); 31 | 32 | module.exports = ga; 33 | } else { 34 | module.exports = ga.getByName(trackerName); 35 | } 36 | -------------------------------------------------------------------------------- /sdk/js/flux/events.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | player: { 3 | audioInterface: { 4 | TRACK_ERROR: 'player.audioInterface.track_error', 5 | TRACK_FINISHED: 'player.audioInterface.track_finished', 6 | TRACK_LOAD_AMOUNT_CHANGED: 'player.audioInterface.track_load_amount_changed', 7 | TRACK_LOAD_START: 'player.audioInterface.track_load_start', 8 | TRACK_PLAY_POSITION_CHANGED: 'player.audioInterface.track_play_position_changed', 9 | TRACK_PLAY_START: 'player.audioInterface.track_play_start', 10 | TRACK_PLAYING_CHANGED: 'player.audioInterface.track_playing_changed', 11 | TRACK_READY: 'player.audioInterface.track_ready', 12 | TRACK_RESOLVED: 'player.audioInterface.track_resolved', 13 | TRACK_UPDATED: 'player.audioInterface.track_updated' 14 | }, 15 | CONFIG_UPDATED: 'player.config_updated', 16 | CREATE: 'player.create', 17 | DESTROY: 'player.destroy', 18 | NEXT_TRACK: 'player.next_track', 19 | PREVIOUS_TRACK: 'player.previous_track', 20 | queue: { 21 | SET_DEFAULTS: 'player.queue.set_defaults', 22 | QUEUE_TRACK: 'player.queue.queue_track', 23 | UNQUEUE_INDEX: 'player.queue.unqueue_index' 24 | }, 25 | track: { 26 | SELECTED: 'player.track.selected', 27 | TOGGLE_PAUSE: 'player.track.toggle_pause' 28 | }, 29 | UPDATE: 'player.update' 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Default/NowPlaying.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Columns = require('../../Columns'); 4 | var Row = require('../../Row'); 5 | 6 | var NowPlaying = React.createClass({ 7 | render: function() { 8 | var nowPlaying = this.props.nowPlaying; 9 | var resolved = nowPlaying.get('resolved'); 10 | 11 | return ( 12 | 13 | 14 | 15 |
16 | 17 |
18 |
19 | 20 |
21 | 22 | 23 | 24 | {resolved.get('title')} 25 | 26 | 27 | 28 | 29 | {resolved.getIn(['user', 'username'])} 30 | 31 | 32 | 33 |
34 | ); 35 | } 36 | }); 37 | 38 | module.exports = NowPlaying; 39 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/common/PlaybackButtons.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var React = require('react'); 3 | 4 | var Columns = require('../../Columns'); 5 | 6 | var PlaybackButtons = React.createClass({ 7 | mixins: [ 8 | Fluxxor.FluxMixin(React), 9 | require('../../mixins/PlayerControls') 10 | ], 11 | getDefaultProps: function() { 12 | return { 13 | showNextAndPrevious: true 14 | }; 15 | }, 16 | render: function() { 17 | var playButtonClass; 18 | 19 | if(this.props.player.getIn(['nowPlaying', 'playing'])) { 20 | playButtonClass = 'tdicon-pause-circle-outline player-play play'; 21 | } else { 22 | playButtonClass = 'tdicon-play-circle-outline player-play play'; 23 | } 24 | 25 | return ( 26 | 27 | 32 | 36 | 41 | 42 | ); 43 | } 44 | }); 45 | 46 | module.exports = PlaybackButtons; 47 | -------------------------------------------------------------------------------- /sdk/js/helpers.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | 3 | module.exports = { 4 | getElementOffset: function(element) { 5 | var rect = element.getBoundingClientRect(); 6 | return { 7 | left: rect.left + document.body.scrollLeft, 8 | top: rect.top + document.body.scrollTop 9 | }; 10 | }, 11 | msToTimestamp: function(milliseconds) { 12 | var totalSeconds = Math.round(milliseconds / 1000); 13 | var minutes = Math.floor(totalSeconds / 60); 14 | var seconds = totalSeconds - minutes * 60; 15 | 16 | if(isNaN(minutes)) { 17 | minutes = ''; 18 | } 19 | 20 | if(isNaN(seconds)) { 21 | return ''; 22 | } 23 | 24 | if(seconds < 10) { 25 | seconds = '0' + seconds; 26 | } 27 | 28 | return minutes + ':' + seconds; 29 | }, 30 | numberToCommaString: function(num) { 31 | if(num) { 32 | return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 33 | } else { 34 | return '-'; 35 | } 36 | }, 37 | // Hacky way to wait for the dispatcher to finish dispatching the currently active action. Not sure what the best 38 | // way to get around this is, as it probably indicates a flaw in our usage of flux. D: 39 | waitForCurrentAction: function(callback, flux) { 40 | var interval = 50; 41 | flux = flux || this.getFlux(); 42 | 43 | async.until(function() { 44 | return !flux.dispatcher.currentActionType; 45 | }.bind(this), function(done) { 46 | return setTimeout(done, interval); 47 | }, callback.bind(this)); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Default/index.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var React = require('react'); 3 | 4 | var Columns = require('../../Columns'); 5 | var Controls = require('./Controls'); 6 | var Loader = require('../../Loader'); 7 | var NowPlaying = require('./NowPlaying'); 8 | var Row = require('../../Row'); 9 | var Playlist = require('./Playlist'); 10 | var Scrubber = require('../common/Scrubber'); 11 | var SocialInfo = require('./SocialInfo'); 12 | 13 | var helpers = require('../../../../helpers'); 14 | 15 | var Default = React.createClass({ 16 | mixins: [ 17 | Fluxxor.FluxMixin(React) 18 | ], 19 | getInitialState: function() { 20 | return { 21 | isScrubbing: false 22 | }; 23 | }, 24 | render: function() { 25 | var player = this.props.player; 26 | var nowPlaying = player.get('nowPlaying'); 27 | var tracks = player.get('tracks'); 28 | 29 | if(player.get('loading')) { 30 | return ; 31 | } 32 | 33 | return ( 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | }); 49 | 50 | module.exports = Default; 51 | -------------------------------------------------------------------------------- /sdk/js/player/components/Columns.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstraction for a div with a certain number of columns in Foundation's CSS framework. 3 | * Takes a 'large' and a 'small' prop, telling the div how many columns to have. 4 | */ 5 | 6 | var React = require('react'); 7 | 8 | var Columns = React.createClass({ 9 | getDefaultProps: function() { 10 | return { 11 | id: '' 12 | }; 13 | }, 14 | render: function() { 15 | var divClass = 'tdcolumns '; 16 | 17 | var large = this.props.large; 18 | 19 | if(!this.props.large && !this.props.small) { 20 | large = 12; 21 | } 22 | 23 | if(large) { 24 | divClass += 'tdlarge-' + large; 25 | } 26 | 27 | if(this.props.small) { 28 | divClass += ' tdsmall-' + this.props.small; 29 | } else { 30 | divClass += ' tdsmall-' + large; 31 | } 32 | 33 | if(this.props.offset) { 34 | divClass += ' tdoffset-' + this.props.offset; 35 | } 36 | 37 | if(this.props.centered) { 38 | divClass += ' tdcolumns-centered'; 39 | } 40 | 41 | if(this.props['large-centered']) { 42 | divClass += ' tdlarge-centered'; 43 | } 44 | 45 | if(this.props['small-centered']) { 46 | divClass += ' tdsmall-centered'; 47 | } 48 | 49 | if(this.props['large-offset']) { 50 | divClass += ' tdlarge-offset-' + this.props['tdlarge-offset']; 51 | } 52 | 53 | if(this.props.className) { 54 | divClass += ' ' + this.props.className; 55 | } 56 | 57 | return ( 58 |
59 | {this.props.children} 60 |
61 | ); 62 | } 63 | }); 64 | 65 | module.exports = Columns; 66 | -------------------------------------------------------------------------------- /sdk/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'td-player'; 3 | src: 4 | url('//sd.toneden.io/resources/fonts/td-player.woff') format('woff'), 5 | url('//sd.toneden.io/resources/fonts/td-player.eot') format('embedded-opentype'), 6 | url('//sd.toneden.io/resources/fonts/td-player.ttf') format('truetype'), 7 | url('//sd.toneden.io/resources/fonts/td-player.svg') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="tdicon-"], [class*="tdicon-"] { 13 | font-family: 'td-player'; 14 | speak: none; 15 | font-style: normal; 16 | font-weight: normal; 17 | font-variant: normal; 18 | text-transform: none; 19 | line-height: 1; 20 | display: inline-block; 21 | text-rendering: auto; 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | 28 | .tdicon-share:before { 29 | content: "\e60a"; 30 | } 31 | 32 | .tdicon-td_logo:before { 33 | content: "\e60b"; 34 | } 35 | 36 | .tdicon-heart:before { 37 | content: "\f004"; 38 | } 39 | 40 | .tdicon-angle-double-left:before { 41 | content: "\f100"; 42 | } 43 | 44 | .tdicon-angle-double-right:before { 45 | content: "\f101"; 46 | } 47 | 48 | .tdicon-circle-o-notch:before { 49 | content: "\f1ce"; 50 | } 51 | 52 | .tdicon-pause-circle-fill:before { 53 | content: "\e600"; 54 | } 55 | 56 | .tdicon-pause-circle-outline:before { 57 | content: "\e601"; 58 | } 59 | 60 | .tdicon-play-circle-fill:before { 61 | content: "\e602"; 62 | } 63 | 64 | .tdicon-play-circle-outline:before { 65 | content: "\e603"; 66 | } 67 | 68 | .tdicon-skip-next:before { 69 | content: "\e604"; 70 | } 71 | 72 | .tdicon-skip-previous:before { 73 | content: "\e605"; 74 | } 75 | 76 | .tdicon-volume-down:before { 77 | content: "\e606"; 78 | } 79 | 80 | .tdicon-volume-off:before { 81 | content: "\e607"; 82 | } 83 | 84 | .tdicon-volume-up:before { 85 | content: "\e608"; 86 | } 87 | 88 | .tdicon-repeat:before { 89 | content: "\e609"; 90 | } 91 | 92 | .tdicon-soundcloud:before { 93 | content: "\e60c"; 94 | } 95 | 96 | .tdicon-file-download:before { 97 | content: "\e60e"; 98 | } 99 | 100 | .tdicon-warning:before { 101 | content: "\e60d"; 102 | } 103 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/common/Volume.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var React = require('react'); 3 | 4 | var Columns = require('../../Columns'); 5 | var Row = require('../../Row'); 6 | 7 | var Volume = React.createClass({ 8 | mixins: [ 9 | Fluxxor.FluxMixin(React), 10 | require('../../mixins/PlayerControls') 11 | ], 12 | render: function() { 13 | var volume = this.props.volume; 14 | 15 | var activeVolumeClass; 16 | 17 | if(volume === 0) { 18 | activeVolumeClass = 'tdicon-volume-off'; 19 | } else if(volume === 50) { 20 | activeVolumeClass = 'tdicon-volume-down'; 21 | } else { 22 | activeVolumeClass = 'tdicon-volume-up'; 23 | } 24 | 25 | return ( 26 | 27 | 32 | 38 | 43 | 48 | 53 | 54 | 55 | ); 56 | } 57 | }); 58 | 59 | module.exports = Volume; 60 | -------------------------------------------------------------------------------- /sdk/js/player/components/mixins/PlayerControls.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var React = require('react'); 3 | 4 | module.exports = { 5 | getInitialState: function() { 6 | return { 7 | showVolumeOptions: false 8 | }; 9 | }, 10 | getPlayer: function() { 11 | if(this.state && this.state.player) { 12 | return this.state.player; 13 | } else { 14 | return this.props.player; 15 | } 16 | }, 17 | onKeyDown: function(e) { 18 | // Ignore if we're in a textarea or text input. 19 | var targetTag = e.target.tagName.toLowerCase(); 20 | if(targetTag === 'input' || targetTag === 'textarea') { 21 | return; 22 | } 23 | 24 | if(e.keyCode === 32) { 25 | this.onPlayButtonClick(); 26 | e.preventDefault(); 27 | } else if(e.keyCode === 39) { 28 | this.onNextButtonClick(); 29 | e.preventDefault(); 30 | } else if(e.keyCode === 37) { 31 | this.onPreviousButtonClick(); 32 | e.preventDefault(); 33 | } 34 | }, 35 | onNextButtonClick: function() { 36 | this.getFlux().actions.player.nextTrack(this.getPlayer().get('id')); 37 | }, 38 | onPlayButtonClick: function() { 39 | var nowPlaying = this.getPlayer().get('nowPlaying'); 40 | 41 | if(nowPlaying) { 42 | this.getFlux().actions.player.track.togglePause(nowPlaying.toJS()); 43 | } 44 | }, 45 | onPreviousButtonClick: function() { 46 | var player = this.getPlayer(); 47 | var nowPlaying = player.get('nowPlaying'); 48 | 49 | if(player.getIn(['nowPlaying', 'playbackPosition']) < 4000) { 50 | this.getFlux().actions.player.previousTrack(player.get('id')); 51 | } else if(nowPlaying) { 52 | this.getFlux().actions.player.track.seekTo(nowPlaying, 0); 53 | } 54 | }, 55 | onRepeatClick: function() { 56 | this.getFlux().actions.player.setRepeat(!this.props.repeat); 57 | }, 58 | onSetVolumeClick: function(volume) { 59 | this.setState({ 60 | showVolumeOptions: false 61 | }); 62 | this.getFlux().actions.player.setVolume(volume); 63 | }, 64 | onShowVolumeControlsClick: function() { 65 | this.setState({ 66 | showVolumeOptions: true 67 | }); 68 | } 69 | }; 70 | 71 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Feed/SocialInfo.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Columns = require('../../Columns'); 4 | var Row = require('../../Row'); 5 | 6 | var helpers = require('../../../../helpers'); 7 | 8 | var SocialInfo = React.createClass({ 9 | render: function() { 10 | var resolved = this.props.nowPlaying.get('resolved'); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | {helpers.numberToCommaString(resolved.get('playback_count'))} 18 | 19 | 20 | 25 | 30 | 31 | 32 | 33 | {(resolved.get('purchase_url') || resolved.get('download_url')) && ( 34 | 35 | 40 | 41 | 42 | 43 | )} 44 | 45 | 46 | 47 | ); 48 | } 49 | }); 50 | 51 | module.exports = SocialInfo; 52 | -------------------------------------------------------------------------------- /sdk/js/index.js: -------------------------------------------------------------------------------- 1 | require('../css'); 2 | 3 | var _merge = require('lodash/object/merge'); 4 | var BatchingStrategy = require('react/lib/ReactDefaultBatchingStrategy'); 5 | var Fluxxor = require('fluxxor'); 6 | var ReactInjection = require('react/lib/ReactInjection'); 7 | 8 | var analytics = require('./analytics'); 9 | var AudioInterface = require('./player/AudioInterface'); 10 | var constants = require('./constants'); 11 | var events = require('./flux/events'); 12 | 13 | // Record initial load event. 14 | analytics('ToneDenTracker.send', { 15 | hitType: 'event', 16 | eventCategory: 'sdk', 17 | eventAction: 'loaded', 18 | eventLabel: window.location.href 19 | }); 20 | 21 | var flux = ToneDen.flux; 22 | var audioInterface = ToneDen.AudioInterface; 23 | 24 | var doNotLogEventTypes = [ 25 | events.player.audioInterface.TRACK_LOAD_AMOUNT_CHANGED, 26 | events.player.audioInterface.TRACK_PLAY_POSITION_CHANGED, 27 | events.player.audioInterface.TRACK_RESOLVED 28 | ]; 29 | 30 | if(!flux) { 31 | flux = new Fluxxor.Flux(require('./flux/stores'), require('./flux/actions')); 32 | 33 | // Custom dispatch wrapper to prevent errors when dispatching actions in componentDidMount method: 34 | // https://github.com/BinaryMuse/fluxxor/pull/100 35 | flux.setDispatchInterceptor(function(action, dispatch) { 36 | BatchingStrategy.batchedUpdates(function() { 37 | dispatch(action); 38 | }); 39 | }); 40 | 41 | // Debug logging. 42 | flux.on('dispatch', function(type, payload) { 43 | if(doNotLogEventTypes.indexOf(type) === -1) { 44 | ToneDen.log(type + ' event dispatched with payload ', payload); 45 | } 46 | }); 47 | } 48 | 49 | if(!audioInterface) { 50 | audioInterface = new AudioInterface(flux); 51 | } 52 | 53 | // Global ToneDen configuration function. 54 | function configure(parameters) { 55 | _merge(ToneDen.parameters, parameters); 56 | } 57 | 58 | function log() { 59 | if(ToneDen.parameters.debug) { 60 | console.debug.apply(console, arguments); 61 | } 62 | } 63 | 64 | var player = require('./player'); 65 | 66 | module.exports = { 67 | AudioInterface: audioInterface, 68 | configure: configure, 69 | flux: flux, 70 | log: log, 71 | player: player, 72 | ready: true 73 | }; 74 | 75 | // Override default React rootID. 76 | ReactInjection.RootIndex.injectCreateReactRootIndex(function() { 77 | return Math.floor(Math.random() * 10000 + 1); 78 | }); 79 | 80 | ToneDen.ready = true; 81 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Default/SocialInfo.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Columns = require('../../Columns'); 4 | var Row = require('../../Row'); 5 | 6 | var helpers = require('../../../../helpers'); 7 | 8 | var SocialInfo = React.createClass({ 9 | render: function() { 10 | var player = this.props.player.toJS(); 11 | var nowPlaying = player.nowPlaying; 12 | var sizeHideStyle = { 13 | display: player.container.offsetWidth < 500 ? 'none' : '' 14 | }; 15 | 16 | return ( 17 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {helpers.numberToCommaString(nowPlaying.resolved.playback_count)} 32 | 33 | 34 | 35 | {helpers.numberToCommaString(nowPlaying.resolved.favoritings_count)} 36 | 37 | 38 | {nowPlaying.resolved.purchase_url && ( 39 | 40 | 41 | {player.useCustomPurchaseTitle && nowPlaying.resolved.purchase_title || 'BUY'} 42 | 43 | 44 | )} 45 | {!nowPlaying.resolved.purchase_url && nowPlaying.resolved.download_url && ( 46 | 47 | 48 | DOWNLOAD 49 | 50 | 51 | )} 52 | 53 | ); 54 | } 55 | }); 56 | 57 | module.exports = SocialInfo; 58 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Default/Playlist.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var React = require('react'); 3 | 4 | var Columns = require('../../Columns'); 5 | var Row = require('../../Row'); 6 | 7 | var helpers = require('../../../../helpers'); 8 | 9 | var Playlist = React.createClass({ 10 | mixins: [ 11 | Fluxxor.FluxMixin(React) 12 | ], 13 | onTrackClick: function(track) { 14 | this.getFlux().actions.player.track.select(track); 15 | }, 16 | render: function() { 17 | var player = this.props.player.toJS(); 18 | var nowPlaying = player.nowPlaying; 19 | 20 | var playlist = player.tracks.map(function(track, index) { 21 | return ( 22 | 27 | {track.id === nowPlaying.id && ( 28 | 29 | 30 | 31 | )} 32 | {track.id !== nowPlaying.id && ( 33 | 34 |
35 | 36 | )} 37 | 38 | 39 | {track.resolved.title} 40 | 41 | 42 | 46 | 47 | 48 | 49 | {helpers.numberToCommaString(track.resolved.playback_count)} 50 | 51 | 52 | 53 | {helpers.numberToCommaString(track.resolved.favoritings_count)} 54 | 55 | 56 | 57 | 58 | ); 59 | }.bind(this)); 60 | 61 | return ( 62 | 63 | 64 | 65 | {playlist} 66 | 67 |
68 |
69 | ); 70 | } 71 | }); 72 | 73 | module.exports = Playlist; 74 | -------------------------------------------------------------------------------- /test/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url(https://s3-us-west-1.amazonaws.com/toneden-static/production/images/template/bg/cover.jpg) no-repeat center center fixed !important; 3 | -webkit-background-size: cover; 4 | -moz-background-size: cover; 5 | -o-background-size: cover; 6 | background-size: cover; 7 | font-weight: normal; 8 | } 9 | 10 | #demo { 11 | min-height: 800px; 12 | } 13 | 14 | #player { 15 | height: 560px; 16 | } 17 | 18 | #player-banner { 19 | font-family: 'Source Sans Pro', sans-serif; 20 | background: white; 21 | padding:60px 0px; 22 | } 23 | 24 | #header { 25 | font-family: 'Source Sans Pro', sans-serif; 26 | color: white; 27 | text-align: center; 28 | margin: 30px 0px; 29 | } 30 | 31 | #header h1 { 32 | font-size: 16px; 33 | font-weight: 300; 34 | font-style: italic; 35 | text-shadow: 0 0 5px #E0E0E0; 36 | color:black; 37 | margin-top: 10px; 38 | } 39 | 40 | #header h2 { 41 | font-size: 24px; 42 | font-weight: 300; 43 | text-shadow: 0 0 5px #E0E0E0; 44 | color:black; 45 | } 46 | 47 | .skin-change-container { 48 | text-align: center; 49 | margin-bottom: 20px; 50 | } 51 | 52 | .skin-change-btn { 53 | padding: 0px 9px; 54 | border-radius: 100%; 55 | margin: 0px 20px; 56 | opacity: 0.8; 57 | } 58 | 59 | .skin-change-btn:hover { 60 | opacity: 1; 61 | } 62 | 63 | .skin-change-btn.active { 64 | opacity: 1; 65 | border: 2px solid #3498db; 66 | box-sizing: border-box; 67 | } 68 | 69 | #light-btn { 70 | background: rgba(255,255,255,0.8); 71 | } 72 | 73 | #dark-btn { 74 | background: rgba(0,0,0,0.85); 75 | } 76 | 77 | #slate-btn { 78 | background: #6E727E; 79 | } 80 | 81 | #aurora-btn { 82 | background: #02DDA8; 83 | } 84 | 85 | #mojave-btn { 86 | background: #F58F86; 87 | } 88 | 89 | .tagline p { 90 | font-size: 32px; 91 | font-weight: 300; 92 | text-align: center; 93 | } 94 | 95 | .doc-sec { 96 | margin: 50px 0px; 97 | } 98 | 99 | .doc-sec h3 { 100 | font-size: 24px; 101 | font-weight: normal; 102 | } 103 | 104 | pre { 105 | background: #F8FAFC; 106 | margin: 10px 0px !important; 107 | padding: 10px !important; 108 | } 109 | 110 | code { 111 | font-weight: normal !important; 112 | color: #3AC095 !important; 113 | font-size: 14px; 114 | overflow: hidden; 115 | word-wrap: normal; 116 | text-overflow: ellipsis; 117 | } 118 | 119 | #tdsplash { 120 | background: rgba(15, 45, 56, 0.63); 121 | text-align: center; 122 | padding: 80px 0px; 123 | } 124 | 125 | #tdsplash h4, #tdsplash h6 { 126 | color: white; 127 | text-align: center; 128 | } 129 | 130 | #tdsplash h4, #tdsplash h4 code { 131 | font-size:32px; 132 | } 133 | 134 | #tdsplash .showcase { 135 | margin: 40px auto; 136 | } 137 | 138 | #tdsplash .showcase-button { 139 | font-family: 'Source Sans Pro', sans-serif !important; 140 | font-size: 18px; 141 | font-weight: 300; 142 | letter-spacing: 2px; 143 | text-transform: uppercase; 144 | background: none; 145 | border: 1px solid white; 146 | color: white; 147 | border-radius: 2px; 148 | } 149 | -------------------------------------------------------------------------------- /sdk/js/flux/stores/TrackQueueStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores the global queue of tracks to be played. 3 | */ 4 | 5 | var Fluxxor = require('fluxxor'); 6 | var Immutable = require('immutable'); 7 | 8 | var events = require('../events'); 9 | 10 | var TrackQueueStore = Fluxxor.createStore({ 11 | initialize: function() { 12 | this.defaultQueue = Immutable.List(); 13 | this.queue = Immutable.List(); 14 | 15 | this.bindActions( 16 | events.player.audioInterface.TRACK_PLAY_START, this.onTrackPlayStart, 17 | events.player.audioInterface.TRACK_RESOLVED, this.onTrackResolved, 18 | events.player.PREVIOUS_TRACK, this.onPlayerPreviousTrack, 19 | events.player.queue.SET_DEFAULTS, this.onSetDefaults, 20 | events.player.queue.QUEUE_TRACK, this.onQueueTrack, 21 | events.player.queue.UNQUEUE_INDEX, this.onUnqueueIndex 22 | ); 23 | }, 24 | onPlayerPreviousTrack: function(payload) { 25 | this.waitFor(['PlayerInstanceStore'], function(PlayerInstanceStore) { 26 | var playerWasPlaying = PlayerInstanceStore.instances.getIn([payload.playerID, 'nowPlaying']); 27 | var willPlay = PlayerInstanceStore.instances.getIn([payload.playerID, 'nextTrack']); 28 | 29 | if(playerWasPlaying && willPlay && playerWasPlaying !== willPlay) { 30 | this.defaultQueue = this.defaultQueue.unshift(playerWasPlaying); 31 | } 32 | }); 33 | }, 34 | onQueueTrack: function(payload) { 35 | var index = payload.index || this.queue.size; 36 | this.queue = this.queue.splice(index, 0, payload.trackID); 37 | 38 | this.emit('change'); 39 | }, 40 | onSetDefaults: function(payload) { 41 | this.defaultQueue = Immutable.List(payload.result); 42 | this.emit('change'); 43 | }, 44 | onTrackPlayStart: function(payload) { 45 | var trackID = payload.trackID; 46 | 47 | if(this.queue.get(0) === trackID) { 48 | this.queue = this.queue.splice(0, 1); 49 | } 50 | 51 | if(this.defaultQueue.get(0) === trackID) { 52 | this.defaultQueue = this.defaultQueue.splice(0, 1); 53 | } 54 | 55 | this.emit('change'); 56 | }, 57 | onTrackResolved: function(payload) { 58 | var queueIndex = this.queue.indexOf(payload.trackID); 59 | var defaultIndex = this.defaultQueue.indexOf(payload.trackID); 60 | 61 | if(queueIndex !== -1) { 62 | this.queue = Immutable.List.prototype.splice.apply(this.queue, [queueIndex, 1].concat(payload.result)); 63 | } 64 | 65 | if(defaultIndex !== -1) { 66 | this.defaultQueue = Immutable.List.prototype.splice.apply(this.defaultQueue, [defaultIndex, 1].concat(payload.result)); 67 | } 68 | 69 | if(queueIndex !== -1 || defaultIndex !== -1) { 70 | this.emit('change'); 71 | } 72 | }, 73 | onUnqueueIndex: function(payload) { 74 | var index = payload.index; 75 | this.queue = this.queue.splice(index, 1); 76 | 77 | this.emit('change'); 78 | } 79 | }); 80 | 81 | module.exports = TrackQueueStore; 82 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Feed/index.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var React = require('react'); 3 | 4 | var Columns = require('../../Columns'); 5 | var Loader = require('../../Loader'); 6 | var Row = require('../../Row'); 7 | var SocialInfo = require('./SocialInfo'); 8 | 9 | var Feed = React.createClass({ 10 | mixins: [ 11 | Fluxxor.FluxMixin(React), 12 | require('../../mixins/PlayerControls') 13 | ], 14 | renderFeedInfo: function() { 15 | var player = this.props.player; 16 | var nowPlaying = player.get('nowPlaying'); 17 | var resolved = nowPlaying.get('resolved'); 18 | var hasError = nowPlaying.get('error'); 19 | var hasTitle = resolved.get('title'); 20 | 21 | if(hasError && !hasTitle) { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | {nowPlaying.get('errorMessage')} 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } else { 36 | return ( 37 | 38 | 39 | 40 | 41 | 42 | {resolved.get('title')} 43 | 44 | 45 | 46 | 47 | {resolved.getIn(['user', 'username'])} 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | }, 57 | render: function() { 58 | var player = this.props.player; 59 | var nowPlaying = player.get('nowPlaying'); 60 | var playButtonClass; 61 | 62 | if(player.get('loading') || !nowPlaying) { 63 | return 64 | } 65 | 66 | var resolved = nowPlaying.get('resolved'); 67 | 68 | if(nowPlaying.get('playing')) { 69 | playButtonClass = 'tdicon-pause-circle-outline player-play play'; 70 | } else { 71 | playButtonClass = 'tdicon-play-circle-outline player-play play'; 72 | } 73 | 74 | return ( 75 | 76 | {!nowPlaying.get('error') && 77 | 78 | 79 |
80 | 81 |
82 |
83 | 84 | 85 | 86 |
87 |
88 |
89 | } 90 | {this.renderFeedInfo()} 91 |
92 | ); 93 | } 94 | }); 95 | 96 | module.exports = Feed; 97 | -------------------------------------------------------------------------------- /sdk/js/player/components/Player.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var Immutable = require('immutable'); 3 | var React = require('react'); 4 | 5 | var Default = require('./themes/Default'); 6 | var Empty = require('./themes/Empty'); 7 | var Feed = require('./themes/Feed'); 8 | var Mini = require('./themes/Mini'); 9 | var Single = require('./themes/Single'); 10 | var Full = require('./themes/Full'); 11 | 12 | var helpers = require('../../helpers'); 13 | 14 | var Player = React.createClass({ 15 | mixins: [ 16 | Fluxxor.FluxMixin(React), 17 | Fluxxor.StoreWatchMixin('PlayerInstanceStore'), 18 | require('./mixins/PlayerControls') 19 | ], 20 | getStateFromFlux: function() { 21 | var PlayerInstanceStore = this.getFlux().store('PlayerInstanceStore'); 22 | var TrackStore = this.getFlux().store('TrackStore'); 23 | var instance = PlayerInstanceStore.getStateByID(this.props.id); 24 | 25 | if(!instance) { 26 | React.unmountComponentAtNode(this.refs.root.getDOMNode().parentElement); 27 | return; 28 | } 29 | 30 | var nextTrackID = instance.get('nextTrack'); 31 | 32 | instance = instance.merge({ 33 | nextTrack: nextTrackID === 'end' ? nextTrackID : TrackStore.getTracks(nextTrackID).get(0), 34 | nowPlaying: TrackStore.getTracks(instance.get('nowPlaying')).get(0), 35 | tracks: TrackStore.getTracks(instance.get('tracks')) 36 | }); 37 | 38 | var tracks = instance.get('tracks'); 39 | 40 | // Global instances may not have the nowPlaying track in their tracks array. 41 | if(instance.get('global')) { 42 | tracks = tracks.push(instance.get('nowPlaying')); 43 | } 44 | 45 | var loading = !tracks.every(function(track) { 46 | return track && (track.get('resolved') || track.get('error')); 47 | }); 48 | 49 | instance = instance.set('loading', loading); 50 | 51 | return { 52 | player: instance 53 | }; 54 | }, 55 | shouldComponentUpdate: function(nextProps, nextState) { 56 | return !Immutable.is(nextState.player, this.state.player) || !Immutable.is(nextProps, this.props); 57 | }, 58 | componentDidMount: function() { 59 | if(this.props.keyboardEvents) { 60 | document.addEventListener('keydown', this.onKeyDown); 61 | } 62 | }, 63 | componentDidUpdate: function(prevProps, prevState) { 64 | var nextTrack = this.state.player.get('nextTrack'); 65 | 66 | // If the currently playing track has finished, the nextTrack property will be set. In that case, play it. 67 | if(nextTrack && !prevState.player.get('nextTrack')) { 68 | helpers.waitForCurrentAction.bind(this)(function() { 69 | var nowPlaying; 70 | 71 | if(nextTrack === 'end') { 72 | nowPlaying = this.state.player.get('nowPlaying'); 73 | 74 | if(nowPlaying) { 75 | this.getFlux().actions.player.track.togglePause(nowPlaying.toJS()); 76 | } 77 | } else { 78 | this.getFlux().actions.player.track.select(nextTrack.toJS()); 79 | } 80 | }); 81 | } 82 | }, 83 | componentWillUnmount: function() { 84 | if(this.props.keyboardEvents) { 85 | document.removeEventListener('keydown', this.onKeyDown); 86 | } 87 | }, 88 | render: function() { 89 | var playerContent; 90 | var themeClass = ''; 91 | 92 | if(this.state.player.get('empty')) { 93 | playerContent = ; 94 | } else if(this.state.player.get('single')) { 95 | playerContent = ; 96 | themeClass = 'solo'; 97 | } else if(this.state.player.get('mini')) { 98 | playerContent = ; 99 | themeClass = 'mini'; 100 | } else if(this.state.player.get('feed')) { 101 | playerContent = ; 102 | themeClass = 'feed'; 103 | } else if (this.state.player.get('full')) { 104 | playerContent = ; 105 | themeClass = 'full'; 106 | } else { 107 | playerContent = ; 108 | if(this.state.player.get('shrink') && this.state.player.get('container').offsetHeight < 500) { 109 | themeClass = 'shrink'; 110 | } 111 | } 112 | 113 | return ( 114 |
115 | {playerContent} 116 |
117 | ); 118 | } 119 | }); 120 | 121 | module.exports = Player; 122 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Full.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Columns = require('../Columns'); 4 | var Loader = require('../Loader'); 5 | var PlaybackButtons = require('./common/PlaybackButtons'); 6 | var Row = require('../Row'); 7 | var RepeatButton = require('./common/RepeatButton'); 8 | var Scrubber = require('./common/Scrubber'); 9 | var Volume = require('./common/Volume'); 10 | 11 | var helpers = require('../../../helpers'); 12 | 13 | var Full = React.createClass({ 14 | render: function() { 15 | var player = this.props.player; 16 | var isSmallContainer = player.getIn(['container', 'offsetWidth']) < 400; 17 | 18 | if(player.get('loading')) { 19 | return ; 20 | } 21 | 22 | var nowPlaying = player.get('nowPlaying'); 23 | var resolved = nowPlaying.get('resolved'); 24 | 25 | return ( 26 | 27 | 33 | 34 |
35 | 36 |
37 | 43 | 44 | 45 | 46 | 47 | {resolved.get('title')} 48 | 49 | 50 | 51 | 52 | {resolved.getIn(['user', 'username'])} 53 | 54 | 55 | 56 | 57 | {nowPlaying.get('error') && ( 58 | 59 | 60 | 61 | 62 | 63 | {nowPlaying.get('errorMessage')} 64 | 65 | 66 | 67 | 68 | )} 69 | {!nowPlaying.get('error') && ( 70 | 71 | 72 | 73 | )} 74 | 75 | 76 | 77 | 78 | 79 | 80 | 1} 84 | {...this.props} 85 | /> 86 | 87 | 88 | 89 | 90 | 91 |
92 |
93 |
94 | ); 95 | } 96 | }); 97 | 98 | module.exports = Full; 99 | -------------------------------------------------------------------------------- /sdk/js/player/network/soundcloud.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var jsonp = require('jsonp'); 3 | var encodeQuerystring = require('querystring/encode'); 4 | var request = require('superagent'); 5 | 6 | var constants = require('../../constants'); 7 | 8 | var soundcloudApiUrl = constants.protocol + '//api.soundcloud.com/'; 9 | var soundcloudResolveUrl = soundcloudApiUrl + 'resolve?url=http://soundcloud.com'; 10 | 11 | var userAgent = navigator.userAgent; 12 | var isSafari = false; 13 | 14 | if(userAgent.indexOf('Safari') !== -1 && userAgent.indexOf('Chrome') === -1) { 15 | isSafari = true; 16 | } 17 | 18 | function makeSoundCloudRequest(method, url, params, callback) { 19 | params = params || {}; 20 | 21 | // Avoid this problem: http://stackoverflow.com/questions/24175638/remove-cors-accept-encoding-header-on-safari 22 | if(isSafari) { 23 | var query = encodeQuerystring(params); 24 | return jsonp(url + '?' + query, function(err, body) { 25 | if(err) { 26 | return callback(err); 27 | } else { 28 | return callback(null, { 29 | body: body 30 | }); 31 | } 32 | }); 33 | } else { 34 | var requestCreator = request[method](url); 35 | 36 | if(method === 'get') { 37 | requestCreator.query(params); 38 | } else { 39 | requestCreator.send(params); 40 | } 41 | 42 | return requestCreator.end(function(err, res) { 43 | if(err) { 44 | return callback(err, null); 45 | } else { 46 | return callback(null, res); 47 | } 48 | }); 49 | } 50 | } 51 | 52 | function resolve(track, tracksPerArtist, callback) { 53 | async.waterfall([ 54 | function(next) { 55 | var query = { 56 | consumer_key: ToneDen.parameters.soundcloudConsumerKey, 57 | format: 'json', 58 | }; 59 | var url; 60 | 61 | if(track.stream_secret) { 62 | query.secret_token = track.stream_secret 63 | } 64 | 65 | 66 | if(track.stream_id) { 67 | url = soundcloudApiUrl + 'tracks/' + track.stream_id; 68 | } else { 69 | url = soundcloudApiUrl + 'resolve'; 70 | query.url = track.stream_url; 71 | } 72 | 73 | return makeSoundCloudRequest('get', url, query, next); 74 | }, 75 | function(res, next) { 76 | var item = res.body; 77 | 78 | if(item.kind === 'track') { 79 | return processTrack(item, function(err, track) { 80 | return next(err, [track]); 81 | }); 82 | } else if(item.kind === 'playlist') { 83 | return async.map(item.tracks, processTrack, next); 84 | } else if(item.kind === 'user') { 85 | return getTracksForUser(item, tracksPerArtist, next); 86 | } 87 | } 88 | ], callback); 89 | } 90 | 91 | function getTracksForUser(user, limit, callback) { 92 | var tracksUrl = soundcloudApiUrl + 'users/' + user.id + '/tracks.json'; 93 | var query = { 94 | consumer_key: ToneDen.parameters.soundcloudConsumerKey, 95 | limit: limit 96 | }; 97 | 98 | makeSoundCloudRequest('get', tracksUrl, query, function(err, res) { 99 | if(err) { 100 | return callback(err); 101 | } 102 | 103 | var tracks = res.body; 104 | 105 | return async.map(tracks, processTrack, callback); 106 | }); 107 | } 108 | 109 | function processTrack(track, callback) { 110 | var err = null; 111 | var queryStringPrefix; 112 | 113 | if(!track.streamable) { 114 | return callback(new Error('This track is not streamable.')); 115 | } 116 | 117 | if(track.stream_url.indexOf('?') === -1) { 118 | queryStringPrefix = '?'; 119 | } else { 120 | queryStringPrefix = '&'; 121 | } 122 | 123 | track.stream_url += queryStringPrefix + 'consumer_key=' + ToneDen.parameters.soundcloudConsumerKey; 124 | 125 | if(track.artwork_url) { 126 | track.artwork_url = track.artwork_url.replace('large.jpg', 't500x500.jpg'); 127 | 128 | // SoundCloud uses 4 identical CDN domains to allow concurrently loading more images. Randomly select 129 | // one to load from. 130 | var between1And4 = Math.round(Math.random() * 3) + 1; 131 | 132 | if(track.artwork_url.indexOf('i1.sndcdn') !== -1) { 133 | track.artwork_url = track.artwork_url.replace('i1.sndcdn', 'i' + between1And4 + '.sndcdn'); 134 | } 135 | } 136 | 137 | if(track.download_url) { 138 | if(track.sharing === 'private') { 139 | track.download_url += '&client_id=6f85bdf51b0a19b7ab2df7b969233901'; 140 | } else { 141 | track.download_url += '?client_id=6f85bdf51b0a19b7ab2df7b969233901'; 142 | } 143 | } 144 | 145 | return callback(null, track); 146 | } 147 | 148 | module.exports = { 149 | resolve: resolve 150 | }; 151 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Mini.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var React = require('react'); 3 | 4 | var Columns = require('../Columns'); 5 | var Loader = require('../Loader'); 6 | var Row = require('../Row'); 7 | var Scrubber = require('./common/Scrubber'); 8 | var Volume = require('./common/Volume'); 9 | 10 | var helpers = require('../../../helpers'); 11 | 12 | var Mini = React.createClass({ 13 | mixins: [ 14 | Fluxxor.FluxMixin(React), 15 | require('../mixins/PlayerControls') 16 | ], 17 | render: function() { 18 | var player = this.props.player; 19 | 20 | if(player.get('loading')) { 21 | return ; 22 | } 23 | 24 | var nowPlaying = player.get('nowPlaying'); 25 | var resolved = nowPlaying.get('resolved'); 26 | var playButtonClass; 27 | 28 | if(nowPlaying.get('playing')) { 29 | playButtonClass = 'tdicon-pause-circle-outline player-play play'; 30 | } else { 31 | playButtonClass = 'tdicon-play-circle-outline player-play play'; 32 | } 33 | 34 | return ( 35 | 36 | 37 |
38 | 39 | 40 | 41 |
42 |
43 | 44 |
45 |
46 | {nowPlaying.get('error') && ( 47 | 48 | 49 | 50 | 51 | {nowPlaying.get('errorMessage')} 52 | 53 | 54 | 55 | )} 56 | {!nowPlaying.get('error') && ( 57 | 58 | 59 | 60 | )} 61 | 62 | 63 | 64 | 65 | {resolved.get('title')} - {resolved.getIn(['user', 'username'])} 66 | 67 | 68 | 69 | 70 |
    71 |
  • 72 | 73 | {helpers.numberToCommaString(resolved.get('playback_count'))} 74 |
  • 75 |
  • 76 | 77 | {helpers.numberToCommaString(resolved.get('favoritings_count'))} 78 |
  • 79 |
80 | 81 | {resolved.get('purchase_url') && ( 82 | 83 | 84 | {player.get('useCustomPurchaseTitle') ? resolved.get('purchase_title') : 'BUY'} 85 | 86 | 87 | )} 88 | {!resolved.get('purchase_url') && ( 89 | 90 | 91 | DOWNLOAD 92 | 93 | 94 | )} 95 | 96 | 97 | 98 | 99 | 100 | 101 |
102 | ); 103 | } 104 | }); 105 | 106 | module.exports = Mini; 107 | -------------------------------------------------------------------------------- /sdk/js/flux/stores/TrackStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores all tracks that have been loaded on the page. 3 | */ 4 | 5 | var Fluxxor = require('fluxxor'); 6 | var Immutable = require('immutable'); 7 | 8 | var events = require('../events'); 9 | 10 | var TrackStore = Fluxxor.createStore({ 11 | initialize: function() { 12 | this.tracks = Immutable.Map(); 13 | 14 | this.bindActions( 15 | events.player.audioInterface.TRACK_ERROR, this.onTrackError, 16 | events.player.audioInterface.TRACK_FINISHED, this.onTrackFinished, 17 | events.player.audioInterface.TRACK_LOAD_AMOUNT_CHANGED, this.onTrackLoadAmountChanged, 18 | events.player.audioInterface.TRACK_PLAY_POSITION_CHANGED, this.onTrackPlayPositionChanged, 19 | events.player.audioInterface.TRACK_PLAYING_CHANGED, this.onTrackPlayingChanged, 20 | events.player.audioInterface.TRACK_PLAY_START, this.onTrackPlayStart, 21 | events.player.audioInterface.TRACK_READY, this.onTrackReady, 22 | events.player.audioInterface.TRACK_RESOLVED, this.onTrackResolved, 23 | events.player.audioInterface.TRACK_UPDATED, this.onTrackUpdated, 24 | events.player.CREATE, this.onTrackUpdated, 25 | events.player.track.SELECTED, this.onTrackSelected, 26 | events.player.track.TOGGLE_PAUSE, this.onTrackTogglePause 27 | ); 28 | }, 29 | getTracks: function(trackIDs) { 30 | if(typeof trackIDs === 'string' || typeof trackIDs === 'number') { 31 | trackIDs = [String(trackIDs)]; 32 | } else if(trackIDs instanceof Array || trackIDs instanceof Immutable.List) { 33 | trackIDs = trackIDs.map(String); 34 | } else if(!trackIDs) { 35 | return Immutable.List(); 36 | } 37 | 38 | trackIDs = Immutable.List(trackIDs); 39 | 40 | return trackIDs.map(function(id) { 41 | return this.tracks.get(id); 42 | }.bind(this)).toList(); 43 | }, 44 | onTrackError: function(payload) { 45 | this.tracks = this.tracks.mergeDeepIn([payload.trackID], { 46 | error: true, 47 | errorMessage: payload.error.message, 48 | loading: false, 49 | playing: false, 50 | resolved: { 51 | user: {} 52 | } 53 | }); 54 | 55 | this.emit('change'); 56 | }, 57 | onTrackFinished: function(payload) { 58 | this.tracks = this.tracks.setIn([payload.trackID, 'playing'], false); 59 | this.tracks = this.tracks.setIn([payload.trackID, 'playbackPosition'], 0); 60 | 61 | ToneDen.player.emit('track.finished', this.tracks.get(payload.trackID).toJS()); 62 | this.emit('change'); 63 | }, 64 | onTrackLoadAmountChanged: function(payload) { 65 | this.tracks = this.tracks.setIn([payload.trackID, 'loading'], true); 66 | this.emit('change'); 67 | }, 68 | onTrackPlayingChanged: function(payload) { 69 | this.tracks = this.tracks.setIn([payload.trackID, 'playing'], payload.isPlaying); 70 | 71 | if(payload.isPlaying) { 72 | ToneDen.player.emit('track.played', this.tracks.get(payload.trackID).toJS()); 73 | } else { 74 | ToneDen.player.emit('track.paused', this.tracks.get(payload.trackID).toJS()); 75 | } 76 | 77 | this.emit('change'); 78 | }, 79 | onTrackPlayPositionChanged: function(payload) { 80 | var trackID = payload.trackID; 81 | var currentPosition = this.tracks.getIn([trackID, 'playbackPosition']) || 0; 82 | var newPosition = payload.position; 83 | 84 | // Only fire an update if the last update occurred more than 100ms ago. 85 | if(Math.abs(newPosition - currentPosition) > 200) { 86 | this.tracks = this.tracks.setIn([trackID, 'playbackPosition'], newPosition); 87 | this.emit('change'); 88 | } 89 | }, 90 | onTrackPlayStart: function(payload) { 91 | this.tracks = this.tracks.map(function(track, id) { 92 | if(id === payload.trackID) { 93 | return track.set('playing', true); 94 | } else { 95 | return track.set('playing', false); 96 | } 97 | }); 98 | 99 | ToneDen.player.emit('track.started', this.tracks.get(payload.trackID).toJS()); 100 | 101 | this.emit('change'); 102 | }, 103 | onTrackReady: function(payload) { 104 | this.tracks = this.tracks.setIn([payload.trackID, 'loading'], false); 105 | this.tracks = this.tracks.setIn([payload.trackID, 'resolved', 'duration'], payload.sound.duration); 106 | 107 | this.emit('change'); 108 | }, 109 | onTrackResolved: function(payload) { 110 | this.tracks = this.tracks.mergeDeep(payload.entities.tracks); 111 | this.emit('change'); 112 | }, 113 | onTrackSelected: function(payload) { 114 | this.tracks = this.tracks.setIn([payload.result, 'loading'], true); 115 | this.emit('change'); 116 | }, 117 | onTrackTogglePause: function(payload) { 118 | this.tracks = this.tracks.setIn([payload.trackID, 'playing'], !this.tracks.getIn([payload.trackID, 'playing'])); 119 | this.emit('change'); 120 | }, 121 | onTrackUpdated: function(payload) { 122 | this.tracks = this.tracks.mergeDeep(payload.entities.tracks); 123 | this.emit('change'); 124 | } 125 | }); 126 | 127 | module.exports = TrackStore; 128 | -------------------------------------------------------------------------------- /sdk/js/flux/actions.js: -------------------------------------------------------------------------------- 1 | var Immutable = require('immutable'); 2 | var normalizr = require('normalizr'); 3 | 4 | // Define schema for nested items in actions. 5 | var Player = new normalizr.Schema('players'); 6 | var Track = new normalizr.Schema('tracks'); 7 | 8 | Player.define({ 9 | nowPlaying: Track, 10 | tracks: normalizr.arrayOf(Track) 11 | }); 12 | 13 | var events = require('./events'); 14 | 15 | module.exports = { 16 | player: { 17 | audioInterface: { 18 | onTrackError: function(trackID, err) { 19 | this.dispatch(events.player.audioInterface.TRACK_ERROR, { 20 | error: err, 21 | trackID: trackID 22 | }); 23 | }, 24 | onTrackFinish: function(trackID) { 25 | this.dispatch(events.player.audioInterface.TRACK_FINISHED, { 26 | trackID: trackID 27 | }); 28 | }, 29 | onTrackLoadAmountChange: function(trackID, bytesLoaded) { 30 | this.dispatch(events.player.audioInterface.TRACK_LOAD_AMOUNT_CHANGED, { 31 | bytesLoaded: bytesLoaded, 32 | trackID: trackID 33 | }); 34 | }, 35 | onTrackPlayingChange: function(trackID, isPlaying) { 36 | this.dispatch(events.player.audioInterface.TRACK_PLAYING_CHANGED, { 37 | isPlaying: isPlaying, 38 | trackID: trackID 39 | }); 40 | }, 41 | onTrackPlayPositionChange: function(trackID, position) { 42 | this.dispatch(events.player.audioInterface.TRACK_PLAY_POSITION_CHANGED, { 43 | position: position, 44 | trackID: trackID 45 | }); 46 | }, 47 | onTrackPlayStart: function(trackID) { 48 | this.dispatch(events.player.audioInterface.TRACK_PLAY_START, { 49 | trackID: trackID 50 | }); 51 | }, 52 | onTrackReady: function(trackID, sound) { 53 | this.dispatch(events.player.audioInterface.TRACK_READY, { 54 | sound: sound, 55 | trackID: trackID 56 | }); 57 | }, 58 | onTrackResolved: function(trackID, tracks) { 59 | var payload = normalizr.normalize(tracks, normalizr.arrayOf(Track)); 60 | payload.trackID = trackID; 61 | 62 | this.dispatch(events.player.audioInterface.TRACK_RESOLVED, payload); 63 | }, 64 | onTrackSoundAdded: function(track) { 65 | // The track may or may not be playing, so don't change its play state in the store. 66 | delete track.playing; 67 | track.loading = !track.sound.loaded; 68 | 69 | var payload = normalizr.normalize(track, Track); 70 | this.dispatch(events.player.audioInterface.TRACK_UPDATED, payload); 71 | } 72 | }, 73 | create: function(player) { 74 | player.nowPlaying = player.tracks[0]; 75 | 76 | var payload = normalizr.normalize(player, Player); 77 | this.dispatch(events.player.CREATE, payload); 78 | 79 | player.tracks.forEach(function(track) { 80 | ToneDen.AudioInterface.resolveTrack(track, player.tracksPerArtist); 81 | }); 82 | }, 83 | destroy: function(playerID) { 84 | this.dispatch(events.player.DESTROY, { 85 | playerID: playerID 86 | }); 87 | }, 88 | nextTrack: function(playerID) { 89 | this.dispatch(events.player.NEXT_TRACK, { 90 | playerID: playerID 91 | }); 92 | }, 93 | previousTrack: function(playerID) { 94 | this.dispatch(events.player.PREVIOUS_TRACK, { 95 | playerID: playerID 96 | }); 97 | }, 98 | queue: { 99 | queueTrack: function(track, index) { 100 | this.dispatch(events.player.queue.QUEUE_TRACK, { 101 | index: index, 102 | trackID: track.id 103 | }); 104 | 105 | ToneDen.AudioInterface.resolveTrack(track); 106 | }, 107 | setDefaultTracks: function(tracks) { 108 | var payload = normalizr.normalize(tracks, normalizr.arrayOf(Track)); 109 | this.dispatch(events.player.queue.SET_DEFAULTS, payload); 110 | 111 | tracks.forEach(ToneDen.AudioInterface.resolveTrack); 112 | }, 113 | unqueueIndex: function(index) { 114 | this.dispatch(events.player.queue.UNQUEUE_INDEX, { 115 | index: index 116 | }); 117 | } 118 | }, 119 | track: { 120 | seekTo: function(track, position) { 121 | ToneDen.AudioInterface.seekTrack(track, position); 122 | }, 123 | select: function(track) { 124 | ToneDen.AudioInterface.loadTrack(track, true); 125 | this.dispatch(events.player.track.SELECTED, normalizr.normalize(track, Track)); 126 | }, 127 | togglePause: function(track, paused) { 128 | ToneDen.AudioInterface.togglePause(track, paused); 129 | } 130 | }, 131 | setRepeat: function(repeat) { 132 | repeat = repeat || false; 133 | this.dispatch(events.player.CONFIG_UPDATED, { 134 | config: { 135 | repeat: repeat 136 | } 137 | }); 138 | }, 139 | setVolume: function(level) { 140 | ToneDen.AudioInterface.setVolume(level); 141 | this.dispatch(events.player.CONFIG_UPDATED, { 142 | config: { 143 | volume: level 144 | } 145 | }); 146 | }, 147 | update: function(playerID, params) { 148 | params.id = playerID; 149 | 150 | if(params.tracks) { 151 | params.nowPlaying = params.tracks[0]; 152 | params.tracks.forEach(function(track) { 153 | ToneDen.AudioInterface.resolveTrack(track, params.tracksPerArtist); 154 | }); 155 | } 156 | 157 | var payload = normalizr.normalize(params, Player); 158 | this.dispatch(events.player.UPDATE, payload); 159 | } 160 | } 161 | }; 162 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/common/Scrubber.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Draggable scrubber component to allow quick movement through the song. 3 | * 4 | * Thanks to Jared Forsyth: 5 | * http://stackoverflow.com/questions/20926551/recommended-way-of-making-react-component-div-draggable 6 | */ 7 | 8 | var Fluxxor = require('fluxxor'); 9 | var React = require('react'); 10 | 11 | var Columns = require('../../Columns'); 12 | var Row = require('../../Row'); 13 | 14 | var helpers = require('../../../../helpers'); 15 | 16 | var Scrubber = React.createClass({ 17 | mixins: [ 18 | Fluxxor.FluxMixin(React) 19 | ], 20 | getDefaultProps: function() { 21 | return { 22 | initialPosition: 0 23 | }; 24 | }, 25 | getInitialState: function() { 26 | return { 27 | dragging: false, 28 | handlePosition: this.props.initialPosition 29 | }; 30 | }, 31 | componentDidUpdate: function(props, state) { 32 | if(this.state.dragging && !state.dragging) { 33 | document.addEventListener('mousemove', this.onMouseMove); 34 | document.addEventListener('mouseup', this.onMouseUp); 35 | } else if(!this.state.dragging && state.dragging) { 36 | document.removeEventListener('mousemove', this.onMouseMove); 37 | document.removeEventListener('mouseup', this.onMouseUp); 38 | } 39 | }, 40 | getHandlePosition: function(e) { 41 | var scrubber = this.refs.scrubber.getDOMNode(); 42 | 43 | var minPosition = 0; 44 | var maxPosition = scrubber.offsetWidth; 45 | var position = e.pageX - helpers.getElementOffset(scrubber).left; 46 | 47 | if(position < minPosition) { 48 | return minPosition; 49 | } else if(position > maxPosition) { 50 | return maxPosition; 51 | } else { 52 | return position; 53 | } 54 | }, 55 | onMouseDown: function(e) { 56 | if(e.button !== 0) { 57 | return 58 | } 59 | 60 | this.setState({ 61 | dragging: true, 62 | handlePosition: this.getHandlePosition(e) 63 | }); 64 | 65 | e.preventDefault(); 66 | }, 67 | onMouseUp: function(e) { 68 | this.setState({ 69 | dragging: false 70 | }); 71 | 72 | var nowPlaying = this.props.nowPlaying; 73 | var value = this.state.handlePosition / this.refs.scrubber.getDOMNode().offsetWidth * 74 | nowPlaying.getIn(['resolved', 'duration']); 75 | 76 | this.getFlux().actions.player.track.seekTo(nowPlaying.toJS(), value); 77 | 78 | e.preventDefault() 79 | }, 80 | onMouseMove: function(e) { 81 | if(!this.state.dragging) { 82 | return; 83 | } 84 | 85 | this.setState({ 86 | handlePosition: this.getHandlePosition(e) 87 | }); 88 | 89 | e.preventDefault(); 90 | }, 91 | onScrubberTrackClick: function(e) { 92 | var track = this.refs.track.getDOMNode(); 93 | var locationToWidthRatio = (e.pageX - helpers.getElementOffset(track).left) / track.offsetWidth; 94 | var milliseconds = locationToWidthRatio * this.props.nowPlaying.getIn(['resolved', 'duration']); 95 | 96 | this.getFlux().actions.player.track.seekTo(this.props.nowPlaying.toJS(), milliseconds); 97 | }, 98 | render: function() { 99 | var nowPlaying = this.props.nowPlaying; 100 | var resolved = nowPlaying.get('resolved'); 101 | var handlePosition; 102 | 103 | if(nowPlaying.get('error')) { 104 | return ( 105 | 106 | 107 | 108 | 109 | 110 | {nowPlaying.get('errorMessage')} 111 | 112 | 113 | 114 | 115 | ); 116 | } else if(!resolved.get('streamable')) { 117 | return ( 118 | 119 | 120 | 121 | 122 | 123 | This track is not streamable. 124 | 125 | 126 | 127 | 128 | ); 129 | } else { 130 | if(this.refs.scrubber && !this.state.dragging) { 131 | handlePosition = nowPlaying.get('playbackPosition') / resolved.get('duration') * 132 | this.refs.scrubber.getDOMNode().offsetWidth; 133 | } else { 134 | handlePosition = this.state.handlePosition; 135 | } 136 | 137 | return ( 138 | 139 | 140 | {helpers.msToTimestamp(nowPlaying.get('playbackPosition') || 0)} 141 | 142 | 143 |
147 |
148 |
153 |
159 |
160 | 161 | 162 | {!nowPlaying.get('loading') && 163 | helpers.msToTimestamp(resolved.get('duration') - (nowPlaying.get('playbackPosition') || 0))} 164 | {nowPlaying.get('loading') && } 165 | 166 | 167 | ); 168 | } 169 | } 170 | }); 171 | 172 | module.exports = Scrubber; 173 | -------------------------------------------------------------------------------- /sdk/js/player/components/themes/Single.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var Columns = require('../Columns'); 4 | var Loader = require('../Loader'); 5 | var PlaybackButtons = require('./common/PlaybackButtons'); 6 | var Row = require('../Row'); 7 | var RepeatButton = require('./common/RepeatButton'); 8 | var Scrubber = require('./common/Scrubber'); 9 | var Volume = require('./common/Volume'); 10 | 11 | var helpers = require('../../../helpers'); 12 | 13 | var Single = React.createClass({ 14 | render: function() { 15 | var player = this.props.player; 16 | var isSmallContainer = player.getIn(['container', 'offsetWidth']) < 400; 17 | 18 | if(player.get('loading')) { 19 | return ; 20 | } 21 | 22 | var nowPlaying = player.get('nowPlaying'); 23 | var resolved = nowPlaying.get('resolved'); 24 | 25 | return ( 26 | 27 | 28 | 29 | 30 | 36 | 37 |
38 | 39 |
40 |
41 | 1} 45 | {...this.props} 46 | /> 47 |
48 |
49 | 50 | 55 | 56 | 57 | 58 | 59 | 60 | 65 | {this.props.useCustomPurchaseTitle ? resolved.get('purchase_title') : 'BUY'} 66 | {!resolved.get('purchase_url') && 'DOWNLOAD'} 67 | 68 | 69 | 70 |
71 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | {resolved.get('title')} 85 | 86 | 87 | 88 | 89 | {resolved.getIn(['user', 'username'])} 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {nowPlaying.get('error') && ( 98 | 99 | 100 | 101 | 102 | 103 | {nowPlaying.get('errorMessage')} 104 | 105 | 106 | 107 | 108 | )} 109 | {!nowPlaying.get('error') && ( 110 | 111 | 112 | 113 | )} 114 | 115 | 116 | 117 | 118 | {helpers.numberToCommaString(resolved.get('playback_count'))} 119 | 120 | 121 | 122 | {helpers.numberToCommaString(resolved.get('favoritings_count'))} 123 | 124 | 125 | 126 | 127 |
128 | ); 129 | } 130 | }); 131 | 132 | module.exports = Single; 133 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Thanks to Pete Hunt for the great webpack how-to: https://github.com/petehunt/webpack-howto 3 | */ 4 | 5 | var fs = require('fs'); 6 | 7 | var webpack = require('webpack'); 8 | var webpackEnv; 9 | 10 | if(process.argv.indexOf('--production') !== -1 || process.argv.indexOf('deploy-production') !== -1) { 11 | webpackEnv = 'production'; 12 | } else if(process.argv.indexOf('--dev') !== -1 || process.argv.indexOf('deploy-dev') !== -1) { 13 | webpackEnv = 'staging'; 14 | } else { 15 | webpackEnv = 'local'; 16 | } 17 | 18 | var webpackPlugins = [ 19 | new webpack.DefinePlugin({ 20 | env: JSON.stringify(webpackEnv) 21 | }) 22 | ]; 23 | 24 | if(webpackEnv === 'production') { 25 | webpackPlugins.push(new webpack.optimize.UglifyJsPlugin({ 26 | compress: { 27 | warnings: false 28 | }, 29 | mangle: true, 30 | sourceMap: webpackEnv === 'local' 31 | })); 32 | } 33 | 34 | module.exports = function(grunt) { 35 | grunt.initConfig({ 36 | aws_s3: { 37 | options: { 38 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 39 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 40 | bucket: 'sd.toneden.io', 41 | debug: false 42 | }, 43 | dev: { 44 | options: { 45 | gzipRename: 'ext' 46 | }, 47 | files: [ 48 | { 49 | expand: true, 50 | src: ['toneden.loader.js.gz', 'toneden.js.gz'], 51 | dest: 'dev/v2/', 52 | options: { 53 | params: { 54 | ContentEncoding: 'gzip', 55 | ContentType: 'application/javascript' 56 | } 57 | } 58 | } 59 | ] 60 | }, 61 | production: { 62 | options: { 63 | gzipRename: 'ext' 64 | }, 65 | files: [ 66 | { 67 | src: ['toneden.loader.js.gz', 'toneden.js.gz'], 68 | dest: 'production/v2/', 69 | options: { 70 | params: { 71 | ContentEncoding: 'gzip', 72 | ContentType: 'application/javascript' 73 | } 74 | } 75 | } 76 | ] 77 | } 78 | }, 79 | clean: { 80 | postUpload: { 81 | src: ['toneden.js.gz', 'toneden.loader.js.gz'] 82 | } 83 | }, 84 | cloudfront_clear: { 85 | dev: { 86 | access_key: process.env.AWS_ACCESS_KEY_ID, 87 | secret_key: process.env.AWS_SECRET_ACCESS_KEY, 88 | dist: 'E3SYREX4SS26L7', 89 | resourcePaths: ['/dev/v2/toneden.loader.js', '/dev/v2/toneden.js'] 90 | }, 91 | production: { 92 | access_key: process.env.AWS_ACCESS_KEY_ID, 93 | secret_key: process.env.AWS_SECRET_ACCESS_KEY, 94 | dist: 'E3SYREX4SS26L7', 95 | resourcePaths: ['/production/v2/toneden.loader.js', '/production/v2/toneden.js'] 96 | } 97 | }, 98 | compress: { 99 | sdk: { 100 | files: [{ 101 | dest: 'toneden.js.gz', 102 | src: 'toneden.js' 103 | }, { 104 | dest: 'toneden.loader.js.gz', 105 | src: 'toneden.loader.js' 106 | }], 107 | pretty: true 108 | } 109 | }, 110 | webpack: { 111 | watch: { 112 | entry: './loader/index.js', 113 | output: { 114 | chunkFilename: 'toneden.js', 115 | crossOriginLoading: 'anonymous', 116 | filename: 'toneden.loader.js', 117 | library: 'ToneDenSDK' 118 | }, 119 | module: { 120 | loaders: [{ 121 | loader: 'style-loader?insertAt=top!css-loader', 122 | test: /\.css$/ 123 | }, { 124 | loader: 'url-loader?limit=8192', 125 | test: /\.(png|jpg)$/ 126 | }, { 127 | loader: 'jsx-loader', 128 | test: /\.jsx$/ 129 | }] 130 | }, 131 | plugins: webpackPlugins, 132 | resolve: { 133 | extensions: ['', '.js', '.jsx', '.css'] 134 | }, 135 | devtool: 'inline-source-map', 136 | keepalive: true, 137 | watch: true 138 | }, 139 | build: { 140 | entry: './loader/index.js', 141 | output: { 142 | chunkFilename: 'toneden.js', 143 | crossOriginLoading: 'anonymous', 144 | filename: 'toneden.loader.js', 145 | library: 'ToneDenSDK' 146 | }, 147 | module: { 148 | loaders: [{ 149 | loader: 'style-loader?insertAt=top!css-loader', 150 | test: /\.css$/ 151 | }, { 152 | loader: 'jsx-loader', 153 | test: /\.jsx$/ 154 | }, { 155 | loader: 'url-loader?limit=8192', 156 | test: /\.(png|jpg)$/ 157 | }] 158 | }, 159 | plugins: webpackPlugins, 160 | resolve: { 161 | extensions: ['', '.js', '.jsx', '.css'] 162 | }, 163 | storeStatsTo: 'webpackStats' 164 | } 165 | } 166 | }); 167 | 168 | grunt.loadNpmTasks('grunt-aws-s3'); 169 | grunt.loadNpmTasks('grunt-cloudfront-clear'); 170 | grunt.loadNpmTasks('grunt-contrib-clean'); 171 | grunt.loadNpmTasks('grunt-contrib-compress'); 172 | grunt.loadNpmTasks('grunt-webpack'); 173 | 174 | grunt.registerTask('writeWebpackStats', 'Writes webpack stats to webpack.json.', function() { 175 | grunt.file.write('webpack.json', JSON.stringify(grunt.config.getRaw('webpackStats'))); 176 | }); 177 | 178 | if(webpackEnv === 'local') { 179 | grunt.registerTask('default', 'webpack:watch'); 180 | } else { 181 | grunt.registerTask('default', ['webpack:build', 'writeWebpackStats']); 182 | } 183 | 184 | function getBuildConfig(env) { 185 | return [ 186 | 'webpack:build', 187 | 'compress:sdk', 188 | 'aws_s3:' + env, 189 | 'clean:postUpload', 190 | 'cloudfront_clear:' + env 191 | ] 192 | } 193 | 194 | grunt.registerTask('deploy-dev', getBuildConfig('dev')); 195 | grunt.registerTask('deploy-production', getBuildConfig('production')); 196 | }; 197 | -------------------------------------------------------------------------------- /sdk/js/player/index.js: -------------------------------------------------------------------------------- 1 | var _find = require('lodash/collection/find'); 2 | var _merge = require('lodash/object/merge'); 3 | var _uniqueId = require('lodash/utility/uniqueId'); 4 | 5 | var EventEmitter = require('events').EventEmitter; 6 | var inherits = require('util').inherits; 7 | var React = require('react'); 8 | 9 | var constants = require('../constants'); 10 | 11 | var Player = require('./components/Player'); 12 | 13 | function processUrlInput(url) { 14 | var id = _uniqueId('track_'); 15 | 16 | if(typeof url === 'object') { 17 | url = { 18 | id: url.id || id, 19 | stream_id: url.stream_id, 20 | stream_secret: url.stream_secret, 21 | stream_url: url.stream_url, 22 | title: url.title, 23 | user: url.user 24 | }; 25 | } else if(typeof url === 'string') { 26 | url = { 27 | id: id, 28 | stream_url: url 29 | }; 30 | } 31 | 32 | url.id = String(url.id); 33 | 34 | return url; 35 | } 36 | 37 | function escapeDomSelector(dom) { 38 | // Escape first number in css selector. 39 | if(dom.charAt(0) === '#' && !isNaN(dom.charAt(1))) { 40 | dom = '#\\' + dom.charCodeAt(1).toString(16) + ' ' + dom.substring(2); 41 | } 42 | 43 | return dom; 44 | } 45 | 46 | function ToneDenPlayer() { 47 | EventEmitter.call(this); 48 | 49 | this.create = function(urls, dom, parameters) { 50 | if(arguments.length === 1) { 51 | parameters = urls; 52 | urls = null; 53 | } 54 | 55 | urls = urls || parameters.urls || []; 56 | dom = dom || parameters.dom; 57 | 58 | _merge(ToneDen.AudioInterface.parameters, { 59 | cache: parameters.cache, 60 | volume: parameters.volume 61 | }); 62 | 63 | var dom = escapeDomSelector(dom); 64 | var container = document.querySelector(dom); 65 | var tracks = urls.map(processUrlInput); 66 | 67 | if(parameters.soundcloudConsumerKey || parameters.debug) { 68 | ToneDen.configure({ 69 | debug: parameters.debug, 70 | soundcloudConsumerKey: parameters.soundcloudConsumerKey 71 | }); 72 | } 73 | 74 | // Set up default parameters. 75 | parameters = _merge({ 76 | container: container, 77 | debug: false, // Output debug messages? 78 | feed: false, 79 | flux: ToneDen.flux, 80 | global: false, // Should this player show what is playing on any player in the page? 81 | keyboardEvents: false, // Should we listen to keyboard events? 82 | id: _uniqueId('player_'), 83 | instance: instance, 84 | mini: false, 85 | playFromQueue: false, 86 | repeat: false, 87 | shrink: true, // Default option to shrink player responsively if container is too small 88 | single: null, 89 | skin: 'light', 90 | staticUrl: constants.protocol + '//sd.toneden.io/', 91 | togglePause: true, // Default option to toggle pause/play when clicked 92 | tracksPerArtist: 10, // How many tracks to load when given an artist SoundCloud URL. 93 | tracks: tracks, 94 | useCustomPurchaseTitle: true, 95 | visualizerType: 'waves', // Equalizer type. 'waves' or 'bars' 96 | volume: 100 97 | }, parameters); 98 | 99 | var instance = { 100 | id: parameters.id, 101 | destroy: function() { 102 | var instance = ToneDen.flux.store('PlayerInstanceStore').instances.get(parameters.id); 103 | var nowPlaying; 104 | 105 | if(instance) { 106 | nowPlaying = ToneDen.flux.store('TrackStore').tracks.get(instance.get('nowPlaying')); 107 | ToneDen.flux.actions.player.destroy(parameters.id); 108 | } 109 | }, 110 | next: function() { 111 | ToneDen.flux.actions.player.nextTrack(parameters.id); 112 | }, 113 | prev: function() { 114 | ToneDen.flux.actions.player.previousTrack(parameters.id); 115 | }, 116 | skipTo: function(index) { 117 | var instance = ToneDen.flux.store('PlayerInstanceStore').instances.get(parameters.id); 118 | var trackID = instance.getIn(['tracks', index]); 119 | var track = trackID && ToneDen.flux.store('TrackStore').tracks.get(trackID); 120 | 121 | if(track) { 122 | ToneDen.flux.actions.player.track.select(track.toJS()); 123 | } 124 | }, 125 | togglePause: function(paused) { 126 | var instance = ToneDen.flux.store('PlayerInstanceStore').instances.get(parameters.id); 127 | var nowPlaying = ToneDen.flux.store('TrackStore').tracks.get(instance.get('nowPlaying')); 128 | 129 | if(nowPlaying) { 130 | ToneDen.flux.actions.player.track.togglePause(nowPlaying.toJS(), paused); 131 | } 132 | }, 133 | update: function(newParameters) { 134 | if(newParameters.urls) { 135 | newParameters.tracks = newParameters.urls.map(processUrlInput); 136 | } 137 | 138 | ToneDen.flux.actions.player.update(parameters.id, newParameters); 139 | } 140 | }; 141 | 142 | var PlayerFactory = React.createFactory(Player); 143 | 144 | ToneDen.flux.actions.player.create(parameters); 145 | 146 | ToneDen.player.instances = ToneDen.player.instances || []; 147 | ToneDen.player.instances.push(instance); 148 | 149 | if(container) { 150 | React.render(PlayerFactory(parameters), container); 151 | } else { 152 | console.error('The dom component specified by "' + dom + '" does not exist.'); 153 | } 154 | 155 | return instance; 156 | }; 157 | this.getInstanceByDom = function(dom) { 158 | dom = escapeDomSelector(dom); 159 | 160 | var PlayerInstanceStore = ToneDen.flux.store('PlayerInstanceStore'); 161 | var domElements = document.querySelectorAll(dom); 162 | 163 | var instance = PlayerInstanceStore.instances.find(function(testInstance) { 164 | var instanceContainer = testInstance.get('container'); 165 | var containerIndex = Array.prototype.indexOf.call(domElements, instanceContainer); 166 | return containerIndex !== -1; 167 | }); 168 | 169 | if(instance) { 170 | return _find(ToneDen.player.instances, { 171 | id: instance.get('id') 172 | }); 173 | } else { 174 | return null; 175 | } 176 | }; 177 | /** 178 | * Functions that act on the global player instance. 179 | */ 180 | this.global = { 181 | nowPlaying: function() { 182 | var globalPlayer = ToneDen.flux.store('PlayerInstanceStore').getGlobalInstance(); 183 | var track; 184 | 185 | if(globalPlayer) { 186 | track = ToneDen.flux.store('TrackStore').tracks.get(globalPlayer.get('nowPlaying')); 187 | 188 | if(track) { 189 | track = track.toJS(); 190 | track.id = Number(track.id); 191 | 192 | return track; 193 | } else { 194 | return false; 195 | } 196 | } else { 197 | return false; 198 | } 199 | }, 200 | /** 201 | * Loads and plays a track in the global player. 202 | */ 203 | playTrack: function(url) { 204 | ToneDen.flux.actions.player.track.select(processUrlInput(url)); 205 | }, 206 | /** 207 | * Adds a single track to the global player's queue. 208 | */ 209 | queueTrack: function(track, index) { 210 | ToneDen.flux.actions.player.queue.queueTrack(processUrlInput(track), index); 211 | }, 212 | /** 213 | * Sets the default list of tracks to be played if the global player's queue runs out of tracks. 214 | */ 215 | setDefaultTracks: function(tracks, insertLocation) { 216 | ToneDen.flux.actions.player.queue.setDefaultTracks(tracks.map(processUrlInput), insertLocation); 217 | }, 218 | /** 219 | * Sets the play state of the global player. 220 | */ 221 | togglePause: function(paused) { 222 | var track = this.nowPlaying(); 223 | if(track) { 224 | ToneDen.flux.actions.player.track.togglePause(track, paused); 225 | } 226 | }, 227 | /** 228 | * Removes a track from the queue at the specified index. 229 | */ 230 | unqueueIndex: function(index) { 231 | ToneDen.actions.player.queue.unqueueIndex(index); 232 | } 233 | }; 234 | /** 235 | * Set whether or not the player should repeat the song it is playing. 236 | */ 237 | this.setRepeat = function(repeat) { 238 | ToneDen.flux.actions.player.setRepeat(!!repeat); 239 | }; 240 | /** 241 | * Set the volume level (between 0 and 100) on all audio. 242 | */ 243 | this.setVolume = function(level) { 244 | if(level < 0 || level > 100) { 245 | console.error('Volume level must be between 0 and 100!'); 246 | return; 247 | } 248 | 249 | ToneDen.flux.actions.player.setVolume(level); 250 | }; 251 | }; 252 | 253 | inherits(ToneDenPlayer, EventEmitter); 254 | 255 | module.exports = new ToneDenPlayer(); 256 | -------------------------------------------------------------------------------- /sdk/js/player/AudioInterface.js: -------------------------------------------------------------------------------- 1 | var _clone = require('lodash/lang/clone'); 2 | var _merge = require('lodash/object/merge'); 3 | var _uniqueId = require('lodash/utility/uniqueId'); 4 | 5 | var async = require('async'); 6 | var soundManager = require('../vendor/soundmanager2').soundManager; 7 | 8 | var constants = require('../constants'); 9 | var helpers = require('../helpers'); 10 | var soundcloud = require('./network/soundcloud'); 11 | 12 | soundManager.setup({ 13 | debugMode: false, 14 | forceUseGlobalHTML5Audio: true 15 | }); 16 | 17 | var AudioInterface = function(flux, parameters) { 18 | // Initialize configuration. 19 | var defaultParameters = { 20 | cache: true, 21 | volume: 100 22 | }; 23 | 24 | this.parameters = _merge(defaultParameters, parameters); 25 | this.resolveCache = {}; 26 | this.soundCache = {}; 27 | this.nowPlaying = null; 28 | 29 | var actions = flux.actions; 30 | var self = this; 31 | var urlRegex = new RegExp(/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/gi); 32 | 33 | function createSound(track, autoPlay) { 34 | var sound = soundManager.createSound({ 35 | autoLoad: true, 36 | autoPlay: autoPlay, 37 | id: track.id, 38 | onfinish: function() { 39 | actions.player.audioInterface.onTrackFinish(this.id); 40 | }, 41 | onload: function() { 42 | actions.player.audioInterface.onTrackReady(this.id, this); 43 | }, 44 | onpause: function() { 45 | actions.player.audioInterface.onTrackPlayingChange(this.id, false); 46 | }, 47 | onplay: function() { 48 | self.nowPlaying = this; 49 | actions.player.audioInterface.onTrackPlayStart(this.id); 50 | }, 51 | onresume: function() { 52 | actions.player.audioInterface.onTrackPlayingChange(this.id, true); 53 | }, 54 | url: track.resolved.stream_url, 55 | volume: self.parameters.volume, 56 | whileloading: function() { 57 | //actions.player.audioInterface.onTrackLoadAmountChange(this.id, this.bytesLoaded); 58 | }, 59 | whileplaying: function() { 60 | actions.player.audioInterface.onTrackPlayPositionChange(this.id, this.position); 61 | } 62 | }); 63 | 64 | return sound; 65 | } 66 | 67 | this.loadTrack = function(track, autoPlay, callback) { 68 | if(track.sound) { 69 | this.seekTrack(track, 0, true); 70 | track.sound._a && track.sound.setVolume(self.parameters.volume); 71 | 72 | if(autoPlay && (track.sound.paused || track.sound.playState === 0)) { 73 | track.sound.play(); 74 | } 75 | } 76 | 77 | async.waterfall([ 78 | function(next) { 79 | if(!track.resolved) { 80 | return this.resolveTrack(track, next); 81 | } else { 82 | return next(null, [track]); 83 | } 84 | }.bind(this), 85 | function(tracks, next) { 86 | var trackToPlay = tracks[0]; 87 | var trackSound = trackToPlay.sound; 88 | 89 | if(!trackToPlay.error) { 90 | if(!trackSound) { 91 | trackToPlay.sound = createSound(trackToPlay, autoPlay); 92 | 93 | async.nextTick(function() { 94 | actions.player.audioInterface.onTrackSoundAdded(trackToPlay); 95 | }); 96 | } else if(autoPlay) { 97 | var oldPosition = trackSound.position; 98 | 99 | trackSound.play(); 100 | trackSound.setPosition(oldPosition); 101 | } 102 | } 103 | 104 | return next(null, trackToPlay); 105 | } 106 | ], function(err, trackToPlay) { 107 | if(err) { 108 | actions.player.audioInterface.onTrackError(trackToPlay.id, err); 109 | } 110 | 111 | if(typeof callback === 'function') { 112 | return callback(err); 113 | } 114 | }); 115 | }; 116 | 117 | this.resolveTrack = function(originalTrack, tracksPerArtist, callback) { 118 | var streamUrl; 119 | 120 | if(typeof originalTrack === 'object') { 121 | streamUrl = originalTrack.stream_url; 122 | } else if(typeof originalTrack === 'string') { 123 | streamUrl = originalTrack; 124 | } else { 125 | return callback('Unrecognized track passed to resolveTrack: ' + originalTrack); 126 | } 127 | 128 | if(typeof tracksPerArtist === 'function') { 129 | callback = tracksPerArtist; 130 | tracksPerArtist = 10; 131 | } 132 | 133 | if(self.parameters.cache && self.resolveCache[streamUrl]) { 134 | return async.nextTick(function() { 135 | actions.player.audioInterface.onTrackResolved(originalTrack.id, self.resolveCache[streamUrl]); 136 | 137 | if(typeof callback === 'function') { 138 | return callback(null, self.resolveCache[streamUrl]); 139 | } 140 | }); 141 | } 142 | 143 | async.waterfall([ 144 | function(next) { 145 | if(streamUrl && streamUrl.search(/soundcloud\.com/i) !== -1 && streamUrl.search(/api\.soundcloud\.com/) === -1) { 146 | return soundcloud.resolve(originalTrack, tracksPerArtist, next); 147 | } else if(streamUrl && streamUrl.match(urlRegex)) { 148 | return next(null, [{ 149 | streamable: true, 150 | stream_url: streamUrl, 151 | user: originalTrack.user 152 | }]); 153 | } else { 154 | return next(new Error('I don\'t know how to deal with that URL.', streamUrl)); 155 | } 156 | } 157 | ], function(err, resolvedTracks) { 158 | // Since the single original track object may resolve into multiple tracks (in the case of a user or set 159 | // URL), we have to turn that original track into an array of new ones with new IDs. 160 | resolvedTracks = resolvedTracks || [originalTrack]; 161 | resolvedTracks = resolvedTracks.map(function(resolvedTrack, index) { 162 | var track = _clone(originalTrack); 163 | var randomID = _uniqueId('track_'); 164 | 165 | if(index === 0) { 166 | track.id = originalTrack.id || randomID; 167 | } else { 168 | track.id = randomID; 169 | } 170 | 171 | track.id = String(track.id); 172 | track.resolved = resolvedTrack; 173 | delete track.playing; 174 | 175 | if(track.title) { 176 | resolvedTrack.title = track.title; 177 | } 178 | 179 | if(err) { 180 | track.error = true; 181 | 182 | if(err.status === 404) { 183 | track.errorMessage = 'We can\'t stream this track.' 184 | } else { 185 | track.errorMessage = err.message; 186 | } 187 | } 188 | 189 | return track; 190 | }); 191 | 192 | if(self.parameters.cache) { 193 | self.resolveCache[streamUrl] = resolvedTracks; 194 | } 195 | 196 | actions.player.audioInterface.onTrackResolved(originalTrack.id, resolvedTracks); 197 | 198 | if(typeof callback === 'function') { 199 | return callback(err, resolvedTracks); 200 | } 201 | }); 202 | }; 203 | 204 | this.seekTrack = function(track, position, triggerTrackPlayStarted) { 205 | async.waterfall([ 206 | function(next) { 207 | if(!track.sound) { 208 | return this.loadTrack(track, true, next); 209 | } else { 210 | return next(); 211 | } 212 | }.bind(this) 213 | ], function(err) { 214 | if(err) { 215 | return; 216 | } 217 | 218 | track.sound.setPosition(position); 219 | track.sound.options.position = position; 220 | 221 | actions.player.audioInterface.onTrackPlayPositionChange(track.id, position); 222 | 223 | if(track.sound.paused || track.sound.playState === 0) { 224 | track.sound.play(); 225 | } 226 | 227 | if(triggerTrackPlayStarted) { 228 | actions.player.audioInterface.onTrackPlayStart(track.id); 229 | } 230 | }); 231 | }; 232 | 233 | this.setVolume = function(level) { 234 | self.parameters.volume = level; 235 | 236 | if(self.nowPlaying) { 237 | self.nowPlaying.setVolume(level); 238 | } 239 | }; 240 | 241 | this.togglePause = function(track, paused) { 242 | // Prevent soundManager from resetting the track's position. 243 | var currentPosition; 244 | 245 | if(track.sound) { 246 | currentPosition = track.sound.position; 247 | 248 | if(typeof paused === 'undefined') { 249 | track.sound.togglePause(); 250 | } else if(paused) { 251 | track.sound.pause(); 252 | } else { 253 | track.sound.play(); 254 | } 255 | 256 | track.sound.setPosition(currentPosition); 257 | } else { 258 | this.loadTrack(track, paused === false || typeof paused === 'undefined'); 259 | } 260 | }; 261 | 262 | return { 263 | loadTrack: this.loadTrack, 264 | resolveTrack: this.resolveTrack, 265 | seekTrack: this.seekTrack, 266 | setVolume: this.setVolume, 267 | togglePause: this.togglePause 268 | }; 269 | }; 270 | 271 | module.exports = AudioInterface; 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Embeddable Goodness, by ToneDen 2 | === 3 | 4 | The ToneDen SDK is loaded asynchronously, which means your page doesn't have to wait for the SDK to load before rendering. 5 | This means that loading it is a little more complicated than just including a ` 30 | ``` 31 | 32 | Player 33 | --- 34 | 35 | A pure JS customizable audio player for your SoundCloud. 36 | 37 | JS API, responsive, customizable. Demo at https://www.toneden.io/player. 38 | 39 | ![alt tag](https://raw.github.com/toneden/toneden-sdk/master/mockupv1.png) 40 | 41 | Sample Embed Code: 42 | ``` 43 | 72 | ``` 73 | 74 | Global Player and Play Queues 75 | --- 76 | Documentation coming soon! 77 | 78 | API 79 | --- 80 | 81 | **ToneDen** 82 | * **.configure** 83 | * **configure(params)** 84 | Sets global SDK options. 85 | Parameters: 86 | * **debug** 87 | *default: false* 88 | True to output debug messages to the console. 89 | * **soundcloudConsumerKey** 90 | You now have to provide your own SoundCloud consumer key in order to stream tracks. 91 | Because of [SoundCloud's new daily stream limits](https://developers.soundcloud.com/blog/limits), we can't afford to use a single consumer key for everyone using the player. 92 | Sorry for the inconvenience! 93 | * **.player** 94 | * **.create(params)** 95 | Creates and returns a new player instance according to the given parameters object. 96 | Parameters: 97 | * **feed** 98 | *default: false* 99 | True to display a simplified version of the player, no bells and whistles here. 100 | * **keyboardEvents** 101 | *default: false* 102 | True to listen to keyboard events on the document body to control the player. 103 | Left arrow key returns to the previous track, right arrow key skips to the next, and spacebar plays/pauses the current track. 104 | * **mini** 105 | *default: false* 106 | True to use the 'mini' version of the player. This renders as a narrower bar with controls laid out horizontally. 107 | For an example, check out the player on [ToneDen OneSheets](https://apedrums.toneden.io/onesheet). 108 | * **shrink** 109 | *default: true* 110 | By default, the player shrinks to the size of its parent container. 111 | Set this to false to disable that behavior. 112 | * **single** 113 | *default: false* 114 | True to force the player to render as if there were only one track in the playlist. 115 | This parameter is internally set to true if the playlist has only one track, 116 | *unless* the single parameter is explicitly set to false. 117 | * **skin** 118 | *default: 'light'* 119 | The player color scheme to render. Options are 'light', 'dark', 'mojave', and 'aurora'. 120 | * **tracksPerArtist** 121 | *default: 10* 122 | How many tracks to load from an artist's SoundCloud account when the artist's SoundCloud URL is specified in the urls parameter. 123 | * **useCustomPurchaseTitle** 124 | *default: true* 125 | Whether to use tracks' custom purchase titles. If false, the purchase link text will be 'BUY'. 126 | * **.getInstanceByDom(dom)** 127 | Returns the player instance that is associated with the given dom selector string. 128 | * **.setRepeat(repeat)** 129 | Sets whether players should repeat their songs. 130 | * **.setVolume(level)** 131 | Sets the volume level on all players. Level should be an integer between 0 and 100, inclusive. 132 | * **.global** 133 | These functions act on the page's global player instance, if one exists. 134 | * **.nowPlaying()** 135 | Returns the track that the global player is currently playing. 136 | * **.playTrack(url)** 137 | Loads and starts playing a track in the global player. 138 | * **.queueTrack(track, index)** 139 | Adds a track to the global player's queue. 140 | * **.setDefaultTracks(tracks, insertLocation) 141 | Sets the default list of tracks to be played if the global player's queue runs out of tracks. 142 | * **.togglePause(paused)** 143 | Sets the play state of the global player. 144 | * **.unqueueIndex(index)** 145 | Removes a track from the global play queue at the specified index. 146 | 147 | 148 | **Player Instance** 149 | * **id** 150 | Randomly generated ID, unique to the player instance. 151 | * **.destroy()** 152 | Destroys the player instance and clears the containing element's HTML. 153 | * **.next(play)** 154 | Skip to the next track, and play it if `play` is true. 155 | * **.prev()** 156 | Jumps to the previous track. 157 | * **.skipTo(index)** 158 | Jumps to track number `index`. 159 | * **.togglePause(paused)** 160 | Toggles the paused state of the player, or sets it to the value of the 'paused' argument if provided. 161 | * **.update(params)** 162 | Updates the player with the given parameters. All parameters are supported except 'dom'. 163 | 164 | Sample API Usage: 165 | ``` 166 | ToneDen.player.getInstanceByDom("#player").togglePause(false); // The player will start playing, if it isn't already. 167 | ToneDen.player.getInstanceByDom("#player").togglePause(true); // The player will pause. 168 | ToneDen.player.getInstanceByDom("#player").next(); // Play the next track in the list. 169 | ``` 170 | 171 | Development 172 | === 173 | 174 | Setup 175 | --- 176 | 177 | 1. Add the following lines to /etc/hosts: 178 | ``` 179 | 127.0.0.1 publisher.dev 180 | 127.0.0.1 widget.dev 181 | ``` 182 | 2. Add the following lines to /etc/apache2/extra/https-vhosts.conf: 183 | ``` 184 | 185 | DocumentRoot "/toneden-sdk/test" 186 | ServerName publisher.dev 187 | 188 | 189 | DocumentRoot "/toneden-sdk" 190 | ServerName widget.dev 191 | 192 | ``` 193 | 3. Restart Apache. 194 | 4. Install [NPM](https://github.com/npm/npm) and [Grunt](http://gruntjs.com/). 195 | 5. Run `npm install` in the root directory. 196 | 6. Navigate to publisher.dev in your browser of choice. 197 | This will load test/index.html, which includes a script snippet that loads toneden.loader.js from the domain widget.dev to simulate a cross-origin environment. 198 | 199 | Building 200 | --- 201 | 202 | We use [Webpack](https://webpack.github.io) to compile the player and all of it's dependencies into a single file. 203 | When you run `grunt` from the root directory, the Webpack dev server will start. From this point on, any changes you make 204 | to files in the repo will cause Webpack to rebuild /toneden.js and /toneden.loader.js. 205 | 206 | **Don't use files built by the webpack dev server in production- they're huge! Run grunt --production to build a minified version.** 207 | 208 | `grunt --dev` will compile a readable, debug-friendly version of the scripts, while `grunt --production` will make a minified version. 209 | 210 | Overview 211 | --- 212 | 213 | The loader/ directory contains the scripts that manage (surprise!) loading the SDK when embedded in a webpage. 214 | You probably won't have to touch anything in this directory, but here's how it works: 215 | 216 | The loader script (toneden.loader.js) uses webpack to asynchronously load the SDK script (toneden.js) from the ToneDen CDN. 217 | This system allows embedding pages to include (either asynchronously or synchronously) the relatively small loading script, 218 | which then loads the much larger SDK files in a non-blocking way. 219 | When the SDK has been loaded, the loader calls all the functions in the global ToneDenReady array, 220 | allowing developers to access the functionality of the SDK. 221 | (Inspired by/copied from the [Shootitlive](https://github.com/shootitlive/widgetloader) folks, who are way smarter than me!) 222 | 223 | The sdk/ directory contains all the good stuff. The file index.js is the hub of the action. 224 | It loads all the functions of the SDK (currently only the player) as dependencies, 225 | and returns them so that they can be attached to the global ToneDen object. 226 | 227 | Questions? 228 | === 229 | 230 | You can contact us on GitHub or on Twitter: [@tonedenmusic](https://twitter.com/tonedenmusic) 231 | 232 | License 233 | === 234 | 235 | [MIT License](http://toneden.mit-license.org/) 236 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Publisher Test Page 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 71 |
72 |
73 |
74 |
75 | 76 | 77 | 78 | 79 | 80 |
81 |
82 |
83 |
84 |
85 |

Fully customizable, responsive, and extensible.

86 |
87 |

Basic usage:

88 |

Add the following snippet of code to your page (before the closing body tag).

89 |
<script>
 90 |         (function() {
 91 |             var script = document.createElement('script');
 92 | 
 93 |             script.type = 'text/javascript';
 94 |             script.async = true;
 95 |             script.src = '//sd.toneden.io/production/toneden.loader.js'
 96 | 
 97 |             var entry = document.getElementsByTagName('script')[0];
 98 |             entry.parentNode.insertBefore(script, entry);
 99 |         }());
100 | 
101 |         ToneDenReady = window.ToneDenReady || [];
102 |         ToneDenReady.push(function() {
103 |             // This is where all the action happens:
104 |             ToneDen.player.create({
105 |                 dom: '#player',
106 |                 urls: [
107 |                     'https://soundcloud.com/giraffage'
108 |                 ]
109 |             });
110 |         });
111 | </script>
112 | 113 |
114 | 115 |
116 |

Player Configuration:

117 |

There are several configuration options that you can explore. However dom and urls must be specified.

118 |
    119 |
  • dom: The container in which you want your player to render in.
  • 120 |
  • urls: Urls to SoundCloud artist, tracks, or set. Parameter can take an array of multiple urls.
  • 121 |
122 |

Check out the documentation for a full list of the config parameters and defaults.

123 |
124 |
125 |

Single Track Players:

126 |

If you want to feature a single track instead of a playlist, the player will automatically default to the single player below if a single track url is provided.

127 |

You can also set tracksPerArtist: 1 to force a single track to play for an artist url.

128 |

Example:

129 |

130 |  ToneDen.player.create({
131 |     dom: '#player',
132 |     urls: [
133 |         'https://soundcloud.com/saintpepsi/unhappy'
134 |     ]
135 | });
136 |                     
137 |

Demo:

138 |
139 |
140 |
141 |

Multiple Players:

142 |

You can create as many players as you like by pushing multiple ToneDen.player objects to ToneDenReady.

143 |

Example:

144 |

145 | ToneDenReady.push(function() {
146 |         ToneDen.player.create({
147 |             dom: '#player1',
148 |             urls: [
149 |                 'https://soundcloud.com/giraffage'
150 |             ]
151 |         });
152 |         ToneDen.player.create({
153 |             dom: '#player2',
154 |             urls: [
155 |                 'https://soundcloud.com/teendaze'
156 |             ]
157 |         });
158 |         ToneDen.player.create({
159 |             dom: '#player3',
160 |             urls: [
161 |                 'https://soundcloud.com/beat-culture'
162 |             ]
163 |         });
164 |     });
165 | 
166 |
167 |
168 |
169 |
170 |
171 |
172 |

Create your own custom player without the codez

173 |
At ToneDen, we make it easy to show off your music. Make a free player for your tunes by syncing your SoundCloud.
174 |
175 |
176 |
177 | Learn More 178 |
179 | 182 |
183 |
184 |
185 | 186 | 187 | 202 |
203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /sdk/js/flux/stores/PlayerInstanceStore.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores state for individual player instances. 3 | */ 4 | 5 | var Fluxxor = require('fluxxor'); 6 | var Immutable = require('immutable'); 7 | 8 | var events = require('../events'); 9 | 10 | var PlayerInstanceStore = Fluxxor.createStore({ 11 | initialize: function() { 12 | this.instances = Immutable.Map(); 13 | this.playHistory = Immutable.List(); 14 | 15 | this.bindActions( 16 | events.player.audioInterface.TRACK_ERROR, this.emitChangeAfterTrackStore, 17 | events.player.audioInterface.TRACK_FINISHED, this.onTrackFinished, 18 | events.player.audioInterface.TRACK_LOAD_AMOUNT_CHANGED, this.emitChangeAfterTrackStore, 19 | events.player.audioInterface.TRACK_PLAY_POSITION_CHANGED, this.emitChangeAfterTrackStore, 20 | events.player.audioInterface.TRACK_PLAYING_CHANGED, this.emitChangeAfterTrackStore, 21 | events.player.audioInterface.TRACK_PLAY_START, this.onTrackPlayStart, 22 | events.player.audioInterface.TRACK_RESOLVED, this.onTrackResolved, 23 | events.player.audioInterface.TRACK_UPDATED, this.emitChangeAfterTrackStore, 24 | events.player.CONFIG_UPDATED, this.onConfigUpdated, 25 | events.player.CREATE, this.onPlayerUpdate, 26 | events.player.DESTROY, this.onPlayerDestroy, 27 | events.player.NEXT_TRACK, this.onPlayerNextTrack, 28 | events.player.PREVIOUS_TRACK, this.onPlayerPreviousTrack, 29 | events.player.track.SELECTED, this.onTrackSelected, 30 | events.player.track.TOGGLE_PAUSE, this.emitChangeAfterTrackStore, 31 | events.player.UPDATE, this.onPlayerUpdate 32 | ); 33 | }, 34 | emitChangeAfterTrackStore: function() { 35 | this.waitFor(['TrackStore'], function() { 36 | this.emit('change'); 37 | }); 38 | }, 39 | getGlobalInstance: function() { 40 | return this.instances.find(function(player) { 41 | return player.get('global'); 42 | }); 43 | }, 44 | getStateByID: function(id) { 45 | var TrackStore = this.flux.store('TrackStore'); 46 | var instance = this.instances.get(id); 47 | var empty; 48 | var state; 49 | 50 | if(!instance) { 51 | return null; 52 | } 53 | 54 | empty = !instance.get('nowPlaying') && !instance.getIn(['tracks', 'size']);; 55 | 56 | if(instance) { 57 | state = instance.set('empty', empty); 58 | } else { 59 | state = Immutable.Map({ 60 | loading: true 61 | }); 62 | } 63 | 64 | return state; 65 | }, 66 | addGlobalNowPlayingToPlayHistory: function() { 67 | var globalPlayer = this.getGlobalInstance(); 68 | var globalNowPlaying = globalPlayer && globalPlayer.get('nowPlaying'); 69 | var lastPlayed = this.playHistory.get(this.playHistory.size - 1); 70 | 71 | if(globalNowPlaying && globalNowPlaying !== lastPlayed) { 72 | this.playHistory = this.playHistory.push(globalNowPlaying); 73 | } 74 | }, 75 | getNextTrackForInstance: function(instance, TrackStore, TrackQueueStore, previousAttemptedTrack) { 76 | var instanceTracks = instance.get('tracks'); 77 | var nowPlayingID = instance.get('nowPlaying'); 78 | var nowPlayingIndex = instanceTracks.indexOf(nowPlayingID); 79 | var getFromDefaultQueue = !TrackQueueStore.queue.get(0); 80 | var nextTrackID; 81 | var nextTrack; 82 | 83 | if(instance.get('repeat')) { 84 | nextTrackID = nowPlayingID; 85 | } else if(instance.get('playFromQueue')) { 86 | if(getFromDefaultQueue) { 87 | nextTrackID = TrackQueueStore.defaultQueue.get(0); 88 | } else { 89 | nextTrackID = TrackQueueStore.queue.get(0) 90 | } 91 | } else if(instanceTracks.get(nowPlayingIndex + 1)) { 92 | nextTrackID = instanceTracks.get(nowPlayingIndex + 1); 93 | } else { 94 | nextTrackID = null; 95 | } 96 | 97 | nextTrack = TrackStore.tracks.get(nextTrackID); 98 | 99 | if(nextTrack && nextTrack.get('error') && previousAttemptedTrack !== nextTrack) { 100 | if(getFromDefaultQueue) { 101 | TrackQueueStore.defaultQueue = TrackQueueStore.defaultQueue.splice(0, 1); 102 | } else { 103 | TrackQueueStore.queue = TrackQueueStore.queue.splice(0, 1); 104 | } 105 | 106 | return this.getNextTrackForInstance(instance, TrackStore, TrackQueueStore, nextTrack); 107 | } else { 108 | return nextTrackID || 'end'; 109 | } 110 | }, 111 | onConfigUpdated: function(payload) { 112 | this.instances = this.instances.map(function(instance) { 113 | return instance.merge(payload.config); 114 | }); 115 | 116 | this.emit('change'); 117 | }, 118 | onPlayerUpdate: function(payload) { 119 | // Custom merger function which replaces arrays instead of concatenating them. 120 | function merger(a, b, key) { 121 | if(Immutable.List.isList(a) && Immutable.List.isList(b)) { 122 | return b; 123 | } else if(a && a.mergeDeepWith) { 124 | return a.mergeDeepWith(merger, b); 125 | } else { 126 | return b; 127 | } 128 | } 129 | 130 | this.instances = this.instances.mergeDeepWith(merger, Immutable.fromJS(payload.entities.players)); 131 | 132 | this.instances = this.instances.mergeDeep(payload.entities.players); 133 | this.emit('change'); 134 | }, 135 | onPlayerDestroy: function(payload) { 136 | var destroyedInstance = this.instances.get(payload.playerID); 137 | this.waitFor(['TrackStore'], function(TrackStore) { 138 | var nowPlaying = destroyedInstance && TrackStore.tracks.get(destroyedInstance.get('nowPlaying')); 139 | var isPlayingInOtherPlayer = this.instances.some(function(instance) { 140 | return instance.get('nowPlaying') === nowPlaying; 141 | }); 142 | 143 | // Kind of anti-fluxy here. 144 | if(nowPlaying && nowPlaying.sound && !isPlayingInOtherPlayer) { 145 | nowPlaying.sound.destroy(); 146 | } 147 | 148 | this.instances = this.instances.delete(payload.playerID); 149 | this.emit('change'); 150 | }.bind(this)); 151 | }, 152 | onPlayerNextTrack: function(payload) { 153 | var player = this.instances.get(payload.playerID); 154 | 155 | this.addGlobalNowPlayingToPlayHistory(); 156 | 157 | this.waitFor(['TrackStore', 'TrackQueueStore'], function(TrackStore, TrackQueueStore) { 158 | var nextTrack = this.getNextTrackForInstance(player, TrackStore, TrackQueueStore); 159 | this.instances = this.instances.setIn([payload.playerID, 'nextTrack'], nextTrack); 160 | 161 | if(nextTrack === 'end') { 162 | ToneDen.player.emit('queue.finished'); 163 | } 164 | 165 | this.emit('change'); 166 | }.bind(this)); 167 | }, 168 | onPlayerPreviousTrack: function(payload) { 169 | var player = this.instances.get(payload.playerID); 170 | var nowPlaying = player.get('nowPlaying'); 171 | var previousIndex = player.get('tracks').indexOf(nowPlaying) - 1; 172 | var nextTrack; 173 | 174 | this.waitFor(['TrackStore'], function() { 175 | if(player.get('global')) { 176 | nextTrack = this.playHistory.get(this.playHistory.size - 1); 177 | this.playHistory = this.playHistory.pop(); 178 | } else { 179 | if(previousIndex < 0) { 180 | nextTrack = nowPlaying; 181 | } 182 | 183 | nextTrack = this.instances.getIn([payload.playerID, 'tracks', previousIndex]); 184 | } 185 | 186 | this.instances = this.instances.setIn([payload.playerID, 'nextTrack'], nextTrack); 187 | this.emit('change'); 188 | }.bind(this)); 189 | }, 190 | onTrackFinished: function(payload) { 191 | this.addGlobalNowPlayingToPlayHistory(); 192 | 193 | this.waitFor(['TrackStore', 'TrackQueueStore'], function(TrackStore, TrackQueueStore) { 194 | this.instances = this.instances.map(function(player) { 195 | if(player.get('nowPlaying')) { 196 | var nextTrack = this.getNextTrackForInstance(player, TrackStore, TrackQueueStore); 197 | player = player.set('nextTrack', nextTrack); 198 | 199 | if(nextTrack === 'end') { 200 | ToneDen.player.emit('queue.finished'); 201 | } 202 | } 203 | 204 | return player; 205 | }.bind(this)); 206 | 207 | this.emit('change'); 208 | }.bind(this)); 209 | }, 210 | onTrackPlayStart: function(payload) { 211 | var trackID = payload.trackID; 212 | 213 | this.instances = this.instances.map(function(player) { 214 | if(player.get('global')) { 215 | player = player.set('nowPlaying', trackID); 216 | } 217 | 218 | return player; 219 | }); 220 | 221 | this.emit('change'); 222 | }, 223 | onTrackResolved: function(payload) { 224 | var originalTrackID = payload.trackID; 225 | 226 | // Go through each player and replace the original track with the new array of tracks. 227 | this.instances = this.instances.map(function(player) { 228 | var playerTracks = player.get('tracks'); 229 | var originalTrackIndex = playerTracks.indexOf(originalTrackID); 230 | 231 | if(playerTracks.contains(originalTrackID)) { 232 | var spliceArgs = [originalTrackIndex, 1].concat(payload.result); 233 | 234 | player = player.set('tracks', Immutable.List.prototype.splice.apply(playerTracks, spliceArgs)); 235 | } 236 | 237 | if(player.get('nowPlaying') === originalTrackID) { 238 | player = player.set('nowPlaying', payload.result[0]); 239 | } 240 | 241 | return player; 242 | }); 243 | 244 | this.waitFor(['TrackStore'], function() { 245 | this.emit('change'); 246 | }); 247 | }, 248 | onTrackSelected: function(payload) { 249 | this.waitFor(['TrackStore'], function(TrackStore) { 250 | var selectedTrack = TrackStore.tracks.get(payload.result); 251 | 252 | if(selectedTrack.get('error')) { 253 | ToneDen.player.emit('track.error', selectedTrack.get('errorMessage')); 254 | return; 255 | } 256 | 257 | this.instances = this.instances.map(function(player) { 258 | if(player.get('tracks').contains(payload.result) || player.get('global')) { 259 | player = player.set('nowPlaying', payload.result); 260 | } 261 | 262 | player = player.delete('nextTrack'); 263 | 264 | return player; 265 | }); 266 | 267 | this.emit('change'); 268 | }); 269 | } 270 | }); 271 | 272 | module.exports = PlayerInstanceStore; 273 | -------------------------------------------------------------------------------- /sdk/css/themes.css: -------------------------------------------------------------------------------- 1 | .td.player.light { 2 | background: rgba(255,255,255,0.8); 3 | color: black; 4 | border-color: #E4E4E4; 5 | } 6 | 7 | .td.player.dark { 8 | background: rgba(0,0,0,0.85); 9 | color: white; 10 | border-color: #333; 11 | } 12 | 13 | .td.player.aurora { 14 | color: white; 15 | background: #192F41; 16 | border-color: #1B4B4B; 17 | } 18 | 19 | .td.player.mojave { 20 | color: white; 21 | background: #4F4E70; 22 | border-color: #6D6C9A; 23 | } 24 | 25 | .td.player.slate { 26 | color: white; 27 | background: #5E6371; 28 | border-color: #6E727E; 29 | } 30 | 31 | .td.player.light div, .td.player.light ul { 32 | border-color: #E4E4E4!important; 33 | } 34 | 35 | .td.player.dark div, .td.player.dark ul { 36 | border-color: #333 !important; 37 | } 38 | 39 | .td.player.aurora div, .td.player.aurora ul { 40 | border-color: #1B4B4B !important; 41 | } 42 | 43 | .td.player.mojave div, .td.player.mojave ul { 44 | border-color: #6D6C9A !important; 45 | } 46 | 47 | .td.player.slate div, .td.player.slate ul { 48 | border-color: #6E727E !important; 49 | } 50 | 51 | .td.player.light .waveform path { 52 | fill: none; 53 | stroke: #000; 54 | } 55 | 56 | .td.player.dark .waveform path { 57 | fill: none; 58 | stroke: white; 59 | } 60 | 61 | .td.player.aurora .waveform path { 62 | fill: none; 63 | stroke: #02DDA8; 64 | } 65 | 66 | .td.player.mojave .waveform path { 67 | fill: none; 68 | stroke: #F58F86; 69 | } 70 | 71 | .td.player.slate .waveform path { 72 | fill: none; 73 | stroke: #FFF; 74 | } 75 | 76 | .td.player.light .info { 77 | border-color: #E4E4E4; 78 | color: black; 79 | } 80 | 81 | .td.player.dark .info { 82 | border-color: #333; 83 | color: white; 84 | } 85 | 86 | .td.player.aurora .info { 87 | border-color: #1B4B4B; 88 | color: white; 89 | } 90 | 91 | .td.player.mojave .info { 92 | border-color: #6D6C9A; 93 | color: white; 94 | } 95 | 96 | .td.player.slate .info { 97 | border-color: #6E727E; 98 | color: white; 99 | } 100 | 101 | .td.player.light .info a, .td.player.light .mini-song-info a{ 102 | color: black; 103 | } 104 | 105 | .td.player.dark .info a, .td.player.dark .mini-song-info a{ 106 | color: white; 107 | } 108 | 109 | .td.player.aurora .info a, .td.player.aurora .mini-song-info a{ 110 | color: white; 111 | } 112 | 113 | .td.player.mojave .info a, .td.player.mojave .mini-song-info a{ 114 | color: white; 115 | } 116 | 117 | .td.player.slate .info a, .td.player.slate .mini-song-info a{ 118 | color: white; 119 | } 120 | 121 | .td.player.light .icon-td_logo { 122 | color: #222; 123 | } 124 | 125 | .td.player.dark .icon-td_logo { 126 | color:white; 127 | } 128 | 129 | .td.player.aurora .icon-td_logo { 130 | color:white; 131 | } 132 | 133 | .td.player.mojave .icon-td_logo { 134 | color:white; 135 | } 136 | 137 | .td.player.slate .icon-td_logo { 138 | color:white; 139 | } 140 | 141 | .td.player.light .soundcloud-icon { 142 | } 143 | 144 | .td.player.dark .soundcloud-icon { 145 | color:white; 146 | } 147 | 148 | .td.player.aurora .soundcloud-icon { 149 | color:white; 150 | } 151 | 152 | .td.player.mojave .soundcloud-icon { 153 | color:white; 154 | } 155 | 156 | .td.player.slate .soundcloud-icon { 157 | color:white; 158 | } 159 | 160 | .td.player.light .follow-link { 161 | border-color: #E4E4E4!important; 162 | color: #222; 163 | } 164 | 165 | .td.player.dark .follow-link { 166 | border-color: #333 !important; 167 | color: white; 168 | } 169 | 170 | .td.player.aurora .follow-link { 171 | border-color: #1B4B4B !important; 172 | color: white; 173 | } 174 | 175 | .td.player.mojave .follow-link { 176 | border-color: #6D6C9A !important; 177 | color: white; 178 | } 179 | 180 | .td.player.slate .follow-link { 181 | border-color: #6E727E !important; 182 | color: white; 183 | } 184 | 185 | .td.player.light .buy-link { 186 | border-color: #E4E4E4!important; 187 | color: #222; 188 | } 189 | 190 | .td.player.dark .buy-link { 191 | border-color: #333 !important; 192 | color: white; 193 | } 194 | 195 | .td.player.aurora .buy-link { 196 | border-color: #1B4B4B !important; 197 | color: white; 198 | } 199 | 200 | .td.player.mojave .buy-link { 201 | border-color: #6D6C9A !important; 202 | color: white; 203 | } 204 | 205 | .td.player.slate .buy-link { 206 | border-color: #6E727E !important; 207 | color: white; 208 | } 209 | 210 | .td.player.light .follow-link:hover,.td.player.light .buy-link:hover { 211 | background: rgba(255,255,255,0.8); 212 | color: #000; 213 | } 214 | 215 | .td.player.dark .follow-link:hover,.td.player.dark .buy-link:hover { 216 | background: rgba(0,0,0,0.8); 217 | color: white; 218 | } 219 | 220 | .td.player.aurora .follow-link:hover,.td.player.aurora .buy-link:hover { 221 | background: #122230; 222 | color: white; 223 | } 224 | 225 | .td.player.mojave .follow-link:hover,.td.player.mojave .buy-link:hover { 226 | background: #44435F; 227 | color: white; 228 | } 229 | 230 | .td.player.slate .follow-link:hover,.td.player.slate .buy-link:hover { 231 | background: #727887; 232 | color: white; 233 | } 234 | 235 | .td.player.light .controls { 236 | color: #000; 237 | border-color: #E4E4E4; 238 | } 239 | 240 | .td.player.dark .controls { 241 | color: white; 242 | border-color: #333; 243 | } 244 | 245 | .td.player.aurora .controls { 246 | color: #02DDA8; 247 | border-color: #1B4B4B; 248 | } 249 | 250 | .td.player.mojave .controls { 251 | color: #F58F86; 252 | border-color: #6D6C9A; 253 | } 254 | 255 | .td.player.mojave .controls { 256 | color: white; 257 | border-color: #6E727E; 258 | } 259 | 260 | .td.player.light .scrubber { 261 | border-color: #E4E4E4; 262 | } 263 | 264 | .td.player.dark .scrubber { 265 | border-color: #333; 266 | } 267 | 268 | .td.player.aurora .scrubber { 269 | border-color: #1B4B4B; 270 | } 271 | 272 | .td.player.mojave .scrubber { 273 | border-color: #6D6C9A; 274 | } 275 | 276 | .td.player.slate .scrubber { 277 | border-color: #6E727E; 278 | } 279 | 280 | .td.player.light .scrub-bar { 281 | border-color: #E4E4E4; 282 | } 283 | 284 | .td.player.dark .scrub-bar { 285 | border-color: #333; 286 | } 287 | 288 | .td.player.aurora .scrub-bar { 289 | border-color: #1B4B4B; 290 | } 291 | 292 | .td.player.mojave .scrub-bar { 293 | border-color: #6D6C9A; 294 | } 295 | 296 | .td.player.slate .scrub-bar { 297 | border-color: #6E727E; 298 | } 299 | 300 | .td.player.light .title { 301 | border-color: #D6D6D6; 302 | } 303 | 304 | .td.player.dark .title { 305 | border-color: #333; 306 | } 307 | 308 | .td.player.aurora .title { 309 | border-color: #1B4B4B; 310 | } 311 | 312 | .td.player.mojave .title { 313 | border-color: #6D6C9A; 314 | } 315 | 316 | .td.player.slate .title { 317 | border-color: #6E727E; 318 | } 319 | 320 | .td.player.light .playlist-table tbody tr.playing { 321 | color: #000; 322 | } 323 | 324 | .td.player.dark .playlist-table tbody tr.playing { 325 | color: white; 326 | } 327 | 328 | .td.player.aurora .playlist-table tbody tr.playing { 329 | color: #02DDA8; 330 | } 331 | 332 | .td.player.mojave .playlist-table tbody tr.playing { 333 | color: white; 334 | } 335 | 336 | .td.player.slate .playlist-table tbody tr.playing { 337 | color: white; 338 | } 339 | 340 | .td.player.light .playlist-table tr { 341 | color: #000; 342 | } 343 | 344 | .td.player.dark .playlist-table tr { 345 | color: white; 346 | } 347 | 348 | .td.player.aurora .playlist-table tr { 349 | color: #02DDA8; 350 | } 351 | 352 | .td.player.mojave .playlist-table tr { 353 | color: white; 354 | } 355 | 356 | .td.player.slate .playlist-table tr { 357 | color: white; 358 | } 359 | 360 | .td.player.light tbody tr:hover { 361 | border-color: rgba(175, 175, 175, 0.5); 362 | } 363 | 364 | .td.player.dark tbody tr:hover { 365 | border-color: rgba(175, 175, 175, 0.5); 366 | } 367 | 368 | .td.player.aurora tbody tr:hover { 369 | border-color: rgba(175, 175, 175, 0.5); 370 | } 371 | 372 | .td.player.mojave tbody tr:hover { 373 | border-color: rgba(175, 175, 175, 0.5); 374 | } 375 | 376 | .td.player.slate tbody tr:hover { 377 | border-color: rgba(175, 175, 175, 0.5); 378 | } 379 | 380 | .td.player.light .playlist-table tbody tr td { 381 | border-color: #E4E4E4; 382 | } 383 | 384 | .td.player.dark .playlist-table tbody tr td { 385 | border-color: #333; 386 | } 387 | 388 | .td.player.aurora .playlist-table tbody tr td { 389 | border-color: #1B4B4B; 390 | } 391 | 392 | .td.player.mojave .playlist-table tbody tr td { 393 | border-color: #6D6C9A; 394 | } 395 | 396 | .td.player.slate .playlist-table tbody tr td { 397 | border-color: #6E727E; 398 | } 399 | 400 | .td.player.light .slider > .handle { 401 | background: #BFC1CC; 402 | } 403 | 404 | .td.player.dark .slider > .handle { 405 | background: #ACACAC; 406 | } 407 | 408 | .td.player.aurora .slider > .handle { 409 | background: #ACACAC; 410 | } 411 | 412 | .td.player.slate .slider > .handle { 413 | background: #BFC1CC; 414 | } 415 | 416 | .td.player.light .slider > .handle:hover { 417 | background: #000; 418 | } 419 | 420 | .td.player.dark .slider > .handle:hover { 421 | background: white; 422 | } 423 | 424 | .td.player.aurora .slider > .handle:hover { 425 | background: white; 426 | } 427 | 428 | .td.player.mojave .slider > .handle:hover { 429 | background: white; 430 | } 431 | 432 | .td.player.slate .slider > .handle:hover { 433 | background: #00C4FF; 434 | } 435 | 436 | .td.player.light .slider > .track { 437 | background: #EAEAEA; 438 | border-color: #E4E4E4; 439 | } 440 | 441 | .td.player.dark .slider > .track { 442 | background: #494949; 443 | border-color: #333; 444 | } 445 | 446 | .td.player.aurora .slider > .track { 447 | background: #254364; 448 | border-color: #254364; 449 | } 450 | 451 | .td.player.mojave .slider > .track { 452 | background: #254364; 453 | border-color: #6D6C9A; 454 | } 455 | 456 | .td.player.slate .slider > .track { 457 | background: #717787; 458 | border-color: #727791; 459 | } 460 | 461 | .td.player.light .slider > .highlight-track { 462 | border-color: #333; 463 | background: #000; 464 | } 465 | 466 | .td.player.dark .slider > .highlight-track { 467 | border-color: #333; 468 | background: white; 469 | } 470 | 471 | .td.player.aurora .slider > .highlight-track { 472 | border-color: #02DDA8; 473 | background: #02DDA8; 474 | } 475 | 476 | .td.player.mojave .slider > .highlight-track { 477 | border-color: #F58F86; 478 | background: #F58F86; 479 | } 480 | 481 | .td.player.slate .slider > .highlight-track { 482 | border-color: #00C4FF; 483 | background: #00C4FF; 484 | } 485 | 486 | .td.player.light .line { 487 | stroke: #000; 488 | } 489 | 490 | .td.player.dark .line { 491 | stroke: white; 492 | } 493 | 494 | .td.player.aurora .line { 495 | stroke: white; 496 | } 497 | 498 | .td.player.mojave .line { 499 | stroke: white; 500 | } 501 | 502 | .td.player.slate .line { 503 | stroke: white; 504 | } 505 | 506 | .td.player.light .axis path, .td.player.light .axis line { 507 | stroke: #000; 508 | } 509 | 510 | .td.player.dark .axis path, .td.player.dark .axis line { 511 | stroke: white; 512 | } 513 | 514 | .td.player.aurora .axis path, .td.player.aurora .axis line { 515 | stroke: white; 516 | } 517 | 518 | .td.player.mojave .axis path, .td.player.mojave .axis line { 519 | stroke: white; 520 | } 521 | 522 | .td.player.slate .axis path, .td.player.slate .axis line { 523 | stroke: white; 524 | } 525 | 526 | .td.player.light rect { 527 | fill: #000; 528 | } 529 | 530 | .td.player.dark rect { 531 | fill: white; 532 | } 533 | 534 | .td.player.aurora rect { 535 | fill: #02DDA8; 536 | } 537 | 538 | .td.player.mojave rect { 539 | fill: #F58F86; 540 | } 541 | 542 | .td.player.slate rect { 543 | fill: white; 544 | } 545 | 546 | .td.player.light .follow-small .follow-link { 547 | border-color: #E4E4E4!important; 548 | } 549 | 550 | .td.player.dark .follow-small .follow-link { 551 | border-color: #333 !important; 552 | } 553 | 554 | .td.player.aurora .follow-small .follow-link { 555 | border-color: #1B4B4B !important; 556 | } 557 | 558 | .td.player.mojave .follow-small .follow-link { 559 | border-color: #6D6C9A !important; 560 | } 561 | 562 | .td.player.slate .follow-small .follow-link { 563 | border-color: #727791 !important; 564 | } 565 | 566 | .td.player.light .buy-small .buy-link { 567 | border-color: #E4E4E4!important; 568 | } 569 | 570 | .td.player.dark .buy-small .buy-link { 571 | border-color: #333 !important; 572 | } 573 | 574 | .td.player.aurora .buy-small .buy-link { 575 | border-color: #1B4B4B !important; 576 | } 577 | 578 | .td.player.mojave .buy-small .buy-link { 579 | border-color: #6D6C9A !important; 580 | } 581 | 582 | .td.player.slate .buy-small .buy-link { 583 | border-color: #727791 !important; 584 | } 585 | 586 | /*spinner*/ 587 | 588 | .td.player.light .tdspinner > div { 589 | background: #333; 590 | } 591 | 592 | .td.player.dark .tdspinner > div { 593 | background: white ; 594 | } 595 | 596 | .td.player.aurora .tdspinner > div { 597 | background: white ; 598 | } 599 | 600 | .td.player.mojave .tdspinner > div { 601 | background: white; 602 | } 603 | 604 | .td.player.mojave .tdspinner > div { 605 | background: #727791; 606 | } 607 | 608 | .td.player.light .tdloader { 609 | color: #333; 610 | } 611 | 612 | .td.player.dark .tdloader { 613 | color: white ; 614 | } 615 | 616 | .td.player.aurora .tdloader { 617 | color: white ; 618 | } 619 | 620 | .td.player.mojave .tdloader { 621 | color: white; 622 | } 623 | 624 | .td.player.mojave .tdloader { 625 | color: #727791; 626 | } 627 | 628 | .td.player.light .tdempty{ 629 | color: #333; 630 | } 631 | 632 | .td.player.dark .tdempty{ 633 | color: white; 634 | } 635 | 636 | .td.player.aurora .tdempty{ 637 | color: white; 638 | } 639 | 640 | .td.player.mojave .tdempty{ 641 | color: white; 642 | } 643 | 644 | .td.player.slate .tdempty{ 645 | color: white; 646 | } 647 | -------------------------------------------------------------------------------- /sdk/css/player.css: -------------------------------------------------------------------------------- 1 | .td.player { 2 | font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 3 | position: relative; 4 | transition: all 0.5s; 5 | -webkit-transition: all 0.5s; 6 | -moz-transition: all 0.5s; 7 | min-width: 250px; 8 | border: 1px solid; 9 | box-sizing: border-box; 10 | -webkit-animation: fadeIn 0.5s; /* Safari and Chrome */ 11 | -moz-animation: fadeIn 0.5s; /* Firefox */ 12 | -ms-animation: fadeIn 0.5s; /* Internet Explorer */ 13 | -o-animation: fadeIn 0.5s; /* Opera */ 14 | animation: fadeIn 0.5s; 15 | } 16 | 17 | .td.player div,.td.player ul { 18 | -moz-box-sizing: border-box; 19 | -webkit-box-sizing: border-box; 20 | box-sizing: border-box; 21 | line-height: normal; 22 | } 23 | 24 | .td.player .controls:not(.mini-controls), .td.player .buttons{ 25 | width: 100%; 26 | } 27 | 28 | .td.player .controls div { 29 | display: inline-block; 30 | text-align: center; 31 | } 32 | 33 | .td.player .controls i { 34 | cursor: pointer; 35 | } 36 | 37 | .td.player .tdempty{ 38 | margin: 50px auto; 39 | text-align: center; 40 | font-size: 14px; 41 | opacity: 0.4; 42 | } 43 | 44 | .td.player .icon-td_logo-link { 45 | position: absolute; 46 | top: 0; 47 | right: 0; 48 | height: 16px; 49 | width: 16px; 50 | margin: 10px; 51 | display: block; 52 | z-index: 999; 53 | opacity: 0.2; 54 | -webkit-transition: opacity 300ms ease-out; 55 | -moz-transition: opacity 300ms ease-out; 56 | transition: opacity 300ms ease-out; 57 | color: #222; 58 | } 59 | 60 | .td.player .icon-td_logo-link:hover { 61 | opacity: 1; 62 | } 63 | 64 | .td.player .tdicon-td_logo { 65 | width: 100%; 66 | height: 100%; 67 | position: absolute; 68 | right: 0; 69 | top:0; 70 | font-size: 12px; 71 | padding: 10px; 72 | color: #222; 73 | } 74 | 75 | .td.player .header { 76 | height: 100%; 77 | text-align: center; 78 | position: relative; 79 | } 80 | 81 | .td.player .cover { 82 | height: 100%; 83 | display: table; 84 | z-index: 1; 85 | } 86 | 87 | .td.player .cover .cover-img { 88 | display: table-cell; 89 | vertical-align: middle; 90 | -webkit-animation: fadeIn 1s; /* Safari and Chrome */ 91 | -moz-animation: fadeIn 1s; /* Firefox */ 92 | -ms-animation: fadeIn 1s; /* Internet Explorer */ 93 | -o-animation: fadeIn 1s; /* Opera */ 94 | animation: fadeIn 1s; 95 | } 96 | 97 | .td.player .cover .cover-img img { 98 | max-height: 125px; 99 | border-radius: 100%; 100 | margin: 20px 0px; 101 | -webkit-animation: fadeIn 1s; /* Safari and Chrome */ 102 | -moz-animation: fadeIn 1s; /* Firefox */ 103 | -ms-animation: fadeIn 1s; /* Internet Explorer */ 104 | -o-animation: fadeIn 1s; /* Opera */ 105 | animation: fadeIn 1s; 106 | } 107 | 108 | .td.player .waveform { 109 | position: absolute; 110 | left:0; 111 | right:0; 112 | } 113 | 114 | .td.player .info { 115 | border-bottom: 1px solid; 116 | font-weight: bold; 117 | padding: 5px 45px; 118 | font-size: 14px; 119 | } 120 | 121 | .td.player .info a { 122 | text-decoration: none; 123 | } 124 | 125 | .td.player .song-name { 126 | margin: 10px 0px 0px; 127 | text-align: center; 128 | font-size:14px; 129 | font-weight: normal; 130 | -webkit-animation: fadeInRight 1s; /* Safari and Chrome */ 131 | -moz-animation: fadeInRight 1s; /* Firefox */ 132 | -ms-animation: fadeInRight 1s; /* Internet Explorer */ 133 | -o-animation: fadeInRight 1s; /* Opera */ 134 | animation: fadeInRight 1s; 135 | } 136 | 137 | .td.player .artist-name { 138 | margin: 10px 0px; 139 | text-align: center; 140 | -webkit-animation: fadeInRight 1s; /* Safari and Chrome */ 141 | -moz-animation: fadeInRight 1s; /* Firefox */ 142 | -ms-animation: fadeInRight 1s; /* Internet Explorer */ 143 | -o-animation: fadeInRight 1s; /* Opera */ 144 | animation: fadeInRight 1s; 145 | } 146 | 147 | .td.player .soundcloud-icon { 148 | height: 14px; 149 | width: 16px; 150 | background-size: 16px; 151 | vertical-align: top; 152 | margin-right: 5px; 153 | -webkit-animation: fadeIn 0.5s; /* Safari and Chrome */ 154 | -moz-animation: fadeIn 0.5s; /* Firefox */ 155 | -ms-animation: fadeIn 0.5s; /* Internet Explorer */ 156 | -o-animation: fadeIn 0.5s; /* Opera */ 157 | animation: fadeIn 0.5s; 158 | } 159 | 160 | .td.player .follow-link { 161 | border-right: 1px solid; 162 | } 163 | 164 | .td.player .buy-link { 165 | border-left: 1px solid; 166 | } 167 | 168 | .td.player .follow-link img { 169 | height: 16px; 170 | margin-right: 10px; 171 | vertical-align: middle; 172 | } 173 | 174 | .td.player .follow-link,.td.player .buy-link { 175 | letter-spacing: 2px; 176 | font-weight: 400; 177 | font-size: 12px; 178 | background: none; 179 | } 180 | 181 | .td.player .current-song-info { 182 | font-weight: 300; 183 | font-size: 12px; 184 | text-align: center; 185 | vertical-align: middle; 186 | padding-top: 1rem; 187 | padding-bottom: 1.0625rem; 188 | } 189 | 190 | .td.player .current-song-social-icon { 191 | font-size: 12px; 192 | margin-right: 5px; 193 | vertical-align: middle; 194 | } 195 | 196 | .td.player .controls { 197 | padding: 10px; 198 | border-top: 1px solid; 199 | border-bottom: 1px solid; 200 | } 201 | 202 | .td.player .player-prev { 203 | font-size: 36px; 204 | line-height: 50px !important; 205 | vertical-align: top; 206 | opacity: 0.5; 207 | transition: all 0.2s; 208 | -webkit-transition: all 0.2s; 209 | -moz-transition: all 0.2s; 210 | } 211 | 212 | .td.player .player-prev:hover { 213 | opacity: 1; 214 | } 215 | 216 | .td.player .player-play { 217 | width: 50%; 218 | line-height:50px !important; 219 | } 220 | 221 | .td.player .tdicon-play-circle-outline { 222 | font-size: 42px; 223 | opacity: 0.8; 224 | transition: all 0.2s; 225 | -webkit-transition: all 0.2s; 226 | -moz-transition: all 0.2s; 227 | } 228 | 229 | .td.player .tdicon-play-circle-outline:hover { 230 | opacity: 1; 231 | font-size: 48px; 232 | } 233 | 234 | .td.player .tdicon-pause-circle-outline { 235 | font-size: 42px; 236 | opacity: 0.8; 237 | transition: all 0.2s; 238 | -webkit-transition: all 0.2s; 239 | -moz-transition: all 0.2s; 240 | } 241 | 242 | .td.player .tdicon-pause-circle-outline:hover { 243 | opacity: 1; 244 | font-size: 48px; 245 | } 246 | 247 | .td.player .player-next { 248 | font-size: 36px; 249 | line-height: 50px !important; 250 | vertical-align: top; 251 | opacity: 0.5; 252 | transition: all 0.2s; 253 | -webkit-transition: all 0.2s; 254 | -moz-transition: all 0.2s; 255 | } 256 | 257 | .td.player .player-next:hover { 258 | opacity: 1; 259 | } 260 | 261 | .td.player .repeat-init { 262 | line-height: 50px; 263 | font-size: 18px; 264 | opacity: 0.4; 265 | } 266 | 267 | .td.player .repeat-init.repeat-on { 268 | opacity: 1 !important; 269 | } 270 | 271 | .td.player .volume-controls { 272 | text-align: center; 273 | } 274 | 275 | .td.player .volume-init { 276 | line-height: 50px; 277 | font-size: 16px; 278 | opacity: 0.4; 279 | } 280 | 281 | .td.player .volume-init:hover { 282 | opacity: 1; 283 | } 284 | 285 | .td.player .volume-select i{ 286 | line-height: 50px; 287 | margin: 0px 10px; 288 | font-size: 16px; 289 | opacity: 0.4; 290 | transition: all 0.2s; 291 | -webkit-transition: all 0.2s; 292 | -moz-transition: all 0.2s; 293 | } 294 | 295 | .td.player .volume-select i:hover { 296 | opacity: 1; 297 | } 298 | 299 | .td.player .volume-select i.volume-active { 300 | opacity: 1; 301 | } 302 | 303 | .td.player .scrubber { 304 | line-height: 50px; 305 | text-align: center; 306 | padding: 0px 10px; 307 | border-bottom: 1px solid; 308 | -webkit-animation: fadeIn 1s; /* Safari and Chrome */ 309 | -moz-animation: fadeIn 1s; /* Firefox */ 310 | -ms-animation: fadeIn 1s; /* Internet Explorer */ 311 | -o-animation: fadeIn 1s; /* Opera */ 312 | animation: fadeIn 1s; 313 | } 314 | 315 | .td.player .scrubber-box { 316 | display: inline-block; 317 | line-height: 50px; 318 | } 319 | 320 | .td.player .scrub-bar-box { 321 | display: table; 322 | padding: 24px 0px; 323 | } 324 | 325 | .td.player .track-error-box { 326 | display: table; 327 | padding: 18px 0; 328 | text-align: center; 329 | font-size: 12px; 330 | border-bottom: 1px solid; 331 | } 332 | 333 | .td.player .track-error-box-span { 334 | opacity: 0.4; 335 | } 336 | 337 | .td.player .scrub-bar { 338 | display: table-cell; 339 | vertical-align: middle; 340 | line-height: 50px; 341 | height: 5px; 342 | border-radius: 20px; 343 | width: 100%; 344 | margin-bottom: 3px; 345 | border: 1px solid; 346 | } 347 | 348 | .td.player .start-time,.td.player .stop-time { 349 | font-size: 12px; 350 | text-align: center; 351 | line-height: 50px; 352 | } 353 | 354 | .td.player .playlist-social-icon { 355 | font-size: 14px; 356 | margin-right: 5px; 357 | vertical-align: middle; 358 | } 359 | 360 | .td.player .title { 361 | border-left: 1px solid; 362 | margin-left: 40px; 363 | height: 40px; 364 | } 365 | 366 | .td.player .track-info-social { 367 | font-weight: 300; 368 | vertical-align: middle; 369 | } 370 | 371 | .td.player .track-user, .track-title { 372 | font-size: 12px; 373 | display: inline-block; 374 | margin: 14px 10px; 375 | } 376 | 377 | .td.player .track-title { 378 | font-weight: bold; 379 | } 380 | 381 | .td.player .playlist { 382 | font-size: 12px; 383 | max-height: 150px; 384 | overflow-y: scroll; 385 | overflow-x: hidden; 386 | } 387 | 388 | .td.player .playlist-table { 389 | width: 100%; 390 | } 391 | 392 | .td.player .playlist-table tr { 393 | cursor: pointer; 394 | opacity: 0.5; 395 | } 396 | 397 | .td.player .playlist-table tbody tr:hover { 398 | cursor: pointer; 399 | opacity: 1; 400 | border-bottom: 1px solid; 401 | } 402 | 403 | .td.player .current-play-icon { 404 | font-size: 14px; 405 | } 406 | 407 | .td.player .playlist-table { 408 | border-spacing: 0px; 409 | border:0; 410 | } 411 | 412 | .td.player .playlist-table tbody tr.playing { 413 | opacity: 1; 414 | } 415 | 416 | .td.player .playlist-table tbody tr td { 417 | border-right:0px; 418 | border-left:0px; 419 | border-top:0px; 420 | border-bottom: 1px solid; 421 | font-size: 12px; 422 | } 423 | 424 | /*Slider*/ 425 | .td.player .slider { 426 | -webkit-user-select: none; 427 | box-sizing: border-box; 428 | width: 100%; 429 | display:table-cell; 430 | vertical-align: middle; 431 | line-height: 50px; 432 | min-height: 10px; 433 | margin-left: 5px; 434 | margin-right: 5px; 435 | position: relative; 436 | } 437 | 438 | .td.player .slider > .handle { 439 | -webkit-border-radius: 10px; 440 | -webkit-user-select: none; 441 | -moz-border-radius: 10px; 442 | border-radius: 10px; 443 | width: 10px; 444 | height: 10px; 445 | position: absolute; 446 | top: 50%; 447 | cursor: pointer; 448 | margin-top: -4px; 449 | margin-left: -4px; 450 | -webkit-transition: -webkit-transform 0.2s ease-in-out; 451 | -moz-transition: -moz-transform all 0.2s ease-in-out; 452 | -o-transition: -o-transform 0.2s ease-in-out; 453 | transition: transform 0.2s ease-in-out; 454 | } 455 | 456 | .td.player .slider > .handle:hover { 457 | transform: scale(1.3); 458 | -webkit-transform: scale(1.3); 459 | -mox-transform: scale(1.3); 460 | -o-transform: scale(1.3); 461 | } 462 | 463 | .td.player .slider > .track, .td.player .slider > .highlight-track { 464 | -webkit-border-radius: 8px; 465 | -webkit-user-select: none; 466 | -moz-border-radius: 8px; 467 | border-radius: 8px; 468 | height: 4px; 469 | cursor: pointer; 470 | position: absolute; 471 | top: 50%; 472 | width: 100%; 473 | margin-top: -1px; 474 | } 475 | 476 | /*Equalizer*/ 477 | .td.player svg { 478 | font: 10px sans-serif; 479 | } 480 | 481 | .td.player .axis path, 482 | .td.player .axis line { 483 | fill: none; 484 | shape-rendering: crispEdges; 485 | } 486 | 487 | /*General*/ 488 | 489 | .tdrow { 490 | width: 100%; 491 | margin-left: auto; 492 | margin-right: auto; 493 | margin-top: 0; 494 | margin-bottom: 0; 495 | max-width: 62.5em; 496 | *zoom: 1; } 497 | .tdrow:before, .tdrow:after { 498 | content: " "; 499 | display: table; } 500 | .tdrow:after { 501 | clear: both; } 502 | .tdrow.collapse > .tdcolumn, 503 | .tdrow.collapse > .tdcolumns { 504 | padding-left: 0; 505 | padding-right: 0; } 506 | .tdrow.collapse .tdrow { 507 | margin-left: 0; 508 | margin-right: 0; } 509 | .tdrow .tdrow { 510 | width: auto; 511 | margin-top: 0; 512 | margin-bottom: 0; 513 | max-width: none; 514 | *zoom: 1; } 515 | .tdrow .tdrow:before, .tdrow .tdrow:after { 516 | content: " "; 517 | display: table; } 518 | .tdrow .tdrow:after { 519 | clear: both; } 520 | .tdrow .tdrow.collapse { 521 | width: auto; 522 | margin: 0; 523 | max-width: none; 524 | *zoom: 1; } 525 | .tdrow .tdrow.collapse:before, .tdrow .tdrow.collapse:after { 526 | content: " "; 527 | display: table; } 528 | .tdrow .tdrow.collapse:after { 529 | clear: both; } 530 | 531 | .tdcolumn, 532 | .tdcolumns { 533 | width: 100%; 534 | float: left; } 535 | 536 | .left { 537 | float: left !important; } 538 | 539 | .right { 540 | float: right !important; } 541 | 542 | /*Single Player*/ 543 | .td.player.solo { 544 | min-width: 185px; 545 | } 546 | 547 | .td.player.solo .tdicon-td_logo-link { 548 | left:0; 549 | right: inherit; 550 | } 551 | 552 | .td.player.solo .soundcloud-icon { 553 | height: 10px; 554 | width: 12px; 555 | background-size: 12px; 556 | } 557 | 558 | .td.player.solo .cover { 559 | position: relative; 560 | z-index: inherit; 561 | height: 118px; 562 | display: block; 563 | } 564 | 565 | .td.player.solo .controls { 566 | border-top: 0px; 567 | border-bottom: 0px; 568 | position: absolute; 569 | left: 0; 570 | width: 100%; 571 | top: 0; 572 | bottom: 0; 573 | right: 0; 574 | margin: auto; 575 | padding: 0px; 576 | z-index: 100; 577 | } 578 | 579 | .td.player.solo .solo-cover{ 580 | -webkit-animation: fadeIn 1s; /* Safari and Chrome */ 581 | -moz-animation: fadeIn 1s; /* Firefox */ 582 | -ms-animation: fadeIn 1s; /* Internet Explorer */ 583 | -o-animation: fadeIn 1s; /* Opera */ 584 | animation: fadeIn 1s; 585 | } 586 | 587 | .td.player.solo .solo-cover img { 588 | max-height: 95px; 589 | border-radius: 100%; 590 | vertical-align: initial; 591 | margin: 10px 0px; 592 | -webkit-animation: fadeIn 1s; /* Safari and Chrome */ 593 | -moz-animation: fadeIn 1s; /* Firefox */ 594 | -ms-animation: fadeIn 1s; /* Internet Explorer */ 595 | -o-animation: fadeIn 1s; /* Opera */ 596 | animation: fadeIn 1s; 597 | } 598 | 599 | .td.player.solo .info-solo { 600 | border-left:1px solid; 601 | position: relative; 602 | } 603 | 604 | .td.player.solo .scrubber { 605 | border-left: 1px solid; 606 | padding: 0px; 607 | line-height: 55px; 608 | } 609 | 610 | .td.player.solo .scrubber-box { 611 | border-top: 1px solid; 612 | } 613 | 614 | .td.player.solo .scrub-bar-box { 615 | height: 55px; 616 | } 617 | 618 | .td.player.solo .solo-buttons { 619 | border-top: 1px solid; 620 | } 621 | 622 | .td.player.solo .info{ 623 | border-bottom: 0px; 624 | position: relative; 625 | padding: 6px 10px; 626 | } 627 | 628 | .td.player.solo .buttons { 629 | margin: 35px 0px; 630 | height: inherit; 631 | padding: 0px; 632 | width: inherit; 633 | display: table; 634 | } 635 | 636 | .td.player.solo .buttons i { 637 | display: table-cell; 638 | vertical-align: middle; 639 | } 640 | 641 | .td.player.solo .tdicon-play-circle-outline { 642 | font-size: 55px; 643 | color: white; 644 | text-shadow: 0px 0px 5px #4E4E4E; 645 | } 646 | 647 | .td.player.solo .tdicon-pause-circle-outline { 648 | font-size: 55px; 649 | color: white; 650 | text-shadow: 0px 0px 5px #4E4E4E; 651 | } 652 | 653 | .td.player.solo .buy-link, 654 | .td.player.solo .follow-link { 655 | font-size: 10px; 656 | border: 0px; 657 | } 658 | 659 | .td.player.solo .song-name, 660 | .td.player.solo .artist-name, 661 | .td.player.solo .current-song-info { 662 | font-size: 12px; 663 | } 664 | 665 | .td.player.solo .artist-name { 666 | margin: 5px 0px; 667 | } 668 | 669 | .td.player.solo .song-name { 670 | overflow: hidden; 671 | text-overflow: ellipsis; 672 | white-space: nowrap; 673 | } 674 | 675 | .td.player.solo .current-song-info { 676 | font-size: 10px; 677 | vertical-align: middle; 678 | } 679 | 680 | .td.player.solo .current-song-social-icon { 681 | font-size: 10px; 682 | } 683 | 684 | .td.player.solo .repeat-column { 685 | text-align: center; 686 | } 687 | 688 | .td.player.solo .repeat-init { 689 | margin: 5px 5px; 690 | font-size: 14px; 691 | opacity: 0.4; 692 | cursor: pointer; 693 | } 694 | 695 | .td.player.solo .volume-controls { 696 | text-align: center; 697 | position: relative; 698 | } 699 | 700 | .td.player.solo .volume-init { 701 | margin: 5px; 702 | font-size: 14px; 703 | cursor: pointer; 704 | opacity: 0.4; 705 | } 706 | 707 | .td.player.solo .volume-init:hover { 708 | opacity: 1; 709 | } 710 | 711 | .td.player.solo .volume-select { 712 | position: absolute; 713 | } 714 | 715 | .td.player.solo .volume-select i{ 716 | cursor: pointer; 717 | display: inline-block; 718 | margin: 5px; 719 | font-size: 14px; 720 | opacity: 0.4; 721 | transition: all 0.2s; 722 | -webkit-transition: all 0.2s; 723 | -moz-transition: all 0.2s; 724 | } 725 | 726 | .td.player.solo .volume-select i:hover { 727 | opacity: 1; 728 | } 729 | 730 | .td.player.solo .volume-select i.volume-active { 731 | opacity: 1; 732 | } 733 | 734 | .td.player.solo .start-time, 735 | .td.player.solo .stop-time { 736 | height: 55px; 737 | line-height: 55px; 738 | } 739 | 740 | .td.player.solo .stop-time { 741 | display: table; 742 | } 743 | 744 | .td.player.solo .stop-time i{ 745 | display: table-cell; 746 | vertical-align: middle; 747 | } 748 | 749 | .td.player.solo .track-error-box { 750 | display: table; 751 | padding: 20px 0; 752 | text-align: center; 753 | font-size: 12px; 754 | border-bottom: 1px solid; 755 | border-top: 1px solid; 756 | border-left: 1px solid; 757 | } 758 | 759 | .td.player.solo .track-error-box-span { 760 | opacity: 0.4; 761 | } 762 | 763 | /*Mini*/ 764 | .td.player.mini { 765 | position: absolute; 766 | width: 100%; 767 | height:60px; 768 | } 769 | 770 | .td.player.mini.tdrow { 771 | max-width: 100%; 772 | } 773 | 774 | .td.player.mini div { 775 | display: inline-block; 776 | } 777 | 778 | .td.player.mini .cover { 779 | max-height: 32px; 780 | vertical-align: middle; 781 | z-index: 1; 782 | border-radius: 50px; 783 | } 784 | 785 | .td.player.mini .mini-controls { 786 | line-height: 60px; 787 | text-align: center; 788 | padding:0px; 789 | border-top: 0px; 790 | border-bottom: 0px; 791 | } 792 | 793 | .td.player.mini .mini-buttons { 794 | padding: 0px; 795 | } 796 | 797 | .td.player.mini .player-prev { 798 | font-size: 21px; 799 | margin: 0px 10px; 800 | line-height: 60px !important; 801 | } 802 | 803 | .td.player.mini .player-play { 804 | font-size: 32px; 805 | width:initial; 806 | line-height: 60px !important; 807 | } 808 | 809 | .td.player.mini .player-play:hover { 810 | font-size: 38px; 811 | } 812 | 813 | .td.player.mini .tdicon-pause-circle-outline { 814 | font-size: 22px; 815 | } 816 | 817 | .td.player.mini .tdicon-pause-circle-outline:hover { 818 | font-size: 28px; 819 | } 820 | 821 | .td.player.mini .player-next { 822 | font-size: 21px; 823 | margin: 0px 10px; 824 | line-height: 60px !important; 825 | } 826 | 827 | .td.player.mini .mini-info { 828 | font-size: 12px; 829 | display: table; 830 | height: 100%; 831 | box-sizing: border-box; 832 | border-left: 1px solid; 833 | } 834 | 835 | .td.player.mini .song-name { 836 | overflow: hidden; 837 | text-overflow: ellipsis; 838 | white-space: nowrap; 839 | margin-top: 0px; 840 | font-size: 12px; 841 | } 842 | 843 | .td.player.mini .artist-name { 844 | margin: 0px; 845 | font-weight: bold; 846 | } 847 | 848 | .td.player.mini .mini-social { 849 | list-style: none; 850 | list-style-type: none; 851 | font-size:12px; 852 | text-align: center; 853 | box-sizing: border-box; 854 | border-left: 1px solid; 855 | } 856 | 857 | .td.player.mini .mini-scrubber { 858 | padding: 0px 24px; 859 | height: 100%; 860 | border-left: 1px solid; 861 | box-sizing: border-box; 862 | } 863 | 864 | .td.player.mini .scrubber-box { 865 | padding:0px; 866 | } 867 | 868 | .td.player.mini .scrub-bar-box { 869 | padding:25px 0px; 870 | } 871 | 872 | .td.player.mini .mini-song-info { 873 | display: inline-block; 874 | display: table-cell; 875 | vertical-align: middle; 876 | float: none; 877 | text-align: center; 878 | } 879 | 880 | .td.player.mini .start-time, 881 | .td.player.mini .stop-time { 882 | line-height: 60px; 883 | } 884 | 885 | .td.player.mini .slider { 886 | padding-top: 10px; 887 | } 888 | 889 | .td.player.mini ul { 890 | padding: 0px; 891 | margin: 0px; 892 | height: 100%; 893 | display: table; 894 | } 895 | 896 | .td.player.mini li { 897 | margin: 0px 10px; 898 | display: table-cell; 899 | vertical-align: middle; 900 | height: 100%; 901 | } 902 | 903 | .td.player.mini .slider { 904 | vertical-align: top; 905 | } 906 | 907 | .td.player.mini .mini-connect { 908 | padding: 0px; 909 | } 910 | 911 | .td.player.mini .follow-link, .td.player.mini .buy-link { 912 | line-height: 58px; 913 | padding-top: 0px; 914 | padding-bottom: 0px; 915 | border:0px; 916 | border-left:1px solid; 917 | border-right: 0px; 918 | box-sizing: border-box; 919 | font-size: 10px; 920 | letter-spacing: 1px; 921 | margin-bottom: 0px; 922 | } 923 | 924 | .td.player.mini .tdicon-soundcloud { 925 | vertical-align: middle; 926 | line-height: 60px; 927 | } 928 | 929 | .td.player.mini .tdspinner { 930 | margin: 15px auto; 931 | position: absolute; 932 | left: 0; 933 | right: 0; 934 | } 935 | 936 | .td.player.mini .track-error-box { 937 | display: table; 938 | padding: 22px 0; 939 | text-align: center; 940 | font-size: 12px; 941 | border-bottom: 0px; 942 | border-top: 0px; 943 | border-left: 1px solid; 944 | } 945 | 946 | .td.player.mini .track-error-box-span { 947 | opacity: 0.4; 948 | } 949 | 950 | /*Feed*/ 951 | .td.player.feed .header { 952 | left: 0; 953 | height: auto; 954 | } 955 | 956 | .td.player.feed .song-name { 957 | font-size: 14px; 958 | margin-top: 0px; 959 | } 960 | 961 | .td.player.feed .artist-name { 962 | font-size: 12px; 963 | font-weight: normal; 964 | } 965 | 966 | .td.player.feed .cover { 967 | position: relative; 968 | z-index: inherit; 969 | height: inherit; 970 | display: block; 971 | } 972 | 973 | .td.player.feed .controls { 974 | border-top: 0px; 975 | border-bottom: 0px; 976 | position: absolute; 977 | left: 0; 978 | width: 100%; 979 | top: 0; 980 | bottom: 0; 981 | right: 0; 982 | margin: auto; 983 | padding: 0px; 984 | z-index: 100; 985 | } 986 | 987 | .td.player.info-feed { 988 | margin-bottom: 5px; 989 | } 990 | 991 | .td.player.feed .feed-cover{ 992 | -webkit-animation: fadeIn 1s; /* Safari and Chrome */ 993 | -moz-animation: fadeIn 1s; /* Firefox */ 994 | -ms-animation: fadeIn 1s; /* Internet Explorer */ 995 | -o-animation: fadeIn 1s; /* Opera */ 996 | animation: fadeIn 1s; 997 | } 998 | 999 | .td.player.feed .feed-cover img { 1000 | max-height: 80px; 1001 | vertical-align: initial; 1002 | border-radius: 3px; 1003 | -webkit-animation: fadeIn 1s; /* Safari and Chrome */ 1004 | -moz-animation: fadeIn 1s; /* Firefox */ 1005 | -ms-animation: fadeIn 1s; /* Internet Explorer */ 1006 | -o-animation: fadeIn 1s; /* Opera */ 1007 | animation: fadeIn 1s; 1008 | } 1009 | 1010 | .td.player.feed .buttons { 1011 | display: table-cell; 1012 | vertical-align: middle; 1013 | padding:0px; 1014 | } 1015 | 1016 | .td.player.feed .tdicon-play-circle-outline { 1017 | font-size: 55px; 1018 | color: white; 1019 | text-shadow: 0px 0px 5px #4E4E4E; 1020 | } 1021 | 1022 | .td.player.feed .tdicon-pause-circle-outline { 1023 | font-size: 55px; 1024 | color: white; 1025 | text-shadow: 0px 0px 5px #4E4E4E; 1026 | } 1027 | 1028 | .td.player.feed .current-song-info { 1029 | padding: 0; 1030 | text-align: left; 1031 | } 1032 | 1033 | .td.player.feed .buy-link, .td.player.feed .follow-link { 1034 | text-align: right; 1035 | padding: 0; 1036 | height: 100%; 1037 | } 1038 | 1039 | .td.player.feed .player-play { 1040 | width: 100%; 1041 | line-height: 80px !important; 1042 | } 1043 | 1044 | .td.player.feed .tdicon-play-circle-outline { 1045 | font-size: 42px; 1046 | } 1047 | 1048 | .td.player.feed .tdicon-pause-circle-outline { 1049 | font-size: 42px; 1050 | } 1051 | 1052 | .td.player.feed .info { 1053 | border-bottom: 0px; 1054 | text-align: left; 1055 | padding: 0px; 1056 | } 1057 | 1058 | .td.player.feed .tdbutton { 1059 | border: 0; 1060 | opacity: 0.3; 1061 | -webkit-animation: fadeIn 1s; /* Safari and Chrome */ 1062 | -moz-animation: fadeIn 1s; /* Firefox */ 1063 | -ms-animation: fadeIn 1s; /* Internet Explorer */ 1064 | -o-animation: fadeIn 1s; /* Opera */ 1065 | animation: fadeIn 1s; 1066 | } 1067 | 1068 | .td.player.feed .tdbutton:hover { 1069 | opacity: 1; 1070 | } 1071 | 1072 | .td.player.feed .song-name, .td.player.feed .artist-name { 1073 | text-align: left; 1074 | } 1075 | 1076 | .td.player.feed .scrubber { 1077 | border-bottom:0px; 1078 | } 1079 | 1080 | .td.player.feed .feed-container { 1081 | border-left: 0px; 1082 | } 1083 | 1084 | .td.player.feed .tdspinner{ 1085 | max-height: 80px; 1086 | margin: 30px auto; 1087 | } 1088 | 1089 | .td.player.feed .start-time, .td.player.feed .stop-time { 1090 | line-height: 40px; 1091 | } 1092 | 1093 | .td.player.feed .scrub-bar-box { 1094 | padding: 20px 0px; 1095 | } 1096 | 1097 | .td.player.feed .track-error-box { 1098 | display: table; 1099 | padding: 15px 0; 1100 | text-align: center; 1101 | font-size: 12px; 1102 | border: 0 none; 1103 | } 1104 | 1105 | .td.player.feed .track-error-box-span { 1106 | opacity: 0.4; 1107 | } 1108 | 1109 | .td.player.feed .playlist-social-icon { 1110 | font-size: 14px; 1111 | margin-right: 5px; 1112 | vertical-align: top; 1113 | } 1114 | 1115 | /*Full Player*/ 1116 | .td.player.full { 1117 | position: fixed; 1118 | width: 100%; 1119 | height: 100%; 1120 | top: 0; 1121 | right: 0; 1122 | bottom: 0; 1123 | left: 0; 1124 | border: 0; 1125 | z-index: auto; 1126 | max-width: 100%; 1127 | } 1128 | 1129 | .td.player.full div, .td.player.full ul { 1130 | border: 0; 1131 | } 1132 | 1133 | 1134 | /*Container Responsiveness*/ 1135 | .td.player .follow-small, 1136 | .td.player .buy-small { 1137 | width: 100%; 1138 | } 1139 | 1140 | .td.player .follow-small .follow-link { 1141 | border-right: 0px !important; 1142 | border-top: 1px solid; 1143 | } 1144 | 1145 | .td.player .buy-small .buy-link { 1146 | border-left: 0px !important; 1147 | border-top: 1px solid; 1148 | } 1149 | 1150 | .td.player.solo .solo-container-small { 1151 | border-bottom: 1px solid; 1152 | } 1153 | 1154 | .td.player.shrink .cover .cover-img img { 1155 | max-height: 80px; 1156 | margin: 10px 0px; 1157 | } 1158 | 1159 | .td.player.shrink .info { 1160 | font-size: 12px; 1161 | padding: 0px 45px; 1162 | } 1163 | 1164 | .td.player.shrink .song-name { 1165 | font-size: 12px; 1166 | } 1167 | 1168 | .td.player.shrink .artist-name { 1169 | margin: 5px 0; 1170 | } 1171 | 1172 | .td.player.shrink .tdbutton { 1173 | padding-top: 10px; 1174 | padding-bottom: 10px; 1175 | } 1176 | 1177 | .td.player.shrink .buy-link, .td.player.shrink .follow-link { 1178 | font-size: 10px; 1179 | } 1180 | 1181 | .td.player.shrink .soundcloud-icon { 1182 | height: 12px; 1183 | width: 14px; 1184 | background-size: 14px; 1185 | } 1186 | 1187 | .td.player.shrink .current-song-info { 1188 | font-size: 10px; 1189 | padding-top: 10px; 1190 | padding-bottom: 10px; 1191 | } 1192 | 1193 | .td.player.shrink .controls { 1194 | padding:0px; 1195 | } 1196 | 1197 | .td.player .player-prev, 1198 | .td.player .player-next{ 1199 | font-size: 24px; 1200 | } 1201 | 1202 | .td.player.shrink .tdicon-play-circle-outline{ 1203 | font-size: 32px; 1204 | } 1205 | 1206 | .td.player.shrink .tdicon-pause-circle-outline { 1207 | font-size: 32px; 1208 | } 1209 | 1210 | .td.player.shrink .tdicon-play-circle-outline:hover{ 1211 | font-size: 38px; 1212 | } 1213 | 1214 | .td.player.shrink .tdicon-pause-circle-outline { 1215 | font-size: 38px; 1216 | } 1217 | 1218 | .td.player.shrink .scrubber { 1219 | line-height: 40px; 1220 | padding:0px 10px; 1221 | } 1222 | 1223 | .td.player.shrink .stop-time, .td.player.shrink .start-time { 1224 | font-size: 10px; 1225 | } 1226 | 1227 | .td.player.shrink .scrub-bar-box { 1228 | padding: 25px 0px; 1229 | } 1230 | 1231 | .td.player.shrink .playlist { 1232 | font-size: 12px; 1233 | max-height: 75px; 1234 | } 1235 | 1236 | .td.player.shrink .playlist-table tbody tr td { 1237 | font-size: 10px; 1238 | padding: 5px 10px; 1239 | } 1240 | 1241 | .td.player.shrink .current-play-icon { 1242 | font-size: 12px; 1243 | } 1244 | 1245 | .td.player.shrink .playlist-social-icon { 1246 | font-size: 10px; 1247 | margin-right: 5px; 1248 | vertical-align: top; 1249 | } 1250 | 1251 | .td.player.shrink .track-error-box { 1252 | display: table; 1253 | padding: 18px 0; 1254 | text-align: center; 1255 | font-size: 12px; 1256 | border: 0px; 1257 | border-bottom: 1px solid; 1258 | } 1259 | 1260 | /*Misc*/ 1261 | 1262 | 1263 | /*Animations*/ 1264 | @-webkit-keyframes fadeIn { 1265 | 0% { 1266 | opacity: 0; 1267 | } 1268 | 1269 | 100% { 1270 | opacity: 1; 1271 | } 1272 | } 1273 | 1274 | @keyframes fadeIn { 1275 | 0% { 1276 | opacity: 0; 1277 | } 1278 | 1279 | 100% { 1280 | opacity: 1; 1281 | } 1282 | } 1283 | 1284 | @-webkit-keyframes fadeInLeft { 1285 | 0% { 1286 | opacity: 0; 1287 | -webkit-transform: translateX(-20px); 1288 | transform: translateX(-20px); 1289 | } 1290 | 1291 | 100% { 1292 | opacity: 1; 1293 | -webkit-transform: translateX(0); 1294 | transform: translateX(0); 1295 | } 1296 | } 1297 | 1298 | @keyframes fadeInLeft { 1299 | 0% { 1300 | opacity: 0; 1301 | -webkit-transform: translateX(-20px); 1302 | -ms-transform: translateX(-20px); 1303 | transform: translateX(-20px); 1304 | } 1305 | 1306 | 100% { 1307 | opacity: 1; 1308 | -webkit-transform: translateX(0); 1309 | -ms-transform: translateX(0); 1310 | transform: translateX(0); 1311 | } 1312 | } 1313 | 1314 | .fadeInLeft { 1315 | -webkit-animation-name: fadeInLeft; 1316 | animation-name: fadeInLeft; 1317 | } 1318 | 1319 | @-webkit-keyframes fadeInRight { 1320 | 0% { 1321 | opacity: 0; 1322 | -webkit-transform: translateX(20px); 1323 | transform: translateX(20px); 1324 | } 1325 | 1326 | 100% { 1327 | opacity: 1; 1328 | -webkit-transform: translateX(0); 1329 | transform: translateX(0); 1330 | } 1331 | } 1332 | 1333 | @keyframes fadeInRight { 1334 | 0% { 1335 | opacity: 0; 1336 | -webkit-transform: translateX(20px); 1337 | -ms-transform: translateX(20px); 1338 | transform: translateX(20px); 1339 | } 1340 | 1341 | 100% { 1342 | opacity: 1; 1343 | -webkit-transform: translateX(0); 1344 | -ms-transform: translateX(0); 1345 | transform: translateX(0); 1346 | } 1347 | } 1348 | 1349 | .fadeInRight { 1350 | -webkit-animation-name: fadeInRight; 1351 | animation-name: fadeInRight; 1352 | } 1353 | 1354 | @-webkit-keyframes fadeOutDown { 1355 | 0% { 1356 | opacity: 1; 1357 | -webkit-transform: translateY(0); 1358 | transform: translateY(0); 1359 | } 1360 | 1361 | 100% { 1362 | opacity: 0; 1363 | -webkit-transform: translateY(5px); 1364 | transform: translateY(5px); 1365 | } 1366 | } 1367 | 1368 | @keyframes fadeOutDown { 1369 | 0% { 1370 | opacity: 1; 1371 | -webkit-transform: translateY(0); 1372 | -ms-transform: translateY(0); 1373 | transform: translateY(0); 1374 | } 1375 | 1376 | 100% { 1377 | opacity: 0; 1378 | -webkit-transform: translateY(5px); 1379 | -ms-transform: translateY(5px); 1380 | transform: translateY(5px); 1381 | } 1382 | } 1383 | 1384 | /*Mozilla Fixes*/ 1385 | @-moz-document url-prefix() { 1386 | .td.player .slider { 1387 | display: block; 1388 | } 1389 | 1390 | .td.player .scrub-bar-box { 1391 | padding: 20px 0px; 1392 | } 1393 | } 1394 | 1395 | /*Loader*/ 1396 | .tdspinner { 1397 | margin: 50px auto; 1398 | width: 50px; 1399 | height: 30px; 1400 | text-align: center; 1401 | font-size: 10px; 1402 | } 1403 | 1404 | .tdspinner > div { 1405 | height: 100%; 1406 | width: 6px; 1407 | display: inline-block; 1408 | 1409 | -webkit-animation: stretchdelay 1.2s infinite ease-in-out; 1410 | animation: stretchdelay 1.2s infinite ease-in-out; 1411 | } 1412 | 1413 | .tdspinner .rect2 { 1414 | -webkit-animation-delay: -1.1s; 1415 | animation-delay: -1.1s; 1416 | } 1417 | 1418 | .tdspinner .rect3 { 1419 | -webkit-animation-delay: -1.0s; 1420 | animation-delay: -1.0s; 1421 | } 1422 | 1423 | .tdspinner .rect4 { 1424 | -webkit-animation-delay: -0.9s; 1425 | animation-delay: -0.9s; 1426 | } 1427 | 1428 | .tdspinner .rect5 { 1429 | -webkit-animation-delay: -0.8s; 1430 | animation-delay: -0.8s; 1431 | } 1432 | 1433 | @-webkit-keyframes stretchdelay { 1434 | 0%, 40%, 100% { -webkit-transform: scaleY(0.4) } 1435 | 20% { -webkit-transform: scaleY(1.0) } 1436 | } 1437 | 1438 | @keyframes stretchdelay { 1439 | 0%, 40%, 100% { 1440 | transform: scaleY(0.4); 1441 | -webkit-transform: scaleY(0.4); 1442 | } 20% { 1443 | transform: scaleY(1.0); 1444 | -webkit-transform: scaleY(1.0); 1445 | } 1446 | } 1447 | 1448 | .tdloader { 1449 | font-size: 12px; 1450 | vertical-align: middle; 1451 | line-height: 60px; 1452 | } 1453 | 1454 | .spin { 1455 | -webkit-animation: spin 2s infinite linear; 1456 | animation: spin 2s infinite linear; 1457 | } 1458 | @-webkit-keyframes spin { 1459 | 0% { 1460 | -webkit-transform: rotate(0deg); 1461 | transform: rotate(0deg); 1462 | } 1463 | 100% { 1464 | -webkit-transform: rotate(359deg); 1465 | transform: rotate(359deg); 1466 | } 1467 | } 1468 | @keyframes spin { 1469 | 0% { 1470 | -webkit-transform: rotate(0deg); 1471 | transform: rotate(0deg); 1472 | } 1473 | 100% { 1474 | -webkit-transform: rotate(359deg); 1475 | transform: rotate(359deg); 1476 | } 1477 | } 1478 | 1479 | @media only screen { 1480 | .tdcolumn.tdsmall-centered, 1481 | .tdcolumns.tdsmall-centered { 1482 | margin-left: auto; 1483 | margin-right: auto; 1484 | float: none !important; } 1485 | 1486 | .tdcolumn.tdsmall-uncentered, 1487 | .tdcolumns.tdsmall-uncentered { 1488 | margin-left: 0; 1489 | margin-right: 0; 1490 | float: left !important; } 1491 | 1492 | .tdcolumn.tdsmall-uncentered.opposite, 1493 | .tdcolumns.tdsmall-uncentered.opposite { 1494 | float: right; } 1495 | 1496 | .tdsmall-1 { 1497 | width: 8.33333%; } 1498 | 1499 | .tdsmall-2 { 1500 | width: 16.66667%; } 1501 | 1502 | .tdsmall-3 { 1503 | width: 25%; } 1504 | 1505 | .tdsmall-4 { 1506 | width: 33.33333%; } 1507 | 1508 | .tdsmall-5 { 1509 | width: 41.66667%; } 1510 | 1511 | .tdsmall-6 { 1512 | width: 50%; } 1513 | 1514 | .tdsmall-7 { 1515 | width: 58.33333%; } 1516 | 1517 | .tdsmall-8 { 1518 | width: 66.66667%; } 1519 | 1520 | .tdsmall-9 { 1521 | width: 75%; } 1522 | 1523 | .tdsmall-10 { 1524 | width: 83.33333%; } 1525 | 1526 | .tdsmall-11 { 1527 | width: 91.66667%; } 1528 | 1529 | .tdsmall-12 { 1530 | width: 100%; } 1531 | 1532 | [class*="tdcolumn"] + [class*="tdcolumn"]:last-child { 1533 | float: right; } 1534 | 1535 | [class*="tdcolumn"] + [class*="tdcolumn"].end { 1536 | float: left; } 1537 | 1538 | .tdbutton { 1539 | border-style: solid; 1540 | border-width: 0px; 1541 | cursor: pointer; 1542 | line-height: normal; 1543 | position: relative; 1544 | text-decoration: none; 1545 | text-align: center; 1546 | display: inline-block; 1547 | padding-top: 1rem; 1548 | padding-right: 2rem; 1549 | padding-bottom: 1.0625rem; 1550 | padding-left: 2rem; 1551 | /* @else { font-size: $padding - rem-calc(2); } */ 1552 | color: #000; 1553 | -webkit-transition: background-color 300ms ease-out; 1554 | -moz-transition: background-color 300ms ease-out; 1555 | transition: background-color 300ms ease-out; 1556 | padding-top: 1rem; 1557 | padding-right: 2rem; 1558 | padding-bottom: 1.0625rem; 1559 | padding-left: 2rem; 1560 | /* @else { font-size: $padding - rem-calc(2); } */ } 1561 | .tdbutton:hover, .tdbutton:focus { 1562 | /*background-color: #2285a2;*/ } 1563 | .tdbutton:hover, .tdbutton:focus { 1564 | /*color: white; */} 1565 | .tdbutton.tdlarge { 1566 | padding-top: 1.125rem; 1567 | padding-right: 2.25rem; 1568 | padding-bottom: 1.1875rem; 1569 | padding-left: 2.25rem; 1570 | font-size: 1.25rem; 1571 | /* @else { font-size: $padding - rem-calc(2); } */ } 1572 | .tdbutton.tdsmall { 1573 | padding-top: 0.875rem; 1574 | padding-right: 1.75rem; 1575 | padding-bottom: 0.9375rem; 1576 | padding-left: 1.75rem; 1577 | font-size: 0.8125rem; 1578 | /* @else { font-size: $padding - rem-calc(2); } */ } 1579 | .tdbutton.expand { 1580 | padding-right: 0; 1581 | padding-left: 0; 1582 | width: 100%; } 1583 | } 1584 | 1585 | .td.player table { 1586 | background:none; 1587 | margin: 0px; 1588 | padding:0px; 1589 | } 1590 | .td.player table thead, 1591 | .td.player table tfoot { 1592 | background:none; 1593 | margin: 0px; 1594 | padding:0px; 1595 | } 1596 | .td.player table thead tr th, 1597 | .td.player table thead tr td, 1598 | .td.player table tfoot tr th, 1599 | .td.player table tfoot tr td { 1600 | padding: 0.5rem 0.625rem 0.625rem; 1601 | text-align: left; } 1602 | .td.player table tr th, 1603 | .td.player table tr td { 1604 | padding: 0.5625rem 0.625rem; 1605 | color: inherit; } 1606 | .td.player table tr.even, .td.player table tr.alt, .td.player table tr:nth-of-type(even) { 1607 | background: none; 1608 | } 1609 | .td.player table thead tr th, 1610 | .td.player table tfoot tr th, 1611 | .td.player table tbody tr td, 1612 | .td.player table tr td, 1613 | .td.player table tfoot tr td { 1614 | display: table-cell; 1615 | line-height: 1.125rem; 1616 | } 1617 | 1618 | @media only screen and (min-width: 64.063em) { 1619 | .tdcolumn.tdlarge-centered, 1620 | .tdcolumns.tdlarge-centered { 1621 | margin-left: auto; 1622 | margin-right: auto; 1623 | float: none !important; } 1624 | 1625 | .tdcolumn.tdlarge-uncentered, 1626 | .tdcolumns.tdlarge-uncentered { 1627 | margin-left: 0; 1628 | margin-right: 0; 1629 | float: left !important; } 1630 | 1631 | .tdcolumn.tdlarge-uncentered.opposite, 1632 | .tdcolumns.tdlarge-uncentered.opposite { 1633 | float: right; } 1634 | 1635 | .tdlarge-1 { 1636 | width: 8.33333%; } 1637 | 1638 | .tdlarge-2 { 1639 | width: 16.66667%; } 1640 | 1641 | .tdlarge-3 { 1642 | width: 25%; } 1643 | 1644 | .tdlarge-4 { 1645 | width: 33.33333%; } 1646 | 1647 | .tdlarge-5 { 1648 | width: 41.66667%; } 1649 | 1650 | .tdlarge-6 { 1651 | width: 50%; } 1652 | 1653 | .tdlarge-7 { 1654 | width: 58.33333%; } 1655 | 1656 | .tdlarge-8 { 1657 | width: 66.66667%; } 1658 | 1659 | .tdlarge-9 { 1660 | width: 75%; } 1661 | 1662 | .tdlarge-10 { 1663 | width: 83.33333%; } 1664 | 1665 | .tdlarge-11 { 1666 | width: 91.66667%; } 1667 | 1668 | .tdlarge-12 { 1669 | width: 100%; } 1670 | 1671 | [class*="tdcolumn"] + [class*="tdcolumn"]:last-child { 1672 | float: right; } 1673 | 1674 | [class*="tdcolumn"] + [class*="tdcolumn"].end { 1675 | float: left; } 1676 | 1677 | } 1678 | 1679 | @media only screen and (max-width: 40em){ 1680 | 1681 | .td.player .track-info-stats { 1682 | display: none; 1683 | } 1684 | 1685 | .td.player .follow, .td.player.buy { 1686 | display: none; 1687 | } 1688 | 1689 | .td.player.mini { 1690 | height: initial; 1691 | } 1692 | 1693 | .td.player.mini .player-play { 1694 | width: 80px; 1695 | } 1696 | 1697 | .td.player.mini .tdspinner { 1698 | display: none; 1699 | } 1700 | 1701 | .td.player.mini .mini-scrubber, .td.player.mini .mini-connect{ 1702 | display: none; 1703 | } 1704 | 1705 | .td.player.mini .mini-info, .td.player.mini .mini-social { 1706 | padding: 10px 0px; 1707 | border-top:1px solid; 1708 | } 1709 | 1710 | .td.player .repeat-init, .td.player .volume-init, .td.player .volume-select { 1711 | margin: 15px 0px; 1712 | } 1713 | 1714 | .td.player.solo .repeat-init, .td.player.solo .volume-init { 1715 | display: none; 1716 | } 1717 | } 1718 | --------------------------------------------------------------------------------