diff --git a/package.json b/package.json index d5b05b928bf39b4fb98c1e958b535602211ff3b0..a094b12769ac3b837223a6f888767629e91660a0 100644 --- a/package.json +++ b/package.json @@ -30,15 +30,18 @@ "react-dom": "15.x.x" }, "devDependencies": { + "autoprefixer": "6.5.3", "babel-core": "6.14.0", "babel-eslint": "7.0.0", "babel-loader": "6.2.5", "babel-plugin-transform-object-rest-spread": "6.16.0", "babel-preset-es2015": "6.14.0", "babel-preset-react": "6.11.1", + "classnames": "2.2.5", "copy-webpack-plugin": "3.0.1", + "css-loader": "0.26.1", "eslint": "3.8.1", - "eslint-config-scratch": "^2.0.0", + "eslint-config-scratch": "^3.0.0", "eslint-plugin-react": "6.4.1", "gh-pages": "0.11.0", "html-webpack-plugin": "2.22.0", @@ -48,17 +51,22 @@ "lodash.defaultsdeep": "4.4.0", "minilog": "3.0.1", "opt-cli": "1.5.1", + "postcss-loader": "1.2.0", "react": "15.3.2", "react-dom": "15.3.2", "react-modal": "1.5.2", + "react-redux": "4.4.6", "react-style-proptype": "1.2.0", + "redux": "3.6.0", "scratch-blocks": "latest", "scratch-render": "latest", "scratch-vm": "latest", + "style-loader": "0.13.1", "svg-to-image": "1.1.3", "svg-url-loader": "1.1.0", "travis-after-all": "1.4.4", "webpack": "1.13.2", + "webpack-combine-loaders": "2.0.3", "webpack-dev-server": "1.15.2", "xhr": "2.2.2" } diff --git a/src/.eslintrc.js b/src/.eslintrc.js index 77bf09f3fe1d42f5d7249ec84af903ec8670c28a..0abd74d675fae217d8745dcba17a049e96cc5fa5 100644 --- a/src/.eslintrc.js +++ b/src/.eslintrc.js @@ -1,9 +1,9 @@ module.exports = { + root: true, + extends: ['scratch', 'scratch/es6', 'scratch/react'], env: { - node: false, browser: true }, - extends: ['scratch/es6', 'scratch/react'], globals: { process: true } diff --git a/src/components/blocks/blocks.css b/src/components/blocks/blocks.css new file mode 100644 index 0000000000000000000000000000000000000000..3d00662496134202e23248933958e7abf6481746 --- /dev/null +++ b/src/components/blocks/blocks.css @@ -0,0 +1,7 @@ +.blocks { + position: absolute; + top: 40px; + right: 500px; + bottom: 0; + left: 0; +} diff --git a/src/components/blocks.jsx b/src/components/blocks/blocks.jsx similarity index 62% rename from src/components/blocks.jsx rename to src/components/blocks/blocks.jsx index 8e1b9a19a916cdcec349b2fb7fa7d5abeb05fa4a..fa00102abe51ca1f8437caa83d5c9619e2789e85 100644 --- a/src/components/blocks.jsx +++ b/src/components/blocks/blocks.jsx @@ -1,5 +1,7 @@ const React = require('react'); +const styles = require('./blocks.css'); + class BlocksComponent extends React.Component { render () { const { @@ -8,15 +10,8 @@ class BlocksComponent extends React.Component { } = this.props; return ( <div - className="scratch-blocks" + className={styles.blocks} ref={componentRef} - style={{ - position: 'absolute', - top: 0, - right: 500, - bottom: 0, - left: 0 - }} {...props} /> ); diff --git a/src/components/costume-canvas.jsx b/src/components/costume-canvas/costume-canvas.jsx similarity index 100% rename from src/components/costume-canvas.jsx rename to src/components/costume-canvas/costume-canvas.jsx diff --git a/src/components/green-flag/green-flag.css b/src/components/green-flag/green-flag.css new file mode 100644 index 0000000000000000000000000000000000000000..b8000e48277db1b70c458db492e1ff21df36d1af --- /dev/null +++ b/src/components/green-flag/green-flag.css @@ -0,0 +1,11 @@ +.green-flag { + position: absolute; + top: 8px; + right: calc(480px - 16px); + width: 16px; + height: 16px; +} + +.active { + filter: saturate(200%) brightness(150%); +} diff --git a/src/components/green-flag/green-flag.jsx b/src/components/green-flag/green-flag.jsx index 8e0c12d593cef9bf4113a2907f89931962a07ce1..f1b24b1516471c1cd650181c008df3a3b180c9d8 100644 --- a/src/components/green-flag/green-flag.jsx +++ b/src/components/green-flag/green-flag.jsx @@ -1,5 +1,8 @@ +const classNames = require('classnames'); const React = require('react'); + const greenFlagIcon = require('./green-flag.svg'); +const styles = require('./green-flag.css'); const GreenFlagComponent = function (props) { const { @@ -10,16 +13,11 @@ const GreenFlagComponent = function (props) { } = props; return ( <img - className="scratch-green-flag" + className={classNames({ + [styles.greenFlag]: true, + [styles.active]: active + })} src={greenFlagIcon} - style={{ - position: 'absolute', - top: 380, - right: 440, - width: 50, - // @todo Get real design here - filter: active ? 'saturate(200%) brightness(150%)' : 'none' - }} title={title} onClick={onClick} {...componentProps} diff --git a/src/components/gui.jsx b/src/components/gui.jsx deleted file mode 100644 index 568d2e169e870ef21f690265f97db58796dfd3b2..0000000000000000000000000000000000000000 --- a/src/components/gui.jsx +++ /dev/null @@ -1,29 +0,0 @@ -const React = require('react'); - -const GUIComponent = function (props) { - const { - children, - ...componentProps - } = props; - return ( - <div - className="scratch-gui" - style={{ - position: 'absolute', - top: 0, - right: 0, - bottom: 0, - left: 0 - }} - {...componentProps} - > - {children} - </div> - ); -}; - -GUIComponent.propTypes = { - children: React.PropTypes.node -}; - -module.exports = GUIComponent; diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css new file mode 100644 index 0000000000000000000000000000000000000000..78b6f009ac5791ea7b1e1ec0f40a635f68193d0d --- /dev/null +++ b/src/components/gui/gui.css @@ -0,0 +1,7 @@ +.gui { + position: absolute; + top: 0; + right: 4px; + bottom: 0; + left: 4px; +} diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx new file mode 100644 index 0000000000000000000000000000000000000000..31e5e6ae2d0e4d6a21c8bc9cf6d7ee985e647f8e --- /dev/null +++ b/src/components/gui/gui.jsx @@ -0,0 +1,90 @@ +const defaultsDeep = require('lodash.defaultsdeep'); +const React = require('react'); +const VM = require('scratch-vm'); + +const MediaLibrary = require('../../lib/media-library'); +const shapeFromPropTypes = require('../../lib/shape-from-prop-types'); + +const Blocks = require('../../containers/blocks.jsx'); +const GreenFlag = require('../../containers/green-flag.jsx'); +const TargetPane = require('../../containers/target-pane.jsx'); +const Stage = require('../../containers/stage.jsx'); +const StopAll = require('../../containers/stop-all.jsx'); + +const styles = require('./gui.css'); + +const GUIComponent = props => { + let { + basePath, + blocksProps, + children, + greenFlagProps, + mediaLibrary, + targetPaneProps, + stageProps, + stopAllProps, + vm + } = props; + blocksProps = defaultsDeep({}, blocksProps, { + options: { + media: `${basePath}static/blocks-media/` + } + }); + if (children) { + return ( + <div className={styles.gui}> + {children} + </div> + ); + } + return ( + <div className={styles.gui}> + <GreenFlag + vm={vm} + {...greenFlagProps} + /> + <StopAll + vm={vm} + {...stopAllProps} + /> + <Stage + vm={vm} + {...stageProps} + /> + <TargetPane + mediaLibrary={mediaLibrary} + vm={vm} + {...targetPaneProps} + /> + <Blocks + vm={vm} + {...blocksProps} + /> + </div> + ); +}; + +GUIComponent.propTypes = { + basePath: React.PropTypes.string, + blocksProps: shapeFromPropTypes(Blocks.propTypes, {omit: ['vm']}), + children: React.PropTypes.node, + greenFlagProps: shapeFromPropTypes(GreenFlag.propTypes, {omit: ['vm']}), + mediaLibrary: React.PropTypes.instanceOf(MediaLibrary), + stageProps: shapeFromPropTypes(Stage.propTypes, {omit: ['vm']}), + stopAllProps: shapeFromPropTypes(StopAll.propTypes, {omit: ['vm']}), + targetPaneProps: shapeFromPropTypes(TargetPane.propTypes, {omit: ['vm']}), + vm: React.PropTypes.instanceOf(VM) +}; + +GUIComponent.defaultProps = { + basePath: '/', + blocksProps: {}, + greenFlagProps: {}, + mediaLibrary: new MediaLibrary(), + targetPaneProps: {}, + stageProps: {}, + stopAllProps: {}, + vm: new VM() +}; + +module.exports = GUIComponent; diff --git a/src/components/library-item.jsx b/src/components/library-item.jsx deleted file mode 100644 index 307bd97866f82eb396904acabc2940d78a014f12..0000000000000000000000000000000000000000 --- a/src/components/library-item.jsx +++ /dev/null @@ -1,62 +0,0 @@ -const bindAll = require('lodash.bindall'); -const React = require('react'); -const stylePropType = require('react-style-proptype'); - -const CostumeCanvas = require('./costume-canvas.jsx'); - -class LibraryItem extends React.Component { - constructor (props) { - super(props); - bindAll(this, ['handleClick']); - } - handleClick (e) { - this.props.onSelect(this.props.id); - e.preventDefault(); - } - render () { - const style = (this.props.selected) ? - this.props.selectedGridTileStyle : this.props.gridTileStyle; - return ( - <div - style={style} - onClick={this.handleClick} - > - <CostumeCanvas url={this.props.iconURL} /> - <p>{this.props.name}</p> - </div> - ); - } -} - -LibraryItem.defaultProps = { - gridTileStyle: { - float: 'left', - width: '140px', - marginLeft: '5px', - marginRight: '5px', - textAlign: 'center', - cursor: 'pointer' - }, - selectedGridTileStyle: { - float: 'left', - width: '140px', - marginLeft: '5px', - marginRight: '5px', - textAlign: 'center', - cursor: 'pointer', - background: '#aaa', - borderRadius: '6px' - } -}; - -LibraryItem.propTypes = { - gridTileStyle: stylePropType, - iconURL: React.PropTypes.string, - id: React.PropTypes.number, - name: React.PropTypes.string, - onSelect: React.PropTypes.func, - selected: React.PropTypes.bool, - selectedGridTileStyle: stylePropType -}; - -module.exports = LibraryItem; diff --git a/src/components/library-item/library-item.css b/src/components/library-item/library-item.css new file mode 100644 index 0000000000000000000000000000000000000000..ab3342835774064b2e75168829829eea6f711dfd --- /dev/null +++ b/src/components/library-item/library-item.css @@ -0,0 +1,12 @@ +.library-item { + float: left; + width: 140px; + margin-left: 5px; + margin-right: 5px; + text-align: center; + cursor: pointer; +} +.library-item.is-selected { + background: #aaa; + border-radius: 6px; +} diff --git a/src/components/library-item/library-item.jsx b/src/components/library-item/library-item.jsx new file mode 100644 index 0000000000000000000000000000000000000000..641f989da0ec694391014500cc99a30079ccb7d6 --- /dev/null +++ b/src/components/library-item/library-item.jsx @@ -0,0 +1,41 @@ +const classNames = require('classnames'); +const bindAll = require('lodash.bindall'); +const React = require('react'); + +const CostumeCanvas = require('../costume-canvas/costume-canvas.jsx'); +const styles = require('./library-item.css'); + +class LibraryItem extends React.Component { + constructor (props) { + super(props); + bindAll(this, ['handleClick']); + } + handleClick (e) { + this.props.onSelect(this.props.id); + e.preventDefault(); + } + render () { + return ( + <div + className={classNames({ + [styles.libraryItem]: true, + [styles.isSelected]: this.props.selected + })} + onClick={this.handleClick} + > + <CostumeCanvas url={this.props.iconURL} /> + <p>{this.props.name}</p> + </div> + ); + } +} + +LibraryItem.propTypes = { + iconURL: React.PropTypes.string, + id: React.PropTypes.number, + name: React.PropTypes.string, + onSelect: React.PropTypes.func, + selected: React.PropTypes.bool +}; + +module.exports = LibraryItem; diff --git a/src/components/library/library.css b/src/components/library/library.css new file mode 100644 index 0000000000000000000000000000000000000000..02101f223974d00a9054bf5a83cd342ab08010ad --- /dev/null +++ b/src/components/library/library.css @@ -0,0 +1,8 @@ +.library-scroll-grid { + overflow: scroll; + position: absolute; + top: 70px; + bottom: 20px; + left: 30px; + right: 30px; +} diff --git a/src/components/library.jsx b/src/components/library/library.jsx similarity index 86% rename from src/components/library.jsx rename to src/components/library/library.jsx index 66faaa32306630b7b781acf39968d7714d3a1ba8..043ece729151d4d6529fe74f468f2ac4b67efaf1 100644 --- a/src/components/library.jsx +++ b/src/components/library/library.jsx @@ -1,8 +1,10 @@ const bindAll = require('lodash.bindall'); const React = require('react'); -const LibraryItem = require('./library-item.jsx'); -const ModalComponent = require('./modal.jsx'); +const LibraryItem = require('../library-item/library-item.jsx'); +const ModalComponent = require('../modal/modal.jsx'); + +const styles = require('./library.css'); class LibraryComponent extends React.Component { constructor (props) { @@ -19,21 +21,13 @@ class LibraryComponent extends React.Component { this.setState({selectedItem: id}); } render () { - const scrollGridStyle = { - overflow: 'scroll', - position: 'absolute', - top: '70px', - bottom: '20px', - left: '30px', - right: '30px' - }; return ( <ModalComponent visible={this.props.visible} onRequestClose={this.props.onRequestClose} > <h1>{this.props.title}</h1> - <div style={scrollGridStyle}> + <div className={styles.libraryScrollGrid}> {this.props.data.map((dataItem, itemId) => { const scratchURL = dataItem.md5 ? `https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` : diff --git a/src/components/modal.jsx b/src/components/modal.jsx deleted file mode 100644 index 65a7275e6d28bf1c27cba409e27753a083edb8b8..0000000000000000000000000000000000000000 --- a/src/components/modal.jsx +++ /dev/null @@ -1,74 +0,0 @@ -const React = require('react'); -const ReactModal = require('react-modal'); -const stylePropType = require('react-style-proptype'); - -class ModalComponent extends React.Component { - render () { - return ( - <ReactModal - isOpen={this.props.visible} - ref={m => (this.modal = m)} - style={this.props.modalStyle} - onRequestClose={this.props.onRequestClose} - > - <div - style={this.props.closeButtonStyle} - onClick={this.props.onRequestClose} - > - {'x'} - </div> - {this.props.children} - </ReactModal> - ); - } -} - -const modalStyle = { - overlay: { - zIndex: 1000, - backgroundColor: 'rgba(0, 0, 0, .75)' - }, - content: { - position: 'absolute', - overflow: 'visible', - borderRadius: '6px', - padding: 0, - top: '5%', - bottom: '5%', - left: '5%', - right: '5%', - background: '#fcfcfc' - } -}; - -const closeButtonStyle = { - color: 'rgb(255, 255, 255)', - background: 'rgb(50, 50, 50)', - borderRadius: '15px', - width: '30px', - height: '25px', - textAlign: 'center', - paddingTop: '5px', - position: 'absolute', - right: '3px', - top: '3px', - cursor: 'pointer' -}; - -ModalComponent.defaultProps = { - modalStyle: modalStyle, - closeButtonStyle: closeButtonStyle -}; - -ModalComponent.propTypes = { - children: React.PropTypes.node, - closeButtonStyle: stylePropType, - modalStyle: React.PropTypes.shape({ - overlay: stylePropType, // eslint-disable-line react/no-unused-prop-types - content: stylePropType // eslint-disable-line react/no-unused-prop-types - }), - onRequestClose: React.PropTypes.func, - visible: React.PropTypes.bool -}; - -module.exports = ModalComponent; diff --git a/src/components/modal/modal.css b/src/components/modal/modal.css new file mode 100644 index 0000000000000000000000000000000000000000..b9cb8527b1d31d627de55b0d8bec120c42b61095 --- /dev/null +++ b/src/components/modal/modal.css @@ -0,0 +1,36 @@ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: rgba(0, 0, 0, .75); +} +.modal-content { + outline: none; + position: absolute; + overflow: visible; + -webkit-overflow-scrolling: 'touch'; + border: 1px solid #ccc; + border-radius: 6px; + padding: 0; + top: 5%; + right: 5%; + bottom: 5%; + left: 5%; + background: #fcfcfc; +} +.modal-close-button { + color: rgb(255, 255, 255); + background: rgb(50, 50, 50); + border-radius: 15px; + width: 30px; + height: 25px; + text-align: center; + padding-top: 5px; + position: absolute; + right: 3px; + top: 3px; + cursor: pointer +} diff --git a/src/components/modal/modal.jsx b/src/components/modal/modal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3c36ba50921e50e8386f8be8e6ff56e53bc7793c --- /dev/null +++ b/src/components/modal/modal.jsx @@ -0,0 +1,34 @@ +const React = require('react'); +const ReactModal = require('react-modal'); + +const styles = require('./modal.css'); + +class ModalComponent extends React.Component { + render () { + return ( + <ReactModal + className={styles.modalContent} + isOpen={this.props.visible} + overlayClassName={styles.modalOverlay} + ref={m => (this.modal = m)} + onRequestClose={this.props.onRequestClose} + > + <div + className={styles.modalCloseButton} + onClick={this.props.onRequestClose} + > + {'x'} + </div> + {this.props.children} + </ReactModal> + ); + } +} + +ModalComponent.propTypes = { + children: React.PropTypes.node, + onRequestClose: React.PropTypes.func, + visible: React.PropTypes.bool +}; + +module.exports = ModalComponent; diff --git a/src/components/sprite-selector-item/sprite-selector-item.css b/src/components/sprite-selector-item/sprite-selector-item.css new file mode 100644 index 0000000000000000000000000000000000000000..29941a9f9c98b91510366016e1f73acd09de3639 --- /dev/null +++ b/src/components/sprite-selector-item/sprite-selector-item.css @@ -0,0 +1,10 @@ +.sprite-selector-item { + border: 1px solid; + border-color: transparent; + display: inline-block; + height: 72px; + width: 72px; +} +.sprite-selector-item.is-selected { + border-color: black; +} diff --git a/src/components/sprite-selector-item/sprite-selector-item.jsx b/src/components/sprite-selector-item/sprite-selector-item.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a7de0d93c864cde916db9cfbb91f4da8c6e17f79 --- /dev/null +++ b/src/components/sprite-selector-item/sprite-selector-item.jsx @@ -0,0 +1,33 @@ +const classNames = require('classnames'); +const React = require('react'); + +const CostumeCanvas = require('../costume-canvas/costume-canvas.jsx'); +const styles = require('./sprite-selector-item.css'); + +const SpriteSelectorItem = props => ( + <div + className={classNames({ + [styles.spriteSelectorItem]: true, + [styles.isSelected]: props.selected + })} + onClick={props.onClick} + > + {props.costumeURL ? ( + <CostumeCanvas + height={50} + url={props.costumeURL} + width={50} + /> + ) : null} + <div>{props.name}</div> + </div> +); + +SpriteSelectorItem.propTypes = { + costumeURL: React.PropTypes.string, + name: React.PropTypes.string, + onClick: React.PropTypes.func, + selected: React.PropTypes.bool +}; + +module.exports = SpriteSelectorItem; diff --git a/src/components/sprite-selector.jsx b/src/components/sprite-selector.jsx deleted file mode 100644 index 069829b7347f50a90dce6ee2ec0595c4b1a86c25..0000000000000000000000000000000000000000 --- a/src/components/sprite-selector.jsx +++ /dev/null @@ -1,59 +0,0 @@ -const React = require('react'); - -const SpriteSelectorComponent = function (props) { - const { - onChange, - sprites, - value, - openNewSprite, - openNewCostume, - openNewBackdrop, - ...componentProps - } = props; - return ( - <div - style={{ - position: 'absolute', - top: 380, - right: 10 - }} - {...componentProps} - > - <select - multiple - value={value} - onChange={onChange} - > - {sprites.map(sprite => ( - <option - key={sprite.id} - value={sprite.id} - > - {sprite.name} - </option> - ))} - </select> - <p> - <button onClick={openNewSprite}>New sprite</button> - <button onClick={openNewCostume}>New costume</button> - <button onClick={openNewBackdrop}>New backdrop</button> - </p> - </div> - ); -}; - -SpriteSelectorComponent.propTypes = { - onChange: React.PropTypes.func, - openNewBackdrop: React.PropTypes.func, - openNewCostume: React.PropTypes.func, - openNewSprite: React.PropTypes.func, - sprites: React.PropTypes.arrayOf( - React.PropTypes.shape({ - id: React.PropTypes.string, - name: React.PropTypes.string - }) - ), - value: React.PropTypes.arrayOf(React.PropTypes.string) -}; - -module.exports = SpriteSelectorComponent; diff --git a/src/components/sprite-selector/sprite-selector.css b/src/components/sprite-selector/sprite-selector.css new file mode 100644 index 0000000000000000000000000000000000000000..1eadc56d506fe17bad44bb1348d64fe54b124412 --- /dev/null +++ b/src/components/sprite-selector/sprite-selector.css @@ -0,0 +1,3 @@ +.sprite-selector { + width: 400px; +} diff --git a/src/components/sprite-selector/sprite-selector.jsx b/src/components/sprite-selector/sprite-selector.jsx new file mode 100644 index 0000000000000000000000000000000000000000..770e7ca6ffd08786257cb0808bc371aa85810dde --- /dev/null +++ b/src/components/sprite-selector/sprite-selector.jsx @@ -0,0 +1,55 @@ +const React = require('react'); + +const SpriteSelectorItem = require('../../containers/sprite-selector-item.jsx'); + +const styles = require('./sprite-selector.css'); + +const SpriteSelectorComponent = function (props) { + const { + onSelectSprite, + selectedId, + sprites, + ...componentProps + } = props; + return ( + <div + className={styles.spriteSelector} + {...componentProps} + > + {Object.keys(sprites) + // Re-order by list order + .sort((id1, id2) => sprites[id1].order - sprites[id2].order) + .map(id => ( + <SpriteSelectorItem + costumeURL={sprites[id].costume.skin} + id={id} + key={id} + name={sprites[id].name} + selected={id === selectedId} + onClick={onSelectSprite} + /> + )) + } + </div> + ); +}; + +SpriteSelectorComponent.propTypes = { + onSelectSprite: React.PropTypes.func, + selectedId: React.PropTypes.string, + sprites: React.PropTypes.shape({ + id: React.PropTypes.shape({ + costume: React.PropTypes.shape({ + skin: React.PropTypes.string, + name: React.PropTypes.string, + bitmapResolution: React.PropTypes.number, + rotationCenterX: React.PropTypes.number, + rotationCenterY: React.PropTypes.number + }), + name: React.PropTypes.string, + order: React.PropTypes.number + }) + }) +}; + +module.exports = SpriteSelectorComponent; diff --git a/src/components/stage-selector/stage-selector.css b/src/components/stage-selector/stage-selector.css new file mode 100644 index 0000000000000000000000000000000000000000..0981e0ebeeae4eef41fa4a2dc3965b64069aea21 --- /dev/null +++ b/src/components/stage-selector/stage-selector.css @@ -0,0 +1,11 @@ +.stage-selector { + position: absolute; + top: 0; + right: 0; + width: 72px; + border: 1px solid; + border-color: transparent; +} +.stage-selector.is-selected { + border-color: black; +} diff --git a/src/components/stage-selector/stage-selector.jsx b/src/components/stage-selector/stage-selector.jsx new file mode 100644 index 0000000000000000000000000000000000000000..db6b93e97c104c31d91a81d0ff0b0a31330548d5 --- /dev/null +++ b/src/components/stage-selector/stage-selector.jsx @@ -0,0 +1,36 @@ +const classNames = require('classnames'); +const React = require('react'); + +const CostumeCanvas = require('../costume-canvas/costume-canvas.jsx'); +const styles = require('./stage-selector.css'); + +const StageSelector = props => ( + <div + className={classNames({ + [styles.stageSelector]: true, + [styles.isSelected]: props.selected + })} + onClick={props.onClick} + > + <div>Stage</div> + <div>Backgrounds</div> + <div>{props.backdropCount}</div> + <hr /> + {props.url ? ( + <CostumeCanvas + height={42} + url={props.url} + width={50} + /> + ) : null} + </div> +); + +StageSelector.propTypes = { + backdropCount: React.PropTypes.number, + onClick: React.PropTypes.func, + selected: React.PropTypes.bool, + url: React.PropTypes.string +}; + +module.exports = StageSelector; diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css new file mode 100644 index 0000000000000000000000000000000000000000..60d9f9a1023b2bda348bb08c01fc8eb8e88db516 --- /dev/null +++ b/src/components/stage/stage.css @@ -0,0 +1,5 @@ +.stage { + position: absolute; + top: 40px; + right: 0; +} diff --git a/src/components/stage.jsx b/src/components/stage/stage.jsx similarity index 83% rename from src/components/stage.jsx rename to src/components/stage/stage.jsx index 10e31209f73ecf4507bd4688a3bd9abbb60cba39..daf447999b67910517a2c63f3a99723e7c51ca9e 100644 --- a/src/components/stage.jsx +++ b/src/components/stage/stage.jsx @@ -1,5 +1,7 @@ const React = require('react'); +const styles = require('./stage.css'); + class StageComponent extends React.Component { render () { const { @@ -10,12 +12,9 @@ class StageComponent extends React.Component { } = this.props; return ( <canvas - className="scratch-stage" + className={styles.stage} ref={canvasRef} style={{ - position: 'absolute', - top: 10, - right: 10, width: width, height: height }} diff --git a/src/components/stop-all/stop-all.css b/src/components/stop-all/stop-all.css new file mode 100644 index 0000000000000000000000000000000000000000..ff44af122de95cbf7344bece99700e37a5c895c1 --- /dev/null +++ b/src/components/stop-all/stop-all.css @@ -0,0 +1,11 @@ +.stop-all { + position: absolute; + top: 8px; + right: calc(480px - 16px - 12px - 16px); + width: 16px; + height: 16px; +} + +.active { + filter: saturate(200%) brightness(150%); +} diff --git a/src/components/stop-all/stop-all.jsx b/src/components/stop-all/stop-all.jsx index 0facbe40c5cd2a7885891be8f3afcfac3eb6bf97..8d0d8ccfb00004e4ec79c00b33e0e200ee98ebac 100644 --- a/src/components/stop-all/stop-all.jsx +++ b/src/components/stop-all/stop-all.jsx @@ -1,5 +1,8 @@ +const classNames = require('classnames'); const React = require('react'); + const stopAllIcon = require('./stop-all.svg'); +const styles = require('./stop-all.css'); const StopAllComponent = function (props) { const { @@ -10,16 +13,11 @@ const StopAllComponent = function (props) { } = props; return ( <img - className="scratch-stop-all" + className={classNames({ + [styles.stopAll]: true, + [styles.active]: active + })} src={stopAllIcon} - style={{ - position: 'absolute', - top: 380, - right: 380, - width: 50, - // @todo Get real design here - filter: active ? 'saturate(200%) brightness(150%)' : 'none' - }} title={title} onClick={onClick} {...componentProps} diff --git a/src/components/target-pane/target-pane.css b/src/components/target-pane/target-pane.css new file mode 100644 index 0000000000000000000000000000000000000000..626220df8b14442d1b05b38eb03122e3111d13a9 --- /dev/null +++ b/src/components/target-pane/target-pane.css @@ -0,0 +1,12 @@ +.target-pane { + position: absolute; + top: calc(40px + 360px + 8px); + right: 0; + width: 480px; +} +.target-pane-library-buttons { + position: absolute; + right: 0; + top: 108px; + width: 72px; +} diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx new file mode 100644 index 0000000000000000000000000000000000000000..48883747aa4f27e5b440c92e221f4ea63356bfe9 --- /dev/null +++ b/src/components/target-pane/target-pane.jsx @@ -0,0 +1,116 @@ +const React = require('react'); + +const MediaLibrary = require('../../lib/media-library'); +const VM = require('scratch-vm'); + +const BackdropLibrary = require('../../containers/backdrop-library.jsx'); +const CostumeLibrary = require('../../containers/costume-library.jsx'); +const SpriteLibrary = require('../../containers/sprite-library.jsx'); +const SpriteSelectorComponent = require('../sprite-selector/sprite-selector.jsx'); +const StageSelector = require('../../containers/stage-selector.jsx'); + +const styles = require('./target-pane.css'); + +/* + * Pane that contains the sprite selector, sprite info, stage selector, + * and the new sprite, costume and backdrop buttons + * @param {object} props Props for the component + * @returns {React.Component} rendered component + */ +const TargetPane = function (props) { + const { + editingTarget, + mediaLibrary, + backdropLibraryVisible, + costumeLibraryVisible, + spriteLibraryVisible, + onNewSpriteClick, + onNewCostumeClick, + onNewBackdropClick, + onRequestCloseBackdropLibrary, + onRequestCloseCostumeLibrary, + onRequestCloseSpriteLibrary, + onSelectSprite, + stage, + sprites, + vm, + ...componentProps + } = props; + return ( + <div + className={styles.targetPane} + {...componentProps} + > + <SpriteSelectorComponent + selectedId={editingTarget} + sprites={sprites} + onSelectSprite={onSelectSprite} + /> + <StageSelector + backdropCount={stage.costumeCount} + id={stage.id} + selected={stage.id === editingTarget} + url={stage.costume.skin} + onSelect={onSelectSprite} + /> + <p className={styles.targetPaneLibraryButtons}> + <button onClick={onNewSpriteClick}>New Sprite</button> + {editingTarget === stage.id ? ( + <button onClick={onNewBackdropClick}>New Backdrop</button> + ) : ( + <button onClick={onNewCostumeClick}>New Costume</button> + )} + <SpriteLibrary + mediaLibrary={mediaLibrary} + visible={spriteLibraryVisible} + vm={vm} + onRequestClose={onRequestCloseSpriteLibrary} + /> + <CostumeLibrary + mediaLibrary={mediaLibrary} + visible={costumeLibraryVisible} + vm={vm} + onRequestClose={onRequestCloseCostumeLibrary} + /> + <BackdropLibrary + mediaLibrary={mediaLibrary} + visible={backdropLibraryVisible} + vm={vm} + onRequestClose={onRequestCloseBackdropLibrary} + /> + </p> + </div> + ); +}; +const spriteShape = React.PropTypes.shape({ + costume: React.PropTypes.shape({ + skin: React.PropTypes.string, + name: React.PropTypes.string, + bitmapResolution: React.PropTypes.number, + rotationCenterX: React.PropTypes.number, + rotationCenterY: React.PropTypes.number + }), + id: React.PropTypes.string, + name: React.PropTypes.string, + order: React.PropTypes.number +}); + +TargetPane.propTypes = { + backdropLibraryVisible: React.PropTypes.bool, + costumeLibraryVisible: React.PropTypes.bool, + editingTarget: React.PropTypes.string, + mediaLibrary: React.PropTypes.instanceOf(MediaLibrary), + onNewBackdropClick: React.PropTypes.func, + onNewCostumeClick: React.PropTypes.func, + onNewSpriteClick: React.PropTypes.func, + onRequestCloseBackdropLibrary: React.PropTypes.func, + onRequestCloseCostumeLibrary: React.PropTypes.func, + onRequestCloseSpriteLibrary: React.PropTypes.func, + onSelectSprite: React.PropTypes.func, + spriteLibraryVisible: React.PropTypes.bool, + sprites: React.PropTypes.objectOf(spriteShape), + stage: spriteShape, + vm: React.PropTypes.instanceOf(VM) +}; + +module.exports = TargetPane; diff --git a/src/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx index ace824bead4a9897a40cd85a35c910788fa26503..dbd06b5acef5ab9785587d98dfab07d1a45dcbeb 100644 --- a/src/containers/backdrop-library.jsx +++ b/src/containers/backdrop-library.jsx @@ -3,13 +3,16 @@ const React = require('react'); const VM = require('scratch-vm'); const MediaLibrary = require('../lib/media-library'); -const LibaryComponent = require('../components/library.jsx'); +const LibaryComponent = require('../components/library/library.jsx'); class BackdropLibrary extends React.Component { constructor (props) { super(props); - bindAll(this, ['setData', 'handleItemSelect']); + bindAll(this, [ + 'setData', + 'handleItemSelect' + ]); this.state = {backdropData: []}; } componentWillReceiveProps (nextProps) { diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index f0ac707e9b29b9ee2fc05224251079b570a3a130..5a579f6e459807eef1e0cb4149f38eb55a92da4b 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -4,7 +4,7 @@ const React = require('react'); const ScratchBlocks = require('scratch-blocks'); const VM = require('scratch-vm'); -const BlocksComponent = require('../components/blocks.jsx'); +const BlocksComponent = require('../components/blocks/blocks.jsx'); class Blocks extends React.Component { constructor (props) { diff --git a/src/containers/costume-library.jsx b/src/containers/costume-library.jsx index 0dd08eabe6c40409cc9330882f962e87ea83c23d..edb1e8c3f15aa6f878956adc3a52f632e2e2f305 100644 --- a/src/containers/costume-library.jsx +++ b/src/containers/costume-library.jsx @@ -3,13 +3,16 @@ const React = require('react'); const VM = require('scratch-vm'); const MediaLibrary = require('../lib/media-library'); -const LibaryComponent = require('../components/library.jsx'); +const LibaryComponent = require('../components/library/library.jsx'); class CostumeLibrary extends React.Component { constructor (props) { super(props); - bindAll(this, ['setData', 'handleItemSelected']); + bindAll(this, [ + 'handleItemSelected', + 'setData' + ]); this.state = {costumeData: []}; } componentWillReceiveProps (nextProps) { diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx index bb44a60ca84b6c614ae6b221c67f6bbb1d0d3a9b..c254817b4bd02a77149645fcf28e32675cf38dde 100644 --- a/src/containers/gui.jsx +++ b/src/containers/gui.jsx @@ -1,33 +1,12 @@ -const bindAll = require('lodash.bindall'); -const defaultsDeep = require('lodash.defaultsdeep'); const React = require('react'); const VM = require('scratch-vm'); -const VMManager = require('../lib/vm-manager'); -const MediaLibrary = require('../lib/media-library'); -const shapeFromPropTypes = require('../lib/shape-from-prop-types'); +const vmListenerHOC = require('../lib/vm-listener-hoc.jsx'); -const Blocks = require('./blocks.jsx'); -const GUIComponent = require('../components/gui.jsx'); -const GreenFlag = require('./green-flag.jsx'); -const SpriteSelector = require('./sprite-selector.jsx'); -const Stage = require('./stage.jsx'); -const StopAll = require('./stop-all.jsx'); - -const SpriteLibrary = require('./sprite-library.jsx'); -const CostumeLibrary = require('./costume-library.jsx'); -const BackdropLibrary = require('./backdrop-library.jsx'); +const GUIComponent = require('../components/gui/gui.jsx'); class GUI extends React.Component { - constructor (props) { - super(props); - bindAll(this, ['closeModal']); - this.vmManager = new VMManager(this.props.vm); - this.mediaLibrary = new MediaLibrary(); - this.state = {currentModal: null}; - } componentDidMount () { - this.vmManager.attachKeyboardEvents(); this.props.vm.loadProject(this.props.projectData); this.props.vm.start(); } @@ -37,105 +16,29 @@ class GUI extends React.Component { } } componentWillUnmount () { - this.vmManager.detachKeyboardEvents(); this.props.vm.stopAll(); } - openModal (modalName) { - this.setState({currentModal: modalName}); - } - closeModal () { - this.setState({currentModal: null}); - } render () { - let { - backdropLibraryProps, - basePath, - blocksProps, - costumeLibraryProps, - greenFlagProps, + const { projectData, // eslint-disable-line no-unused-vars - spriteLibraryProps, - spriteSelectorProps, - stageProps, - stopAllProps, vm, - ...guiProps + ...componentProps } = this.props; - backdropLibraryProps = defaultsDeep({}, backdropLibraryProps, { - mediaLibrary: this.mediaLibrary, - onRequestClose: this.closeModal, - visible: this.state.currentModal === 'backdrop-library' - }); - blocksProps = defaultsDeep({}, blocksProps, { - options: { - media: `${basePath}static/blocks-media/` - } - }); - costumeLibraryProps = defaultsDeep({}, costumeLibraryProps, { - mediaLibrary: this.mediaLibrary, - onRequestClose: this.closeModal, - visible: this.state.currentModal === 'costume-library' - }); - spriteLibraryProps = defaultsDeep({}, spriteLibraryProps, { - mediaLibrary: this.mediaLibrary, - onRequestClose: this.closeModal, - visible: this.state.currentModal === 'sprite-library' - }); - spriteSelectorProps = defaultsDeep({}, spriteSelectorProps, { - openNewBackdrop: () => this.openModal('backdrop-library'), - openNewCostume: () => this.openModal('costume-library'), - openNewSprite: () => this.openModal('sprite-library') - }); - if (this.props.children) { - return ( - <GUIComponent {... guiProps}> - {this.props.children} - </GUIComponent> - ); - } - /* eslint-disable react/jsx-max-props-per-line, lines-around-comment */ return ( - <GUIComponent {... guiProps}> - <GreenFlag vm={vm} {...greenFlagProps} /> - <StopAll vm={vm} {...stopAllProps} /> - <Stage vm={vm} {...stageProps} /> - <SpriteSelector vm={vm} {... spriteSelectorProps} /> - <Blocks vm={vm} {... blocksProps} /> - <SpriteLibrary vm={vm} {...spriteLibraryProps} /> - <CostumeLibrary vm={vm} {...costumeLibraryProps} /> - <BackdropLibrary vm={vm} {...backdropLibraryProps} /> - </GUIComponent> + <GUIComponent + vm={vm} + {...componentProps} + /> ); - /* eslint-enable react/jsx-max-props-per-line, lines-around-comment */ } } GUI.propTypes = { - backdropLibraryProps: shapeFromPropTypes(BackdropLibrary.propTypes, {omit: ['vm']}), - basePath: React.PropTypes.string, - blocksProps: shapeFromPropTypes(Blocks.propTypes, {omit: ['vm']}), - children: React.PropTypes.node, - costumeLibraryProps: shapeFromPropTypes(CostumeLibrary.propTypes, {omit: ['vm']}), - greenFlagProps: shapeFromPropTypes(GreenFlag.propTypes, {omit: ['vm']}), + ...GUIComponent.propTypes, projectData: React.PropTypes.string, - spriteLibraryProps: shapeFromPropTypes(SpriteLibrary.propTypes, {omit: ['vm']}), - spriteSelectorProps: shapeFromPropTypes(SpriteSelector.propTypes, {omit: ['vm']}), - stageProps: shapeFromPropTypes(Stage.propTypes, {omit: ['vm']}), - stopAllProps: shapeFromPropTypes(StopAll.propTypes, {omit: ['vm']}), vm: React.PropTypes.instanceOf(VM) }; -GUI.defaultProps = { - backdropLibraryProps: {}, - basePath: '/', - blocksProps: {}, - costumeLibraryProps: {}, - greenFlagProps: {}, - spriteSelectorProps: {}, - spriteLibraryProps: {}, - stageProps: {}, - stopAllProps: {}, - vm: new VM() -}; +GUI.defaultProps = GUIComponent.defaultProps; -module.exports = GUI; +module.exports = vmListenerHOC(GUI); diff --git a/src/containers/sprite-library.jsx b/src/containers/sprite-library.jsx index 9f2cb0a67cdd7455e27c5f5f2c3852958e32c27f..462240088e500989988405aad02678e38adfd77e 100644 --- a/src/containers/sprite-library.jsx +++ b/src/containers/sprite-library.jsx @@ -3,13 +3,20 @@ const React = require('react'); const VM = require('scratch-vm'); const MediaLibrary = require('../lib/media-library'); -const LibaryComponent = require('../components/library.jsx'); +const LibaryComponent = require('../components/library/library.jsx'); class SpriteLibrary extends React.Component { constructor (props) { super(props); - bindAll(this, ['setData', 'handleItemSelect', 'setSpriteData']); - this.state = {data: [], spriteData: {}}; + bindAll(this, [ + 'handleItemSelect', + 'setData', + 'setSpriteData' + ]); + this.state = { + data: [], + spriteData: {} + }; } componentWillReceiveProps (nextProps) { if (nextProps.visible && this.state.data.length === 0) { diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9ea66dcf3c8650bf059dddcc54266ce837756ba5 --- /dev/null +++ b/src/containers/sprite-selector-item.jsx @@ -0,0 +1,40 @@ +const bindAll = require('lodash.bindall'); +const React = require('react'); + +const SpriteSelectorItemComponent = require('../components/sprite-selector-item/sprite-selector-item.jsx'); + +class SpriteSelectorItem extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClick' + ]); + } + handleClick (e) { + e.preventDefault(); + this.props.onClick(this.props.id); + } + render () { + const { + id, // eslint-disable-line no-unused-vars + onClick, // eslint-disable-line no-unused-vars + ...props + } = this.props; + return ( + <SpriteSelectorItemComponent + onClick={this.handleClick} + {...props} + /> + ); + } +} + +SpriteSelectorItem.propTypes = { + costumeURL: React.PropTypes.string, + id: React.PropTypes.string, + name: React.PropTypes.string, + onClick: React.PropTypes.func, + selected: React.PropTypes.bool +}; + +module.exports = SpriteSelectorItem; diff --git a/src/containers/sprite-selector.jsx b/src/containers/sprite-selector.jsx deleted file mode 100644 index 52beebd1c47a288aa77c9921af9be3a8bc2cbe03..0000000000000000000000000000000000000000 --- a/src/containers/sprite-selector.jsx +++ /dev/null @@ -1,60 +0,0 @@ -const bindAll = require('lodash.bindall'); -const React = require('react'); -const VM = require('scratch-vm'); - -const SpriteSelectorComponent = require('../components/sprite-selector.jsx'); - -class SpriteSelector extends React.Component { - constructor (props) { - super(props); - bindAll(this, ['handleChange', 'targetsUpdate']); - this.state = { - targets: { - targetList: [] - } - }; - } - componentDidMount () { - this.props.vm.on('targetsUpdate', this.targetsUpdate); - } - handleChange (event) { - this.props.vm.setEditingTarget(event.target.value); - } - targetsUpdate (data) { - this.setState({targets: data}); - } - render () { - const { - vm, // eslint-disable-line no-unused-vars - openNewSprite, - openNewCostume, - openNewBackdrop, - ...props - } = this.props; - return ( - <SpriteSelectorComponent - openNewBackdrop={openNewBackdrop} - openNewCostume={openNewCostume} - openNewSprite={openNewSprite} - sprites={this.state.targets.targetList.map(target => ( - { - id: target[0], - name: target[1] - } - ))} - value={this.state.targets.editingTarget && [this.state.targets.editingTarget]} - onChange={this.handleChange} - {...props} - /> - ); - } -} - -SpriteSelector.propTypes = { - openNewBackdrop: React.PropTypes.func, - openNewCostume: React.PropTypes.func, - openNewSprite: React.PropTypes.func, - vm: React.PropTypes.instanceOf(VM) -}; - -module.exports = SpriteSelector; diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6748ce2c09df727ce13fdf80eb8cca55aa6cdcc4 --- /dev/null +++ b/src/containers/stage-selector.jsx @@ -0,0 +1,38 @@ +const bindAll = require('lodash.bindall'); +const React = require('react'); + +const StageSelectorComponent = require('../components/stage-selector/stage-selector.jsx'); + +class StageSelector extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleClick' + ]); + } + handleClick (e) { + e.preventDefault(); + this.props.onSelect(this.props.id); + } + render () { + const { + /* eslint-disable no-unused-vars */ + id, + onSelect, + /* eslint-enable no-unused-vars */ + ...componentProps + } = this.props; + return ( + <StageSelectorComponent + onClick={this.handleClick} + {...componentProps} + /> + ); + } +} +StageSelector.propTypes = { + ...StageSelectorComponent.propTypes, + id: React.PropTypes.string, + onSelect: React.PropTypes.func +}; +module.exports = StageSelector; diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index 53f2aa869959fa65b500321b44588eab01a30a08..55c3607248f60868f3e2527a97140a7c1cf62d5c 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -3,7 +3,7 @@ const React = require('react'); const Renderer = require('scratch-render'); const VM = require('scratch-vm'); -const StageComponent = require('../components/stage.jsx'); +const StageComponent = require('../components/stage/stage.jsx'); class Stage extends React.Component { constructor (props) { diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx new file mode 100644 index 0000000000000000000000000000000000000000..119628930b7043c6c0180848ec8c4972b3938369 --- /dev/null +++ b/src/containers/target-pane.jsx @@ -0,0 +1,81 @@ +const bindAll = require('lodash.bindall'); +const React = require('react'); + +const {connect} = require('react-redux'); + +const { + openBackdropLibrary, + openCostumeLibrary, + openSpriteLibrary, + closeBackdropLibrary, + closeCostumeLibrary, + closeSpriteLibrary +} = require('../reducers/modals'); + +const TargetPaneComponent = require('../components/target-pane/target-pane.jsx'); + +class TargetPane extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleSelectSprite' + ]); + } + handleSelectSprite (id) { + this.props.vm.setEditingTarget(id); + } + render () { + return ( + <TargetPaneComponent + {...this.props} + onSelectSprite={this.handleSelectSprite} + /> + ); + } +} + +const { + onSelectSprite, // eslint-disable-line no-unused-vars + ...targetSelectorProps +} = TargetPaneComponent.propTypes; + +TargetPane.propTypes = { + ...targetSelectorProps +}; + +const mapStateToProps = state => ({ + editingTarget: state.targets.editingTarget, + sprites: state.targets.sprites, + stage: state.targets.stage, + spriteLibraryVisible: state.modals.spriteLibrary, + costumeLibraryVisible: state.modals.costumeLibrary, + backdropLibraryVisible: state.modals.backdropLibrary +}); +const mapDispatchToProps = dispatch => ({ + onNewBackdropClick: e => { + e.preventDefault(); + dispatch(openBackdropLibrary()); + }, + onNewCostumeClick: e => { + e.preventDefault(); + dispatch(openCostumeLibrary()); + }, + onNewSpriteClick: e => { + e.preventDefault(); + dispatch(openSpriteLibrary()); + }, + onRequestCloseBackdropLibrary: () => { + dispatch(closeBackdropLibrary()); + }, + onRequestCloseCostumeLibrary: () => { + dispatch(closeCostumeLibrary()); + }, + onRequestCloseSpriteLibrary: () => { + dispatch(closeSpriteLibrary()); + } +}); + +module.exports = connect( + mapStateToProps, + mapDispatchToProps +)(TargetPane); diff --git a/src/index.jsx b/src/index.jsx index 7f2d8c07319c9335f47f64058478ba509d9a732a..393cb95deec95510962b293282667b293b15a295 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,8 +1,12 @@ const React = require('react'); const ReactDOM = require('react-dom'); +const {Provider} = require('react-redux'); +const {createStore} = require('redux'); + const GUI = require('./containers/gui.jsx'); const log = require('./lib/log'); const ProjectLoader = require('./lib/project-loader'); +const reducer = require('./reducers/gui'); class App extends React.Component { constructor (props) { @@ -22,7 +26,7 @@ class App extends React.Component { window.removeEventListener('hashchange', this.updateProject); } fetchProjectId () { - return location.hash.substring(1); + return window.location.hash.substring(1); } updateProject () { const projectId = this.fetchProjectId(); @@ -56,5 +60,12 @@ App.propTypes = { const appTarget = document.createElement('div'); document.body.appendChild(appTarget); - -ReactDOM.render(<App basePath={process.env.BASE_PATH} />, appTarget); +const store = createStore( + reducer, + window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() +); +ReactDOM.render(( + <Provider store={store}> + <App basePath={process.env.BASE_PATH} /> + </Provider> +), appTarget); diff --git a/src/lib/media-library.js b/src/lib/media-library.js index 6e2d122495e704b95b731c0aac1f9d07c4ac23e6..cadcfc761fa5185eb8d3c7ee7ffc0e0d537b8fb4 100644 --- a/src/lib/media-library.js +++ b/src/lib/media-library.js @@ -1,7 +1,6 @@ const xhr = require('xhr'); -const LIBRARY_PREFIX = 'https://cdn.scratch.mit.edu/scratchr2/static/' + - '__8d9c95eb5aa1272a311775ca32568417__/medialibraries/'; +const LIBRARY_PREFIX = 'https://cdn.scratch.mit.edu/scratchr2/static/__8d9c95eb5aa1272a311775ca32568417__/medialibraries/'; const LIBRARY_URL = { sprite: `${LIBRARY_PREFIX}spriteLibrary.json`, costume: `${LIBRARY_PREFIX}costumeLibrary.json`, diff --git a/src/lib/vm-listener-hoc.jsx b/src/lib/vm-listener-hoc.jsx new file mode 100644 index 0000000000000000000000000000000000000000..87c76c56aef4fe7173f0db79ff59248f4ee48633 --- /dev/null +++ b/src/lib/vm-listener-hoc.jsx @@ -0,0 +1,112 @@ +const bindAll = require('lodash.bindall'); +const React = require('react'); +const VM = require('scratch-vm'); + +const {connect} = require('react-redux'); + +const targets = require('../reducers/targets'); + +/* + * Higher Order Component to manage events emitted by the VM + * @param {React.Component} WrappedComponent component to manage VM events for + * @returns {React.Component} connected component with vm events bound to redux + */ +const vmListenerHOC = function (WrappedComponent) { + class VMListener extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'handleKeyDown', + 'handleKeyUp' + ]); + // We have to start listening to the vm here rather than in + // componentDidMount because the HOC mounts the wrapped component, + // so the HOC componentDidMount triggers after the wrapped component + // mounts. + // If the wrapped component uses the vm in componentDidMount, then + // we need to start listening before mounting the wrapped component. + this.props.vm.on('targetsUpdate', this.props.onTargetsUpdate); + this.props.vm.on('SPRITE_INFO_REPORT', this.props.onSpriteInfoReport); + } + componentDidMount () { + if (this.props.attachKeyboardEvents) { + document.addEventListener('keydown', this.handleKeyDown); + document.addEventListener('keyup', this.handleKeyUp); + } + } + componentWillUnmount () { + if (this.props.attachKeyboardEvents) { + document.removeEventListener('keydown', this.handleKeyDown); + document.removeEventListener('keyup', this.handleKeyUp); + } + } + handleKeyDown (e) { + // Don't capture keys intended for Blockly inputs. + if (e.target !== document && e.target !== document.body) return; + + this.props.vm.postIOData('keyboard', { + keyCode: e.keyCode, + isDown: true + }); + + // Don't stop browser keyboard shortcuts + if (e.metaKey || e.altKey || e.ctrlKey) return; + + e.preventDefault(); + } + handleKeyUp (e) { + // Always capture up events, + // even those that have switched to other targets. + this.props.vm.postIOData('keyboard', { + keyCode: e.keyCode, + isDown: false + }); + + // E.g., prevent scroll. + if (e.target !== document && e.target !== document.body) { + e.preventDefault(); + } + } + render () { + const { + /* eslint-disable no-unused-vars */ + attachKeyboardEvents, + onKeyDown, + onKeyUp, + onSpriteInfoReport, + onTargetsUpdate, + /* eslint-enable no-unused-vars */ + ...props + } = this.props; + return <WrappedComponent {...props} />; + } + } + VMListener.propTypes = { + attachKeyboardEvents: React.PropTypes.bool, + onKeyDown: React.PropTypes.func, + onKeyUp: React.PropTypes.func, + onSpriteInfoReport: React.PropTypes.func, + onTargetsUpdate: React.PropTypes.func, + vm: React.PropTypes.instanceOf(VM).isRequired + }; + VMListener.defaultProps = { + attachKeyboardEvents: true, + vm: new VM() + }; + const mapStateToProps = () => ({}); + const mapDispatchToProps = dispatch => ({ + onTargetsUpdate: data => { + dispatch(targets.updateEditingTarget(data.editingTarget)); + dispatch(targets.updateTargets(data.targetList)); + }, + onSpriteInfoReport: spriteInfo => { + dispatch(targets.updateTarget(spriteInfo)); + } + }); + return connect( + mapStateToProps, + mapDispatchToProps + )(VMListener); +}; + +module.exports = vmListenerHOC; diff --git a/src/lib/vm-manager.js b/src/lib/vm-manager.js deleted file mode 100644 index 9ac6f2b8197da6efd606315bdb6001c67db3d057..0000000000000000000000000000000000000000 --- a/src/lib/vm-manager.js +++ /dev/null @@ -1,51 +0,0 @@ -const bindAll = require('lodash.bindall'); - -class VMManager { - constructor (vm) { - bindAll(this, [ - 'attachKeyboardEvents', - 'detachKeyboardEvents', - 'onKeyDown', - 'onKeyUp' - ]); - this.vm = vm; - } - attachKeyboardEvents () { - // Feed keyboard events as VM I/O events. - document.addEventListener('keydown', this.onKeyDown); - document.addEventListener('keyup', this.onKeyUp); - } - detachKeyboardEvents () { - document.removeEventListener('keydown', this.onKeyDown); - document.removeEventListener('keyup', this.onKeyUp); - } - onKeyDown (e) { - // Don't capture keys intended for Blockly inputs. - if (e.target !== document && e.target !== document.body) return; - - this.vm.postIOData('keyboard', { - keyCode: e.keyCode, - isDown: true - }); - - // Don't stop browser keyboard shortcuts - if (e.metaKey || e.altKey || e.ctrlKey) return; - - e.preventDefault(); - } - onKeyUp (e) { - // Always capture up events, - // even those that have switched to other targets. - this.vm.postIOData('keyboard', { - keyCode: e.keyCode, - isDown: false - }); - - // E.g., prevent scroll. - if (e.target !== document && e.target !== document.body) { - e.preventDefault(); - } - } -} - -module.exports = VMManager; diff --git a/src/reducers/gui.js b/src/reducers/gui.js new file mode 100644 index 0000000000000000000000000000000000000000..325c85b11382c189973bbccf5c9ff735c6265ed5 --- /dev/null +++ b/src/reducers/gui.js @@ -0,0 +1,6 @@ +const {combineReducers} = require('redux'); + +module.exports = combineReducers({ + modals: require('./modals'), + targets: require('./targets') +}); diff --git a/src/reducers/modals.js b/src/reducers/modals.js new file mode 100644 index 0000000000000000000000000000000000000000..2acce9e7621422b41cd723e69d28e7985451f648 --- /dev/null +++ b/src/reducers/modals.js @@ -0,0 +1,59 @@ +const OPEN_MODAL = 'scratch-gui/modals/OPEN_MODAL'; +const CLOSE_MODAL = 'scratch-gui/modals/CLOSE_MODAL'; + +const MODAL_BACKDROP_LIBRARY = 'backdropLibrary'; +const MODAL_COSTUME_LIBRARY = 'costumeLibrary'; +const MODAL_SPRITE_LIBRARY = 'spriteLibrary'; + +const initialState = { + [MODAL_BACKDROP_LIBRARY]: false, + [MODAL_COSTUME_LIBRARY]: false, + [MODAL_SPRITE_LIBRARY]: false +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case OPEN_MODAL: + return Object.assign({}, state, { + [action.modal]: true + }); + case CLOSE_MODAL: + return Object.assign({}, state, { + [action.modal]: false + }); + default: + return state; + } +}; +reducer.openModal = function (modal) { + return { + type: OPEN_MODAL, + modal: modal + }; +}; +reducer.closeModal = function (modal) { + return { + type: CLOSE_MODAL, + modal: modal + }; +}; +reducer.openBackdropLibrary = function () { + return reducer.openModal(MODAL_BACKDROP_LIBRARY); +}; +reducer.openCostumeLibrary = function () { + return reducer.openModal(MODAL_COSTUME_LIBRARY); +}; +reducer.openSpriteLibrary = function () { + return reducer.openModal(MODAL_SPRITE_LIBRARY); +}; +reducer.closeBackdropLibrary = function () { + return reducer.closeModal(MODAL_BACKDROP_LIBRARY); +}; +reducer.closeCostumeLibrary = function () { + return reducer.closeModal(MODAL_COSTUME_LIBRARY); +}; +reducer.closeSpriteLibrary = function () { + return reducer.closeModal(MODAL_SPRITE_LIBRARY); +}; +module.exports = reducer; diff --git a/src/reducers/targets.js b/src/reducers/targets.js new file mode 100644 index 0000000000000000000000000000000000000000..61c01ebb36d9505349a2831f3d4021e89da6dd44 --- /dev/null +++ b/src/reducers/targets.js @@ -0,0 +1,73 @@ +const defaultsDeep = require('lodash.defaultsdeep'); + +const UPDATE_EDITING_TARGET = 'scratch-gui/targets/UPDATE_EDITING_TARGET'; +const UPDATE_TARGET_LIST = 'scratch-gui/targets/UPDATE_TARGET_LIST'; +const UPDATE_TARGET = 'scratch/targets/UPDATE_TARGET'; + +const initialState = { + sprites: {}, + stage: {} +}; + +const reducer = function (state, action) { + if (typeof state === 'undefined') state = initialState; + switch (action.type) { + case UPDATE_TARGET: + if (action.target.id === state.stage.id) { + return Object.assign({}, state, { + stage: Object.assign({}, state.stage, action.target) + }); + } + return Object.assign({}, state, { + sprites: defaultsDeep( + {[action.target.id]: action.target}, + state.sprites + ) + }); + case UPDATE_TARGET_LIST: + return Object.assign({}, state, { + sprites: action.targets + .filter(target => !target.isStage) + .reduce( + (targets, target, listId) => defaultsDeep( + {[target.id]: {order: listId}}, + {[target.id]: state.sprites[target.id]}, + targets + ), + {} + ), + stage: action.targets + .filter(target => target.isStage) + .reduce( + (stage, target) => { + if (target.id !== stage.id) return target; + return defaultsDeep(target, stage); + }, + state.stage + ) + }); + case UPDATE_EDITING_TARGET: + return Object.assign({}, state, {editingTarget: action.target}); + default: + return state; + } +}; +reducer.updateTarget = function (target) { + return { + type: UPDATE_TARGET, + target: target + }; +}; +reducer.updateTargets = function (targetList) { + return { + type: UPDATE_TARGET_LIST, + targets: targetList + }; +}; +reducer.updateEditingTarget = function (editingTarget) { + return { + type: UPDATE_EDITING_TARGET, + target: editingTarget + }; +}; +module.exports = reducer; diff --git a/webpack.config.js b/webpack.config.js index c5dc1544f85e35170c30caf161ec362c6d87d42e..3f5e68802968face09fed1809ae648f4720fbd2e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,5 @@ +var autoprefixer = require('autoprefixer'); +var combineLoaders = require('webpack-combine-loaders'); var path = require('path'); var CopyWebpackPlugin = require('copy-webpack-plugin'); var HtmlWebpackPlugin = require('html-webpack-plugin'); @@ -26,6 +28,21 @@ module.exports = { presets: ['es2015', 'react'] } }, + { + test: /\.css$/, + loader: combineLoaders([{ + loader: 'style' + }, { + loader: 'css', + query: { + modules: true, + localIdentName: '[name]_[local]_[hash:base64:5]', + camelCase: true + } + }, { + loader: 'postcss' + }]) + }, { test: /\.svg$/, loader: 'svg-url-loader?noquotes' @@ -35,6 +52,7 @@ module.exports = { loader: 'json-loader' }] }, + postcss: [autoprefixer], plugins: [ new webpack.DefinePlugin({ 'process.env.BASE_PATH': '"' + (process.env.BASE_PATH || '/') + '"'