Move to thunks. Use display state from store (#164)

This commit is contained in:
Jordan Eldredge 2016-07-27 23:17:32 -07:00 committed by GitHub
parent 336a888eb4
commit 1e49d0a908
13 changed files with 166 additions and 185 deletions

View file

@ -1,5 +1,6 @@
import React from 'react';
import {connect} from 'react-redux';
import {play, pause, stop} from './actionCreators';
class Actions extends React.Component {
@ -10,13 +11,13 @@ class Actions extends React.Component {
this.stop = this.stop.bind(this);
}
play() {
this.props.dispatch({type: 'PLAY'});
this.props.dispatch(play(this.props.mediaPlayer));
}
pause() {
this.props.dispatch({type: 'PAUSE'});
this.props.dispatch(pause(this.props.mediaPlayer));
}
stop() {
this.props.dispatch({type: 'STOP'});
this.props.dispatch(stop(this.props.mediaPlayer));
}
render() {
return <div className='actions'>

View file

@ -1,6 +1,8 @@
import React from 'react';
import {connect} from 'react-redux';
import {close} from './actionCreators';
class Close extends React.Component {
constructor(props) {
@ -8,7 +10,7 @@ class Close extends React.Component {
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.dispatch({type: 'CLOSE'});
this.props.dispatch(close(this.props.mediaPlayer));
}
render() {
return <div

View file

@ -1,6 +1,8 @@
import React from 'react';
import {connect} from 'react-redux';
import {close, setSkinFromFilename} from './actionCreators';
import '../css/context-menu.css';
class ContextMenu extends React.Component {
@ -27,7 +29,7 @@ class ContextMenu extends React.Component {
}
close() {
this.props.dispatch({type: 'CLOSE_WINAMP'});
this.props.dispatch(close(this.props.mediaPlayer));
}
closeMenu() {
@ -40,9 +42,7 @@ class ContextMenu extends React.Component {
}
setSkin(e) {
const filename = e.target.dataset.filename;
const url = 'https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/' + filename;
this.props.dispatch({type: 'SET_SKIN_FROM_URL', url: url});
setSkinFromFilename(this.props.winamp, e.target.dataset.filename);
}
render() {

View file

@ -1,4 +1,6 @@
import React from 'react';
import {connect} from 'react-redux';
import classnames from 'classnames';
import Actions from './Actions.jsx';
import Balance from './Balance.jsx';
@ -11,6 +13,7 @@ import Marquee from './Marquee.jsx';
import MonoStereo from './MonoStereo.jsx';
import Position from './Position.jsx';
import Repeat from './Repeat.jsx';
import Shade from './Shade.jsx';
import ShadeTime from './ShadeTime.jsx';
import Shuffle from './Shuffle.jsx';
import Time from './Time.jsx';
@ -18,14 +21,28 @@ import Volume from './Volume.jsx';
import '../css/main-window.css';
const MainWindow = () => {
return <div id='main-window' className='loading stop'>
const MainWindow = (props) => {
const {status} = props.media;
const {loading, doubled, shade, closed, llama} = props.display;
const className = classnames({
// TODO: Handle these status changes in the individual components
play: status === 'PLAYING',
stop: status === 'STOPPED',
pause: status === 'PAUSED',
loading,
doubled,
llama,
shade,
closed
});
return <div id='main-window' className={className}>
<div id='loading'>Loading...</div>
<div id='title-bar' className='selected'>
<ContextMenu />
<ContextMenu mediaPlayer={props.mediaPlayer} winamp={props.winamp} />
<ShadeTime />
<div id='minimize'></div>
<div id='shade'></div>
<Shade />
<Close />
</div>
<div className='status'>
@ -33,11 +50,11 @@ const MainWindow = () => {
<div id='button-o'></div>
<div id='button-a'></div>
<div id='button-i'></div>
<div id='button-d'></div>
<div id='button-d' className={props.display.doubled ? 'selected' : ''}></div>
<div id='button-v'></div>
</div>
<div id='play-pause'></div>
<div id='work-indicator'></div>
<div id='work-indicator' className={props.display.working ? 'selected' : ''}></div>
<Time />
<canvas id='visualizer' width='152' height='32'></canvas>
</div>
@ -53,8 +70,8 @@ const MainWindow = () => {
<div id='equalizer-button'></div>
<div id='playlist-button'></div>
</div>
<Position />
<Actions />
<Position mediaPlayer={props.mediaPlayer} />
<Actions mediaPlayer={props.mediaPlayer} />
<Eject />
<div className='shuffle-repeat'>
<Shuffle />
@ -64,4 +81,4 @@ const MainWindow = () => {
</div>;
};
module.exports = MainWindow;
module.exports = connect(state => state)(MainWindow);

View file

@ -15,7 +15,7 @@ class Position extends React.Component {
}
onMouseUp(e) {
this.props.dispatch({type: 'SET_POSITION', position: e.target.value});
this.props.mediaPlayer.seekToPercentComplete(e.target.value);
this.props.dispatch({type: 'UNSET_FOCUS'});
}

20
js/Shade.jsx Normal file
View file

@ -0,0 +1,20 @@
import React from 'react';
import {connect} from 'react-redux';
class Shade extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.dispatch({type: 'TOGGLE_SHADE_MODE'});
}
render() {
return <div
id='shade'
onClick={this.handleClick}
/>;
}
}
module.exports = connect()(Shade);

51
js/actionCreators.js Normal file
View file

@ -0,0 +1,51 @@
import MyFile from './my-file';
export function play(mediaPlayer) {
return (dispatch, getState) => {
if (getState().media.status === 'PLAYING') {
mediaPlayer.stop();
dispatch({type: 'MEDIA_IS_STOPPED'});
} else {
mediaPlayer.play();
dispatch({type: 'MEDIA_IS_PLAYING'});
}
};
}
export function pause(mediaPlayer) {
return (dispatch, getState) => {
const status = getState().media.status;
switch (status) {
case 'PAUSED':
mediaPlayer.play();
dispatch({type: 'MEDIA_IS_PLAYING'});
break;
case 'PLAYING':
mediaPlayer.pause();
dispatch({type: 'MEDIA_IS_PAUSED'});
break;
}
};
}
export function stop(mediaPlayer) {
return (dispatch) => {
mediaPlayer.stop();
dispatch({type: 'MEDIA_IS_STOPPED'});
};
}
export function close(mediaPlayer) {
return (dispatch) => {
mediaPlayer.stop();
dispatch({type: 'CLOSE_WINAMP'});
};
}
export function setSkinFromFilename(winamp, filename) {
const url = `https://cdn.rawgit.com/captbaritone/winamp-skins/master/v2/${filename}`;
const skinFile = new MyFile();
skinFile.setUrl(url);
winamp.setSkin(skinFile);
return {type: 'START_LOADING'};
}

View file

@ -1,3 +1,5 @@
import {play, pause, stop} from './actionCreators';
module.exports = function(winamp, store) {
var keylog = [];
var trigger = [78, 85, 76, 27, 76, 27, 83, 79, 70, 84];
@ -26,13 +28,16 @@ module.exports = function(winamp, store) {
store.dispatch({type: 'SET_VOLUME', volume: decrementedVolume});
break;
case 66: winamp.next(); break; // B
case 67: winamp.pause(); break; // C
// C
case 67: winamp.dispatch(pause(winamp.media)); break;
// L
case 76: store.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
// V
case 86: winamp.dispatch(stop(winamp.media)); break;
// X
case 88: winamp.dispatch(play(winamp.media)); break;
case 90: winamp.previous(); break; // Z
// numpad 0
case 96: store.dispatch({type: 'OPEN_FILE_DIALOG'}); break;
@ -40,7 +45,8 @@ module.exports = function(winamp, store) {
case 98: winamp.incrementVolumeBy(-1); break; // numpad 2
case 99: winamp.next(10); break; // numpad 3
case 100: winamp.previous(); break; // numpad 4
case 101: winamp.play(); break; // numpad 5
// numpad 5
case 101: winamp.dispatch(play(winamp.media)); break;
case 102: winamp.next(); break; // numpad 6
case 103: winamp.seekForwardBy(-5); break; // numpad 7
case 104: winamp.incrementVolumeBy(1); break; // numpad 8
@ -52,7 +58,7 @@ module.exports = function(winamp, store) {
keylog.push(e.keyCode);
keylog = keylog.slice(-10);
if (keylog.toString() === trigger.toString()) {
winamp.toggleLlama();
winamp.dispatch({type: 'TOGGLE_LLAMA_MODE'});
}
});
};

View file

@ -2,7 +2,6 @@ module.exports = {
init: function(winamp) {
this.winamp = winamp;
this.nodes = {
shade: document.getElementById('shade'),
buttonD: document.getElementById('button-d'),
visualizer: document.getElementById('visualizer'),
workIndicator: document.getElementById('work-indicator'),
@ -19,10 +18,6 @@ module.exports = {
_registerListeners: function() {
var self = this;
this.nodes.shade.onclick = function() {
self.nodes.window.classList.toggle('shade');
};
this.nodes.buttonD.onmousedown = function() {
self.winamp.dispatch({type: 'SET_FOCUS', input: 'double'});
};
@ -36,70 +31,11 @@ module.exports = {
self.winamp.toggleVisualizer();
};
window.addEventListener('startWaiting', function() {
self.setWorkingIndicator();
});
window.addEventListener('stopWaiting', function() {
self.unsetWorkingIndicator();
});
window.addEventListener('startLoading', function() {
self.setLoadingState();
});
window.addEventListener('stopLoading', function() {
self.unsetLoadingState();
});
window.addEventListener('changeState', function() {
self.changeState();
});
window.addEventListener('doubledModeToggled', function() {
self.toggleDoubledMode();
});
window.addEventListener('llamaToggled', function() {
self.toggleLlama();
});
window.addEventListener('close', function() {
self.nodes.window.classList.add('closed');
});
this.nodes.window.addEventListener('dragenter', this.dragenter.bind(this));
this.nodes.window.addEventListener('dragover', this.dragover.bind(this));
this.nodes.window.addEventListener('drop', this.drop.bind(this));
},
toggleDoubledMode: function() {
this.nodes.buttonD.classList.toggle('selected');
this.nodes.window.classList.toggle('doubled');
},
setWorkingIndicator: function() {
this.nodes.workIndicator.classList.add('selected');
},
unsetWorkingIndicator: function() {
this.nodes.workIndicator.classList.remove('selected');
},
setLoadingState: function() {
this.nodes.window.classList.add('loading');
},
unsetLoadingState: function() {
this.nodes.window.classList.remove('loading');
},
changeState: function() {
var state = this.winamp.getState();
var stateOptions = ['play', 'stop', 'pause'];
for (var i = 0; i < stateOptions.length; i++) {
this.nodes.window.classList.remove(stateOptions[i]);
}
this.nodes.window.classList.add(state);
},
toggleLlama: function() {
this.nodes.window.classList.toggle('llama');
},
dragenter: function(e) {
e.stopPropagation();
e.preventDefault();

View file

@ -1,7 +1,8 @@
import React from 'react';
import {render} from 'react-dom';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import createReducer from './reducers';
@ -14,12 +15,13 @@ if (new Browser(window).isCompatible) {
var winamp = Winamp;
let store = createStore(
createReducer(winamp),
window.devToolsExtension && window.devToolsExtension()
window.devToolsExtension && window.devToolsExtension(),
applyMiddleware(thunk)
);
render(
<Provider store={store}>
<MainWindow />
<MainWindow winamp={winamp} mediaPlayer={winamp.media} />
</Provider>,
document.getElementById('winamp2-js')
);

View file

@ -1,4 +1,3 @@
import MyFile from './my-file';
import {combineReducers} from 'redux';
const userInput = (state, action) => {
@ -24,15 +23,34 @@ const display = (state, action) => {
if (!state) {
return {
doubled: false,
marqueeStep: 0
marqueeStep: 0,
loading: true,
llama: false,
closed: false,
shade: false,
working: false
};
}
switch (action.type) {
case 'TOGGLE_DOUBLESIZE_MODE':
return {...state, doubled: !state.doubled};
case 'TOGGLE_SHADE_MODE':
return {...state, shade: !state.shade};
case 'TOGGLE_LLAMA_MODE':
return {...state, llama: !state.llama};
case 'STEP_MARQUEE':
// TODO: Prevent this from becoming huge
return {...state, marqueeStep: state.marqueeStep + 1};
case 'STOP_WORKING':
return {...state, working: false};
case 'START_WORKING':
return {...state, working: true};
case 'STOP_LOADING':
return {...state, loading: false};
case 'START_LOADING':
return {...state, loading: true};
case 'CLOSE_WINAMP':
return {...state, closed: true};
default:
return state;
}
@ -67,7 +85,9 @@ const media = (state, action) => {
name: '',
channels: null,
shuffle: false,
repeat: false
repeat: false,
// TODO: Enforce possible values
status: 'STOPPED'
};
}
switch (action.type) {
@ -94,6 +114,12 @@ const media = (state, action) => {
return {...state, repeat: !state.repeat};
case 'TOGGLE_SHUFFLE':
return {...state, shuffle: !state.shuffle};
case 'MEDIA_IS_PLAYING':
return {...state, status: 'PLAYING'};
case 'MEDIA_IS_PAUSED':
return {...state, status: 'PAUSED'};
case 'MEDIA_IS_STOPPED':
return {...state, status: 'STOPPED'};
default:
return state;
}
@ -112,49 +138,22 @@ const createReducer = (winamp) => {
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 'SET_VOLUME':
winamp.setVolume(action.volume);
return state;
case 'SET_BALANCE':
winamp.setBalance(action.balance);
return state;
case 'SET_POSITION':
winamp.seekToPercentComplete(action.position);
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;
case 'TOGGLE_DOUBLESIZE_MODE':
winamp.toggleDoubledMode();
return state;
case 'TOGGLE_REPEAT':
winamp.toggleRepeat();
return state;
case 'TOGGLE_SHUFFLE':
winamp.toggleShuffle();
return state;
case 'CLOSE':
winamp.close();
return state;
default:
return state;
}

View file

@ -8,28 +8,20 @@ import MyFile from './my-file';
import '../css/winamp.css';
module.exports = {
media: Media.init(),
init: function(options) {
this.fileInput = document.createElement('input');
this.fileInput.type = 'file';
this.fileInput.style.display = 'none';
this.windowManager = WindowManager;
this.media = Media.init();
this.skin = Skin.init(document.getElementById('visualizer'), this.media._analyser);
this.state = '';
this.mainWindow = MainWindow.init(this);
this.events = {
timeUpdated: new Event('timeUpdated'),
startWaiting: new Event('startWaiting'),
stopWaiting: new Event('stopWaiting'),
startLoading: new Event('startLoading'),
stopLoading: new Event('stopLoading'),
changeState: new Event('changeState'),
doubledModeToggled: new Event('doubledModeToggled'),
llamaToggled: new Event('llamaToggled'),
close: new Event('close')
timeUpdated: new Event('timeUpdated')
};
this.dispatch({type: 'SET_VOLUME', volume: options.volume});
@ -60,19 +52,19 @@ module.exports = {
this.media.addEventListener('ended', function() {
self.skin.visualizer.clear();
self.setState('stop');
self.dispatch({type: 'MEDIA_IS_STOPPED'});
});
this.media.addEventListener('waiting', function() {
window.dispatchEvent(self.events.startWaiting);
self.dispatch({type: 'START_WORKING'});
});
this.media.addEventListener('stopWaiting', function() {
window.dispatchEvent(self.events.stopWaiting);
self.dispatch({type: 'STOP_WORKING'});
});
this.media.addEventListener('playing', function() {
self.setState('play');
self.dispatch({type: 'MEDIA_IS_PLAYING'});
});
this.fileInput.onchange = function(e){
@ -81,15 +73,6 @@ module.exports = {
},
/* Functions */
setState: function(state) {
this.state = state;
window.dispatchEvent(this.events.changeState);
},
getState: function() {
return this.state;
},
getDuration: function() {
return this.media.duration();
},
@ -110,27 +93,6 @@ module.exports = {
this.media.seekToPercentComplete(percent);
},
play: function() {
if (this.getState() === 'play'){
this.media.stop();
}
this.media.play();
this.setState('play');
},
pause: function() {
if (this.getState() === 'pause'){
this.media.play();
} else if (this.getState() === 'play') {
this.media.pause();
this.setState('pause');
}
},
stop: function() {
this.media.stop();
this.setState('stop');
},
// From 0-100
setVolume: function(volume) {
// Ensure volume does not go out of bounds
@ -140,10 +102,6 @@ module.exports = {
this.media.setVolume(volume);
},
toggleDoubledMode: function() {
window.dispatchEvent(this.events.doubledModeToggled);
},
// From -100 to 100
setBalance: function(balance) {
this.media.setBalance(balance);
@ -162,14 +120,9 @@ module.exports = {
this.media.toggleShuffle();
},
toggleLlama: function() {
window.dispatchEvent(this.events.llamaToggled);
},
close: function() {
window.dispatchEvent(this.events.close);
this.media.stop();
this.setState('stop'); // Currently unneeded
this.dispatch({type: 'MEDIA_IS_STOPPED'});
},
openFileDialog: function() {
@ -201,16 +154,8 @@ module.exports = {
},
setSkin: function(file) {
this.setLoadingState();
this.skin.setSkinByFile(file, this.unsetLoadingState.bind(this));
},
setLoadingState: function() {
window.dispatchEvent(this.events.startLoading);
},
unsetLoadingState: function() {
window.dispatchEvent(this.events.stopLoading);
this.dispatch({type: 'START_LOADING'});
this.skin.setSkinByFile(file, () => this.dispatch({type: 'STOP_LOADING'}));
},
toggleVisualizer: function() {

View file

@ -49,10 +49,12 @@
"dependencies": {
"babel-plugin-transform-object-rest-spread": "^6.8.0",
"babel-plugin-transform-react-jsx": "^6.8.0",
"classnames": "^2.2.5",
"jszip": "^2.6.0",
"react": "^15.2.0",
"react-dom": "^15.2.0",
"react-redux": "^4.4.5",
"redux": "^3.5.2"
"redux": "^3.5.2",
"redux-thunk": "^2.1.0"
}
}