Merge branch 'develop' into patch-10

This commit is contained in:
TheBrokenRail authored and GitHub committed 2017-05-17 15:28:57 -04:00
commit 840c9cfa3d
22 files changed
+403 -188

No files matched your search

+1 -1
View File
@@ -62,7 +62,7 @@
"scratch-audio": "^0.1.0-prerelease.0",
"scratch-blocks": "^0.1.0-prerelease.0",
"scratch-render": "^0.1.0-prerelease.0",
"scratch-storage": "^0.0.1-prerelease.0",
"scratch-storage": "^0.1.0",
"scratch-vm": "^0.1.0-prerelease.0",
"style-loader": "0.16.1",
"svg-to-image": "1.1.3",
+1
View File
@@ -27,6 +27,7 @@ const Selector = props => {
<Box className={styles.listArea}>
{items.map((item, index) => (
<SpriteSelectorItem
assetId={item.assetId}
className={styles.listItem}
costumeURL={item.url}
id={index}
+3
View File
@@ -0,0 +1,3 @@
.button {
cursor: pointer;
}
+31
View File
@@ -0,0 +1,31 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
const styles = require('./button.css');
const ButtonComponent = ({
className,
onClick,
children,
...props
}) => (
<span
className={classNames(
styles.button,
className
)}
role="button"
onClick={onClick}
{...props}
>
{children}
</span>
);
ButtonComponent.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
onClick: PropTypes.func.isRequired
};
module.exports = ButtonComponent;
+6 -8
View File
@@ -19,6 +19,8 @@ const GUIComponent = props => {
basePath,
children,
vm,
onTabSelect,
tabIndex,
...componentProps
} = props;
if (children) {
@@ -29,13 +31,6 @@ const GUIComponent = props => {
);
}
// @todo hack to resize blockly manually in case resize happened while hidden
const handleTabSelect = tabIndex => {
if (tabIndex === 0) {
setTimeout(() => window.dispatchEvent(new Event('resize')));
}
};
return (
<Box
className={styles.pageWrapper}
@@ -48,7 +43,7 @@ const GUIComponent = props => {
<Tabs
className={styles.tabs}
forceRenderTabPanel={true} // eslint-disable-line react/jsx-boolean-value
onSelect={handleTabSelect}
onSelect={onTabSelect}
>
<TabList className={styles.tabList}>
<Tab className={styles.tab}>Scripts</Tab>
@@ -59,6 +54,7 @@ const GUIComponent = props => {
<Box className={styles.blocksWrapper}>
<Blocks
grow={1}
isVisible={tabIndex === 0} // Scripts tab
options={{
media: `${basePath}static/blocks-media/`
}}
@@ -102,6 +98,8 @@ const GUIComponent = props => {
GUIComponent.propTypes = {
basePath: PropTypes.string,
children: PropTypes.node,
onTabSelect: PropTypes.func,
tabIndex: PropTypes.number,
vm: PropTypes.instanceOf(VM).isRequired
};
GUIComponent.defaultProps = {
@@ -0,0 +1,3 @@
.file-input {
display: none;
}
@@ -0,0 +1,36 @@
const PropTypes = require('prop-types');
const React = require('react');
const ButtonComponent = require('../button/button.jsx');
const styles = require('./load-button.css');
const LoadButtonComponent = ({
inputRef,
onChange,
onClick,
title,
...props
}) => (
<span {...props}>
<ButtonComponent onClick={onClick}>{title}</ButtonComponent>
<input
className={styles.fileInput}
ref={inputRef}
type="file"
onChange={onChange}
/>
</span>
);
LoadButtonComponent.propTypes = {
className: PropTypes.string,
inputRef: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
title: PropTypes.string
};
LoadButtonComponent.defaultProps = {
title: 'Load'
};
module.exports = LoadButtonComponent;
+5 -1
View File
@@ -2,6 +2,9 @@ const classNames = require('classnames');
const React = require('react');
const Box = require('../box/box.jsx');
const LoadButton = require('../../containers/load-button.jsx');
const SaveButton = require('../../containers/save-button.jsx');
const styles = require('./menu-bar.css');
const scratchLogo = require('./scratch-logo.svg');
@@ -18,7 +21,8 @@ const MenuBar = function MenuBar () {
src={scratchLogo}
/>
</div>
<div className={styles.menuItem} >Animation Playtest Prototype</div>
<SaveButton className={styles.menuItem} />
<LoadButton className={styles.menuItem} />
</Box>
);
};
@@ -53,17 +53,15 @@ const SpriteSelectorComponent = function (props) {
{Object.keys(sprites)
// Re-order by list order
.sort((id1, id2) => sprites[id1].order - sprites[id2].order)
.map(id => (
.map(id => sprites[id])
.map(sprite => (
<SpriteSelectorItem
assetId={sprite.costume && sprite.costume.assetId}
className={styles.sprite}
costumeURL={
sprites[id].costume &&
sprites[id].costume.url
}
id={id}
key={id}
name={sprites[id].name}
selected={id === selectedId}
id={sprite.id}
key={sprite.id}
name={sprite.name}
selected={sprite.id === selectedId}
onClick={onSelectSprite}
onDeleteButtonClick={onDeleteSprite}
/>
+1
View File
@@ -23,4 +23,5 @@
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
+97 -112
View File
@@ -1,5 +1,3 @@
const isEqual = require('lodash.isequal');
const omit = require('lodash.omit');
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
@@ -23,119 +21,106 @@ const addIcon = require('./icon--add.svg');
* @param {object} props Props for the component
* @returns {React.Component} rendered component
*/
class TargetPane extends React.Component {
shouldComponentUpdate (nextProps) {
return (
// Do a normal shallow compare on all props except sprites
Object.keys(omit(nextProps, ['sprites']))
.reduce((all, k) => all || nextProps[k] !== this.props[k], false) ||
// Deep compare on sprites object
!isEqual(this.props.sprites, nextProps.sprites)
);
}
render () {
const {
editingTarget,
backdropLibraryVisible,
costumeLibraryVisible,
soundLibraryVisible,
spriteLibraryVisible,
onChangeSpriteDirection,
onChangeSpriteName,
onChangeSpriteRotationStyle,
onChangeSpriteVisibility,
onChangeSpriteX,
onChangeSpriteY,
onDeleteSprite,
onNewSpriteClick,
onNewBackdropClick,
onRequestCloseBackdropLibrary,
onRequestCloseCostumeLibrary,
onRequestCloseSoundLibrary,
onRequestCloseSpriteLibrary,
onSelectSprite,
stage,
sprites,
vm,
...componentProps
} = this.props;
return (
<Box
className={styles.targetPane}
{...componentProps}
>
const TargetPane = ({
editingTarget,
backdropLibraryVisible,
costumeLibraryVisible,
soundLibraryVisible,
spriteLibraryVisible,
onChangeSpriteDirection,
onChangeSpriteName,
onChangeSpriteRotationStyle,
onChangeSpriteVisibility,
onChangeSpriteX,
onChangeSpriteY,
onDeleteSprite,
onNewSpriteClick,
onNewBackdropClick,
onRequestCloseBackdropLibrary,
onRequestCloseCostumeLibrary,
onRequestCloseSoundLibrary,
onRequestCloseSpriteLibrary,
onSelectSprite,
stage,
sprites,
vm,
...componentProps
}) => (
<Box
className={styles.targetPane}
{...componentProps}
>
<SpriteSelectorComponent
selectedId={editingTarget}
sprites={sprites}
onChangeSpriteDirection={onChangeSpriteDirection}
onChangeSpriteName={onChangeSpriteName}
onChangeSpriteRotationStyle={onChangeSpriteRotationStyle}
onChangeSpriteVisibility={onChangeSpriteVisibility}
onChangeSpriteX={onChangeSpriteX}
onChangeSpriteY={onChangeSpriteY}
onDeleteSprite={onDeleteSprite}
onSelectSprite={onSelectSprite}
<SpriteSelectorComponent
selectedId={editingTarget}
sprites={sprites}
onChangeSpriteDirection={onChangeSpriteDirection}
onChangeSpriteName={onChangeSpriteName}
onChangeSpriteRotationStyle={onChangeSpriteRotationStyle}
onChangeSpriteVisibility={onChangeSpriteVisibility}
onChangeSpriteX={onChangeSpriteX}
onChangeSpriteY={onChangeSpriteY}
onDeleteSprite={onDeleteSprite}
onSelectSprite={onSelectSprite}
/>
<Box className={styles.stageSelectorWrapper}>
{stage.id && <StageSelector
assetId={
stage.costume &&
stage.costume.assetId
}
backdropCount={stage.costumeCount}
id={stage.id}
selected={stage.id === editingTarget}
onSelect={onSelectSprite}
/>}
<Box>
<button
className={classNames(styles.addButtonWrapper, styles.addButtonWrapperSprite)}
onClick={onNewSpriteClick}
>
<img
className={styles.addButton}
src={addIcon}
/>
</button>
<button
className={classNames(styles.addButtonWrapper, styles.addButtonWrapperStage)}
onClick={onNewBackdropClick}
>
<img
className={styles.addButton}
src={addIcon}
/>
</button>
<SpriteLibrary
visible={spriteLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseSpriteLibrary}
/>
<CostumeLibrary
visible={costumeLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseCostumeLibrary}
/>
<SoundLibrary
visible={soundLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseSoundLibrary}
/>
<BackdropLibrary
visible={backdropLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseBackdropLibrary}
/>
<Box className={styles.stageSelectorWrapper}>
{stage.id && <StageSelector
backdropCount={stage.costumeCount}
id={stage.id}
selected={stage.id === editingTarget}
url={
stage.costume &&
stage.costume.url
}
onSelect={onSelectSprite}
/>}
<Box>
<button
className={classNames(styles.addButtonWrapper, styles.addButtonWrapperSprite)}
onClick={onNewSpriteClick}
>
<img
className={styles.addButton}
src={addIcon}
/>
</button>
<button
className={classNames(styles.addButtonWrapper, styles.addButtonWrapperStage)}
onClick={onNewBackdropClick}
>
<img
className={styles.addButton}
src={addIcon}
/>
</button>
<SpriteLibrary
visible={spriteLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseSpriteLibrary}
/>
<CostumeLibrary
visible={costumeLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseCostumeLibrary}
/>
<SoundLibrary
visible={soundLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseSoundLibrary}
/>
<BackdropLibrary
visible={backdropLibraryVisible}
vm={vm}
onRequestClose={onRequestCloseBackdropLibrary}
/>
</Box>
</Box>
</Box>
);
}
}
</Box>
</Box>
);
const spriteShape = PropTypes.shape({
costume: PropTypes.shape({
url: PropTypes.string,
+19 -3
View File
@@ -52,7 +52,21 @@ class Blocks extends React.Component {
this.attachVM();
}
shouldComponentUpdate (nextProps, nextState) {
return this.state.prompt !== nextState.prompt;
return this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible;
}
componentDidUpdate (prevProps) {
if (this.props.isVisible === prevProps.isVisible) {
return;
}
// @todo hack to resize blockly manually in case resize happened while hidden
if (this.props.isVisible) { // Scripts tab
window.dispatchEvent(new Event('resize'));
this.workspace.setVisible(true);
this.workspace.toolbox_.refreshSelection();
} else {
this.workspace.setVisible(false);
}
}
componentWillUnmount () {
this.detachVM();
@@ -146,6 +160,7 @@ class Blocks extends React.Component {
const {
options, // eslint-disable-line no-unused-vars
vm, // eslint-disable-line no-unused-vars
isVisible, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
@@ -169,11 +184,12 @@ class Blocks extends React.Component {
}
Blocks.propTypes = {
isVisible: PropTypes.bool.isRequired,
options: PropTypes.shape({
media: PropTypes.string,
zoom: PropTypes.shape({
controls: PropTypes.boolean,
wheel: PropTypes.boolean,
controls: PropTypes.bool,
wheel: PropTypes.bool,
startScale: PropTypes.number
}),
colours: PropTypes.shape({
+1 -1
View File
@@ -38,7 +38,7 @@ class CostumeTab extends React.Component {
editingTarget.sprite.costumes = editingTarget.sprite.costumes
.slice(0, costumeIndex)
.concat(editingTarget.sprite.costumes.slice(costumeIndex + 1));
this.props.vm.runtime.spriteInfoReport(editingTarget);
this.props.vm.runtime.requestTargetsUpdate(editingTarget);
// @todo not sure if this is getting redrawn correctly
this.props.vm.runtime.requestRedraw();
+13
View File
@@ -1,12 +1,20 @@
const PropTypes = require('prop-types');
const React = require('react');
const VM = require('scratch-vm');
const bindAll = require('lodash.bindall');
const vmListenerHOC = require('../lib/vm-listener-hoc.jsx');
const GUIComponent = require('../components/gui/gui.jsx');
class GUI extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleTabSelect'
]);
this.state = {tabIndex: 0};
}
componentDidMount () {
this.props.vm.loadProject(this.props.projectData);
this.props.vm.setCompatibilityMode(true);
@@ -20,6 +28,9 @@ class GUI extends React.Component {
componentWillUnmount () {
this.props.vm.stopAll();
}
handleTabSelect (tabIndex) {
this.setState({tabIndex});
}
render () {
const {
projectData, // eslint-disable-line no-unused-vars
@@ -28,7 +39,9 @@ class GUI extends React.Component {
} = this.props;
return (
<GUIComponent
tabIndex={this.state.tabIndex}
vm={vm}
onTabSelect={this.handleTabSelect}
{...componentProps}
/>
);
+55
View File
@@ -0,0 +1,55 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const {connect} = require('react-redux');
const LoadButtonComponent = require('../components/load-button/load-button.jsx');
class LoadButton extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'setFileInput',
'handleChange',
'handleClick'
]);
}
handleChange (e) {
const reader = new FileReader();
reader.onload = () => this.props.loadProject(reader.result);
reader.readAsText(e.target.files[0]);
}
handleClick () {
this.fileInput.click();
}
setFileInput (input) {
this.fileInput = input;
}
render () {
const {
loadProject, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
<LoadButtonComponent
inputRef={this.setFileInput}
onChange={this.handleChange}
onClick={this.handleClick}
{...props}
/>
);
}
}
LoadButton.propTypes = {
loadProject: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
loadProject: state.vm.fromJSON.bind(state.vm)
});
module.exports = connect(
mapStateToProps,
() => ({}) // omit dispatch prop
)(LoadButton);
+62
View File
@@ -0,0 +1,62 @@
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const {connect} = require('react-redux');
const ButtonComponent = require('../components/button/button.jsx');
class SaveButton extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleClick'
]);
}
handleClick () {
const json = this.props.saveProjectSb3();
// Download project data into a file - create link element,
// simulate click on it, and then remove it.
const saveLink = document.createElement('a');
document.body.appendChild(saveLink);
const data = new Blob([json], {type: 'text'});
const url = window.URL.createObjectURL(data);
saveLink.href = url;
// File name: project-DATE-TIME
const date = new Date();
const timestamp = `${date.toLocaleDateString()}-${date.toLocaleTimeString()}`;
saveLink.download = `project-${timestamp}.json`;
saveLink.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(saveLink);
}
render () {
const {
saveProjectSb3, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
<ButtonComponent
onClick={this.handleClick}
{...props}
>
Save
</ButtonComponent>
);
}
}
SaveButton.propTypes = {
saveProjectSb3: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
saveProjectSb3: state.vm.saveProjectSb3.bind(state.vm)
});
module.exports = connect(
mapStateToProps,
() => ({}) // omit dispatch prop
)(SaveButton);
+16 -4
View File
@@ -2,6 +2,8 @@ const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const {connect} = require('react-redux');
const SpriteSelectorItemComponent = require('../components/sprite-selector-item/sprite-selector-item.jsx');
class SpriteSelectorItem extends React.Component {
@@ -24,9 +26,12 @@ class SpriteSelectorItem extends React.Component {
}
render () {
const {
id, // eslint-disable-line no-unused-vars
onClick, // eslint-disable-line no-unused-vars
onDeleteButtonClick, // eslint-disable-line no-unused-vars
/* eslint-disable no-unused-vars */
assetId,
id,
onClick,
onDeleteButtonClick,
/* eslint-enable no-unused-vars */
...props
} = this.props;
return (
@@ -40,6 +45,7 @@ class SpriteSelectorItem extends React.Component {
}
SpriteSelectorItem.propTypes = {
assetId: PropTypes.string,
costumeURL: PropTypes.string,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
@@ -48,4 +54,10 @@ SpriteSelectorItem.propTypes = {
selected: PropTypes.bool
};
module.exports = SpriteSelectorItem;
const mapStateToProps = (state, {assetId, costumeURL}) => ({
costumeURL: costumeURL || (assetId && state.vm.runtime.storage.get(assetId).encodeDataURI())
});
module.exports = connect(
mapStateToProps
)(SpriteSelectorItem);
+12 -1
View File
@@ -2,6 +2,8 @@ const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
const {connect} = require('react-redux');
const StageSelectorComponent = require('../components/stage-selector/stage-selector.jsx');
class StageSelector extends React.Component {
@@ -18,6 +20,7 @@ class StageSelector extends React.Component {
render () {
const {
/* eslint-disable no-unused-vars */
assetId,
id,
onSelect,
/* eslint-enable no-unused-vars */
@@ -36,4 +39,12 @@ StageSelector.propTypes = {
id: PropTypes.string,
onSelect: PropTypes.func
};
module.exports = StageSelector;
const mapStateToProps = (state, {assetId}) => ({
url: assetId && state.vm.runtime.storage.get(assetId).encodeDataURI()
});
module.exports = connect(
mapStateToProps,
() => ({}) // omit dispatch prop
)(StageSelector);
+4 -11
View File
@@ -3,8 +3,6 @@ const PropTypes = require('prop-types');
const React = require('react');
const VM = require('scratch-vm');
const Storage = require('./storage');
const {connect} = require('react-redux');
const targets = require('../reducers/targets');
@@ -29,7 +27,6 @@ const vmListenerHOC = function (WrappedComponent) {
// 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) {
@@ -92,20 +89,16 @@ const vmListenerHOC = function (WrappedComponent) {
onTargetsUpdate: PropTypes.func,
vm: PropTypes.instanceOf(VM).isRequired
};
const defaultVM = new VM('vm-listener-hoc');
defaultVM.attachStorage(new Storage());
VMListener.defaultProps = {
attachKeyboardEvents: true,
vm: defaultVM
attachKeyboardEvents: true
};
const mapStateToProps = () => ({});
const mapStateToProps = state => ({
vm: state.vm
});
const mapDispatchToProps = dispatch => ({
onTargetsUpdate: data => {
dispatch(targets.updateEditingTarget(data.editingTarget));
dispatch(targets.updateTargets(data.targetList));
},
onSpriteInfoReport: spriteInfo => {
dispatch(targets.updateTarget(spriteInfo));
}
});
return connect(
+2 -1
View File
@@ -2,6 +2,7 @@ const {combineReducers} = require('redux');
module.exports = combineReducers({
modals: require('./modals'),
monitors: require('./monitors'),
targets: require('./targets'),
monitors: require('./monitors')
vm: require('./vm')
});
+4 -36
View File
@@ -1,8 +1,5 @@
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: {},
@@ -12,39 +9,19 @@ const initialState = {
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}},
{[target.id]: state.sprites[target.id]},
targets
(targets, target, listId) => Object.assign(
targets,
{[target.id]: {order: listId, ...target}}
),
{}
),
stage: action.targets
.filter(target => target.isStage)
.reduce(
(stage, target) => {
if (target.id !== stage.id) return target;
return defaultsDeep(target, stage);
},
state.stage
)
.filter(target => target.isStage)[0] || {}
});
case UPDATE_EDITING_TARGET:
return Object.assign({}, state, {editingTarget: action.target});
@@ -52,15 +29,6 @@ const reducer = function (state, action) {
return state;
}
};
reducer.updateTarget = function (target) {
return {
type: UPDATE_TARGET,
target: target,
meta: {
throttle: 30
}
};
};
reducer.updateTargets = function (targetList) {
return {
type: UPDATE_TARGET_LIST,
+24
View File
@@ -0,0 +1,24 @@
const VM = require('scratch-vm');
const Storage = require('../lib/storage');
const SET_VM = 'scratch-gui/vm/SET_VM';
const defaultVM = new VM();
defaultVM.attachStorage(new Storage());
const initialState = defaultVM;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case SET_VM:
return action.vm;
default:
return state;
}
};
reducer.setVM = function (vm) {
return {
type: SET_VM,
vm: vm
};
};
module.exports = reducer;