diff --git a/js/Actions.jsx b/js/Actions.jsx index 11227591..b7700607 100644 --- a/js/Actions.jsx +++ b/js/Actions.jsx @@ -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
diff --git a/js/Close.jsx b/js/Close.jsx index 0ef471d4..a1cdcc93 100644 --- a/js/Close.jsx +++ b/js/Close.jsx @@ -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
{ - return
+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
Loading...
- +
-
+
@@ -33,11 +50,11 @@ const MainWindow = () => {
-
+
-
+
@@ -53,8 +70,8 @@ const MainWindow = () => {
- - + +
@@ -64,4 +81,4 @@ const MainWindow = () => {
; }; -module.exports = MainWindow; +module.exports = connect(state => state)(MainWindow); diff --git a/js/Position.jsx b/js/Position.jsx index 1e53a0c6..ce188104 100644 --- a/js/Position.jsx +++ b/js/Position.jsx @@ -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'}); } diff --git a/js/Shade.jsx b/js/Shade.jsx new file mode 100644 index 00000000..626d5301 --- /dev/null +++ b/js/Shade.jsx @@ -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
; + } +} + +module.exports = connect()(Shade); diff --git a/js/actionCreators.js b/js/actionCreators.js new file mode 100644 index 00000000..0b91659a --- /dev/null +++ b/js/actionCreators.js @@ -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'}; +} diff --git a/js/hotkeys.js b/js/hotkeys.js index a52c6799..bf8a3e7c 100644 --- a/js/hotkeys.js +++ b/js/hotkeys.js @@ -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'}); } }); }; diff --git a/js/main-window.js b/js/main-window.js index 800ed642..0fd5a914 100644 --- a/js/main-window.js +++ b/js/main-window.js @@ -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(); diff --git a/js/main.js b/js/main.js index 0db50f5b..3dceeb37 100644 --- a/js/main.js +++ b/js/main.js @@ -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( - + , document.getElementById('winamp2-js') ); diff --git a/js/reducers.js b/js/reducers.js index ba3f2644..edd84f8c 100644 --- a/js/reducers.js +++ b/js/reducers.js @@ -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; } diff --git a/js/winamp.js b/js/winamp.js index 745c6318..4f5b0441 100755 --- a/js/winamp.js +++ b/js/winamp.js @@ -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() { diff --git a/package.json b/package.json index 61df63a9..50b391fe 100644 --- a/package.json +++ b/package.json @@ -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" } }