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 (
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const App = AppStateHOC(ProjectLoaderHOC(Player));
+
+const appTarget = document.createElement('div');
+document.body.appendChild(appTarget);
+
+ReactDOM.render(, appTarget);
diff --git a/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg b/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg
new file mode 100755
index 000000000..d449b3d15
--- /dev/null
+++ b/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg
@@ -0,0 +1,42 @@
+
\ No newline at end of file
diff --git a/src/lib/default-project/3696356a03a8d938318876a593572843.svg b/src/lib/default-project/3696356a03a8d938318876a593572843.svg
new file mode 100755
index 000000000..0ecb2de81
--- /dev/null
+++ b/src/lib/default-project/3696356a03a8d938318876a593572843.svg
@@ -0,0 +1,37 @@
+
\ No newline at end of file
diff --git a/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png b/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png
new file mode 100755
index 000000000..da373d2cf
Binary files /dev/null and b/src/lib/default-project/5c81a336fab8be57adc039a8a2b33ca9.png differ
diff --git a/src/lib/default-project/739b5e2a2435f6e1ec2993791b423146.png b/src/lib/default-project/739b5e2a2435f6e1ec2993791b423146.png
new file mode 100755
index 000000000..b395ac94c
Binary files /dev/null and b/src/lib/default-project/739b5e2a2435f6e1ec2993791b423146.png differ
diff --git a/src/lib/default-project/83a9787d4cb6f3b7632b4ddfebf74367.wav b/src/lib/default-project/83a9787d4cb6f3b7632b4ddfebf74367.wav
new file mode 100755
index 000000000..fc3b2724a
Binary files /dev/null and b/src/lib/default-project/83a9787d4cb6f3b7632b4ddfebf74367.wav differ
diff --git a/src/lib/default-project/83c36d806dc92327b9e7049a565c6bff.wav b/src/lib/default-project/83c36d806dc92327b9e7049a565c6bff.wav
new file mode 100755
index 000000000..45742d5ef
Binary files /dev/null and b/src/lib/default-project/83c36d806dc92327b9e7049a565c6bff.wav differ
diff --git a/src/lib/default-project/index.js b/src/lib/default-project/index.js
new file mode 100644
index 000000000..bc1ace999
--- /dev/null
+++ b/src/lib/default-project/index.js
@@ -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)
+}];
diff --git a/src/lib/empty-project.json b/src/lib/default-project/project.json
similarity index 100%
rename from src/lib/empty-project.json
rename to src/lib/default-project/project.json
diff --git a/src/lib/libraries/extensions.json b/src/lib/libraries/extensions.json
deleted file mode 100644
index 80edbcb55..000000000
--- a/src/lib/libraries/extensions.json
+++ /dev/null
@@ -1,9 +0,0 @@
-[
- {
- "name": "Pen",
- "extensionURL": "pen",
- "md5": "1a2ec605e73000897797f0c851134f5b.png",
- "description": "Draw with your sprites.",
- "featured": true
- }
-]
diff --git a/src/lib/libraries/extensions/index.js b/src/lib/libraries/extensions/index.js
new file mode 100644
index 000000000..4a03a896b
--- /dev/null
+++ b/src/lib/libraries/extensions/index.js
@@ -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
+ }
+];
diff --git a/src/lib/libraries/extensions/pen.png b/src/lib/libraries/extensions/pen.png
new file mode 100644
index 000000000..f02064c42
Binary files /dev/null and b/src/lib/libraries/extensions/pen.png differ
diff --git a/src/lib/libraries/extensions/wedo.png b/src/lib/libraries/extensions/wedo.png
new file mode 100644
index 000000000..e85192567
Binary files /dev/null and b/src/lib/libraries/extensions/wedo.png differ
diff --git a/src/lib/make-toolbox-xml.js b/src/lib/make-toolbox-xml.js
index 174319ec4..c822ef332 100644
--- a/src/lib/make-toolbox-xml.js
+++ b/src/lib/make-toolbox-xml.js
@@ -1,78 +1,6 @@
-const separator = '';
+const categorySeparator = '';
-const top = `
-
-
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 15
-
-
-
-
-
-
-
-
-
-
-
-
- Hello!
-
-
-
-
- 2
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 10
-
-
-
-
-
-
- 1
-
-
-
-
-
-
- 1
-
-
-
-
- 10
-
-
-
-
-`;
+const blockSeparator = ''; // At default scale, about 28px
const motion = `
@@ -97,6 +25,7 @@ const motion = `
+ ${blockSeparator}
@@ -110,6 +39,7 @@ const motion = `
+ ${blockSeparator}
@@ -156,6 +86,7 @@ const motion = `
+ ${blockSeparator}
@@ -184,11 +115,15 @@ const motion = `
+ ${blockSeparator}
+ ${blockSeparator}
-
-
-
+ ${blockSeparator}
+
+
+
+ ${categorySeparator}
`;
@@ -232,8 +167,10 @@ const looks = `
+ ${blockSeparator}
+ ${blockSeparator}
@@ -251,6 +188,7 @@ const looks = `
+ ${blockSeparator}
@@ -266,6 +204,7 @@ const looks = `
+ ${blockSeparator}
@@ -280,6 +219,7 @@ const looks = `
+ ${blockSeparator}
@@ -288,10 +228,12 @@ const looks = `
-
-
-
-
+ ${blockSeparator}
+
+
+
+
+ ${categorySeparator}
`;
@@ -308,6 +250,7 @@ const sound = `
+ ${blockSeparator}
@@ -325,6 +268,7 @@ const sound = `
+ ${blockSeparator}
@@ -342,6 +286,7 @@ const sound = `
+ ${blockSeparator}
@@ -357,6 +302,7 @@ const sound = `
+ ${blockSeparator}
@@ -371,7 +317,8 @@ const sound = `
-
+
+ ${blockSeparator}
@@ -386,7 +333,8 @@ const sound = `
-
+
+ ${categorySeparator}
`;
@@ -398,6 +346,7 @@ const events = `
+ ${blockSeparator}
@@ -405,6 +354,7 @@ const events = `
+ ${blockSeparator}
@@ -417,6 +367,7 @@ const events = `
+ ${categorySeparator}
`;
@@ -429,6 +380,7 @@ const control = `
+ ${blockSeparator}
@@ -437,11 +389,14 @@ const control = `
+ ${blockSeparator}
+ ${blockSeparator}
+ ${blockSeparator}
@@ -449,6 +404,7 @@ const control = `
+ ${categorySeparator}
`;
@@ -477,6 +433,7 @@ const sensing = `
+ ${blockSeparator}
@@ -484,7 +441,8 @@ const sensing = `
-
+
+ ${blockSeparator}
@@ -493,23 +451,28 @@ const sensing = `
-
-
+ ${blockSeparator}
+
+ ${blockSeparator}
+
-
+ ${blockSeparator}
+
-
+
-
+
-
+ ${blockSeparator}
+
-
+
+ ${categorySeparator}
`;
@@ -563,6 +526,7 @@ const operators = `
+ ${blockSeparator}
@@ -575,6 +539,7 @@ const operators = `
+ ${blockSeparator}
@@ -611,9 +576,11 @@ const operators = `
+ ${blockSeparator}
+ ${blockSeparator}
@@ -645,6 +612,19 @@ const operators = `
+
+
+
+ hello
+
+
+
+
+ world
+
+
+
+ ${blockSeparator}
@@ -664,6 +644,7 @@ const operators = `
+ ${blockSeparator}
@@ -671,6 +652,7 @@ const operators = `
+ ${categorySeparator}
`;
@@ -687,11 +669,10 @@ const xmlClose = '';
* @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');
};
diff --git a/src/lib/project-loader-hoc.jsx b/src/lib/project-loader-hoc.jsx
index 5468e7a09..23ad4694a 100644
--- a/src/lib/project-loader-hoc.jsx
+++ b/src/lib/project-loader-hoc.jsx
@@ -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
};
diff --git a/src/lib/storage.js b/src/lib/storage.js
index e1dc91bd6..6d858e6c2 100644
--- a/src/lib/storage.js
+++ b/src/lib/storage.js
@@ -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;
diff --git a/src/reducers/vm.js b/src/reducers/vm.js
index 5d0e6f91f..dd70f3f4f 100644
--- a/src/reducers/vm.js
+++ b/src/reducers/vm.js
@@ -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) {
diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js
index e2b0c8ab4..7fffa903a 100644
--- a/test/integration/examples.test.js
+++ b/test/integration/examples.test.js
@@ -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');
diff --git a/test/integration/test.js b/test/integration/test.js
index efd0732f2..86729cf27 100644
--- a/test/integration/test.js
+++ b/test/integration/test.js
@@ -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([]);
});
diff --git a/test/unit/util/project-loader-hoc.test.jsx b/test/unit/util/project-loader-hoc.test.jsx
index d61352d36..3771f3769 100644
--- a/test/unit/util/project-loader-hoc.test.jsx
+++ b/test/unit/util/project-loader-hoc.test.jsx
@@ -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}) => {projectData}
;
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();
- 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}) => {projectData}
;
const WrappedComponent = ProjectLoaderHOC(Component);
window.location.hash = '';
+ const originalLoad = storage.load;
+ storage.load = jest.fn((type, id) => Promise.resolve(id));
const mounted = mount();
- 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}) => {projectData}
;
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();
- 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();
- 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;
});
});
diff --git a/webpack.config.js b/webpack.config.js
index 6c486cc8a..e9b97f9ef 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -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',