diff --git a/package.json b/package.json index 690963a1d..13a4b8c1f 100644 --- a/package.json +++ b/package.json @@ -38,16 +38,18 @@ "babel-plugin-transform-object-rest-spread": "^6.22.0", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.22.0", + "buffer-loader": "0.0.1", "chromedriver": "2.33.1", "classnames": "2.2.5", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.7", "enzyme": "^3.1.0", - "enzyme-adapter-react-16": "1.0.2", + "enzyme-adapter-react-16": "1.0.3", "eslint": "^4.7.1", "eslint-config-scratch": "^5.0.0", "eslint-plugin-import": "^2.7.0", "eslint-plugin-react": "^7.2.1", + "file-loader": "1.1.5", "get-float-time-domain-data": "0.1.0", "gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder", "html-webpack-plugin": "^2.30.0", @@ -66,6 +68,7 @@ "postcss-simple-vars": "^4.0.0", "prop-types": "^15.5.10", "raf": "^3.4.0", + "raw-loader": "0.5.1", "react": "16.0.0", "react-contextmenu": "2.8.0", "react-dom": "16.0.0", @@ -83,17 +86,17 @@ "redux-throttle": "0.1.1", "rimraf": "^2.6.1", "scratch-audio": "latest", - "scratch-paint": "latest", "scratch-blocks": "latest", "scratch-l10n": "^2.0.0", + "scratch-paint": "latest", "scratch-render": "latest", - "scratch-storage": "^0.2.0", + "scratch-storage": "^0.3.0", "scratch-vm": "latest", "selenium-webdriver": "3.5.0", "startaudiocontext": "1.2.1", "style-loader": "^0.19.0", "svg-to-image": "1.1.3", - "svg-url-loader": "^2.1.0", + "text-encoding": "0.6.4", "wav-encoder": "1.3.0", "web-audio-test-api": "^0.5.2", "webpack": "^3.6.0", diff --git a/src/components/gui/gui.css b/src/components/gui/gui.css index bb336c40d..9f9b84743 100644 --- a/src/components/gui/gui.css +++ b/src/components/gui/gui.css @@ -150,7 +150,7 @@ } .extension-button-container { - width: 60px; + width: 3.25rem; height: 3.25rem; position: absolute; bottom: 0; diff --git a/src/components/stage/stage.css b/src/components/stage/stage.css index f13554a02..9c784e956 100644 --- a/src/components/stage/stage.css +++ b/src/components/stage/stage.css @@ -1,4 +1,5 @@ @import "../../css/units.css"; +@import "../../css/colors.css"; .stage { /* @@ -7,8 +8,6 @@ */ display: block; - border-radius: $space; - /* @todo: This is for overriding the value being set somewhere. Where is it being set? */ background-color: transparent; } @@ -31,6 +30,10 @@ .stage-wrapper { position: relative; + border-radius: $space; + border: 1px solid $ui-pane-border; + /* Keep the canvas inside the border radius */ + overflow: hidden; } .monitor-wrapper, .color-picker-wrapper { diff --git a/src/components/target-pane/target-pane.jsx b/src/components/target-pane/target-pane.jsx index 9ee7b5f4e..2f147447d 100644 --- a/src/components/target-pane/target-pane.jsx +++ b/src/components/target-pane/target-pane.jsx @@ -7,7 +7,6 @@ import BackdropLibrary from '../../containers/backdrop-library.jsx'; import CostumeLibrary from '../../containers/costume-library.jsx'; import SoundLibrary from '../../containers/sound-library.jsx'; import SpriteLibrary from '../../containers/sprite-library.jsx'; -import ExtensionLibrary from '../../containers/extension-library.jsx'; import SpriteSelectorComponent from '../sprite-selector/sprite-selector.jsx'; import StageSelector from '../../containers/stage-selector.jsx'; @@ -22,7 +21,6 @@ import styles from './target-pane.css'; */ const TargetPane = ({ editingTarget, - extensionLibraryVisible, backdropLibraryVisible, costumeLibraryVisible, soundLibraryVisible, @@ -40,7 +38,6 @@ const TargetPane = ({ onRequestCloseCostumeLibrary, onRequestCloseSoundLibrary, onRequestCloseSpriteLibrary, - onRequestCloseExtensionLibrary, onSelectSprite, stage, sprites, @@ -78,12 +75,6 @@ const TargetPane = ({ onSelect={onSelectSprite} />}
- {extensionLibraryVisible ? ( - - ) : null} {spriteLibraryVisible ? ( { const oldFn = object[property]; @@ -29,6 +31,7 @@ class Blocks extends React.Component { bindAll(this, [ 'attachVM', 'detachVM', + 'handleCategorySelected', 'handlePromptStart', 'handlePromptCallback', 'handlePromptClose', @@ -53,12 +56,13 @@ class Blocks extends React.Component { componentDidMount () { this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker; - const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options); + const workspaceConfig = defaultsDeep({}, + Blocks.defaultOptions, + this.props.options, + {toolbox: this.props.toolboxXML} + ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); - // Load the toolbox from the GUI (otherwise we get the scratch-blocks default toolbox) - this.workspace.updateToolbox(this.props.toolboxXML); - // @todo change this when blockly supports UI events addFunctionListener(this.workspace, 'translate', this.onWorkspaceMetricsChange); addFunctionListener(this.workspace, 'zoom', this.onWorkspaceMetricsChange); @@ -69,7 +73,8 @@ class Blocks extends React.Component { return ( this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible || - this.props.toolboxXML !== nextProps.toolboxXML + this.props.toolboxXML !== nextProps.toolboxXML || + this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible ); } componentDidUpdate (prevProps) { @@ -87,7 +92,6 @@ class Blocks extends React.Component { this.workspace.setVisible(true); this.props.vm.refreshWorkspace(); window.dispatchEvent(new Event('resize')); - this.workspace.toolbox_.refreshSelection(); } else { this.workspace.setVisible(false); } @@ -172,13 +176,11 @@ class Blocks extends React.Component { this.onWorkspaceMetricsChange(); } - this.ScratchBlocks.Events.disable(); - this.workspace.clear(); - + // Remove and reattach the workspace listener (but allow flyout events) + this.workspace.removeChangeListener(this.props.vm.blockListener); const dom = this.ScratchBlocks.Xml.textToDom(data.xml); - this.ScratchBlocks.Xml.domToWorkspace(dom, this.workspace); - this.ScratchBlocks.Events.enable(); - this.workspace.toolbox_.refreshSelection(); + this.ScratchBlocks.Xml.clearWorkspaceAndLoadFromXml(dom, this.workspace); + this.workspace.addChangeListener(this.props.vm.blockListener); if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) { const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id]; @@ -193,7 +195,8 @@ class Blocks extends React.Component { const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(); const toolboxXML = makeToolboxXML(dynamicBlocksXML); this.props.onExtensionAdded(toolboxXML); - const categoryName = blocksInfo[0].json.category; + } + handleCategorySelected (categoryName) { this.workspace.toolbox_.setSelectedCategoryByName(categoryName); } setBlocks (blocks) { @@ -212,11 +215,13 @@ class Blocks extends React.Component { render () { /* eslint-disable no-unused-vars */ const { + extensionLibraryVisible, options, vm, isVisible, onActivateColorPicker, onExtensionAdded, + onRequestCloseExtensionLibrary, toolboxXML, ...props } = this.props; @@ -236,15 +241,24 @@ class Blocks extends React.Component { onOk={this.handlePromptCallback} /> ) : null} + {extensionLibraryVisible ? ( + + ) : null}
); } } Blocks.propTypes = { + extensionLibraryVisible: PropTypes.bool, isVisible: PropTypes.bool, onActivateColorPicker: PropTypes.func, onExtensionAdded: PropTypes.func, + onRequestCloseExtensionLibrary: PropTypes.func, options: PropTypes.shape({ media: PropTypes.string, zoom: PropTypes.shape({ @@ -302,6 +316,7 @@ Blocks.defaultProps = { }; const mapStateToProps = state => ({ + extensionLibraryVisible: state.modals.extensionLibrary, toolboxXML: state.toolbox.toolboxXML }); @@ -309,6 +324,9 @@ const mapDispatchToProps = dispatch => ({ onActivateColorPicker: callback => dispatch(activateColorPicker(callback)), onExtensionAdded: toolboxXML => { dispatch(updateToolbox(toolboxXML)); + }, + onRequestCloseExtensionLibrary: () => { + dispatch(closeExtensionLibrary()); } }); diff --git a/src/containers/extension-library.jsx b/src/containers/extension-library.jsx index 343d0747d..09fd65ff3 100644 --- a/src/containers/extension-library.jsx +++ b/src/containers/extension-library.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import VM from 'scratch-vm'; -import extensionLibraryContent from '../lib/libraries/extensions.json'; +import extensionLibraryContent from '../lib/libraries/extensions/index'; import LibraryComponent from '../components/library/library.jsx'; import extensionIcon from '../components/sprite-selector/icon--sprite.svg'; @@ -19,7 +19,13 @@ class ExtensionLibrary extends React.PureComponent { // eslint-disable-next-line no-alert const url = item.extensionURL || prompt('Enter the URL of the extension'); if (url) { - this.props.vm.extensionManager.loadExtensionURL(url); + if (this.props.vm.extensionManager.isExtensionLoaded(url)) { + this.props.onCategorySelected(item.name); + } else { + this.props.vm.extensionManager.loadExtensionURL(url).then(() => { + this.props.onCategorySelected(item.name); + }); + } } } render () { @@ -40,6 +46,7 @@ class ExtensionLibrary extends React.PureComponent { } ExtensionLibrary.propTypes = { + onCategorySelected: PropTypes.func, onRequestClose: PropTypes.func, visible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types diff --git a/src/containers/paint-editor-wrapper.jsx b/src/containers/paint-editor-wrapper.jsx index ca033c061..6448f3e9e 100644 --- a/src/containers/paint-editor-wrapper.jsx +++ b/src/containers/paint-editor-wrapper.jsx @@ -55,7 +55,7 @@ const mapStateToProps = (state, {selectedCostumeIndex}) => { name: costume && costume.name, rotationCenterX: costume && costume.rotationCenterX, rotationCenterY: costume && costume.rotationCenterY, - svgId: editingTarget && `${editingTarget}${selectedCostumeIndex}` + svgId: editingTarget && `${editingTarget}${costume.skinId}` }; }; diff --git a/src/containers/target-pane.jsx b/src/containers/target-pane.jsx index 3a68847c6..ba0438fae 100644 --- a/src/containers/target-pane.jsx +++ b/src/containers/target-pane.jsx @@ -7,7 +7,6 @@ import { openSpriteLibrary, closeBackdropLibrary, closeCostumeLibrary, - closeExtensionLibrary, closeSoundLibrary, closeSpriteLibrary } from '../reducers/modals'; @@ -97,8 +96,7 @@ const mapStateToProps = state => ({ soundLibraryVisible: state.modals.soundLibrary, spriteLibraryVisible: state.modals.spriteLibrary, costumeLibraryVisible: state.modals.costumeLibrary, - backdropLibraryVisible: state.modals.backdropLibrary, - extensionLibraryVisible: state.modals.extensionLibrary + backdropLibraryVisible: state.modals.backdropLibrary }); const mapDispatchToProps = dispatch => ({ onNewSpriteClick: e => { @@ -111,9 +109,6 @@ const mapDispatchToProps = dispatch => ({ onRequestCloseCostumeLibrary: () => { dispatch(closeCostumeLibrary()); }, - onRequestCloseExtensionLibrary: () => { - dispatch(closeExtensionLibrary()); - }, onRequestCloseSoundLibrary: () => { dispatch(closeSoundLibrary()); }, diff --git a/src/examples/compatibility-testing.jsx b/src/examples/compatibility-testing.jsx new file mode 100644 index 000000000..3fa8e71d9 --- /dev/null +++ b/src/examples/compatibility-testing.jsx @@ -0,0 +1,80 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {connect} from 'react-redux'; + +import AppStateHOC from '../lib/app-state-hoc.jsx'; +import Controls from '../containers/controls.jsx'; +import Stage from '../containers/stage.jsx'; +import Box from '../components/box/box.jsx'; +import GUI from '../containers/gui.jsx'; +import ProjectLoaderHOC from '../lib/project-loader-hoc.jsx'; + +const mapStateToProps = state => ({vm: state.vm}); + +const VMStage = connect(mapStateToProps)(Stage); +const VMControls = connect(mapStateToProps)(Controls); + +const DEFAULT_PROJECT_ID = '10015059'; + +class Player extends React.Component { + constructor (props) { + super(props); + this.updateProject = this.updateProject.bind(this); + + this.state = { + projectId: window.location.hash.substring(1) || DEFAULT_PROJECT_ID + }; + } + componentDidMount () { + window.addEventListener('hashchange', this.updateProject); + if (!window.location.hash.substring(1)) { + window.location.hash = DEFAULT_PROJECT_ID; + } + } + componentWillUnmount () { + window.addEventListener('hashchange', this.updateProject); + } + updateProject () { + this.setState({projectId: window.location.hash.substring(1)}); + } + render () { + const width = 480; + const height = 360; + return ( +
+ + + + + + +