Merge pull request #153 from captbaritone/redux

Begin migrating from vanilla JavaScript to React/Rexux
This commit is contained in:
Jordan Eldredge 2016-07-12 18:47:31 -07:00 committed by GitHub
commit 8ba5e1bb5b
25 changed files with 486 additions and 398 deletions

View file

@ -23,6 +23,12 @@ supported](http://caniuse.com/#feat=audio-api).
Open `index.html` in your browser.
## Development
npm run serve
Open `http://localhost:8080/webpack-dev-server/` in your browser.
## Reference
- [skinspecs.pdf](http://members.xoom.it/skinart/tutorial/skinspecs..pdf)
@ -56,7 +62,7 @@ for Winamp which inspired so many of us.
## License
While the Winamp name, interface, sample audio file and surely property of
While the Winamp name, interface, and, sample audio file are surely property of
Nullsoft, the code within this project is released under the [MIT
License](LICENSE.txt). That being said, if you do anything interesting with
this code, please let me know. I'd love to see it.

View file

@ -3,7 +3,7 @@
position: absolute;
height: 116px;
width: 275px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/MAINMENU.PNG'), auto;
cursor:url('../cursors/MAINMENU.PNG'), auto;
/* Ask the browser to scale showing large pixels if possible */
image-rendering: -moz-crisp-edges; /* Firefox */
image-rendering: -o-crisp-edges; /* Opera */
@ -59,7 +59,7 @@
left: 0;
height: 14px;
width: 275px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/TITLEBAR.PNG'), auto;
cursor:url('../cursors/TITLEBAR.PNG'), auto;
}
#winamp2-js #option,
@ -70,7 +70,7 @@
height: 9px;
width: 9px;
top: 3px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/MAINMENU.PNG'), auto;
cursor:url('../cursors/MAINMENU.PNG'), auto;
}
#winamp2-js #title-bar #option {
@ -94,7 +94,7 @@
}
#winamp2-js #title-bar #close {
left: 264px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/CLOSE.PNG'), auto;
cursor:url('../cursors/CLOSE.PNG'), auto;
}
#winamp2-js #clutter-bar {
position: absolute;
@ -161,6 +161,7 @@
}
#winamp2-js .status #time #minus-sign {
/* Note that this get's augmented by the skin CSS if NUM_EX.BMP is present */
position: absolute;
top: 6px;
left: -1px;
@ -168,13 +169,6 @@
height: 1px;
}
/* Alternate format for minus sign, when skin supports it */
#winamp2-js .status #time.ex #minus-sign {
top: 0px;
left: -1px;
width: 9px;
height: 13px;
}
#winamp2-js .status #time #minute-first-digit {
position: absolute;
@ -279,19 +273,19 @@
height: 13px;
width: 68px;
background-position: 0 0;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
}
#winamp2-js #volume::-webkit-slider-thumb {
top: 1px;
height: 11px;
width: 14px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
}
#winamp2-js #volume::-moz-range-thumb {
top: 1px;
height: 11px;
width: 14px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
}
#winamp2-js #balance {
@ -301,20 +295,20 @@
height: 13px;
width: 38px;
background-position: 0 0;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
}
#winamp2-js #balance::-webkit-slider-thumb {
top: 1px;
height: 11px;
width: 14px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
}
#winamp2-js #balance::-moz-range-thumb {
top: 1px;
height: 11px;
width: 14px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
}
#winamp2-js .windows {
@ -345,13 +339,13 @@
top: 72px;
width: 248px;
height: 10px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
}
#winamp2-js #position::-webkit-slider-thumb {
height: 10px;
width: 29px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
/*
* Fix the strange bug in Safair/mobile-chrome
* http://stackoverflow.com/questions/26727769/rendering-glitch-when-manipulating-range-input-value-via-javascript-in-webkit
@ -363,7 +357,7 @@
#winamp2-js #position::-moz-range-thumb {
height: 10px;
width: 29px;
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/POSBAR.PNG'), auto;
cursor:url('../cursors/POSBAR.PNG'), auto;
}
/* For some reason, we can't use display: none here */
@ -450,7 +444,7 @@
}
#winamp2-js #shade-time {
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/MAINMENU.PNG'), auto;
cursor:url('../cursors/MAINMENU.PNG'), auto;
}
@ -463,7 +457,7 @@
display: none;
}
#winamp2-js .shade #title-bar {
cursor:url('https://jordaneldredge.com/projects/winamp2-js/cursors/MAINMENU.PNG'), auto;
cursor:url('../cursors/MAINMENU.PNG'), auto;
}
#winamp2-js .shade .actions div {

View file

@ -1,85 +0,0 @@
<div id='main-window' class='loading stop'>
<div id='loading'>Loading...</div>
<div id='title-bar' class='selected'>
<div id='option'>
<ul id='context-menu'>
<li><a href='https://github.com/captbaritone/winamp2-js' target='_blank'>Winamp2-js...</a></li>
<li class='hr'><hr /></li>
<li id='context-play-file'>Play File...</li>
<li class='parent'>
<ul>
<li id='context-load-skin'>Load Skin...</li>
<li class='hr'><hr /></li>
<li class='skin-select' data-skin-url='https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/base-2.91.wsz'>&lt;Base Skin&gt;</li>
<li class='skin-select' data-skin-url='https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/MacOSXAqua1-5.wsz'>Mac OSX v1.5 (Aqua)</li>
<li class='skin-select' data-skin-url='https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/TopazAmp1-2.wsz'>TopazAmp</li>
<li class='skin-select' data-skin-url='https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/Vizor1-01.wsz'>Vizor</li>
<li class='skin-select' data-skin-url='https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/XMMS-Turquoise.wsz'>XMMS Turquoise </li>
<li class='skin-select' data-skin-url='https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/ZaxonRemake1-0.wsz'>Zaxon Remake</li>
</ul>
Skins
</li>
<li class='hr'><hr /></li>
<li id='context-exit'>Exit</li>
</ul>
</div>
<div id='shade-time'>
<div id='shade-minus-sign'></div>
<div id='shade-minute-first-digit' class='character'></div>
<div id='shade-minute-second-digit' class='character'></div>
<div id='shade-second-first-digit' class='character'></div>
<div id='shade-second-second-digit' class='character'></div>
</div>
<div id='minimize'></div>
<div id='shade'></div>
<div id='close'></div>
</div>
<div class='status'>
<div id='clutter-bar'>
<div id='button-o'></div>
<div id='button-a'></div>
<div id='button-i'></div>
<div id='button-d'></div>
<div id='button-v'></div>
</div>
<div id='play-pause'></div>
<div id='work-indicator'></div>
<div id='time'>
<div id='minus-sign'></div>
<div id='minute-first-digit'></div>
<div id='minute-second-digit'></div>
<div id='second-first-digit'></div>
<div id='second-second-digit'></div>
</div>
<canvas id='visualizer' width='152' height='32'></canvas>
</div>
<div class='media-info'>
<div id='song-title' class='text'></div>
<div id='kbps'></div>
<div id='khz'></div>
<div class='mono-stereo'>
<div id='mono'></div>
<div id='stereo'></div>
</div>
</div>
<input id='volume' type='range' min='0' max='100' step='1' value='50' />
<input id='balance' type='range' min='-100' max='100' step='2' value='0' />
<div class='windows'>
<div id='equalizer-button'></div>
<div id='playlist-button'></div>
</div>
<input id='position' type='range' min='0' max='100' step='1' value='0' />
<div class='actions'>
<div id='previous'></div>
<div id='play'></div>
<div id='pause'></div>
<div id='stop'></div>
<div id='next'></div>
</div>
<div id='eject'></div>
<div class='shuffle-repeat'>
<div id="shuffle"></div>
<div id="repeat"></div>
</div>
<a id='about' target='_blank' href='https://github.com/captbaritone/winamp2-js'></a>
</div>

32
js/Actions.jsx Normal file
View file

@ -0,0 +1,32 @@
import React from 'react';
import {connect} from 'react-redux';
class Actions extends React.Component {
constructor(props) {
super(props);
this.play = this.play.bind(this);
this.pause = this.pause.bind(this);
this.stop = this.stop.bind(this);
}
play() {
this.props.dispatch({type: 'PLAY'});
}
pause() {
this.props.dispatch({type: 'PAUSE'});
}
stop() {
this.props.dispatch({type: 'STOP'});
}
render() {
return <div className='actions'>
<div id='previous'></div>
<div id='play' onClick={this.play}></div>
<div id='pause' onClick={this.pause}></div>
<div id='stop' onClick={this.stop}></div>
<div id='next'></div>
</div>;
}
}
module.exports = connect(state => state.media)(Actions);

9
js/Character.jsx Normal file
View file

@ -0,0 +1,9 @@
import React from 'react';
module.exports = (props) => {
const char = '' + props.children;
const className = 'character character-' + char.toLowerCase().charCodeAt(0);
return <div {...props} className={className}>{props.children}</div>;
};
// TODO: Require that props.children be a string

14
js/CharacterString.jsx Normal file
View file

@ -0,0 +1,14 @@
import React from 'react';
import Character from './Character.jsx';
module.exports = (props) => {
const text = '' + props.children;
const chars = text.split('');
return <div {...props}>
{chars.map(character => {
return <Character>{character}</Character>;
})};
</div>;
};
// TODO: Require that props.children be a string

View file

@ -1,15 +1,11 @@
import React from 'react';
import MyFile from './my-file';
import {connect} from 'react-redux';
import '../css/context-menu.css';
class ContextMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
selected: false
};
this.openFileDialog = this.openFileDialog.bind(this);
this.close = this.close.bind(this);
this.toggleMenu = this.toggleMenu.bind(this);
@ -27,34 +23,30 @@ class ContextMenu extends React.Component {
}
openFileDialog() {
this.props.winamp.openFileDialog();
this.props.dispatch({type: 'OPEN_FILE_DIALOG'});
}
close() {
// Close all of Winamp
this.props.winamp.close();
this.props.dispatch({type: 'CLOSE_WINAMP'});
}
closeMenu() {
this.setState({selected: false});
this.props.dispatch({type: 'CLOSE_CONTEXT_MENU'});
}
toggleMenu(event) {
this.setState({selected: !this.state.selected});
this.props.dispatch({type: 'TOGGLE_CONTEXT_MENU'});
event.stopPropagation();
}
setSkin(e) {
const filename = e.target.dataset.filename;
const url = 'https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/' + filename;
const skinFile = new MyFile();
skinFile.setUrl(url);
this.props.winamp.setSkin(skinFile);
this.props.dispatch({type: 'SET_SKIN_FROM_URL', url: url});
}
render() {
var classes = this.state.selected ? 'selected' : '';
var classes = this.props.selected ? 'selected' : '';
return <div id='option' className={classes} onClick={this.toggleMenu}>
<ul id='context-menu'>
<li><a href='https://github.com/captbaritone/winamp2-js' target='_blank'>Winamp2-js...</a></li>
@ -80,4 +72,4 @@ class ContextMenu extends React.Component {
}
}
module.exports = ContextMenu;
module.exports = connect(state => state.contextMenu)(ContextMenu);

16
js/Kbps.jsx Normal file
View file

@ -0,0 +1,16 @@
// Single line text display that can animate and hold multiple registers
import React from 'react';
import {connect} from 'react-redux';
import CharacterString from './CharacterString.jsx';
class Kbps extends React.Component {
render() {
return <CharacterString id='kbps'>
{this.props.kbps}
</CharacterString>;
}
}
module.exports = connect(state => state.media)(Kbps);

16
js/Khz.jsx Normal file
View file

@ -0,0 +1,16 @@
// Single line text display that can animate and hold multiple registers
import React from 'react';
import {connect} from 'react-redux';
import CharacterString from './CharacterString.jsx';
class Khz extends React.Component {
render() {
return <CharacterString id='khz'>
{this.props.khz}
</CharacterString>;
}
}
module.exports = connect(state => state.media)(Khz);

55
js/Marquee.jsx Normal file
View file

@ -0,0 +1,55 @@
// Single line text display that can animate and hold multiple registers
import React from 'react';
import {connect} from 'react-redux';
import CharacterString from './CharacterString.jsx';
class Marquee extends React.Component {
constructor(props) {
super(props);
this.handleMouseDown = this.handleMouseDown.bind(this);
}
componentDidMount() {
const step = () => {
setTimeout(() => {
this.props.dispatch({type: 'STEP_MARQUEE'});
step();
}, 220);
};
step();
}
selectedRegister() {
const selected = this.props.registers.filter(register => {
return register.id === this.props.selectedRegister;
});
return selected ? selected[0] : selected;
}
handleMouseDown() {
this.props.dispatch({type: 'PAUSE_MARQUEE'});
document.addEventListener('mouseup', () => {
// TODO: Remove this listener
setTimeout(() => {
this.props.dispatch({type: 'START_MARQUEE'});
}, 1000);
});
}
render() {
const register = this.selectedRegister();
let chars = register.text.split('');
if (chars.length > 30 && register.step > 0) {
const start = chars.slice(register.step);
const end = chars.slice(0, register.step);
// TODO: Use the spread operator
chars = start.concat(end).slice(0, 30);
}
const text = chars.join('');
return <CharacterString onMouseDown={this.handleMouseDown}>
{text}
</CharacterString>;
}
}
module.exports = connect(state => state.marquee)(Marquee);

32
js/ShadeTime.jsx Normal file
View file

@ -0,0 +1,32 @@
import React from 'react';
import {connect} from 'react-redux';
import {getTimeObj} from './utils';
import Character from './Character.jsx';
class Time extends React.Component {
constructor(props) {
super(props);
this.toggleTimeMode = this.toggleTimeMode.bind(this);
}
toggleTimeMode() {
this.props.dispatch({type: 'TOGGLE_TIME_MODE'});
}
render() {
const seconds = this.props.timeMode === 'ELAPSED' ?
this.props.timeElapsed :
this.props.length - this.props.timeElapsed;
const timeObj = getTimeObj(seconds);
return <div id='shade-time' onClick={this.toggleTimeMode} className='countdown'>
<Character id='shade-minus-sign'>{this.props.timeMode === 'REMAINING' ? '-' : ''}</Character>
<Character id='shade-minute-first-digit'>{timeObj.minutesFirstDigit}</Character>
<Character id='shade-minute-second-digit'>{timeObj.minutesSecondDigit}</Character>
<Character id='shade-second-first-digit'>{timeObj.secondsFirstDigit}</Character>
<Character id='shade-second-second-digit'>{timeObj.secondsSecondDigit}</Character>
</div>;
}
}
module.exports = connect(state => state.media)(Time);

30
js/Time.jsx Normal file
View file

@ -0,0 +1,30 @@
import React from 'react';
import {connect} from 'react-redux';
import {getTimeObj} from './utils';
class Time extends React.Component {
constructor(props) {
super(props);
this.toggleTimeMode = this.toggleTimeMode.bind(this);
}
toggleTimeMode() {
this.props.dispatch({type: 'TOGGLE_TIME_MODE'});
}
render() {
const seconds = this.props.timeMode === 'ELAPSED' ?
this.props.timeElapsed :
this.props.length - this.props.timeElapsed;
const timeObj = getTimeObj(seconds);
return <div id='time' onClick={this.toggleTimeMode} className='countdown'>
{this.props.timeMode === 'REMAINING' && <div id='minus-sign'></div>}
<div id='minute-first-digit' className={'digit digit-' + timeObj.minutesFirstDigit}></div>
<div id='minute-second-digit' className={'digit digit-' + timeObj.minutesSecondDigit}></div>
<div id='second-first-digit' className={'digit digit-' + timeObj.secondsFirstDigit}></div>
<div id='second-second-digit' className={'digit digit-' + timeObj.secondsSecondDigit}></div>
</div>;
}
}
module.exports = connect(state => state.media)(Time);

View file

@ -1,37 +0,0 @@
// Manage rendering text from this skin's text.bmp file
module.exports = function() {
// Fill a node with a <div> containing character <div>s
this.setNodeToString = function(node, string) {
var stringElement = document.createElement('div');
for (var i = 0, len = string.length; i < len; i++) {
var char = string[i].toLowerCase();
stringElement.appendChild(this.characterNode(char));
}
node.innerHTML = '';
node.appendChild(stringElement);
};
// Get a <div> containing char
this.characterNode = function(char) {
return this.displayCharacterInNode(char, document.createElement('div'));
};
// Style/populate a <div> to display a character
this.displayCharacterInNode = function(character, node) {
character = character.toString();
var className = 'character-' + character.charCodeAt(0);
node.classList.add('character');
node.classList.add(className);
return node;
};
// Get a <div> containing a digit
this.digitNode = function(digit) {
var div = document.createElement('div');
div.classList.add('digit');
div.classList.add('digit-' + digit);
return div;
};
};

View file

@ -1,13 +1,14 @@
module.exports = function(winamp) {
var keylog = [];
var trigger = [78, 85, 76, 27, 76, 27, 83, 79, 70, 84];
document.addEventListener('keyup', function(e){
document.addEventListener('keydown', function(e){
if (e.ctrlKey) { // Is CTRL depressed?
switch (e.keyCode) {
case 68: winamp.toggleDoubledMode(); break; // CTRL+D
// XXX FIXME
case 76: winamp.openOptionMenu(); break; // CTRL+L
case 84: winamp.toggleTimeMode(); break; // CTRL+T
// CTRL+T
case 84: winamp.dispath({type: 'TOGGLE_TIME_MODE'}); break;
}
} else {
switch (e.keyCode) {
@ -17,13 +18,15 @@ module.exports = function(winamp) {
case 40: winamp.incrementVolumeBy(-1); break; // down arrow
case 66: winamp.next(); break; // B
case 67: winamp.pause(); break; // C
case 76: winamp.openFileDialog(); break; // L
// L
case 76: winamp.dispatch({type: 'OPEN_FILE_DIALOG'}); break;
case 82: winamp.toggleRepeat(); break; // R
case 83: winamp.toggleShuffle(); break; // S
case 86: winamp.stop(); break; // V
case 88: winamp.play(); break; // X
case 90: winamp.previous(); break; // Z
case 96: winamp.openFileDialog(); break; // numpad 0
// numpad 0
case 96: winamp.dispatch({type: 'OPEN_FILE_DIALOG'}); break;
case 97: winamp.previous(10); break; // numpad 1
case 98: winamp.incrementVolumeBy(-1); break; // numpad 2
case 99: winamp.next(10); break; // numpad 3

View file

@ -25,13 +25,7 @@ module.exports = el('div', {id: 'main-window', class: 'loading stop'}, [
el('div', {id: 'loading'}, 'Loading...'),
el('div', {id: 'title-bar', class: 'selected'}, [
el('div', {id: 'context-menu-holder'}),
el('div', {id: 'shade-time'}, [
el('div', {id: 'shade-minus-sign'}),
el('div', {id: 'shade-minute-first-digit', class: 'character'}),
el('div', {id: 'shade-minute-second-digit', class: 'character'}),
el('div', {id: 'shade-second-first-digit', class: 'character'}),
el('div', {id: 'shade-second-second-digit', class: 'character'})
]),
el('div', {id: 'shade-time-holder'}),
el('div', {id: 'minimize'}),
el('div', {id: 'shade'}),
el('div', {id: 'close'})
@ -46,19 +40,13 @@ module.exports = el('div', {id: 'main-window', class: 'loading stop'}, [
]),
el('div', {id: 'play-pause'}),
el('div', {id: 'work-indicator'}),
el('div', {id: 'time'}, [
el('div', {id: 'minus-sign'}),
el('div', {id: 'minute-first-digit'}),
el('div', {id: 'minute-second-digit'}),
el('div', {id: 'second-first-digit'}),
el('div', {id: 'second-second-digit'})
]),
el('div', {id: 'time-holder'}),
el('canvas', {id: 'visualizer', width: '152', height: '32'})
]),
el('div', {class: 'media-info'}, [
el('div', {id: 'song-title', class: 'text'}),
el('div', {id: 'kbps'}),
el('div', {id: 'khz'}),
el('div', {id: 'kbps-holder'}),
el('div', {id: 'khz-holder'}),
el('div', {class: 'mono-stereo'}, [
el('div', {id: 'mono'}),
el('div', {id: 'stereo'})
@ -71,13 +59,7 @@ module.exports = el('div', {id: 'main-window', class: 'loading stop'}, [
el('div', {id: 'playlist-button'})
]),
el('input', {id: 'position', type: 'range', min: '0', max: '100', step: '1', value: '0'}),
el('div', {class: 'actions'}, [
el('div', {id: 'previous'}),
el('div', {id: 'play'}),
el('div', {id: 'pause'}),
el('div', {id: 'stop'}),
el('div', {id: 'next'})
]),
el('div', {id: 'actions-holder'}),
el('div', {id: 'eject'}),
el('div', {class: 'shuffle-repeat'}, [
el('div', {id: 'shuffle'}),

View file

@ -1,4 +1,12 @@
import MultiDisplay from './multi-display';
import React from 'react';
import Marquee from './Marquee.jsx';
import Actions from './Actions.jsx';
import Time from './Time.jsx';
import ShadeTime from './ShadeTime.jsx';
import Kbps from './Kbps.jsx';
import Khz from './Khz.jsx';
import '../css/main-window.css';
module.exports = {
init: function(winamp) {
@ -8,25 +16,11 @@ module.exports = {
shade: document.getElementById('shade'),
buttonD: document.getElementById('button-d'),
position: document.getElementById('position'),
volumeMessage: document.getElementById('volume-message'),
balanceMessage: document.getElementById('balance-message'),
positionMessage: document.getElementById('position-message'),
songTitle: document.getElementById('song-title'),
time: document.getElementById('time'),
shadeTime: document.getElementById('shade-time'),
shadeMinusSign: document.getElementById('shade-minus-sign'),
visualizer: document.getElementById('visualizer'),
previous: document.getElementById('previous'),
play: document.getElementById('play'),
pause: document.getElementById('pause'),
stop: document.getElementById('stop'),
next: document.getElementById('next'),
eject: document.getElementById('eject'),
repeat: document.getElementById('repeat'),
shuffle: document.getElementById('shuffle'),
volume: document.getElementById('volume'),
kbps: document.getElementById('kbps'),
khz: document.getElementById('khz'),
mono: document.getElementById('mono'),
stereo: document.getElementById('stereo'),
balance: document.getElementById('balance'),
@ -38,16 +32,12 @@ module.exports = {
this.handle = document.getElementById('title-bar');
this.body = this.nodes.window;
this.textDisplay = new MultiDisplay(this.nodes.songTitle);
this.textDisplay.addRegister('songTitle');
this.textDisplay.addRegister('position');
this.textDisplay.addRegister('volume');
this.textDisplay.addRegister('balance');
this.textDisplay.addRegister('message'); // General purpose
this.textDisplay.showRegister('songTitle');
this.textDisplay.startRegisterMarquee('songTitle');
this.winamp.renderTo(<Marquee />, document.getElementById('song-title'));
this.winamp.renderTo(<Actions />, document.getElementById('actions-holder'));
this.winamp.renderTo(<Time />, document.getElementById('time-holder'));
this.winamp.renderTo(<ShadeTime />, document.getElementById('shade-time-holder'));
this.winamp.renderTo(<Kbps />, document.getElementById('kbps-holder'));
this.winamp.renderTo(<Khz />, document.getElementById('khz-holder'));
this._registerListeners();
return this;
@ -66,44 +56,30 @@ module.exports = {
this.nodes.buttonD.onmousedown = function() {
if (self.nodes.window.classList.contains('doubled')) {
self.textDisplay.setRegisterText('message', 'Disable doublesize mode');
self.winamp.dispatch({type: 'SET_MARQUEE_REGISTER', register: 'message', text: 'Disable doublesize mode'});
} else {
self.textDisplay.setRegisterText('message', 'Enable doublesize mode');
self.winamp.dispatch({type: 'SET_MARQUEE_REGISTER', register: 'message', text: 'Enable doublesize mode'});
}
self.textDisplay.showRegister('message');
self.winamp.dispatch({type: 'SHOW_MARQUEE_REGISTER', register: 'message'});
};
this.nodes.buttonD.onmouseup = function() {
self.textDisplay.showRegister('songTitle');
self.winamp.dispatch({type: 'SHOW_MARQUEE_REGISTER', register: 'songTitle'});
};
this.nodes.buttonD.onclick = function() {
self.winamp.toggleDoubledMode();
};
this.nodes.play.onclick = function() {
self.winamp.play();
};
this.nodes.songTitle.onmousedown = function() {
self.textDisplay.pauseRegisterMarquee('songTitle');
};
this.nodes.songTitle.onmouseup = function() {
setTimeout(function() {
self.textDisplay.startRegisterMarquee('songTitle');
}, 1000);
};
this.nodes.position.onmousedown = function() {
if (!self.nodes.window.classList.contains('stop')){
self.textDisplay.showRegister('position');
self.winamp.dispatch({type: 'SHOW_MARQUEE_REGISTER', register: 'position'});
self.nodes.window.classList.add('setting-position');
}
};
this.nodes.position.onmouseup = function() {
self.textDisplay.showRegister('songTitle');
self.winamp.dispatch({type: 'SHOW_MARQUEE_REGISTER', register: 'songTitle'});
self.nodes.window.classList.remove('setting-position');
};
@ -113,7 +89,7 @@ module.exports = {
var newElapsed = self._timeString(self.winamp.getDuration() * newFractionComplete);
var duration = self._timeString(self.winamp.getDuration());
var message = 'Seek to: ' + newElapsed + '/' + duration + ' (' + newPercentComplete + '%)';
self.textDisplay.setRegisterText('position', message);
self.winamp.dispatch({type: 'SET_MARQUEE_REGISTER', register: 'message', text: message});
};
this.nodes.position.onchange = function() {
@ -122,24 +98,8 @@ module.exports = {
}
};
this.nodes.previous.onclick = function() {
self.winamp.previous();
};
this.nodes.next.onclick = function() {
self.winamp.next();
};
this.nodes.pause.onclick = function() {
self.winamp.pause();
};
this.nodes.stop.onclick = function() {
self.winamp.stop();
};
this.nodes.eject.onclick = function() {
self.winamp.openFileDialog();
self.winamp.dispatch({type: 'OPEN_FILE_DIALOG'});
};
this.nodes.repeat.onclick = function() {
@ -150,32 +110,24 @@ module.exports = {
self.winamp.toggleShuffle();
};
this.nodes.shadeTime.onclick = function() {
self.winamp.toggleTimeMode();
};
this.nodes.volume.onmousedown = function() {
self.textDisplay.showRegister('volume');
self.winamp.dispatch({type: 'SHOW_MARQUEE_REGISTER', register: 'volume'});
};
this.nodes.volume.onmouseup = function() {
self.textDisplay.showRegister('songTitle');
self.winamp.dispatch({type: 'SHOW_MARQUEE_REGISTER', register: 'songTitle'});
};
this.nodes.volume.oninput = function() {
self.winamp.setVolume(this.value);
};
this.nodes.time.onclick = function() {
self.winamp.toggleTimeMode();
};
this.nodes.balance.onmousedown = function() {
self.textDisplay.showRegister('balance');
self.winamp.dispatch({type: 'SHOW_MARQUEE_REGISTER', register: 'balance'});
};
this.nodes.balance.onmouseup = function() {
self.textDisplay.showRegister('songTitle');
self.winamp.dispatch({type: 'SHOW_MARQUEE_REGISTER', register: 'songTitle'});
};
this.nodes.balance.oninput = function() {
@ -204,9 +156,6 @@ module.exports = {
window.addEventListener('stopLoading', function() {
self.unsetLoadingState();
});
window.addEventListener('toggleTimeMode', function() {
self.toggleTimeMode();
});
window.addEventListener('changeState', function() {
self.changeState();
});
@ -271,39 +220,6 @@ module.exports = {
updateTime: function() {
this.updateShadePositionClass();
this.updatePosition();
var shadeMinusCharacter = ' ';
var digits = null;
if (this.nodes.time.classList.contains('countdown')) {
digits = this.winamp._timeObject(this.winamp.getTimeRemaining());
shadeMinusCharacter = '-';
} else {
digits = this.winamp._timeObject(this.winamp.getTimeElapsed());
}
this.winamp.skin.font.displayCharacterInNode(shadeMinusCharacter, this.nodes.shadeMinusSign);
var digitNodes = [
document.getElementById('minute-first-digit'),
document.getElementById('minute-second-digit'),
document.getElementById('second-first-digit'),
document.getElementById('second-second-digit')
];
var shadeDigitNodes = [
document.getElementById('shade-minute-first-digit'),
document.getElementById('shade-minute-second-digit'),
document.getElementById('shade-second-first-digit'),
document.getElementById('shade-second-second-digit')
];
// For each digit/node
for (var i = 0; i < 4; i++) {
var digit = digits[i];
var digitNode = digitNodes[i];
var shadeNode = shadeDigitNodes[i];
digitNode.innerHTML = '';
digitNode.appendChild(this.winamp.skin.font.digitNode(digit));
this.winamp.skin.font.displayCharacterInNode(digit, shadeNode);
}
},
setWorkingIndicator: function() {
@ -322,11 +238,6 @@ module.exports = {
this.nodes.window.classList.remove('loading');
},
toggleTimeMode: function() {
this.nodes.time.classList.toggle('countdown');
this.updateTime();
},
updateVolume: function() {
var volume = this.winamp.getVolume();
var percent = volume / 100;
@ -335,7 +246,7 @@ module.exports = {
this.nodes.volume.style.backgroundPosition = '0 -' + offset + 'px';
var message = 'Volume: ' + volume + '%';
this.textDisplay.setRegisterText('volume', message);
this.winamp.dispatch({type: 'SET_MARQUEE_REGISTER', register: 'volume', text: message});
// This shouldn't trigger an infinite loop with volume.onchange(),
// since the value will be the same
@ -352,7 +263,7 @@ module.exports = {
} else {
string = 'Balance: ' + Math.abs(balance) + '% Left';
}
this.textDisplay.setRegisterText('balance', string);
this.winamp.dispatch({type: 'SET_MARQUEE_REGISTER', register: 'balance', text: string});
balance = Math.abs(balance) / 100;
var sprite = Math.round(balance * 28);
var offset = (sprite - 1) * 15;
@ -375,7 +286,7 @@ module.exports = {
updateTitle: function() {
var duration = this._timeString(this.winamp.getDuration());
var name = this.winamp.fileName + ' (' + duration + ') *** ';
this.textDisplay.setRegisterText('songTitle', name);
this.winamp.dispatch({type: 'SET_MARQUEE_REGISTER', register: 'songTitle', text: name});
},
updateChannelCount: function() {

View file

@ -1,5 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import createReducer from './reducers';
import Browser from './browser';
import mainWindowDom from './main-window-dom';
@ -7,15 +11,27 @@ import Winamp from './winamp';
import ContextMenu from './ContextMenu.jsx';
import Hotkeys from './hotkeys';
import '../css/winamp.css';
import '../css/main-window.css';
if (new Browser(window).isCompatible) {
var mainWindowElement = document.createElement('div');
mainWindowElement.appendChild(mainWindowDom);
document.getElementById('winamp2-js').appendChild(mainWindowElement);
var winamp = Winamp.init({
var winamp = Winamp;
let store = createStore(createReducer(winamp), window.devToolsExtension && window.devToolsExtension());
// TODO: Remove this workaround
winamp.renderTo = (componant, node) => {
render(
<Provider store={store}>
{componant}
</Provider>,
node
);
};
winamp.dispatch = store.dispatch;
winamp.init({
volume: 50,
balance: 0,
mediaFile: {
@ -25,8 +41,10 @@ if (new Browser(window).isCompatible) {
skinUrl: 'https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/base-2.91.wsz'
});
new Hotkeys(winamp);
ReactDOM.render(<ContextMenu winamp={winamp} />, document.getElementById('context-menu-holder'));
winamp.renderTo(<ContextMenu />, document.getElementById('context-menu-holder'));
} else {
document.getElementById('winamp').style.display = 'none';
document.getElementById('browser-compatibility').style.display = 'block';

View file

@ -1,64 +0,0 @@
// Single line text display that can animate and hold multiple registers
import Font from './font';
var MultiDisplay = function(node) {
this.font = new Font();
this.node = node;
this.registers = {};
this._marqueeLoop();
};
MultiDisplay.prototype.addRegister = function(key) {
// Create element node
var register = document.createElement('div');
register.style.display = 'none';
this.node.appendChild(register);
this.registers[key] = {
node: register,
text: '',
marquee: false
};
};
// Set text of register
MultiDisplay.prototype.setRegisterText = function(register, text) {
this.font.setNodeToString(this.registers[register].node, text);
};
MultiDisplay.prototype.showRegister = function(showKey) {
for (var key in this.registers) {
if (this.registers.hasOwnProperty(key)) {
var display = (key === showKey) ? 'block' : 'none';
this.registers[key].node.style.display = display;
}
}
};
MultiDisplay.prototype.startRegisterMarquee = function(key) {
this.registers[key].marquee = true;
};
MultiDisplay.prototype.pauseRegisterMarquee = function(key) {
this.registers[key].marquee = false;
};
MultiDisplay.prototype._marqueeLoop = function() {
var self = this;
setTimeout(function() {
for (var key in self.registers) {
// Check every register to see if it needs to be marqueed
if (self.registers[key].marquee) {
var text = self.registers[key].node.firstChild;
// Only scroll if the text is too long
if (text && text.childNodes.length > 30) {
var characterNode = text.firstChild;
text.removeChild(characterNode);
text.appendChild(characterNode);
}
}
}
self._marqueeLoop();
}, 220);
};
module.exports = MultiDisplay;

144
js/reducers.js Normal file
View file

@ -0,0 +1,144 @@
import MyFile from './my-file';
import {combineReducers} from 'redux';
const register = (state, action) => {
if (!state) {
return {
id: '',
step: 0,
text: ''
};
}
switch (action.type) {
case 'SET_MARQUEE_REGISTER':
if (state.id === action.register) {
return Object.assign({}, state, {step: 0, text: action.text});
}
return state;
case 'STEP_MARQUEE':
return Object.assign({}, state, {step: (state.step + 1) % state.text.length});
default:
return state;
}
};
const marquee = (state, action) => {
if (!state) {
return {
stepping: true,
selectedRegister: 'songTitle',
registers: [
Object.assign(register(), {id: 'songTitle'}),
Object.assign(register(), {id: 'position'}),
Object.assign(register(), {id: 'volume'}),
Object.assign(register(), {id: 'balance'}),
Object.assign(register(), {id: 'message'})
]
};
}
switch (action.type) {
case 'SET_MARQUEE_REGISTER':
return Object.assign({}, state, {registers: state.registers.map(r => register(r, action))});
case 'STEP_MARQUEE':
if (state.stepping) {
return Object.assign({}, state, {registers: state.registers.map(r => register(r, action))});
}
return state;
case 'SHOW_MARQUEE_REGISTER':
if (state.stepping) {
return Object.assign({}, state, {selectedRegister: action.register});
}
return state;
case 'PAUSE_MARQUEE':
return Object.assign({}, state, {stepping: false});
case 'START_MARQUEE':
return Object.assign({}, state, {stepping: true});
default:
return state;
}
};
const contextMenu = (state, action) => {
if (!state) {
return {
selected: false
};
}
switch (action.type) {
case 'TOGGLE_CONTEXT_MENU':
return Object.assign({}, state, {selected: !state.selected});
case 'CLOSE_CONTEXT_MENU':
return Object.assign({}, state, {selected: false});
default:
return state;
}
};
const media = (state, action) => {
if (!state) {
return {
timeMode: 'ELAPSED',
timeElapsed: 0,
length: null,
kbps: null,
khz: null
};
}
switch (action.type) {
case 'TOGGLE_TIME_MODE':
const newMode = state.timeMode === 'REMAINING' ? 'ELAPSED' : 'REMAINING';
return Object.assign({}, state, {timeMode: newMode});
case 'UPDATE_TIME_ELAPSED':
return Object.assign({}, state, {timeElapsed: action.elapsed});
case 'SET_MEDIA_LENGTH':
return Object.assign({}, state, {length: action.length});
case 'SET_MEDIA_KBPS':
return Object.assign({}, state, {kbps: action.kbps});
case 'SET_MEDIA_KHZ':
return Object.assign({}, state, {khz: action.khz});
default:
return state;
}
};
const createReducer = (winamp) => {
const reducer = combineReducers({
marquee,
contextMenu,
media
});
// Add in the temporary actions that don't modify the state.
return (state, action) => {
state = reducer(state, action);
switch (action.type) {
case 'PLAY':
winamp.play();
return state;
case 'PAUSE':
winamp.pause();
return state;
case 'STOP':
winamp.stop();
return state;
case 'CLOSE_WINAMP':
winamp.close();
return state;
case 'OPEN_FILE_DIALOG':
// TODO: Figure out how to make this pure
winamp.openFileDialog();
return state;
case 'SET_SKIN_FROM_URL':
// TODO: Figure out how to make this pure
const skinFile = new MyFile();
skinFile.setUrl(action.url);
winamp.setSkin(skinFile);
return state;
default:
return state;
}
};
};
export default createReducer;

View file

@ -94,8 +94,8 @@ module.exports = [
{
img: 'NUMS_EX',
sprites: [
{selectors: ['#time.ex #minus-sign'], x: 90, y: 0, width: 9, height: 13},
{selectors: ['#time.ex.countdown #minus-sign'], x: 99, y: 0, width: 9, height: 13},
{selectors: ['#time #minus-sign'], x: 90, y: 0, width: 9, height: 13},
{selectors: ['#time.countdown #minus-sign'], x: 99, y: 0, width: 9, height: 13},
{selectors: ['.digit-0'], x: 0, y: 0, width: 9, height: 13},
{selectors: ['.digit-1'], x: 9, y: 0, width: 9, height: 13},
{selectors: ['.digit-2'], x: 18, y: 0, width: 9, height: 13},

View file

@ -1,11 +1,9 @@
// Dynamically set the css background images for all the sprites
import SKIN_SPRITES from './skin-sprites';
import Font from './font';
import Visualizer from './visualizer';
import JSZip from '../node_modules/jszip/dist/jszip'; // Hack
module.exports = {
font: new Font(),
init: function(visualizerNode, analyser) {
this._createNewStyleNode();
this.visualizer = Visualizer.init(visualizerNode, analyser);
@ -25,18 +23,11 @@ module.exports = {
// Gets passed as a callback, so don't have access to `this`
_setSkinByBuffer: function(buffer) {
var zip = new JSZip(buffer);
document.getElementById('time').classList.remove('ex');
var promisedCssRules = this._skinSprites.map(function(spriteObj) {
var file = this._findFileInZip(spriteObj.img, zip);
if (file) {
// CSS has to change if this file is present
if (spriteObj.img === 'NUMS_EX') {
document.getElementById('time').classList.add('ex');
}
var src = 'data:image/bmp;base64,' + btoa(file.asBinary());
return this._spriteCssRule(src, spriteObj);
return this._spriteCssRule(spriteObj.img, src, spriteObj);
}
}, this);
@ -85,7 +76,7 @@ module.exports = {
// Given an image URL and coordinates, returns a data url for a sub-section
// of that image
_spriteCssRule: function(src, spriteObj) {
_spriteCssRule: function(img, src, spriteObj) {
return new Promise(function(resolve) {
var imageObj = new Image();
imageObj.src = src;
@ -105,6 +96,11 @@ module.exports = {
cssRules += '#winamp2-js ' + selector + '{' + value + '}\n';
});
});
if (img === 'NUMS_EX') {
// This alternate number file requires that the minus sign be
// formatted differently.
cssRules += '#winamp2-js .status #time #minus-sign { top: 0px; left: -1px; width: 9px; height: 13px; }\n';
}
resolve(cssRules);
};
});

16
js/utils.js Normal file
View file

@ -0,0 +1,16 @@
const getTimeObj = (time) => {
var minutes = Math.floor(time / 60);
var seconds = time % 60;
return {
minutesFirstDigit: Math.floor(minutes / 10),
minutesSecondDigit: Math.floor(minutes % 10),
secondsFirstDigit: Math.floor(seconds / 10),
secondsSecondDigit: Math.floor(seconds % 10)
};
};
module.exports = {
getTimeObj
};

View file

@ -5,6 +5,8 @@ import Skin from './skin';
import Media from './media';
import MyFile from './my-file';
import '../css/winamp.css';
module.exports = {
init: function(options) {
this.fileInput = document.createElement('input');
@ -24,7 +26,6 @@ module.exports = {
stopWaiting: new Event('stopWaiting'),
startLoading: new Event('startLoading'),
stopLoading: new Event('stopLoading'),
toggleTimeMode: new Event('toggleTimeMode'),
changeState: new Event('changeState'),
titleUpdated: new Event('titleUpdated'),
channelCountUpdated: new Event('channelCountUpdated'),
@ -53,6 +54,8 @@ module.exports = {
this.windowManager.registerWindow(this.mainWindow);
this.media.addEventListener('timeupdate', function() {
self.dispatch({type: 'UPDATE_TIME_ELAPSED', elapsed: self.media.timeElapsed()});
// Legacy
window.dispatchEvent(self.events.timeUpdated);
});
@ -120,10 +123,6 @@ module.exports = {
this.media.seekToPercentComplete(percent);
},
toggleTimeMode: function() {
window.dispatchEvent(this.events.toggleTimeMode);
},
play: function() {
if (this.getState() === 'play'){
this.media.stop();
@ -257,11 +256,12 @@ module.exports = {
function setMetaData() {
var kbps = '128';
var khz = Math.round(this.media.sampleRate() / 1000).toString();
this.skin.font.setNodeToString(document.getElementById('kbps'), kbps);
this.skin.font.setNodeToString(document.getElementById('khz'), khz);
this.dispatch({type: 'SET_MEDIA_KBPS', kbps: kbps});
this.dispatch({type: 'SET_MEDIA_KHZ', khz: khz});
window.dispatchEvent(this.events.channelCountUpdated);
window.dispatchEvent(this.events.titleUpdated);
window.dispatchEvent(this.events.timeUpdated);
this.dispatch({type: 'SET_MEDIA_LENGTH', length: this.media.duration()});
}
// Note, this will not happen right away

View file

@ -34,15 +34,19 @@
"css-loader": "^0.23.1",
"eslint": "2.*",
"eslint-plugin-react": "^5.2.2",
"file-loader": "^0.9.0",
"gzip-size-cli": "^1.0.0",
"pretty-bytes-cli": "^1.0.0",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.13.0"
},
"dependencies": {
"babel-plugin-transform-react-jsx": "^6.8.0",
"jszip": "^2.6.0",
"react": "^15.2.0",
"react-dom": "^15.2.0"
"react-dom": "^15.2.0",
"react-redux": "^4.4.5",
"redux": "^3.5.2"
}
}

View file

@ -15,6 +15,10 @@ module.exports = {
test: /\.css$/,
loader: 'style-loader!css-loader'
},
{
test: /\.png$/i,
loader: 'url-loader?limit=100000'
},
{
test: /\.jsx?$/,
exclude: /(node_modules|bower_components)/,