Merge branch 'develop' into ask-wait-answer-blocks

This commit is contained in:
Paul Kaplan authored and GitHub committed 2017-11-07 09:02:51 -05:00
commit 704864a2ff
29 files changed
+433 -199

No files matched your search

+7 -4
View File
@@ -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",
+1 -1
View File
@@ -150,7 +150,7 @@
}
.extension-button-container {
width: 60px;
width: 3.25rem;
height: 3.25rem;
position: absolute;
bottom: 0;
+5 -2
View File
@@ -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 {
@@ -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}
/>}
<div>
{extensionLibraryVisible ? (
<ExtensionLibrary
vm={vm}
onRequestClose={onRequestCloseExtensionLibrary}
/>
) : null}
{spriteLibraryVisible ? (
<SpriteLibrary
vm={vm}
+31 -13
View File
@@ -8,10 +8,12 @@ import VMScratchBlocks from '../lib/blocks';
import VM from 'scratch-vm';
import Prompt from './prompt.jsx';
import BlocksComponent from '../components/blocks/blocks.jsx';
import ExtensionLibrary from './extension-library.jsx';
import {connect} from 'react-redux';
import {updateToolbox} from '../reducers/toolbox';
import {activateColorPicker} from '../reducers/color-picker';
import {closeExtensionLibrary} from '../reducers/modals';
const addFunctionListener = (object, property, callback) => {
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 ? (
<ExtensionLibrary
vm={vm}
onCategorySelected={this.handleCategorySelected}
onRequestClose={onRequestCloseExtensionLibrary}
/>
) : null}
</div>
);
}
}
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());
}
});
+9 -2
View File
@@ -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
+1 -1
View File
@@ -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}`
};
};
+1 -6
View File
@@ -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());
},
+80
View File
@@ -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 (
<div style={{display: 'flex'}}>
<GUI
{...this.props}
width={width}
>
<Box height={40}>
<VMControls
style={{
marginRight: 10,
height: 40
}}
/>
</Box>
<VMStage
height={height}
width={width}
/>
</GUI>
<iframe
allowFullScreen
allowTransparency
frameBorder="0"
height="402"
src={`https://scratch.mit.edu/projects/embed/${this.state.projectId}/?autostart=true`}
width="485"
/>
</div>
);
}
}
const App = AppStateHOC(ProjectLoaderHOC(Player));
const appTarget = document.createElement('div');
document.body.appendChild(appTarget);
ReactDOM.render(<App />, appTarget);
@@ -0,0 +1,42 @@
<svg version="1.1" id="cat" x="0px" y="0px" width="95px" height="111px" viewBox="0 0 95 111" enable-background="new 0 0 95 111" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<g id="Layer_3">
<path fill="#FAA51D" stroke="#000000" d="M22.462,79.039c-2.415-0.451-5.304-1.309-7.742-3.503&#xD;&#xA;&#x9;&#x9;C9.268,70.629,7.526,62.535,3.672,64.622c-3.856,2.088-3.782,15.165,8.353,19.194c4.182,1.391,7.998,1.396,11.091,1.312&#xD;&#xA;&#x9;&#x9;c0.811-0.025,7.717-0.654,10.079-4.074c2.361-3.42,0.719-4.272-0.09-4.744C32.295,75.838,25.878,79.677,22.462,79.039z"/>
<path fill="#FFFFFF" d="M4.236,64.877c-1.989,0.613-3.075,4.998-2.076,8.484c0.998,3.49,2.634,5.022,3.863,6.398&#xD;&#xA;&#x9;&#x9;c1.528,1.038-0.72-2.402,1.361-4.15c2.075-1.744,5.733-0.914,5.733-0.914s-2.909-3.987-4.57-6.396&#xD;&#xA;&#x9;&#x9;C6.975,65.988,6.359,64.375,4.236,64.877z"/>
</g>
<g>
<path fill="#FAA51D" d="M38.217,86.756c0,0-8.832,6.2-17.071,8.412l0.086,0.215c1.247,1.824,5.87,7.497-0.334,9.496&#xD;&#xA;&#x9;&#x9;c-5.333,1.717-15.12-13.104-10.821-15.902c2.626-1.713,4.892-0.252,4.892-0.252s3.474-1.07,6.001-2.345&#xD;&#xA;&#x9;&#x9;c4.303-2.161,5.784-3.453,5.784-3.453s4.184-4.306,6.856-4.137C36.281,78.96,41.669,83.504,38.217,86.756z"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M21.232,95.383c1.247,1.824,5.87,7.497-0.334,9.496&#xD;&#xA;&#x9;&#x9;c-5.333,1.717-15.329-13.344-11.03-16.145c2.626-1.713,5.101-0.01,5.101-0.01s3.474-1.072,6.001-2.348&#xD;&#xA;&#x9;&#x9;c4.303-2.161,5.784-3.453,5.784-3.453"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M38.217,86.756c0,0-10.123,7.107-18.804,8.819"/>
</g>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.169,74.885c0,0,1.235,0.165,4.744,3.676&#xD;&#xA;&#x9;c3.509,3.508,6.026,2.16,8.911,0.724c2.877-1.443,10.537-6.126,6.49-9.817c-4.049-3.688-6.207,1.146-9.715,2.405&#xD;&#xA;&#x9;c-3.512,1.26-5.061-2.487-6.858-4.287c-0.589-0.593-1.188-1.099-1.729-1.505c0,0-0.971-0.76-1.906,2.79&#xD;&#xA;&#x9;C51.172,72.412,50.162,73.415,52.169,74.885z"/>
<g id="Layer_2_1_">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M46.753,82.012c1.188-0.912,2.397-2.402,3.951-4.713&#xD;&#xA;&#x9;&#x9;c1.296-1.927,2.7-5.578,2.7-5.578c0.875-2.521,1.934-6.576-1.902-7.296c-1.553-0.291-4.079-0.098-7.67-0.776&#xD;&#xA;&#x9;&#x9;c-3.593-0.681-6.798-2.522-9.517,2.233c-2.718,4.757-9.59,8.271-1.056,16.563c0,0,4.901,3.842,10.764,9.639&#xD;&#xA;&#x9;&#x9;c4.831,4.775,12.045,10.602,12.045,10.602s18.972,2.188,19.535-0.693c1.922-9.79-14.777-6.911-14.777-6.911&#xD;&#xA;&#x9;&#x9;s-4.605-3.933-6.725-5.794c-3.478-3.059-11.125-10.771-11.125-10.771"/>
<path fill="#FFFFFF" d="M51.253,75.434c0,0,2.47-2.66-2.469-5.317c-4.939-2.657-7.213-0.017-8.739,1.521&#xD;&#xA;&#x9;&#x9;c-2.644,2.655,3.443,6.611,3.443,6.611l3.176,3.204c0,0,1.738-1.647,2.499-2.979C50.036,77.26,51.253,75.434,51.253,75.434"/>
</g>
<g id="Layer_8"/>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M29.926,73.218c0.749-0.571,2.889-2.202,4.854-3.657&#xD;&#xA;&#x9;c2.428-1.799,6.117-5.849,1.077-7.646c-5.04-1.801-7.507,1.604-11.519,4.946c-2.159,1.801-5.308,2.699-4.319,6.209&#xD;&#xA;&#x9;c0.993,3.511,4.862,13.408,11.789,10.17c6.929-3.239-1.799-9.18-3.06-11.157"/>
<g id="Layer_2">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.709,14.156c-1.54-0.143-4.75-0.316-6.518-0.231&#xD;&#xA;&#x9;&#x9;c-4.728,0.225-9.224,1.928-9.224,1.928L23.949,7.357l2.235,18.906c0.646-0.782-10.555,12.804-3.479,24.224&#xD;&#xA;&#x9;&#x9;c7.08,11.426,22.233,16.518,40.988,12.792c18.755-3.729,23.229-14.531,21.986-20.246c-1.242-5.714-8.322-7.823-8.322-7.823&#xD;&#xA;&#x9;&#x9;s-0.09-4.48-3.328-9.97c-1.926-3.268-8.348-8.041-8.348-8.041L62.822,5.647l-7.452,7.204L52.709,14.156z"/>
<path fill="#FFFFFF" d="M76.42,35.066l-2.482-2.064l-9.115,2.661c0,0,0,3.419-4.367,4.367c-4.37,0.951-11.211-2.277-11.211-2.277&#xD;&#xA;&#x9;&#x9;L41.46,41.17c0,0-8.437,0.928-8.739,6.081C32.048,58.704,46.1,63.479,51.425,63.783c2.905,0.167,8.235-0.338,12.277-1.141&#xD;&#xA;&#x9;&#x9;c17.752-3.234,22.551-13.919,21.31-19.635c-1.242-5.714-7.978-7.196-7.978-7.196L76.42,35.066z"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M10.673,46.155c0,0,4.107,0.374,5.974,0.268&#xD;&#xA;&#x9;&#x9;c1.865-0.107,5.492-0.587,5.492-0.587"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M81.656,40.671c0,0,4.549-0.743,6.859-1.549&#xD;&#xA;&#x9;&#x9;c2.715-0.942,4.543-2.545,4.543-2.545"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M22.337,41.909c0,0-2.384-1.777-6.117-3.43&#xD;&#xA;&#x9;&#x9;c-4.134-1.831-6.405-2.303-6.405-2.303"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M82.117,46.622c0,0,2.726,1.104,5.533,1.385&#xD;&#xA;&#x9;&#x9;c2.77,0.276,4.646,0.11,4.646,0.11"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M52.35,14.212&#xD;&#xA;&#x9;&#x9;c2.84,0.7,3.887,1.469,3.887,1.469"/>
<line fill="none" stroke="#000000" x1="33.898" y1="13.684" x2="39.956" y2="18.042"/>
</g>
<g id="Layer_5">
<path fill="#FFFFFF" stroke="#231F20" d="M71.84,25.366c2.924,4.479,3.033,9.591,0.242,11.415&#xD;&#xA;&#x9;&#x9;c-2.793,1.825-7.426-0.332-10.354-4.813c-2.933-4.48-3.037-9.589-0.244-11.415C64.275,18.73,68.913,20.884,71.84,25.366z"/>
<path fill="#231F20" d="M71.089,32.522c0,1.08-0.802,1.956-1.8,1.956c-0.993,0-1.803-0.877-1.803-1.956&#xD;&#xA;&#x9;&#x9;c0-1.08,0.81-1.958,1.803-1.958C70.287,30.564,71.089,31.442,71.089,32.522"/>
</g>
<g id="Layer_7">
<path fill="#FFFFFF" stroke="#231F20" d="M47.867,28.619c2.926,4.48,2.619,9.862-0.681,12.015&#xD;&#xA;&#x9;&#x9;c-3.302,2.159-8.351,0.272-11.276-4.208c-2.928-4.48-2.624-9.86,0.678-12.017C39.891,22.253,44.938,24.137,47.867,28.619z"/>
<path fill="#231F20" d="M46.079,34.507c0,1.081-0.803,1.957-1.801,1.957c-0.992,0-1.803-0.878-1.803-1.957&#xD;&#xA;&#x9;&#x9;c0-1.08,0.811-1.957,1.803-1.957C45.274,32.55,46.079,33.427,46.079,34.507"/>
</g>
<path fill="#5E4A42" stroke="#000000" d="M59.766,37.926c1.854,0,4.555-0.284,4.697,0.569c0.143,0.855-1.709,4.203-2.988,4.345&#xD;&#xA;&#x9;c-1.283,0.142-6.125-2.353-6.195-3.919C55.206,37.355,58.055,37.926,59.766,37.926z"/>
<g id="Layer_4">
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M46.774,45.235c0,0,10.347,3.054,14.217,3.897&#xD;&#xA;&#x9;&#x9;c3.868,0.842,10.851,1.684,10.851,1.684s-7.99,10.245-17.328,7.644C45.176,55.863,45.345,49.975,46.774,45.235z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,37 @@
<svg version="1.1" id="cat" x="0px" y="0px" width="95px" height="111px" viewBox="0 0 95 111" enable-background="new 0 0 95 111" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g>
<g id="Layer_8">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M23.754,82.506c-2.871-1.402-10.511-2.518-14.771-6.695&#xD;&#xA;&#x9;&#x9;c-9.431-9.246-3.824-21.732,0.704-21.797c3.611-0.061,3.363,5.482,4.622,11.145c0.875,3.939,1.673,6.988,6.407,9.674&#xD;&#xA;&#x9;&#x9;c4.735,2.688,6.918,2.916,6.918,2.916s3.258,0.791,1.596,2.562C27.568,82.076,23.754,82.506,23.754,82.506z"/>
<path fill="#FFFFFF" d="M14.291,66.369c-0.115-0.459-0.222-0.928-0.33-1.413c-1.172-5.271-0.941-10.432-4.303-10.377&#xD;&#xA;&#x9;&#x9;c-3.433,0.05-7.53,7.771-4.19,15.285c0.193-0.143,0.399-0.336,0.569-0.588c0.451-0.664,0.821-2.969,2.654-3.688&#xD;&#xA;&#x9;&#x9;C9.989,65.082,12.778,65.869,14.291,66.369z"/>
</g>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M52.543,61.473c0,0,1.455,2.342,1.519,4.969&#xD;&#xA;&#x9;c0.065,2.723-0.351,6.984,1.804,8.158c3.381,1.842,9.959-0.463,10.174,5.191c0.211,5.652-10.152,3.623-13.299,2.547&#xD;&#xA;&#x9;c-2.695-0.926-4.559-1.758-5.18-5.182c0,0-3.764-13.969-0.196-15.59C50.933,59.945,52.543,61.473,52.543,61.473z"/>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M14.61,89.584c0,0,1.504,10.391-5.922,8.387&#xD;&#xA;&#x9;c-5.586-1.51-5.705-20.68-0.406-20.691c3.237-0.006,4.425,2.859,4.425,2.859s4.385,4.812,6.95,4.312&#xD;&#xA;&#x9;c2.565-0.502,7.978-6.703,7.978-6.703s2.875-2.539,6.146,0.023c3.271,2.561,1.521,6.178,1.521,6.178s-6.07,7.072-11.014,8.072&#xD;&#xA;&#x9;c-4.945,1.002-9.375-2.568-11.58-3.451"/>
<g id="Layer_6">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M42.435,76.238c0,0,4.866,4.062,8.166,7.52&#xD;&#xA;&#x9;&#x9;c4.384,4.592-3.339,15.039-3.339,15.039s11.234,6.467,2.998,11c-2.657,1.467-15.661-13.225-15.661-13.225s7.979-7.475,4.972-10.172&#xD;&#xA;&#x9;&#x9;c-4.964-4.449-10.771-4.959-11.6-9.824c-0.502-2.957,0.613-4.502,2.658-7.053c1.354-1.689,2.084-3.182,2.084-3.182&#xD;&#xA;&#x9;&#x9;s1.135-7.238,4.289-8.254c3.153-1.014,8.842,0.057,10.758,0.395c1.914,0.338,6.645,1.877,4.617,9.922c0,0-1.451,3.771-2.789,5.762&#xD;&#xA;&#x9;&#x9;c-1.604,2.389-2.854,3.924-4.08,4.867"/>
<path fill="#FFFFFF" d="M50.182,72.631c0,0,2.55-2.748-2.551-5.494c-5.102-2.746-7.449-0.016-9.024,1.572&#xD;&#xA;&#x9;&#x9;c-2.731,2.742,3.334,7.453,3.334,7.453l3.383,2.564c0,0,1.916-1.582,2.701-2.957C48.807,74.396,50.182,72.631,50.182,72.631"/>
</g>
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M24.428,65.395c0.939,0.057,3.625,0.213,6.064,0.391&#xD;&#xA;&#x9;c3.014,0.217,8.451-0.451,5.801-5.098c-2.649-4.65-6.74-3.674-11.961-3.754c-2.812-0.043-5.785-1.412-7.322,1.895&#xD;&#xA;&#x9;c-1.534,3.312-5.051,13.338,2.313,15.396c7.366,2.057,4.618-8.137,4.95-10.457"/>
<g id="Layer_2">
<path fill="#FAA51D" stroke="#231F20" stroke-width="1.2" d="M51.709,10.156c-1.54-0.143-4.75-0.316-6.518-0.231&#xD;&#xA;&#x9;&#x9;c-4.728,0.225-9.224,1.928-9.224,1.928L22.949,3.357l2.235,18.906c0.646-0.782-10.555,12.804-3.479,24.224&#xD;&#xA;&#x9;&#x9;c7.08,11.426,22.233,16.518,40.988,12.792c18.755-3.728,23.229-14.531,21.986-20.246c-1.242-5.714-8.322-7.823-8.322-7.823&#xD;&#xA;&#x9;&#x9;s-0.09-4.48-3.328-9.97c-1.926-3.268-8.348-8.041-8.348-8.041L61.822,1.647L54.37,8.851L51.709,10.156z"/>
<path fill="#FFFFFF" d="M75.42,31.066l-2.483-2.064l-9.115,2.661c0,0,0,3.419-4.367,4.367c-4.37,0.951-11.211-2.277-11.211-2.277&#xD;&#xA;&#x9;&#x9;l-7.784,3.417c0,0-8.437,0.928-8.739,6.081C31.048,54.704,45.1,59.478,50.425,59.783c2.905,0.167,8.235-0.337,12.277-1.14&#xD;&#xA;&#x9;&#x9;c17.752-3.235,22.551-13.92,21.309-19.635c-1.242-5.714-7.977-7.196-7.977-7.196L75.42,31.066z"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M9.673,42.155c0,0,4.107,0.374,5.974,0.268&#xD;&#xA;&#x9;&#x9;c1.865-0.107,5.492-0.587,5.492-0.587"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M80.656,36.671c0,0,4.549-0.743,6.859-1.549&#xD;&#xA;&#x9;&#x9;c2.715-0.942,4.543-2.545,4.543-2.545"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M21.337,37.909c0,0-2.384-1.777-6.117-3.43&#xD;&#xA;&#x9;&#x9;c-4.134-1.831-6.405-2.303-6.405-2.303"/>
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M81.117,42.622c0,0,2.726,1.104,5.533,1.385&#xD;&#xA;&#x9;&#x9;c2.77,0.276,4.647,0.11,4.647,0.11"/>
<path fill="none" stroke="#000000" stroke-linecap="round" stroke-miterlimit="10" d="M51.349,10.212&#xD;&#xA;&#x9;&#x9;c2.84,0.7,3.888,1.469,3.888,1.469"/>
<line fill="none" stroke="#000000" x1="32.898" y1="9.684" x2="38.956" y2="14.042"/>
</g>
<g id="Layer_5">
<path fill="#FFFFFF" stroke="#231F20" d="M70.84,21.366c2.924,4.479,3.033,9.591,0.242,11.415&#xD;&#xA;&#x9;&#x9;c-2.793,1.825-7.426-0.332-10.354-4.813c-2.932-4.48-3.037-9.589-0.244-11.415C63.275,14.73,67.913,16.884,70.84,21.366z"/>
<path fill="#231F20" d="M70.089,28.522c0,1.08-0.802,1.956-1.8,1.956c-0.993,0-1.803-0.877-1.803-1.956&#xD;&#xA;&#x9;&#x9;c0-1.08,0.81-1.958,1.803-1.958C69.287,26.564,70.089,27.442,70.089,28.522"/>
</g>
<g id="Layer_7">
<path fill="#FFFFFF" stroke="#231F20" d="M46.867,24.619c2.926,4.48,2.619,9.862-0.681,12.015&#xD;&#xA;&#x9;&#x9;c-3.302,2.159-8.351,0.272-11.276-4.208c-2.928-4.48-2.624-9.86,0.678-12.017C38.891,18.253,43.938,20.137,46.867,24.619z"/>
<path fill="#231F20" d="M45.079,30.507c0,1.081-0.803,1.957-1.801,1.957c-0.992,0-1.803-0.878-1.803-1.957&#xD;&#xA;&#x9;&#x9;c0-1.08,0.811-1.957,1.803-1.957C44.274,28.55,45.079,29.427,45.079,30.507"/>
</g>
<path fill="#5E4A42" stroke="#000000" d="M58.766,33.926c1.854,0,4.555-0.284,4.697,0.569c0.143,0.855-1.709,4.203-2.988,4.345&#xD;&#xA;&#x9;c-1.283,0.142-6.125-2.353-6.195-3.919C54.206,33.355,57.055,33.926,58.766,33.926z"/>
<g id="Layer_4">
<path fill="none" stroke="#231F20" stroke-width="1.2" d="M45.774,41.235c0,0,10.347,3.054,14.217,3.897&#xD;&#xA;&#x9;&#x9;c3.868,0.842,10.851,1.684,10.851,1.684s-7.99,10.245-17.328,7.644C44.176,51.863,44.345,45.975,45.774,41.235z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.
Binary file not shown.
+49
View File
@@ -0,0 +1,49 @@
import {TextEncoder} from 'text-encoding';
import projectJson from './project.json';
/* eslint-disable import/no-unresolved */
import popWav from '!buffer-loader!./83a9787d4cb6f3b7632b4ddfebf74367.wav';
import meowWav from '!buffer-loader!./83c36d806dc92327b9e7049a565c6bff.wav';
import backdrop from '!buffer-loader!./739b5e2a2435f6e1ec2993791b423146.png';
import penLayer from '!buffer-loader!./5c81a336fab8be57adc039a8a2b33ca9.png';
import costume1 from '!raw-loader!./09dc888b0b7df19f70d81588ae73420e.svg';
import costume2 from '!raw-loader!./3696356a03a8d938318876a593572843.svg';
/* eslint-enable import/no-unresolved */
const encoder = new TextEncoder();
export default [{
id: 0,
assetType: 'Project',
dataFormat: 'JSON',
data: JSON.stringify(projectJson)
}, {
id: '83a9787d4cb6f3b7632b4ddfebf74367',
assetType: 'Sound',
dataFormat: 'WAV',
data: popWav
}, {
id: '83c36d806dc92327b9e7049a565c6bff',
assetType: 'Sound',
dataFormat: 'WAV',
data: meowWav
}, {
id: '739b5e2a2435f6e1ec2993791b423146',
assetType: 'ImageBitmap',
dataFormat: 'PNG',
data: backdrop
}, {
id: '5c81a336fab8be57adc039a8a2b33ca9',
assetType: 'ImageBitmap',
dataFormat: 'PNG',
data: penLayer
}, {
id: '09dc888b0b7df19f70d81588ae73420e',
assetType: 'ImageVector',
dataFormat: 'SVG',
data: encoder.encode(costume1)
}, {
id: '3696356a03a8d938318876a593572843',
assetType: 'ImageVector',
dataFormat: 'SVG',
data: encoder.encode(costume2)
}];
File renamed without changes.
-9
View File
@@ -1,9 +0,0 @@
[
{
"name": "Pen",
"extensionURL": "pen",
"md5": "1a2ec605e73000897797f0c851134f5b.png",
"description": "Draw with your sprites.",
"featured": true
}
]
+19
View File
@@ -0,0 +1,19 @@
import penImage from './pen.png';
import wedoImage from './wedo.png';
export default [
{
name: 'Pen',
extensionURL: 'pen',
iconURL: penImage,
description: 'Draw with your sprites.',
featured: true
},
{
name: 'Lego WeDo 2.0',
extensionURL: 'wedo2',
iconURL: wedoImage,
description: 'Build with motors and sensors.',
featured: true
}
];
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+74 -94
View File
@@ -1,78 +1,6 @@
const separator = '<sep gap="45"/>';
const categorySeparator = '<sep gap="36"/>';
const top = `
<category name="Top" colour="#FFFFFF" secondaryColour="#CCCCCC">
<block type="event_whenflagclicked"/>
<block type="event_whenkeypressed">
</block>
<block type="event_whenthisspriteclicked"/>
<block type="motion_movesteps">
<value name="STEPS">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="motion_turnright">
<value name="DEGREES">
<shadow type="math_number">
<field name="NUM">15</field>
</shadow>
</value>
</block>
<block type="motion_ifonedgebounce"/>
<block type="sound_playuntildone">
<value name="SOUND_MENU">
<shadow type="sound_sounds_menu"/>
</value>
</block>
<block type="looks_sayforsecs">
<value name="MESSAGE">
<shadow type="text">
<field name="TEXT">Hello!</field>
</shadow>
</value>
<value name="SECS">
<shadow type="math_number">
<field name="NUM">2</field>
</shadow>
</value>
</block>
<block type="looks_changeeffectby">
<value name="CHANGE">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="control_repeat">
<value name="TIMES">
<shadow type="math_whole_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="control_wait">
<value name="DURATION">
<shadow type="math_positive_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="operator_random">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
</category>
`;
const blockSeparator = '<sep gap="36"/>'; // At default scale, about 28px
const motion = `
<category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
@@ -97,6 +25,7 @@ const motion = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="motion_pointindirection">
<value name="DIRECTION">
<shadow type="math_angle">
@@ -110,6 +39,7 @@ const motion = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="motion_gotoxy">
<value name="X">
<shadow id="movex" type="math_number">
@@ -156,6 +86,7 @@ const motion = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="motion_changexby">
<value name="DX">
<shadow type="math_number">
@@ -184,11 +115,15 @@ const motion = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="motion_ifonedgebounce"/>
${blockSeparator}
<block type="motion_setrotationstyle"/>
<block type="motion_xposition"/>
<block type="motion_yposition"/>
<block type="motion_direction"/>
${blockSeparator}
<block id="xposition" type="motion_xposition"/>
<block id="yposition" type="motion_yposition"/>
<block id="direction" type="motion_direction"/>
${categorySeparator}
</category>
`;
@@ -232,8 +167,10 @@ const looks = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="looks_show"/>
<block type="looks_hide"/>
${blockSeparator}
<block type="looks_switchcostumeto">
<value name="COSTUME">
<shadow type="looks_costume"/>
@@ -251,6 +188,7 @@ const looks = `
<shadow type="looks_backdrops"/>
</value>
</block>
${blockSeparator}
<block type="looks_changeeffectby">
<value name="CHANGE">
<shadow type="math_number">
@@ -266,6 +204,7 @@ const looks = `
</value>
</block>
<block type="looks_cleargraphiceffects"/>
${blockSeparator}
<block type="looks_changesizeby">
<value name="CHANGE">
<shadow type="math_number">
@@ -280,6 +219,7 @@ const looks = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="looks_gotofront"/>
<block type="looks_gobacklayers">
<value name="NUM">
@@ -288,10 +228,12 @@ const looks = `
</shadow>
</value>
</block>
<block type="looks_costumeorder"/>
<block type="looks_backdroporder"/>
<block type="looks_backdropname"/>
<block type="looks_size"/>
${blockSeparator}
<block id="costumeorder" type="looks_costumeorder"/>
<block id="backdroporder" type="looks_backdroporder"/>
<block id="backdropname" type="looks_backdropname"/>
<block id="size" type="looks_size"/>
${categorySeparator}
</category>
`;
@@ -308,6 +250,7 @@ const sound = `
</value>
</block>
<block type="sound_stopallsounds"/>
${blockSeparator}
<block type="sound_playdrumforbeats">
<value name="DRUM">
<shadow type="sound_drums_menu"/>
@@ -325,6 +268,7 @@ const sound = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="sound_playnoteforbeats">
<value name="NOTE">
<shadow type="math_number">
@@ -342,6 +286,7 @@ const sound = `
<shadow type="sound_instruments_menu"/>
</value>
</block>
${blockSeparator}
<block type="sound_changeeffectby">
<value name="VALUE">
<shadow type="math_number">
@@ -357,6 +302,7 @@ const sound = `
</value>
</block>
<block type="sound_cleareffects"/>
${blockSeparator}
<block type="sound_changevolumeby">
<value name="VOLUME">
<shadow type="math_number">
@@ -371,7 +317,8 @@ const sound = `
</shadow>
</value>
</block>
<block type="sound_volume"/>
<block id="volume" type="sound_volume"/>
${blockSeparator}
<block type="sound_changetempoby">
<value name="TEMPO">
<shadow type="math_number">
@@ -386,7 +333,8 @@ const sound = `
</shadow>
</value>
</block>
<block type="sound_tempo"/>
<block id="tempo" type="sound_tempo"/>
${categorySeparator}
</category>
`;
@@ -398,6 +346,7 @@ const events = `
<block type="event_whenthisspriteclicked"/>
<block type="event_whenbackdropswitchesto">
</block>
${blockSeparator}
<block type="event_whengreaterthan">
<value name="VALUE">
<shadow type="math_number">
@@ -405,6 +354,7 @@ const events = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="event_whenbroadcastreceived">
</block>
<block type="event_broadcast">
@@ -417,6 +367,7 @@ const events = `
<shadow type="event_broadcast_menu"/>
</value>
</block>
${categorySeparator}
</category>
`;
@@ -429,6 +380,7 @@ const control = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="control_repeat">
<value name="TIMES">
<shadow type="math_whole_number">
@@ -437,11 +389,14 @@ const control = `
</value>
</block>
<block type="control_forever"/>
${blockSeparator}
<block type="control_if"/>
<block type="control_if_else"/>
<block type="control_wait_until"/>
<block type="control_repeat_until"/>
${blockSeparator}
<block type="control_stop"/>
${blockSeparator}
<block type="control_start_as_clone"/>
<block type="control_create_clone_of">
<value name="CLONE_OPTION">
@@ -449,6 +404,7 @@ const control = `
</value>
</block>
<block type="control_delete_this_clone"/>
${categorySeparator}
</category>
`;
@@ -477,6 +433,7 @@ const sensing = `
<shadow type="sensing_distancetomenu"/>
</value>
</block>
${blockSeparator}
<block type="sensing_askandwait">
<value name="QUESTION">
<shadow type="text">
@@ -484,7 +441,8 @@ const sensing = `
</shadow>
</value>
</block>
<block type="sensing_answer"/>
<block id="answer" type="sensing_answer"/>
${blockSeparator}
<block type="sensing_keypressed">
<value name="KEY_OPTION">
<shadow type="sensing_keyoptions"/>
@@ -493,23 +451,28 @@ const sensing = `
<block type="sensing_mousedown"/>
<block type="sensing_mousex"/>
<block type="sensing_mousey"/>
<block type="sensing_loudness"/>
<block type="sensing_timer"/>
${blockSeparator}
<block id="loudness" type="sensing_loudness"/>
${blockSeparator}
<block id="timer" type="sensing_timer"/>
<block type="sensing_resettimer"/>
<block type="sensing_of">
${blockSeparator}
<block id="of" type="sensing_of">
<value name="PROPERTY">
<shadow type="sensing_of_property_menu"/>
<shadow id="sensing_of_property_menu" type="sensing_of_property_menu"/>
</value>
<value name="OBJECT">
<shadow type="sensing_of_object_menu"/>
<shadow id="sensing_of_object_menu" type="sensing_of_object_menu"/>
</value>
</block>
<block type="sensing_current">
${blockSeparator}
<block id="current" type="sensing_current">
<value name="CURRENTMENU">
<shadow type="sensing_currentmenu"/>
<shadow id="sensing_currentmenu" type="sensing_currentmenu"/>
</value>
</block>
<block type="sensing_dayssince2000"/>
${categorySeparator}
</category>
`;
@@ -563,6 +526,7 @@ const operators = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="operator_random">
<value name="FROM">
<shadow type="math_number">
@@ -575,6 +539,7 @@ const operators = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="operator_lt">
<value name="OPERAND1">
<shadow type="text">
@@ -611,9 +576,11 @@ const operators = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="operator_and"/>
<block type="operator_or"/>
<block type="operator_not"/>
${blockSeparator}
<block type="operator_join">
<value name="STRING1">
<shadow type="text">
@@ -645,6 +612,19 @@ const operators = `
</shadow>
</value>
</block>
<block type="operator_contains" id="operator_contains">
<value name="STRING1">
<shadow type="text">
<field name="TEXT">hello</field>
</shadow>
</value>
<value name="STRING2">
<shadow type="text">
<field name="TEXT">world</field>
</shadow>
</value>
</block>
${blockSeparator}
<block type="operator_mod">
<value name="NUM1">
<shadow type="math_number">
@@ -664,6 +644,7 @@ const operators = `
</shadow>
</value>
</block>
${blockSeparator}
<block type="operator_mathop">
<value name="NUM">
<shadow type="math_number">
@@ -671,6 +652,7 @@ const operators = `
</shadow>
</value>
</block>
${categorySeparator}
</category>
`;
@@ -687,11 +669,10 @@ const xmlClose = '</xml>';
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
*/
const makeToolboxXML = function (categoriesXML) {
const gap = [separator];
const gap = [categorySeparator];
const everything = [
xmlOpen,
top, gap,
motion, gap,
looks, gap,
sound, gap,
@@ -707,7 +688,6 @@ const makeToolboxXML = function (categoriesXML) {
}
everything.push(xmlClose);
return everything.join('\n');
};
+15 -34
View File
@@ -1,26 +1,7 @@
import React from 'react';
import xhr from 'xhr';
import log from './log';
import emptyProject from './empty-project.json';
class ProjectLoaderConstructor {
get DEFAULT_PROJECT_DATA () {
return emptyProject;
}
load (id, callback) {
callback = callback || (err => log.error(err));
xhr({
uri: `https://projects.scratch.mit.edu/internalapi/project/${id}/get/`
}, (err, res, body) => {
if (err) return callback(err);
callback(null, body);
});
}
}
const ProjectLoader = new ProjectLoaderConstructor();
import storage from './storage';
/* Higher Order Component to provide behavior for loading projects by id from
* the window's hash (#this part in the url)
@@ -35,13 +16,23 @@ const ProjectLoaderHOC = function (WrappedComponent) {
this.updateProject = this.updateProject.bind(this);
this.state = {
projectId: null,
projectData: this.fetchProjectId().length ? null : JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)
projectData: null
};
}
componentDidMount () {
window.addEventListener('hashchange', this.updateProject);
this.updateProject();
}
componentDidUpdate (prevProps, prevState) {
if (this.state.projectId !== prevState.projectId) {
storage
.load(storage.AssetType.Project, this.state.projectId, storage.DataFormat.JSON)
.then(projectAsset => projectAsset && this.setState({
projectData: projectAsset.data.toString()
}))
.catch(err => log.error(err));
}
}
componentWillUnmount () {
window.removeEventListener('hashchange', this.updateProject);
}
@@ -49,18 +40,9 @@ const ProjectLoaderHOC = function (WrappedComponent) {
return window.location.hash.substring(1);
}
updateProject () {
const projectId = this.fetchProjectId();
let projectId = this.fetchProjectId();
if (projectId !== this.state.projectId) {
if (projectId.length < 1) {
return this.setState({
projectId: projectId,
projectData: JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA)
});
}
ProjectLoader.load(projectId, (err, body) => {
if (err) return log.error(err);
this.setState({projectData: body});
});
if (projectId.length < 1) projectId = 0;
this.setState({projectId: projectId});
}
}
@@ -80,6 +62,5 @@ const ProjectLoaderHOC = function (WrappedComponent) {
export {
ProjectLoaderHOC as default,
ProjectLoader
ProjectLoaderHOC as default
};
+11 -1
View File
@@ -1,5 +1,7 @@
import ScratchStorage from 'scratch-storage';
import defaultProjectAssets from './default-project';
const PROJECT_SERVER = 'https://cdn.projects.scratch.mit.edu';
const ASSET_SERVER = 'https://cdn.assets.scratch.mit.edu';
@@ -23,7 +25,15 @@ class Storage extends ScratchStorage {
[this.AssetType.ImageVector, this.AssetType.ImageBitmap, this.AssetType.Sound],
asset => `${ASSET_SERVER}/internalapi/asset/${asset.assetId}.${asset.dataFormat}/get/`
);
defaultProjectAssets.forEach(asset => this.cache(
this.AssetType[asset.assetType],
this.DataFormat[asset.dataFormat],
asset.data,
asset.id
));
}
}
export default Storage;
const storage = new Storage();
export default storage;
+2 -2
View File
@@ -1,9 +1,9 @@
import VM from 'scratch-vm';
import Storage from '../lib/storage';
import storage from '../lib/storage';
const SET_VM = 'scratch-gui/vm/SET_VM';
const defaultVM = new VM();
defaultVM.attachStorage(new Storage());
defaultVM.attachStorage(storage);
const initialState = defaultVM;
const reducer = function (state, action) {
+1
View File
@@ -72,6 +72,7 @@ describe('blocks example', () => {
await clickText('Sensing');
await clickText('Operators');
await clickText('Data');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickText('Create variable...');
let el = await findByXpath("//input[@placeholder='']");
await el.sendKeys('score');
+12 -2
View File
@@ -23,6 +23,7 @@ const blocksTabScope = "*[@id='react-tabs-1']";
const costumesTabScope = "*[@id='react-tabs-3']";
const soundsTabScope = "*[@id='react-tabs-5']";
const reportedValueScope = '*[@class="blocklyDropDownContent"]';
const modalScope = '*[@class="ReactModalPortal"]';
describe('costumes, sounds and variables', () => {
beforeAll(() => {
@@ -135,9 +136,18 @@ describe('costumes, sounds and variables', () => {
await driver.get(`file://${uri}`);
await clickText('Blocks');
await clickText('Extensions');
await clickText('Pen'); // Modal closes
await clickText('Pen', blocksTabScope); // Click the new category
await clickText('Pen', modalScope); // Modal closes
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickText('stamp', blocksTabScope); // Click the "stamp" block
// Make sure trying to load the extension again scrolls back down
await clickText('Motion', blocksTabScope); // To scroll the list back to the top
await clickText('Extensions');
await clickText('Pen', modalScope); // Modal closes
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for scroll animation
await clickText('stamp', blocksTabScope); // Would fail if didn't scroll back
const logs = await getLogs(errorWhitelist);
await expect(logs).toEqual([]);
});
+27 -17
View File
@@ -1,5 +1,6 @@
import React from 'react';
import ProjectLoaderHOC, {ProjectLoader} from '../../../src/lib/project-loader-hoc.jsx';
import ProjectLoaderHOC from '../../../src/lib/project-loader-hoc.jsx';
import storage from '../../../src/lib/storage';
import {mount} from 'enzyme';
describe('ProjectLoaderHOC', () => {
@@ -7,34 +8,41 @@ describe('ProjectLoaderHOC', () => {
const Component = ({projectData}) => <div>{projectData}</div>;
const WrappedComponent = ProjectLoaderHOC(Component);
window.location.hash = '#winning';
ProjectLoader.load = jest.fn((id, cb) => cb(null, null));
const originalLoad = storage.load;
storage.load = jest.fn(() => Promise.resolve(null));
const mounted = mount(<WrappedComponent />);
ProjectLoader.load.mockRestore();
storage.load = originalLoad;
window.location.hash = '';
expect(mounted.find('div').exists()).toEqual(false);
const mountedDiv = mounted.find('div');
expect(mountedDiv.exists()).toEqual(false);
});
test('when there is no hash, it loads the default project', () => {
const Component = ({projectData}) => <div>{projectData}</div>;
const WrappedComponent = ProjectLoaderHOC(Component);
window.location.hash = '';
const originalLoad = storage.load;
storage.load = jest.fn((type, id) => Promise.resolve(id));
const mounted = mount(<WrappedComponent />);
expect(mounted.find('div').text()).toEqual(JSON.stringify(ProjectLoader.DEFAULT_PROJECT_DATA));
expect(mounted.state().projectId).toEqual(0);
expect(storage.load).toHaveBeenCalledWith(
storage.AssetType.Project, 0, storage.DataFormat.JSON
);
storage.load = originalLoad;
});
test('when there is a hash, it tries to load that project', () => {
const Component = ({projectData}) => <div>{projectData}</div>;
const WrappedComponent = ProjectLoaderHOC(Component);
window.location.hash = '#winning';
ProjectLoader.load = jest.fn((id, cb) => cb(null, id));
const originalLoad = storage.load;
storage.load = jest.fn((type, id) => Promise.resolve({data: id}));
const mounted = mount(<WrappedComponent />);
mounted.update();
ProjectLoader.load.mockRestore();
window.location.hash = '';
expect(mounted
.find('div')
.text()
).toEqual('winning');
expect(mounted.state().projectId).toEqual('winning');
expect(storage.load).toHaveBeenLastCalledWith(
storage.AssetType.Project, 'winning', storage.DataFormat.JSON
);
storage.load = originalLoad;
});
test('when hash change happens, the project data state is changed', () => {
@@ -42,10 +50,12 @@ describe('ProjectLoaderHOC', () => {
const WrappedComponent = ProjectLoaderHOC(Component);
window.location.hash = '';
const mounted = mount(<WrappedComponent />);
const before = mounted.find('div').text();
ProjectLoader.load = jest.fn((id, cb) => cb(null, id));
window.location.hash = `#winning`;
expect(mounted.state().projectId).toEqual(0);
const originalLoad = storage.load;
storage.load = jest.fn((type, id) => Promise.resolve({data: id}));
window.location.hash = '#winning';
mounted.instance().updateProject();
expect(mounted.find('div').text()).not.toEqual(before);
expect(mounted.state().projectId).toEqual('winning');
storage.load = originalLoad;
});
});
+9 -2
View File
@@ -21,6 +21,7 @@ module.exports = {
lib: ['react', 'react-dom'],
gui: './src/index.jsx',
blocksonly: './src/examples/blocks-only.jsx',
compatibilitytesting: './src/examples/compatibility-testing.jsx',
player: './src/examples/player.jsx'
},
output: {
@@ -66,8 +67,8 @@ module.exports = {
}]
},
{
test: /\.svg$/,
loader: 'svg-url-loader?noquotes'
test: /\.(svg|png|wav)$/,
loader: 'file-loader'
}]
},
plugins: [
@@ -90,6 +91,12 @@ module.exports = {
filename: 'blocks-only.html',
title: 'Scratch 3.0 GUI: Blocks Only Example'
}),
new HtmlWebpackPlugin({
chunks: ['lib', 'compatibilitytesting'],
template: 'src/index.ejs',
filename: 'compatibility-testing.html',
title: 'Scratch 3.0 GUI: Compatibility Testing'
}),
new HtmlWebpackPlugin({
chunks: ['lib', 'player'],
template: 'src/index.ejs',