├── .babelrc ├── .babelrc.save ├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── Connection.js ├── actions │ ├── GlobalActions.js │ └── index.js ├── assets │ ├── 3.PNG │ ├── Capture.PNG │ ├── Capture2.PNG │ ├── facebook.png │ └── hyperobject.jpg ├── backend │ ├── BootTidal.hs │ ├── Client.js │ └── server.js ├── components │ ├── App │ │ ├── index.css │ │ └── index.jsx │ ├── CodeEditor │ │ ├── index.css │ │ └── index.jsx │ ├── ConnectionSettings │ │ ├── index.css │ │ └── index.jsx │ ├── Effect │ │ ├── CodeSlider │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── CodeToggle │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── EffectCreator │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── Slider │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── index.css │ │ ├── index.jsx │ │ └── oldindex.jsx │ ├── Flyout │ │ ├── index.css │ │ └── index.jsx │ ├── Header │ │ ├── index.css │ │ └── index.jsx │ ├── LaunchSpace │ │ ├── index.css │ │ └── index.jsx │ ├── MasterEditor │ │ ├── LanguageControls │ │ │ ├── index.css │ │ │ └── index.jsx │ │ ├── index.css │ │ └── index.jsx │ ├── Modal │ │ ├── index.css │ │ └── index.jsx │ ├── Render │ │ ├── index.css │ │ └── index.jsx │ ├── ResizeDivider │ │ ├── index.css │ │ └── index.jsx │ ├── Settings │ │ ├── index.css │ │ └── index.jsx │ ├── Stem │ │ ├── index.css │ │ └── index.jsx │ ├── StemEditor │ │ ├── index.css │ │ └── index.jsx │ ├── Track │ │ ├── index.css │ │ └── index.jsx │ └── util │ │ ├── CodeWriter │ │ ├── index.css │ │ └── index.jsx │ │ ├── PlusButton │ │ ├── PlusButton.jsx │ │ └── index.css │ │ ├── TemplateInput │ │ ├── index.css │ │ └── index.jsx │ │ ├── TextColorPicker │ │ ├── index.css │ │ └── index.js │ │ └── Toggle │ │ ├── index.css │ │ └── index.jsx ├── containers │ ├── App.jsx │ ├── ConnectionSettings.jsx │ ├── Effect.jsx │ ├── Flyout.jsx │ ├── Header.jsx │ ├── LaunchSpace.jsx │ ├── Render.jsx │ ├── Settings.jsx │ ├── Stem.jsx │ └── Track.jsx ├── index.js ├── middleware │ └── index.js ├── midi │ ├── index.js │ ├── midi-maps │ │ └── akai-lpd8.json │ └── utility.js ├── model │ ├── EffectModel.js │ ├── Id.js │ ├── MasterModel.js │ ├── Midi.js │ ├── Settings.js │ ├── StemModel.js │ ├── TrackModel.js │ ├── index.js │ └── old_state.js ├── reducers │ ├── Connection.js │ ├── Effect.js │ ├── MIDI.js │ ├── Master.js │ ├── Settings.js │ ├── Stem.js │ ├── Track.js │ ├── index.js │ └── old_index.js ├── renderers │ ├── Hydra │ │ ├── dom.jsx │ │ ├── index.css │ │ └── index.js │ ├── TidalCycles │ │ ├── dom.jsx │ │ ├── index.css │ │ └── index.js │ └── index.js ├── store.js └── util.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } -------------------------------------------------------------------------------- /.babelrc.save: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | 26 | .idea 27 | Dead.iml 28 | 29 | .pre-commit-config.yaml 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DeadCode 2 | 3 | Dead is a browser-based, language-agnostic, and tablet-friendly environment for audio-visual live coding 4 | and Code Jockeying. Dead provides an interface for authoring code snippets that can be toggled on and off 5 | on a grid of buttons (called ‘stems’). The interface contains sliders and other continuous control inputs 6 | to facilitate smooth transitioning between stems and ‘tracks’ (columns of stems). Currently Dead supports 7 | the [TidalCycles](https://tidalcycles.org/index.php/Welcome) and [Hydra](https://github.com/ojack/hydra) live 8 | coding languages, with the capability (and desire!) to include renderers for other languages in future versions. 9 | 10 | For more info on how to use Dead see this [demo video](https://youtu.be/nTBwdGbfgmU) and [this short performance](https://www.youtube.com/watch?v=kuJlpd2i25k) 11 | 12 | 13 | ### Versions 14 | `master` branch is stable and consistent with the v1.0.0 release. 15 | 16 | **If you're just using TidalCycles** `development` is stable and much more feature rich/efficient (Hydra support is currently broken in `development`). New features include: 17 | - track-level effects (eg. any Tidal pattern transformer applied over an entire track) 18 | - UI improvements: side bar resizing, better scrolling in button grid, re-ordering of tracks, styling/layout fixes/redesigns 19 | - re-written websocket server and efficiency improvements for slider effects 20 | - Track and master level 'Macros' - defining a macro called `notes` as `"0 4 7"` (in the new track effects editor) allows me to use it like a variable in 21 | the code underneath that track: eg: `up notes # s "synth"` 22 | 23 | ### Installing/Running 24 | 25 | Dependencies: 26 | - NodeJS, npm or yarn 27 | - [TidalCycles](https://tidalcycles.org/index.php/Installation) 28 | - for MIDI, you may need to install dependencies for the npm [node-midi](https://www.npmjs.com/package/midi) and [easymidi](https://www.npmjs.com/package/easymidi) packages 29 | - Chrome web browser (currently untested on other browsers) 30 | 31 | ##### Clone and build JS dependencies: 32 | ```bash 33 | git clone https://github.com/JamieBeverley/DeadCode 34 | cd DeadCode 35 | yarn install 36 | ``` 37 | 38 | ##### Running Dead: 39 | 40 | Run the Dead browser interface. Your browser should open to localhost:3000. You can open the application on tablets and other devices by navigating to `:3000` 41 | ```bash 42 | yarn start 43 | ``` 44 | Run the Dead websocket server. This NodeJS app shares state between all connected clients (eg. your browser on your desktop/tablet). 45 | and is responsible for passing TidalCycles code to GHCI. 46 | ``` 47 | yarn backend 48 | ``` 49 | 50 | `src/backend/` contains a Tidal boot file 'BootTidal.hs' - to specify different Tidal Boot params edit/replace this file. 51 | 52 | ### Using the Performance Interface 53 | 54 | Each button on the left side of the interface (called a 'Stem') contains some code that will be toggled on/off 55 | when the button is clicked/tapped. To edit a Stem's code, right click on a button and it will appear in the 56 | menu on the right of the interface. 57 | 58 | #### Stems 59 | You can give a name to the Stem, choose which language it runs, edit its code, and add effects to apply to 60 | that stem. Enter code in the text box and hit 'eval' or shift+enter to evaluate it - if the stem is toggled on you should 61 | see/hear the result. TidalCycles code should omit the dirt layer (eg. no `d1` `d2`, etc...) - all stems are combined into 62 | a single`stack` expression. 63 | 64 | #### Effects 65 | Stems have effects below the code editor that can be toggled on/off. There are 2 types of effects: sliders and 'code toggles': 66 | Sliders can be used to control parameters that take one numerical value. Code toggles take code in the language of the stem 67 | and apply them to that stem's code when toggled. 68 | 69 | `TidalCycles`: Effects for stems are pre-pended to the code for that stem. Eg. a code toggle effect with the code 70 | `(linger 0.5) . (# speed 2)` applied to a stem with code `s "kick hat clap hat"` would produce `(linger 0.5) . (# speed 2) $ s "kick hat clap hat"` 71 | 72 | `Hydra`: Effects for hydra stems are appended to the code for that stem: Eg. a code toggle effect with the code 73 | `.modulate(noise())` applied to a stem with code `osc()` would produce `osc().modulate(noise())`. 74 | 75 | #### Master Settings 76 | Master effects and settings can be defined under the `master` tab in the menu on the right. 77 | Macros that are evaluated prior to any stem evaluation can be entered here as well. 78 | 79 | For instance you may want a global varialbe `melody` accessible to your Tidal stems. In the `master` 80 | tab you could define: `let melody = "0 3 7 14 19"` and use the `melody` variable in your stems. 81 | 82 | `Tempo` for Tidal can also be defined in the master settings. 83 | 84 | #### Saving and Loading 85 | You can download a `.json` containing your set with CTRL-D (or the download icon) and open a `.json` file with CTRL-O (or the folder icon). 86 | 87 | To save a set to local browser storage hit CTRL+S or hit the floppy disk icon at the top of the interface. You can load 88 | the state from browser storage with CTRL+L, or hitting the icon next to the save button. 89 | 90 | **Note:** Be careful if saving only to local storage if running on `localhost` - another `localhost` application may write-over 91 | your saved data (always download the state as well just in case) 92 | 93 | #### Connecting to the WebSocket Server + Pushing the State 94 | If you lose connection to the websocket server the icon in the top bar will turn red. To re-conenct, click the icon and 95 | click reconnect (you can specify a different address/port here too for the WS Server). 96 | 97 | Sometimes you need to force-push the state of the browser to all connected clients. For instance, if you load a set 98 | from a `.json` file on your desktop browser and want the state to be re synchronized on a tablet that is also connected 99 | you could click on the network icon in the menu bar, and click the 'Push State to Other Clients' button (or CTRL+P). 100 | 101 | The WebSocket Server also needs an up-to-date version of the state when you load a set (to render TidalCycles) 102 | so hitting CTRL+P after loading a set is common. 103 | 104 | ### Contributing and Support 105 | Please feel free to reach out to me at _[at]hotmail.com or make PRs! 106 | 107 | Happy to field feature requests and requests for support for new languages :) 108 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Utility for creating MIDI/OSC mappings 2 | - Inclusion of more languages (Gibber, Punctual, SuperCollider, others?) 3 | - Backend system to support a marketplace for sharing and importing stems 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deadcode", 3 | "version": "0.1.0", 4 | "private": false, 5 | "homepage": "http://jamiebeverley.github.io/DeadCode", 6 | "license": "GPLv3.0", 7 | "dependencies": { 8 | "@material-ui/core": "^4.3.2", 9 | "@material-ui/icons": "^4.2.1", 10 | "@testing-library/jest-dom": "^4.2.4", 11 | "@testing-library/react": "^9.3.2", 12 | "@testing-library/user-event": "^7.1.2", 13 | "child_process": "^1.0.2", 14 | "easymidi": "^1.0.3", 15 | "fs": "^0.0.1-security", 16 | "http": "^0.0.0", 17 | "https": "^1.0.0", 18 | "hydra-synth": "^1.0.25", 19 | "lodash": "^4.17.15", 20 | "nopt": "^4.0.1", 21 | "prompt-promise": "^1.0.3", 22 | "rc-slider": "^8.6.13", 23 | "react": "^16.9.0", 24 | "react-dom": "^16.9.0", 25 | "react-redux": "^7.1.0", 26 | "react-router-dom": "^5.0.1", 27 | "react-scripts": "^3.1.0", 28 | "redux": "^4.0.4", 29 | "redux-actions": "^2.6.5", 30 | "redux-dynamic-middlewares": "^1.0.0", 31 | "redux-logger": "^3.0.6", 32 | "ws": "^7.2.0" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "start-https": "HTTPS=true react-scripts start", 37 | "backend": "nodemon --exec babel-node src/backend/server.js", 38 | "midi": "nodemon --inspect --exec babel-node src/midi/index.js localhost 8080", 39 | "midi-util": "nodemon --inspect --exec babel-node src/midi/utility.js localhost 8081", 40 | "build": "react-scripts build", 41 | "predeploy": "yarn build", 42 | "deploy": "gh-pages -d build", 43 | "test": "react-scripts test --env=jsdom", 44 | "eject": "react-scripts eject" 45 | }, 46 | "eslintConfig": { 47 | "extends": "react-app" 48 | }, 49 | "browserslist": { 50 | "production": [ 51 | ">0.2%", 52 | "not dead", 53 | "not op_mini all" 54 | ], 55 | "development": [ 56 | "last 1 chrome version", 57 | "last 1 firefox version", 58 | "last 1 safari version" 59 | ] 60 | }, 61 | "devDependencies": { 62 | "@babel/core": "^7.7.5", 63 | "@babel/node": "^7.7.4", 64 | "@babel/preset-env": "^7.7.5", 65 | "gh-pages": "^2.2.0", 66 | "nodemon": "^2.0.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamieBeverley/DeadCode/7e85b066447683167d7bc23a728185699a3c9857/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamieBeverley/DeadCode/7e85b066447683167d7bc23a728185699a3c9857/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamieBeverley/DeadCode/7e85b066447683167d7bc23a728185699a3c9857/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/Connection.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO this is probably bad practice.... (?) 3 | // import {store} from "./index"; 4 | 5 | import store from './store' 6 | 7 | const Connection = {}; 8 | 9 | Connection.init = function(host=window.location.hostname,port=8000, onOpen=()=>{}, onClose=()=>{}, onErr=()=>{}){ 10 | if (Connection.ws){ 11 | Connection.ws.close(); 12 | } 13 | var WebSocket; 14 | if(typeof window!=='undefined'){ 15 | WebSocket = window.WebSocket || window.MozWebSocket; 16 | } else{ 17 | WebSocket = require('ws'); 18 | } 19 | 20 | try{ 21 | Connection.ws = new WebSocket('ws://' + host + ":" + port); 22 | Connection.ws.isOpen = false; 23 | Connection.ws.onopen = function open() { 24 | Connection.ws.isOpen = true; 25 | console.log('ws connection established'); 26 | onOpen(); 27 | } 28 | Connection.ws.onmessage = Connection.onMessage; 29 | 30 | Connection.ws.onerror = (e)=>{ 31 | console.warn(e); 32 | onErr(e); 33 | } 34 | Connection.ws.onclose = function incoming(data) { 35 | Connection.ws.isOpen = false; 36 | onClose(data); 37 | }; 38 | } catch (e) { 39 | onErr(); 40 | } 41 | }; 42 | 43 | Connection.onMessage = function(event){ 44 | let message = JSON.parse(event.data); 45 | if(message.type==='action'){ 46 | let action = message.action; 47 | action.meta = action.meta || {}; 48 | action.meta.fromServer = true; 49 | store.dispatch(action) 50 | } else{ 51 | console.warn('Unrecognized message type from WS server: '+message.type, message); 52 | } 53 | }; 54 | 55 | Connection.sendAction = function(action){ 56 | if(!Connection.ws || !Connection.ws.isOpen){ 57 | console.warn('Connection is closed'); 58 | return; 59 | } 60 | const data = {type:'action',action} 61 | Connection.ws.send(JSON.stringify(data)); 62 | } 63 | 64 | Connection.sendCode = function(code){ 65 | if(!Connection.ws || !Connection.ws.isOpen){ 66 | console.warn('Connection is closed'); 67 | return; 68 | } 69 | Connection.ws.send(JSON.stringify({type:'eval',code})); 70 | }; 71 | 72 | export default Connection -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import {createAction} from 'redux-actions' 2 | 3 | 4 | export const ActionSpec = { 5 | 'MIDI_UPDATE':{ 6 | name:'MIDI_UPDATE', 7 | propogateToServer:true 8 | }, 9 | 'PUSH_STATE':{ 10 | name: 'PUSH_STATE', 11 | propogateToServer:true 12 | }, 13 | 'RECEIVE_STATE':{ 14 | name: 'RECEIVE_STATE', 15 | propogateToServer:false 16 | }, 17 | 'CONNECT': { 18 | name: 'CONNECT', 19 | propogateToServer: false 20 | }, 21 | 'SAVE': { 22 | name: 'SAVE', 23 | propogateToServer: false 24 | }, 25 | 'LOAD': { 26 | name: 'LOAD', 27 | propogateToServer: false 28 | }, 29 | 'LOAD_FROM_SERVER': { 30 | name: 'LOAD_FROM_SERVER', 31 | propogateToServer: false 32 | }, 33 | 'DOWNLOAD': { 34 | name: 'DOWNLOAD', 35 | propogateToServer: false 36 | }, 37 | 'MASTER_UPDATE': { 38 | name: 'MASTER_UPDATE', 39 | propogateToServer: true 40 | }, 41 | 'MASTER_ADD_EFFECT': { 42 | name: 'MASTER_ADD_EFFECT', 43 | propogateToServer: true 44 | }, 45 | 'STEM_UPDATE': { 46 | name: 'STEM_UPDATE', 47 | propogateToServer: true 48 | }, 49 | 'STEM_DELETE_EFFECT': { 50 | name: 'STEM_DELETE_EFFECT', 51 | propogateToServer: true 52 | }, 53 | 'STEM_ADD_EFFECT': { 54 | name: 'STEM_ADD_EFFECT', 55 | propogateToServer: true 56 | }, 57 | 'STEM_COPY': { 58 | name: 'STEM_COPY', 59 | propogateToServer: true 60 | }, 61 | 'STEM_PASTE': { 62 | name: 'STEM_PASTE', 63 | propogateToServer: true 64 | }, 65 | 'TRACK_UPDATE': { 66 | name: 'TRACK_UPDATE', 67 | propogateToServer: true 68 | }, 69 | 'TRACK_DELETE_STEM': { 70 | name: 'TRACK_DELETE_STEM', 71 | propogateToServer: true 72 | }, 73 | 'TRACK_ADD_STEM': { 74 | name: 'TRACK_ADD_STEM', 75 | propogateToServer: true 76 | }, 77 | 'TRACK_DELETE_EFFECT': { 78 | name: 'TRACK_DELETE_EFFECT', 79 | propogateToServer: true 80 | }, 81 | 'TRACK_ADD_EFFECT': { 82 | name: 'TRACK_ADD_EFFECT', 83 | propogateToServer: true 84 | }, 85 | 'TRACK_ADD': { 86 | name: 'TRACK_ADD', 87 | propogateToServer: true 88 | }, 89 | 'TRACK_DELETE': { 90 | name: 'TRACK_DELETE', 91 | propogateToServer: true 92 | }, 93 | 'EFFECT_UPDATE': { 94 | name: 'EFFECT_UPDATE', 95 | propogateToServer: true 96 | }, 97 | 'SETTINGS_UPDATE_STYLE':{ 98 | name: 'SETTINGS_UPDATE_STYLE', 99 | propogateToServer:false 100 | } 101 | } 102 | 103 | function camel(capitalSnake) { 104 | let s = capitalSnake.split("_"); 105 | s = s.map(x => { 106 | return x[0] + x.slice(1, x.length).toLowerCase(); 107 | }); 108 | s[0] = s[0].toLowerCase(); 109 | return s.join(""); 110 | } 111 | 112 | export let ActionTypes = {}; 113 | export let Actions = {}; 114 | for (let action in ActionSpec) { 115 | let meta = {propogateToServer: ActionSpec[action].propogateToServer, fromServer:false}; 116 | Actions[camel(action)] = createAction(action, x => x, () => meta); 117 | ActionTypes[action] = action; 118 | } 119 | 120 | // 121 | // export const ActionTypes = [ 122 | // 'CONNECT', 123 | // 'SAVE', 124 | // 'LOAD', 125 | // 'DOWNLOAD', 126 | // 'MASTER_UPDATE', 127 | // 'MASTER_ADD_EFFECT', 128 | // 'STEM_UPDATE', 129 | // 'STEM_DELETE_EFFECT', 130 | // 'STEM_ADD_EFFECT', 131 | // 'STEM_COPY', 132 | // 'STEM_PASTE', 133 | // 'TRACK_UPDATE', 134 | // 'TRACK_DELETE_STEM', 135 | // 'TRACK_ADD_STEM', 136 | // 'TRACK_DELETE_EFFECT', 137 | // 'TRACK_ADD_EFFECT', 138 | // 'TRACK_ADD', 139 | // 'TRACK_DELETE', 140 | // 'EFFECT_UPDATE' 141 | // ] 142 | 143 | // const obj = {}; 144 | // ActionTypes.forEach(x => { 145 | // obj[camel(x)] = createAction(x) 146 | // }); 147 | // export const Actions = obj 148 | 149 | // // MASTER ACTIONS 150 | // Actions.CONNECT = function(url, port, isConnected){ 151 | // return {type:'CONNECT', url, port, isConnected} 152 | // }; 153 | // 154 | // // copy's given stems. if no argument copy's selected stems 155 | // Actions.COPY_STEMS = function(opt_stems){ 156 | // let stems = opt_stems; 157 | // if(!stems){ 158 | // let state = store.getState(); 159 | // stems = state.tracks.map(x=>x.stems).flat().filter(x=>x.selected); 160 | // } 161 | // return {type:"COPY_STEMS", stems}; 162 | // }; 163 | // 164 | // Actions.PASTE_STEMS = function(trackId, stemId){ 165 | // return {type:"PASTE_STEMS",trackId,stemId} 166 | // }; 167 | // 168 | // Actions.ADD_TRACK = function(){ 169 | // return {type:'ADD_TRACK'} 170 | // }; 171 | // 172 | // Actions.REMOVE_TRACK = function(trackId){ 173 | // return {type:'ADD_TRACK', trackId}; 174 | // }; 175 | // 176 | // Actions.ADD_STEM = function(trackId){ 177 | // return {type:'ADD_STEM',trackId}; 178 | // }; 179 | // 180 | // Actions.REMOVE_STEM = function(trackId, stemId){ 181 | // return {type:'REMOVE_STEM', trackId, stemId}; 182 | // }; 183 | // 184 | // Actions.UPDATE_STEM = function(id, value){ 185 | // return {type:'UPDATE_STEM', id, value} 186 | // }; 187 | // 188 | // Actions.ADD_STEM_EFFECT = function(trackId, stemId, effectType){ 189 | // return {type:'ADD_STEM_EFFECT', trackId, stemId, effectType} 190 | // } 191 | // 192 | // Actions.UPDATE_TRACK = function(value){ 193 | // return {type:'UPDATE_TRACK', value} 194 | // }; 195 | // 196 | // Actions.UPDATE_MASTER = function(language,value){ 197 | // return {type:'UPDATE_MASTER', language, value} 198 | // } 199 | // 200 | // Actions.UPDATE_MASTER_EFFECT = function(effect){ 201 | // return {type:'UPDATE_MASTER_EFFECT', effect} 202 | // } 203 | // 204 | // Actions.UPDATE_EFFECT = function (effectId, effect){ 205 | // return {type: "UPDATE_EFFECT", id:effectId, value:effect} 206 | // } 207 | // 208 | // // Saving / Loading 209 | // Actions.SAVE = function(){ 210 | // return {type:'SAVE'} 211 | // }; 212 | // 213 | // Actions.LOAD = function(newState){ 214 | // return {type:'LOAD',newState} 215 | // }; 216 | // 217 | // Actions.DOWNLOAD = function(){ 218 | // return {type:'DOWNLOAD'} 219 | // }; 220 | // 221 | // ActionTypes = {}; 222 | // Object.keys(Actions).forEach(x=>{ 223 | // ActionTypes[x] = x; 224 | // }); 225 | // 226 | // export default Actions 227 | -------------------------------------------------------------------------------- /src/assets/3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamieBeverley/DeadCode/7e85b066447683167d7bc23a728185699a3c9857/src/assets/3.PNG -------------------------------------------------------------------------------- /src/assets/Capture.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamieBeverley/DeadCode/7e85b066447683167d7bc23a728185699a3c9857/src/assets/Capture.PNG -------------------------------------------------------------------------------- /src/assets/Capture2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamieBeverley/DeadCode/7e85b066447683167d7bc23a728185699a3c9857/src/assets/Capture2.PNG -------------------------------------------------------------------------------- /src/assets/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamieBeverley/DeadCode/7e85b066447683167d7bc23a728185699a3c9857/src/assets/facebook.png -------------------------------------------------------------------------------- /src/assets/hyperobject.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JamieBeverley/DeadCode/7e85b066447683167d7bc23a728185699a3c9857/src/assets/hyperobject.jpg -------------------------------------------------------------------------------- /src/backend/BootTidal.hs: -------------------------------------------------------------------------------- 1 | :set -XOverloadedStrings 2 | :set prompt "" 3 | :set prompt-cont "" 4 | 5 | import Sound.Tidal.Context 6 | 7 | -- total latency = oLatency + cFrameTimespan 8 | tidal <- startTidal (superdirtTarget {oLatency = 0.1, oAddress = "127.0.0.1", oPort = 57120}) (defaultConfig {cFrameTimespan = 1/20}) 9 | 10 | :{ 11 | let p = streamReplace tidal 12 | hush = streamHush tidal 13 | list = streamList tidal 14 | mute = streamMute tidal 15 | unmute = streamUnmute tidal 16 | solo = streamSolo tidal 17 | unsolo = streamUnsolo tidal 18 | once = streamOnce tidal 19 | asap = once 20 | nudgeAll = streamNudgeAll tidal 21 | all = streamAll tidal 22 | resetCycles = streamResetCycles tidal 23 | setcps = asap . cps 24 | xfade i = transition tidal True (Sound.Tidal.Transition.xfadeIn 4) i 25 | xfadeIn i t = transition tidal True (Sound.Tidal.Transition.xfadeIn t) i 26 | histpan i t = transition tidal True (Sound.Tidal.Transition.histpan t) i 27 | wait i t = transition tidal True (Sound.Tidal.Transition.wait t) i 28 | waitT i f t = transition tidal True (Sound.Tidal.Transition.waitT f t) i 29 | jump i = transition tidal True (Sound.Tidal.Transition.jump) i 30 | jumpIn i t = transition tidal True (Sound.Tidal.Transition.jumpIn t) i 31 | jumpIn' i t = transition tidal True (Sound.Tidal.Transition.jumpIn' t) i 32 | jumpMod i t = transition tidal True (Sound.Tidal.Transition.jumpMod t) i 33 | mortal i lifespan release = transition tidal True (Sound.Tidal.Transition.mortal lifespan release) i 34 | interpolate i = transition tidal True (Sound.Tidal.Transition.interpolate) i 35 | interpolateIn i t = transition tidal True (Sound.Tidal.Transition.interpolateIn t) i 36 | clutch i = transition tidal True (Sound.Tidal.Transition.clutch) i 37 | clutchIn i t = transition tidal True (Sound.Tidal.Transition.clutchIn t) i 38 | anticipate i = transition tidal True (Sound.Tidal.Transition.anticipate) i 39 | anticipateIn i t = transition tidal True (Sound.Tidal.Transition.anticipateIn t) i 40 | forId i t = transition tidal False (Sound.Tidal.Transition.mortalOverlay t) i 41 | d1 = p 1 . (|< orbit 0) 42 | d2 = p 2 . (|< orbit 1) 43 | d3 = p 3 . (|< orbit 2) 44 | d4 = p 4 . (|< orbit 3) 45 | d5 = p 5 . (|< orbit 4) 46 | d6 = p 6 . (|< orbit 5) 47 | d7 = p 7 . (|< orbit 6) 48 | d8 = p 8 . (|< orbit 7) 49 | d9 = p 9 . (|< orbit 8) 50 | d10 = p 10 . (|< orbit 9) 51 | d11 = p 11 . (|< orbit 10) 52 | d12 = p 12 . (|< orbit 11) 53 | d13 = p 13 54 | d14 = p 14 55 | d15 = p 15 56 | d16 = p 16 57 | :} 58 | 59 | :{ 60 | let setI = streamSetI tidal 61 | setF = streamSetF tidal 62 | setS = streamSetS tidal 63 | setR = streamSetI tidal 64 | setB = streamSetB tidal 65 | :} 66 | 67 | :set prompt "tidal> " 68 | -------------------------------------------------------------------------------- /src/backend/Client.js: -------------------------------------------------------------------------------- 1 | var id = 0; 2 | 3 | export default class Client { 4 | constructor(ws){ 5 | this.ws = ws; 6 | this.id = id++; 7 | return this; 8 | } 9 | 10 | 11 | // onMessage(data){ 12 | // let msg = JSON.parse(data); 13 | // if(msg.type === "eval"){ 14 | // tidal.stdin.write(msg.code+"\n"); 15 | // stderr.write(msg.code+"\n"); 16 | // } else if (msg.type === 'action'){ 17 | // console.log('action received: ', JSON.stringify(msg.action)); 18 | // broadcast(msg,[]); 19 | // } else { 20 | // console.warn('hmm'); 21 | // } 22 | // } 23 | 24 | 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/backend/server.js: -------------------------------------------------------------------------------- 1 | import {applyMiddleware, createStore} from "redux"; 2 | import logger from 'redux-logger'; 3 | import Client from "./Client"; 4 | import DeadReducer from '../reducers' 5 | import {ActionSpec,Actions} from "../actions"; 6 | import {Renderers} from "../renderers"; 7 | const spawn = require('child_process').spawn; 8 | const http = require('http'); 9 | const WebSocket = require('ws'); 10 | const fs = require('fs'); 11 | const server = http.createServer(); 12 | const wss = new WebSocket.Server({ server }); 13 | import {throttle} from 'lodash'; 14 | // let store = createStore(DeadReducer, applyMiddleware(serverMiddleware,renderMiddleWare, logger)); 15 | 16 | 17 | const serverMiddleware = store => next => action => { 18 | if(action.type === ActionSpec.LOAD_FROM_SERVER.name){ 19 | let state = {...store.getState(), connection:null}; 20 | let msg = {type:'action',action:Actions.receiveState(state)}; 21 | broadcast(msg) 22 | } 23 | return next(action); 24 | } 25 | 26 | let tidalCode =''; 27 | const renderMiddleWare = store => next => action => { 28 | next(action); 29 | let state = store.getState(); 30 | if(action.type === ActionSpec.MASTER_UPDATE.name || action.type === ActionSpec.PUSH_STATE.name){ 31 | evalTidal(state.master.TidalCycles.macros); 32 | evalTidal(Renderers.TidalCycles.getTempoCode(state)); 33 | } 34 | let tc = Renderers.TidalCycles.getCode(state); 35 | if(tidalCode!==tc){ 36 | evalTidal(tc); 37 | tidalCode = tc; 38 | } 39 | } 40 | 41 | let store = createStore(DeadReducer, applyMiddleware(serverMiddleware,renderMiddleWare)); 42 | 43 | 44 | 45 | // Cmdline opts 46 | var nopt = require('nopt'); 47 | var path = require('path'); 48 | var knownOpts = {'bootTidal':path}; 49 | var parsed = nopt(knownOpts, {},process.argv); 50 | 51 | 52 | // Tidal bootscript path 53 | // const homedir = require('os').homedir(); 54 | var bootTidal = parsed.bootTidal || "./src/backend/BootTidal.hs"; 55 | 56 | 57 | // var output = process.stdout; 58 | // var stdin = process.stdin; 59 | var stderr = process.stderr; 60 | var defaultFeedbackFunction = function(x) { 61 | stderr.write(x); 62 | } 63 | 64 | var tidal = spawn('ghci', ['-XOverloadedStrings']); 65 | // var bootTidal = "C:\\Users\\jamie\\.atom\\packages\\tidalcycles\\lib\\BootTidal.hs" 66 | console.log(bootTidal) 67 | tidal.on('close', function (code) { 68 | stderr.write('Tidal process exited with code ' + code + "\n"); 69 | }); 70 | 71 | tidal.stderr.addListener("data", function(m) { 72 | defaultFeedbackFunction(m.toString()); 73 | }); 74 | 75 | tidal.stdout.addListener("data", function(m) { 76 | defaultFeedbackFunction(m.toString()); 77 | }); 78 | 79 | 80 | fs.readFile(bootTidal,'utf8', function (err,data) { 81 | if (err) { console.log(err+"\n"); return; } 82 | tidal.stdin .write(data); 83 | console.log("Tidal/GHCI initialized\n"); 84 | }); 85 | 86 | function sanitizeStringForTidal(x) { 87 | var lines = x.split("\n"); 88 | var result = ""; 89 | var blockOpen = false; 90 | for(var n in lines) { 91 | var line = lines[n]; 92 | var startsWithSpace = false; 93 | if(line[0] == " " || line[0] == "\t") startsWithSpace = true; 94 | if(blockOpen == false) { 95 | blockOpen = true; 96 | result = result + ":{\n" + line + "\n"; 97 | } 98 | else if(startsWithSpace == false) { 99 | result = result + ":}\n:{\n" + line + "\n"; 100 | } 101 | else if(startsWithSpace == true) { 102 | result = result + line + "\n"; 103 | } 104 | } 105 | if(blockOpen == true) { 106 | result = result + ":}\n"; 107 | blockOpen = false; 108 | } 109 | return result; 110 | } 111 | 112 | function evalTidal(str){ 113 | tidal.stdin.write(str+"\n"); 114 | stderr.write(str+"\n"); 115 | } 116 | 117 | 118 | const clients = {}; 119 | 120 | function broadcast (msg, exclude=[]){ 121 | exclude = exclude.map(String); 122 | Object.keys(clients).filter(x=>{return !exclude.includes(x)}).forEach(x=>{ 123 | let ws = clients[x].ws; 124 | if(ws.readyState === WebSocket.OPEN){ 125 | ws.send(JSON.stringify(msg)); 126 | } 127 | }) 128 | } 129 | 130 | const effectThrottles = {}; 131 | 132 | const throttledBroadcast = throttle(broadcast,200); 133 | 134 | function onMessage(data){ 135 | var msg = JSON.parse(data); 136 | if(msg.type=="eval"){ 137 | tidal.stdin.write(msg.code+"\n"); 138 | stderr.write(msg.code+"\n"); 139 | } else if (msg.type === 'action'){ 140 | store.dispatch(msg.action); 141 | if(msg.action.type ==='EFFECT_UPDATE'){ 142 | // console.log(msg.action.payload.effectId, JSON.stringify(msg.action.payload)); 143 | effectThrottles[msg.action.payload.effectId] = effectThrottles[msg.action.payload.effectId] || throttle(broadcast,200); 144 | effectThrottles[msg.action.payload.effectId](msg,[this.id]); 145 | } else{ 146 | broadcast(msg,[this.id]); 147 | } 148 | } else { 149 | console.warn('unrecognized ws message type: ',msg.type,JSON.stringify(data)); 150 | } 151 | } 152 | 153 | function onClose(id){ 154 | console.log('closed connection with client', id, new Date()); 155 | delete clients[id]; 156 | } 157 | 158 | 159 | wss.on('connection', function connection(ws) { 160 | let client = new Client(ws); 161 | clients[client.id] = client; 162 | console.log('connected client ', client.id, new Date()); 163 | client.ws.on('message',onMessage.bind(client)) 164 | client.ws.on('close', ()=>{onClose(client.id)}); 165 | }); 166 | 167 | server.listen(8001); 168 | console.log('listening...\n') 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | // 187 | -------------------------------------------------------------------------------- /src/components/App/index.css: -------------------------------------------------------------------------------- 1 | /*:root {*/ 2 | /* --launchspace-bg-color: rgb(0,0,0);*/ 3 | /* --border-radius:5px;*/ 4 | /* --stem-on: rgb(235,255,200);*/ 5 | /* --stem-off: grey;*/ 6 | /* --stem-editor-color:lightgrey;*/ 7 | /* --font-family: 'Helventica', sans-serif;*/ 8 | /* --scrollbar-width: 8px;*/ 9 | /*}*/ 10 | 11 | /* light teal */ 12 | /*:root {*/ 13 | /* --bg-image: linear-gradient(to right, white ,white);*/ 14 | /* --launchspace-bg-color: transparent;*/ 15 | /* --border-radius:8px;*/ 16 | /* --stem-on: rgb(100,255,200);*/ 17 | /* --stem-off: lightgrey;*/ 18 | /* --stem-editor-color:rgba(120,120,120,0.5);*/ 19 | /* --font-family: 'Helventica', sans-serif;*/ 20 | /* --font-color-dark: black;*/ 21 | /* --font-color-light: rgb(30,30,30);*/ 22 | /* --scrollbar-width: 0px;*/ 23 | /* --track-border-color: rgb(215,215,215);*/ 24 | /* --select-color:rgb(100,100,100);*/ 25 | /*}*/ 26 | 27 | 28 | :root { 29 | --bg-image:radial-gradient(rgb(170,170,170), rgb(170,170,170)); 30 | --launchspace-bg-color: transparent; 31 | --border-radius: 0px; 32 | --stem-on: rgb(0,255,240); 33 | --stem-off: rgba(230,230,230); 34 | --stem-editor-color: rgba(200, 200, 200, 0.5); 35 | --font-family: 'Helventica', sans-serif; 36 | --font-color-dark: black; 37 | --font-color-light: rgb(30, 30, 30); 38 | --scrollbar-width: 0px; 39 | --track-border-color: lightgrey; 40 | --select-color: rgb(250,10,10); 41 | --settings-background-color:rgb(50,50,50); 42 | --settings-font-color:rgb(150,150,150); 43 | --midi-background-color:rgba(255,255,255,0.600); 44 | --midi-background-color:rgba(100,100,100,0.600); 45 | } 46 | 47 | 48 | 49 | 50 | /*:root {*/ 51 | /* --bg-image:radial-gradient(rgb(100,100,100), rgb(100,100,100));*/ 52 | /* --launchspace-bg-color: transparent;*/ 53 | /* --border-radius: 0px;*/ 54 | /* --stem-on: rgb(250,250,20);*/ 55 | /* --stem-off: rgba(255,255,255,0.5);*/ 56 | /* --stem-editor-color: rgba(220, 220, 220, 0.5);*/ 57 | /* --font-family: 'Helventica', sans-serif;*/ 58 | /* --font-color-dark: black;*/ 59 | /* --font-color-light: rgb(30, 30, 30);*/ 60 | /* --scrollbar-width: 0px;*/ 61 | /* --track-border-color: lightgrey;*/ 62 | /* --select-color: rgb(250,10,10);*/ 63 | /* --settings-background-color:rgb(50,50,50);*/ 64 | /* --settings-font-color:rgb(150,150,150);*/ 65 | /* --midi-background-color:rgba(255,255,255,0.15)*/ 66 | /*}*/ 67 | 68 | 69 | 70 | /*:root {*/ 71 | /* --bg-image:radial-gradient(rgb(10,10,10), rgb(10,10,10));*/ 72 | /* --launchspace-bg-color: transparent;*/ 73 | /* --border-radius: 2px;*/ 74 | /* --stem-on: rgb(205,20,20);*/ 75 | /* --stem-off: rgba(255,255,255,0.15);*/ 76 | /* --stem-editor-color: rgba(220, 220, 220, 0.5);*/ 77 | /* --font-family: 'Helventica', sans-serif;*/ 78 | /* --font-color-dark: black;*/ 79 | /* --font-color-light: rgb(30, 30, 30);*/ 80 | /* --scrollbar-width: 0px;*/ 81 | /* --track-border-color: transparent;*/ 82 | /* --select-color: rgb(250,10,10);*/ 83 | /* --settings-background-color:rgb(30,30,30);*/ 84 | /* --settings-font-color:rgb(150,150,150);*/ 85 | /* --midi-background-color:rgba(255,255,255,0.15)*/ 86 | /*}*/ 87 | 88 | /*!* img purp *!*/ 89 | /*:root {*/ 90 | /* --bg-image: url(../../assets/3.PNG);*/ 91 | /* --launchspace-bg-color: transparent;*/ 92 | /* --border-radius:8px;*/ 93 | /* --stem-on: rgb(100,30,150);*/ 94 | /* --stem-off: rgba(70,70,70,0.6);*/ 95 | /* --stem-editor-color: rgba(30,30,30,0.9);*/ 96 | /* --font-family: 'Helventica', sans-serif;*/ 97 | /* --font-color-dark: rgb(200,200,200);*/ 98 | /* --font-color-light: lightgrey;*/ 99 | /* --scrollbar-width: 0px;*/ 100 | /* --track-border-color: rgb(40,40,40);*/ 101 | /* --select-color: white;*/ 102 | /*--midi-background-color:rgba(255,255,255,0.15)*/ 103 | /*}*/ 104 | 105 | 106 | /*dark purp */ 107 | /*:root {*/ 108 | /* --bg-image: linear-gradient(to right, black , blk.fack, black, rgb(100,30,150));*/ 109 | /* --launchspace-bg-color: transparent;*/ 110 | /* --border-radius:8px;*/ 111 | /* --stem-on: rgb(100,30,150);*/ 112 | /* --stem-off: rgb(70,70,70);*/ 113 | /* --stem-editor-color: rgb(30,30,30);*/ 114 | /* --font-family: 'Helventica', sans-serif;*/ 115 | /* --font-color-dark: rgb(200,200,200);*/ 116 | /* --font-color-light: rgb(white);*/ 117 | /* --scrollbar-width: 0px;*/ 118 | /* --track-border-color: rgb(40,40,40);*/ 119 | /* --select-color: rgb(200,200,200);*/ 120 | /*}*/ 121 | 122 | #rightPanel{ 123 | box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 3px -2px, rgba(0, 0, 0, 0.14) 0px 3px 4px 0px, rgba(0, 0, 0, 0.12) 0px 1px 8px 0px; 124 | display:flex; 125 | flex-flow: column; 126 | resize: horizontal; 127 | } 128 | 129 | 130 | 131 | 132 | #root{ 133 | width:100%; 134 | height:100%; 135 | } 136 | 137 | .App{ 138 | height:100%; 139 | position: absolute; 140 | width:100%; 141 | overflow: hidden; 142 | display: flex; 143 | flex-flow: column; 144 | background-color: transparent; 145 | } 146 | .App > div{ 147 | display:flex; 148 | width:100%; 149 | } 150 | 151 | .App:focus{ 152 | outline: none; 153 | } 154 | 155 | html{ 156 | width:100%; 157 | height:100%; 158 | background-color: red; 159 | margin:0px; 160 | } 161 | 162 | body{ 163 | width:100%; 164 | height:100%; 165 | background-color: black; 166 | margin:0px; 167 | background-image: var(--bg-image); 168 | background-position: center; /* Center the image */ 169 | background-repeat: no-repeat; /* Do not repeat the image */ 170 | background-size: cover; /* Resize the background image to cover the entire container */ 171 | } 172 | 173 | * { 174 | font-family: var(--font-family); 175 | } 176 | 177 | button{ 178 | border-radius: var(--border-radius); 179 | border:1pt solid var(--stem-off); 180 | padding:5px; 181 | /*border:1pt solid var(--font-color-light);*/ 182 | color:var(--font-color-light); 183 | background-color: var(--stem-off); 184 | box-sizing: border-box; 185 | } 186 | 187 | 188 | 189 | input{ 190 | color:var(--font-color-light); 191 | border-radius: var(--border-radius); 192 | border: 1pt solid var(--stem-off); 193 | padding:5px; 194 | } 195 | 196 | select{ 197 | background-color: var(--stem-off); 198 | color: var(--font-color-light); 199 | border-radius: var(--border-radius); 200 | border:1pt solid var(--stem-off); 201 | padding:5px; 202 | } 203 | 204 | ::-webkit-scrollbar { 205 | width: var(--scrollbar-width); 206 | } 207 | 208 | /* Track */ 209 | ::-webkit-scrollbar-track { 210 | /*border-radius: 10px;*/ 211 | } 212 | 213 | /* Handle */ 214 | ::-webkit-scrollbar-thumb { 215 | background: transparent; 216 | /*border-radius: 10px;*/ 217 | } 218 | 219 | /*!* Handle on hover *!*/ 220 | /*::-webkit-scrollbar-thumb:hover {*/ 221 | /* background: rgb(200,200,200);*/ 222 | /*}*/ 223 | 224 | .verticalCenter{ 225 | top:50%; 226 | position:relative; 227 | transform: translateY(-50%); 228 | } 229 | 230 | .noselect { 231 | -webkit-touch-callout: none; /* iOS Safari */ 232 | -webkit-user-select: none; /* Safari */ 233 | -khtml-user-select: none; /* Konqueror HTML */ 234 | -moz-user-select: none; /* Firefox */ 235 | -ms-user-select: none; /* Internet Explorer/Edge */ 236 | user-select: none; /* Non-prefixed version, currently 237 | supported by Chrome and Opera */ 238 | } 239 | 240 | 241 | 242 | /* probably another way I should do this...*/ 243 | .MuiSwitch-thumb { 244 | background-color: var(--stem-on)!important; 245 | } 246 | 247 | .Mui-checked + .MuiSwitch-track { 248 | background-color: var(--stem-on)!important; 249 | } 250 | 251 | input:focus{ 252 | outline:none; 253 | } 254 | 255 | select:focus{ 256 | outline:none; 257 | } 258 | 259 | -------------------------------------------------------------------------------- /src/components/App/index.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import LaunchSpace from '../../containers/LaunchSpace.jsx'; 3 | import Flyout from '../../containers/Flyout.jsx'; 4 | import './index.css'; 5 | import Header from '../../containers/Header' 6 | 7 | // import ResizeDivider from "../ResizeDivider"; 8 | 9 | export default class App extends Component { 10 | 11 | constructor(props) { 12 | super(props) 13 | this.state = { 14 | divider: 70, //% 15 | horizontalDivider: 75 16 | } 17 | this.appRef = React.createRef() 18 | } 19 | 20 | componentDidMount() { 21 | this.props.globalActions.connect(window.location.hostname, this.props.connection.port); 22 | } 23 | 24 | render() { 25 | return ( 26 |
{ 28 | e.preventDefault() 29 | }} 30 | ref={this.appRef} className='App' tabIndex="0" onKeyDown={this.macros.bind(this)}> 31 |
32 |
33 | 34 |
35 | 36 |