commit
ee54c35fd9
39 files changed
+1619
-107
No files matched your search
@@ -18,6 +18,7 @@ install:
|
||||
- npm --production=false install
|
||||
- npm --production=false update
|
||||
before_deploy:
|
||||
- npm --no-git-tag-version version 0.1.0-prerelease.$(date +%Y%m%d%H%M%S)
|
||||
- git config --global user.email $(git log --pretty=format:"%ae" -n1)
|
||||
- git config --global user.name $(git log --pretty=format:"%an" -n1)
|
||||
deploy:
|
||||
@@ -32,3 +33,10 @@ deploy:
|
||||
branch: master
|
||||
skip_cleanup: true
|
||||
script: npm run --silent deploy -- -x -a -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
|
||||
- provider: npm
|
||||
on:
|
||||
branch: master
|
||||
skip_cleanup: true
|
||||
email: $NPM_EMAIL
|
||||
api_key: $NPM_TOKEN
|
||||
|
||||
+20
-20
@@ -12,8 +12,8 @@
|
||||
"lint": "eslint . --ext .js,.jsx",
|
||||
"start": "npm run i18n:msgs && webpack-dev-server",
|
||||
"unit-test": "jest test/unit",
|
||||
"integration-test": "npm run build && jest test/integration",
|
||||
"test": "npm run lint && npm run unit-test && npm run integration-test",
|
||||
"integration-test": "jest --runInBand test/integration",
|
||||
"test": "npm run lint && npm run unit-test && npm run build",
|
||||
"watch": "webpack --progress --colors --watch"
|
||||
},
|
||||
"author": "Massachusetts Institute of Technology",
|
||||
@@ -28,31 +28,31 @@
|
||||
"react-dom": "^15.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "7.1.2",
|
||||
"babel-cli": "6.26.0",
|
||||
"autoprefixer": "^7.1.3",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-core": "^6.23.1",
|
||||
"babel-eslint": "^7.2.3",
|
||||
"babel-loader": "^7.1.0",
|
||||
"babel-plugin-react-intl": "2.3.1",
|
||||
"babel-plugin-syntax-dynamic-import": "6.18.0",
|
||||
"babel-plugin-react-intl": "^2.3.1",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-async-to-generator": "^6.24.1",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.22.0",
|
||||
"babel-preset-es2015": "^6.22.0",
|
||||
"babel-preset-react": "^6.22.0",
|
||||
"chromedriver": "^2.31.0",
|
||||
"chromedriver": "^2.32.3",
|
||||
"classnames": "2.2.5",
|
||||
"copy-webpack-plugin": "4.0.1",
|
||||
"css-loader": "0.28.5",
|
||||
"copy-webpack-plugin": "^4.0.1",
|
||||
"css-loader": "^0.28.7",
|
||||
"enzyme": "^2.8.2",
|
||||
"eslint": "^4.4.1",
|
||||
"eslint": "^4.7.1",
|
||||
"eslint-config-scratch": "^4.0.0",
|
||||
"eslint-plugin-import": "^2.7.0",
|
||||
"eslint-plugin-react": "^7.2.1",
|
||||
"get-float-time-domain-data": "0.1.0",
|
||||
"gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
|
||||
"html-webpack-plugin": "2.30.0",
|
||||
"html-webpack-plugin": "^2.30.0",
|
||||
"immutable": "3.8.1",
|
||||
"jest": "^20.0.4",
|
||||
"jest": "^21.0.0",
|
||||
"lodash.bindall": "4.4.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.defaultsdeep": "4.6.0",
|
||||
@@ -66,16 +66,16 @@
|
||||
"postcss-simple-vars": "^4.0.0",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "15.6.1",
|
||||
"react-contextmenu": "2.6.5",
|
||||
"react-contextmenu": "2.7.0",
|
||||
"react-dom": "15.6.1",
|
||||
"react-draggable": "2.2.6",
|
||||
"react-intl": "2.3.0",
|
||||
"react-draggable": "3.0.3",
|
||||
"react-intl": "2.4.0",
|
||||
"react-intl-redux": "0.6.0",
|
||||
"react-modal": "2.2.4",
|
||||
"react-modal": "2.3.1",
|
||||
"react-redux": "5.0.6",
|
||||
"react-responsive": "1.3.4",
|
||||
"react-style-proptype": "3.0.0",
|
||||
"react-tabs": "1.1.0",
|
||||
"react-tabs": "2.0.0",
|
||||
"react-test-renderer": "^15.5.4",
|
||||
"redux": "3.7.0",
|
||||
"redux-mock-store": "^1.2.3",
|
||||
@@ -90,11 +90,11 @@
|
||||
"startaudiocontext": "1.2.1",
|
||||
"style-loader": "^0.18.0",
|
||||
"svg-to-image": "1.1.3",
|
||||
"svg-url-loader": "2.1.0",
|
||||
"svg-url-loader": "^2.1.0",
|
||||
"wav-encoder": "1.3.0",
|
||||
"web-audio-test-api": "^0.5.2",
|
||||
"webpack": "^3.5.4",
|
||||
"webpack-dev-server": "^2.7.0",
|
||||
"webpack": "^3.6.0",
|
||||
"webpack-dev-server": "^2.8.2",
|
||||
"xhr": "2.4.0"
|
||||
},
|
||||
"jest": {
|
||||
|
||||
@@ -21,6 +21,7 @@ $border-style: 1px solid $ui-pane-border;
|
||||
border-right: $border-style;
|
||||
border-bottom: $border-style;
|
||||
box-sizing: content-box;
|
||||
height: calc(100% - 3.25rem) !important;
|
||||
}
|
||||
|
||||
.blocks :global(.blocklyFlyout) {
|
||||
|
||||
@@ -148,3 +148,24 @@
|
||||
height: $stage-menu-height;
|
||||
padding: $space;
|
||||
}
|
||||
|
||||
.extension-button-container {
|
||||
width: 60px;
|
||||
height: 3.25rem;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 50; /** Force extension button above the ScratchBlocks flyout. */
|
||||
background: white;
|
||||
|
||||
border: 1px solid #ddd;
|
||||
box-sizing: content-box; /* To match scratch-block vertical toolbox borders */
|
||||
}
|
||||
|
||||
.extension-button {
|
||||
font-size: 0.55rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.extension-button > div {
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -12,18 +12,31 @@ import Controls from '../../containers/controls.jsx';
|
||||
import TargetPane from '../../containers/target-pane.jsx';
|
||||
import SoundTab from '../../containers/sound-tab.jsx';
|
||||
import Stage from '../../containers/stage.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
import IconButton from '../icon-button/icon-button.jsx';
|
||||
import MenuBar from '../menu-bar/menu-bar.jsx';
|
||||
|
||||
import layout from '../../lib/layout-constants.js';
|
||||
import styles from './gui.css';
|
||||
import addExtensionIcon from './icon--extensions.svg';
|
||||
|
||||
const addExtensionMessage = (
|
||||
<FormattedMessage
|
||||
defaultMessage="Extensions"
|
||||
description="Button to add an extension in the target pane"
|
||||
id="targetPane.addExtension"
|
||||
/>
|
||||
);
|
||||
|
||||
const GUIComponent = props => {
|
||||
const {
|
||||
basePath,
|
||||
children,
|
||||
enableExtensions,
|
||||
vm,
|
||||
onExtensionButtonClick,
|
||||
onTabSelect,
|
||||
tabIndex,
|
||||
...componentProps
|
||||
@@ -77,12 +90,22 @@ const GUIComponent = props => {
|
||||
vm={vm}
|
||||
/>
|
||||
</Box>
|
||||
<Box className={styles.extensionButtonContainer}>
|
||||
<IconButton
|
||||
className={classNames(styles.extensionButton, {
|
||||
[styles.hidden]: !enableExtensions
|
||||
})}
|
||||
img={addExtensionIcon}
|
||||
title={addExtensionMessage}
|
||||
onClick={onExtensionButtonClick}
|
||||
/>
|
||||
</Box>
|
||||
</TabPanel>
|
||||
<TabPanel className={tabClassNames.tabPanel}>
|
||||
<CostumeTab vm={vm} />
|
||||
{tabIndex === 1 ? <CostumeTab vm={vm} /> : null}
|
||||
</TabPanel>
|
||||
<TabPanel className={tabClassNames.tabPanel}>
|
||||
<SoundTab vm={vm} />
|
||||
{tabIndex === 2 ? <SoundTab vm={vm} /> : null}
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</Box>
|
||||
@@ -115,6 +138,8 @@ const GUIComponent = props => {
|
||||
GUIComponent.propTypes = {
|
||||
basePath: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
enableExtensions: PropTypes.bool,
|
||||
onExtensionButtonClick: PropTypes.func,
|
||||
onTabSelect: PropTypes.func,
|
||||
tabIndex: PropTypes.number,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="34px" height="34px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>add extension</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="add-extension">
|
||||
<g transform="translate(4.000000, 5.000000)">
|
||||
<path d="M18,15.51 C18,15.786 17.776,16.01 17.5,16.01 L7.197,16.01 C7.064,16.01 6.937,16.062 6.844,16.156 L6.146,16.854 C6.053,16.947 5.926,17 5.793,17 L3.207,17 C3.074,17 2.947,16.947 2.854,16.854 L2.156,16.156 C2.062,16.062 1.936,16.01 1.803,16.01 L0.5,16.01 C0.224,16.01 0,15.786 0,15.51 L0,11.5 C0,11.224 0.224,11 0.5,11 L1.793,11 C1.926,11 2.053,11.053 2.146,11.146 L2.854,11.854 C2.947,11.947 3.074,12 3.207,12 L5.793,12 C5.926,12 6.053,11.947 6.146,11.854 L6.854,11.146 C6.947,11.053 7.074,11 7.207,11 L17.5,11 C17.776,11 18,11.224 18,11.5 L18,15.51 Z M12.9961,8.51 C12.9961,8.786 12.7721,9.01 12.4961,9.01 L7.1971,9.01 C7.0641,9.01 6.9371,9.062 6.8441,9.156 L6.1461,9.854 C6.0531,9.947 5.9261,10 5.7931,10 L3.2071,10 C3.0741,10 2.9471,9.947 2.8531,9.854 L2.1561,9.156 C2.0621,9.062 1.9351,9.01 1.8031,9.01 L0.5001,9.01 C0.2241,9.01 0.0001,8.786 0.0001,8.51 L0.0001,4.5 C0.0001,4.224 0.2241,4 0.5001,4 L1.7931,4 C1.9261,4 2.0531,4.053 2.1461,4.146 L2.8531,4.854 C2.9471,4.947 3.0741,5 3.2071,5 L5.7931,5 C5.9261,5 6.0531,4.947 6.1461,4.854 L6.8531,4.146 C6.9471,4.053 7.0741,4 7.2071,4 L12.4961,4 C12.7721,4 12.9961,4.224 12.9961,4.5 L12.9961,8.51 Z" id="Combined-Shape" fill="#4C97FF"></path>
|
||||
<g id="+" transform="translate(16.000000, 0.000000)" stroke="#4C97FF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2,0 L2,4" id="Shape"></path>
|
||||
<path d="M4,2 L0,2" id="Shape"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -43,13 +43,11 @@ class LibraryComponent extends React.Component {
|
||||
dataItem.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) !== -1);
|
||||
}
|
||||
render () {
|
||||
if (!this.props.visible) return null;
|
||||
return (
|
||||
<ModalComponent
|
||||
className={styles.modalContent}
|
||||
contentLabel={this.props.title}
|
||||
filterQuery={this.state.filterQuery}
|
||||
visible={this.props.visible}
|
||||
onFilterChange={this.handleFilterChange}
|
||||
onFilterClear={this.handleFilterClear}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
@@ -93,8 +91,7 @@ LibraryComponent.propTypes = {
|
||||
onItemMouseLeave: PropTypes.func,
|
||||
onItemSelected: PropTypes.func,
|
||||
onRequestClose: PropTypes.func,
|
||||
title: PropTypes.string.isRequired,
|
||||
visible: PropTypes.bool.isRequired
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default LibraryComponent;
|
||||
@@ -0,0 +1,5 @@
|
||||
.color-picker {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
border: 1px solid #222;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import bindAll from 'lodash.bindall';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
import styles from './loupe.css';
|
||||
|
||||
const zoomScale = 3;
|
||||
|
||||
class LoupeComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'setCanvas'
|
||||
]);
|
||||
}
|
||||
componentDidUpdate () {
|
||||
this.draw();
|
||||
}
|
||||
draw () {
|
||||
const boxSize = 6 / zoomScale;
|
||||
const boxLineWidth = 1 / zoomScale;
|
||||
const colorRingWidth = 15 / zoomScale;
|
||||
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
const {color, data, width, height} = this.props.colorInfo;
|
||||
this.canvas.width = zoomScale * width;
|
||||
this.canvas.height = zoomScale * height;
|
||||
|
||||
// In order to scale the image data, must draw to a tmp canvas first
|
||||
const tmpCanvas = document.createElement('canvas');
|
||||
tmpCanvas.width = width;
|
||||
tmpCanvas.height = height;
|
||||
const tmpCtx = tmpCanvas.getContext('2d');
|
||||
const imageData = tmpCtx.createImageData(width, height);
|
||||
imageData.data.set(data);
|
||||
tmpCtx.putImageData(imageData, 0, 0);
|
||||
|
||||
// Scale the loupe canvas and draw the zoomed image
|
||||
ctx.save();
|
||||
ctx.scale(zoomScale, zoomScale);
|
||||
ctx.drawImage(tmpCanvas, 0, 0, width, height);
|
||||
|
||||
// Draw an outlined square at the cursor position (cursor is hidden)
|
||||
ctx.lineWidth = boxLineWidth;
|
||||
ctx.strokeStyle = 'black';
|
||||
ctx.fillStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
|
||||
ctx.beginPath();
|
||||
ctx.rect((width / 2) - (boxSize / 2), (height / 2) - (boxSize / 2), boxSize, boxSize);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
|
||||
// Draw a thick ring around the loupe showing the current color
|
||||
ctx.strokeStyle = `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
|
||||
ctx.lineWidth = colorRingWidth;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(width, height / 2);
|
||||
ctx.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI);
|
||||
ctx.stroke();
|
||||
ctx.restore();
|
||||
}
|
||||
setCanvas (element) {
|
||||
this.canvas = element;
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
colorInfo,
|
||||
...boxProps
|
||||
} = this.props;
|
||||
return (
|
||||
<Box
|
||||
{...boxProps}
|
||||
className={styles.colorPicker}
|
||||
componentRef={this.setCanvas}
|
||||
element="canvas"
|
||||
height={colorInfo.height}
|
||||
style={{
|
||||
top: colorInfo.y - ((zoomScale * colorInfo.height) / 2),
|
||||
left: colorInfo.x - ((zoomScale * colorInfo.width) / 2),
|
||||
width: colorInfo.width * zoomScale,
|
||||
height: colorInfo.height * zoomScale
|
||||
}}
|
||||
width={colorInfo.width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoupeComponent.propTypes = {
|
||||
colorInfo: PropTypes.shape({
|
||||
color: PropTypes.shape({
|
||||
r: PropTypes.number,
|
||||
g: PropTypes.number,
|
||||
b: PropTypes.number
|
||||
}),
|
||||
data: PropTypes.instanceOf(Uint8Array),
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number
|
||||
})
|
||||
};
|
||||
|
||||
export default LoupeComponent;
|
||||
@@ -13,9 +13,9 @@ class ModalComponent extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen
|
||||
className={classNames(styles.modalContent, this.props.className)}
|
||||
contentLabel={this.props.contentLabel}
|
||||
isOpen={this.props.visible}
|
||||
overlayClassName={styles.modalOverlay}
|
||||
ref={m => (this.modal = m)}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
@@ -68,8 +68,7 @@ ModalComponent.propTypes = {
|
||||
filterQuery: PropTypes.string,
|
||||
onFilterChange: PropTypes.func,
|
||||
onFilterClear: PropTypes.func,
|
||||
onRequestClose: PropTypes.func,
|
||||
visible: PropTypes.bool.isRequired
|
||||
onRequestClose: PropTypes.func
|
||||
};
|
||||
|
||||
export default ModalComponent;
|
||||
@@ -7,7 +7,6 @@ import styles from './prompt.css';
|
||||
|
||||
const PromptComponent = props => (
|
||||
<Modal
|
||||
visible
|
||||
className={styles.modalContent}
|
||||
contentLabel={props.title}
|
||||
onRequestClose={props.onCancel}
|
||||
|
||||
@@ -8,7 +8,6 @@ import styles from './record-modal.css';
|
||||
|
||||
const RecordModal = props => (
|
||||
<Modal
|
||||
visible
|
||||
className={styles.modalContent}
|
||||
contentLabel={'Record Sound'}
|
||||
onRequestClose={props.onCancel}
|
||||
|
||||
@@ -13,15 +13,32 @@
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.with-color-picker {
|
||||
cursor: none;
|
||||
z-index: 2001;
|
||||
}
|
||||
|
||||
.color-picker-background {
|
||||
position: absolute;;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.55);
|
||||
display: block;
|
||||
z-index: 2000;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.stage-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monitor-wrapper {
|
||||
.monitor-wrapper, .color-picker-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
import Loupe from '../loupe/loupe.jsx';
|
||||
import MonitorList from '../../containers/monitor-list.jsx';
|
||||
import styles from './stage.css';
|
||||
|
||||
@@ -10,27 +12,50 @@ const StageComponent = props => {
|
||||
canvasRef,
|
||||
width,
|
||||
height,
|
||||
colorInfo,
|
||||
onDeactivateColorPicker,
|
||||
isColorPicking,
|
||||
...boxProps
|
||||
} = props;
|
||||
return (
|
||||
<Box className={styles.stageWrapper}>
|
||||
<div>
|
||||
<Box
|
||||
className={styles.stage}
|
||||
componentRef={canvasRef}
|
||||
element="canvas"
|
||||
height={height}
|
||||
width={width}
|
||||
{...boxProps}
|
||||
/>
|
||||
<Box className={styles.monitorWrapper}>
|
||||
<MonitorList />
|
||||
className={classNames(styles.stageWrapper, {
|
||||
[styles.withColorPicker]: isColorPicking
|
||||
})}
|
||||
>
|
||||
<Box
|
||||
className={styles.stage}
|
||||
componentRef={canvasRef}
|
||||
element="canvas"
|
||||
height={height}
|
||||
width={width}
|
||||
{...boxProps}
|
||||
/>
|
||||
<Box className={styles.monitorWrapper}>
|
||||
<MonitorList />
|
||||
</Box>
|
||||
{isColorPicking && colorInfo ? (
|
||||
<Box className={styles.colorPickerWrapper}>
|
||||
<Loupe colorInfo={colorInfo} />
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
</Box>
|
||||
{isColorPicking ? (
|
||||
<Box
|
||||
className={styles.colorPickerBackground}
|
||||
onClick={onDeactivateColorPicker}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
StageComponent.propTypes = {
|
||||
canvasRef: PropTypes.func,
|
||||
colorInfo: Loupe.propTypes.colorInfo,
|
||||
height: PropTypes.number,
|
||||
isColorPicking: PropTypes.bool,
|
||||
onDeactivateColorPicker: PropTypes.func,
|
||||
width: PropTypes.number
|
||||
};
|
||||
StageComponent.defaultProps = {
|
||||
|
||||
@@ -3,11 +3,12 @@ import React from 'react';
|
||||
|
||||
import VM from 'scratch-vm';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
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';
|
||||
|
||||
@@ -21,6 +22,7 @@ import styles from './target-pane.css';
|
||||
*/
|
||||
const TargetPane = ({
|
||||
editingTarget,
|
||||
extensionLibraryVisible,
|
||||
backdropLibraryVisible,
|
||||
costumeLibraryVisible,
|
||||
soundLibraryVisible,
|
||||
@@ -37,13 +39,14 @@ const TargetPane = ({
|
||||
onRequestCloseCostumeLibrary,
|
||||
onRequestCloseSoundLibrary,
|
||||
onRequestCloseSpriteLibrary,
|
||||
onRequestCloseExtensionLibrary,
|
||||
onSelectSprite,
|
||||
stage,
|
||||
sprites,
|
||||
vm,
|
||||
...componentProps
|
||||
}) => (
|
||||
<Box
|
||||
<div
|
||||
className={styles.targetPane}
|
||||
{...componentProps}
|
||||
>
|
||||
@@ -61,7 +64,7 @@ const TargetPane = ({
|
||||
onNewSpriteClick={onNewSpriteClick}
|
||||
onSelectSprite={onSelectSprite}
|
||||
/>
|
||||
<Box className={styles.stageSelectorWrapper}>
|
||||
<div className={styles.stageSelectorWrapper}>
|
||||
{stage.id && <StageSelector
|
||||
assetId={
|
||||
stage.costume &&
|
||||
@@ -72,30 +75,40 @@ const TargetPane = ({
|
||||
selected={stage.id === editingTarget}
|
||||
onSelect={onSelectSprite}
|
||||
/>}
|
||||
<Box>
|
||||
<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>
|
||||
<div>
|
||||
{extensionLibraryVisible ? (
|
||||
<ExtensionLibrary
|
||||
vm={vm}
|
||||
onRequestClose={onRequestCloseExtensionLibrary}
|
||||
/>
|
||||
) : null}
|
||||
{spriteLibraryVisible ? (
|
||||
<SpriteLibrary
|
||||
vm={vm}
|
||||
onRequestClose={onRequestCloseSpriteLibrary}
|
||||
/>
|
||||
) : null}
|
||||
{costumeLibraryVisible ? (
|
||||
<CostumeLibrary
|
||||
vm={vm}
|
||||
onRequestClose={onRequestCloseCostumeLibrary}
|
||||
/>
|
||||
) : null}
|
||||
{soundLibraryVisible ? (
|
||||
<SoundLibrary
|
||||
vm={vm}
|
||||
onRequestClose={onRequestCloseSoundLibrary}
|
||||
/>
|
||||
) : null}
|
||||
{backdropLibraryVisible ? (
|
||||
<BackdropLibrary
|
||||
vm={vm}
|
||||
onRequestClose={onRequestCloseBackdropLibrary}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const spriteShape = PropTypes.shape({
|
||||
@@ -120,6 +133,7 @@ TargetPane.propTypes = {
|
||||
backdropLibraryVisible: PropTypes.bool,
|
||||
costumeLibraryVisible: PropTypes.bool,
|
||||
editingTarget: PropTypes.string,
|
||||
extensionLibraryVisible: PropTypes.bool,
|
||||
onChangeSpriteDirection: PropTypes.func,
|
||||
onChangeSpriteName: PropTypes.func,
|
||||
onChangeSpriteRotationStyle: PropTypes.func,
|
||||
@@ -130,6 +144,7 @@ TargetPane.propTypes = {
|
||||
onNewSpriteClick: PropTypes.func,
|
||||
onRequestCloseBackdropLibrary: PropTypes.func,
|
||||
onRequestCloseCostumeLibrary: PropTypes.func,
|
||||
onRequestCloseExtensionLibrary: PropTypes.func,
|
||||
onRequestCloseSoundLibrary: PropTypes.func,
|
||||
onRequestCloseSpriteLibrary: PropTypes.func,
|
||||
onSelectSprite: PropTypes.func,
|
||||
|
||||
@@ -4,6 +4,8 @@ import bindAll from 'lodash.bindall';
|
||||
import AudioTrimmerComponent from '../components/audio-trimmer/audio-trimmer.jsx';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
|
||||
const MIN_LENGTH = 0.01; // Used to stop sounds being trimmed smaller than 1%
|
||||
|
||||
class AudioTrimmer extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@@ -20,14 +22,14 @@ class AudioTrimmer extends React.Component {
|
||||
handleTrimStartMouseMove (e) {
|
||||
const containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
const dx = (getEventXY(e).x - this.initialX) / containerSize;
|
||||
const newTrim = Math.max(0, Math.min(this.props.trimEnd, this.initialTrim + dx));
|
||||
const newTrim = Math.max(0, Math.min(this.props.trimEnd - MIN_LENGTH, this.initialTrim + dx));
|
||||
this.props.onSetTrimStart(newTrim);
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimEndMouseMove (e) {
|
||||
const containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
const dx = (getEventXY(e).x - this.initialX) / containerSize;
|
||||
const newTrim = Math.min(1, Math.max(this.props.trimStart, this.initialTrim + dx));
|
||||
const newTrim = Math.min(1, Math.max(this.props.trimStart + MIN_LENGTH, this.initialTrim + dx));
|
||||
this.props.onSetTrimEnd(newTrim);
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class BackdropLibrary extends React.Component {
|
||||
<LibraryComponent
|
||||
data={backdropLibraryContent}
|
||||
title="Backdrop Library"
|
||||
visible={this.props.visible}
|
||||
onItemSelected={this.handleItemSelect}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
/>
|
||||
@@ -39,7 +38,6 @@ class BackdropLibrary extends React.Component {
|
||||
|
||||
BackdropLibrary.propTypes = {
|
||||
onRequestClose: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import bindAll from 'lodash.bindall';
|
||||
import debounce from 'lodash.debounce';
|
||||
import defaultsDeep from 'lodash.defaultsdeep';
|
||||
import makeToolboxXML from '../lib/make-toolbox-xml';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import VMScratchBlocks from '../lib/blocks';
|
||||
@@ -8,6 +9,10 @@ import VM from 'scratch-vm';
|
||||
import Prompt from './prompt.jsx';
|
||||
import BlocksComponent from '../components/blocks/blocks.jsx';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {updateToolbox} from '../reducers/toolbox';
|
||||
import {activateColorPicker} from '../reducers/color-picker';
|
||||
|
||||
const addFunctionListener = (object, property, callback) => {
|
||||
const oldFn = object[property];
|
||||
object[property] = function () {
|
||||
@@ -31,6 +36,7 @@ class Blocks extends React.Component {
|
||||
'onScriptGlowOff',
|
||||
'onBlockGlowOn',
|
||||
'onBlockGlowOff',
|
||||
'handleExtensionAdded',
|
||||
'onTargetsUpdate',
|
||||
'onVisualReport',
|
||||
'onWorkspaceUpdate',
|
||||
@@ -45,9 +51,14 @@ class Blocks extends React.Component {
|
||||
this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
|
||||
}
|
||||
componentDidMount () {
|
||||
this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker;
|
||||
|
||||
const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
|
||||
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);
|
||||
@@ -55,9 +66,20 @@ class Blocks extends React.Component {
|
||||
this.attachVM();
|
||||
}
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
return this.state.prompt !== nextState.prompt || this.props.isVisible !== nextProps.isVisible;
|
||||
return (
|
||||
this.state.prompt !== nextState.prompt ||
|
||||
this.props.isVisible !== nextProps.isVisible ||
|
||||
this.props.toolboxXML !== nextProps.toolboxXML
|
||||
);
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.toolboxXML !== this.props.toolboxXML) {
|
||||
const selectedCategoryName = this.workspace.toolbox_.getSelectedItem().name_;
|
||||
this.workspace.updateToolbox(this.props.toolboxXML);
|
||||
// Blockly throws if we don't select a category after updating the toolbox.
|
||||
/** @TODO Find a way to avoid the exception without accessing private properties. */
|
||||
this.setToolboxSelectedItemByName(selectedCategoryName);
|
||||
}
|
||||
if (this.props.isVisible === prevProps.isVisible) {
|
||||
return;
|
||||
}
|
||||
@@ -77,6 +99,20 @@ class Blocks extends React.Component {
|
||||
this.detachVM();
|
||||
this.workspace.dispose();
|
||||
}
|
||||
/**
|
||||
* Select a particular category in the toolbox by specifying the category name.
|
||||
* This is a workaround for a bug: @see {@link componentDidUpdate} above.
|
||||
* @TODO Remove this or reimplement using only public APIs.
|
||||
* @param {string} name - the name of the category to select.
|
||||
*/
|
||||
setToolboxSelectedItemByName (name) {
|
||||
const categories = this.workspace.toolbox_.categoryMenu_.categories_;
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
if (categories[i].name_ === name) {
|
||||
this.workspace.toolbox_.setSelectedItem(categories[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
attachVM () {
|
||||
this.workspace.addChangeListener(this.props.vm.blockListener);
|
||||
this.flyoutWorkspace = this.workspace
|
||||
@@ -91,6 +127,7 @@ class Blocks extends React.Component {
|
||||
this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport);
|
||||
this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate);
|
||||
this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate);
|
||||
this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded);
|
||||
}
|
||||
detachVM () {
|
||||
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
|
||||
@@ -100,6 +137,7 @@ class Blocks extends React.Component {
|
||||
this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport);
|
||||
this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate);
|
||||
this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate);
|
||||
this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded);
|
||||
}
|
||||
updateToolboxBlockValue (id, value) {
|
||||
const block = this.workspace
|
||||
@@ -167,6 +205,12 @@ class Blocks extends React.Component {
|
||||
this.workspace.resize();
|
||||
}
|
||||
}
|
||||
handleExtensionAdded (blocksInfo) {
|
||||
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json));
|
||||
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
|
||||
const toolboxXML = makeToolboxXML(dynamicBlocksXML);
|
||||
this.props.onExtensionAdded(toolboxXML);
|
||||
}
|
||||
setBlocks (blocks) {
|
||||
this.blocks = blocks;
|
||||
}
|
||||
@@ -181,12 +225,17 @@ class Blocks extends React.Component {
|
||||
this.setState({prompt: null});
|
||||
}
|
||||
render () {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const {
|
||||
options, // eslint-disable-line no-unused-vars
|
||||
vm, // eslint-disable-line no-unused-vars
|
||||
isVisible, // eslint-disable-line no-unused-vars
|
||||
options,
|
||||
vm,
|
||||
isVisible,
|
||||
onActivateColorPicker,
|
||||
onExtensionAdded,
|
||||
toolboxXML,
|
||||
...props
|
||||
} = this.props;
|
||||
/* eslint-enable no-unused-vars */
|
||||
return (
|
||||
<div>
|
||||
<BlocksComponent
|
||||
@@ -209,6 +258,8 @@ class Blocks extends React.Component {
|
||||
|
||||
Blocks.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
onActivateColorPicker: PropTypes.func,
|
||||
onExtensionAdded: PropTypes.func,
|
||||
options: PropTypes.shape({
|
||||
media: PropTypes.string,
|
||||
zoom: PropTypes.shape({
|
||||
@@ -230,6 +281,7 @@ Blocks.propTypes = {
|
||||
}),
|
||||
comments: PropTypes.bool
|
||||
}),
|
||||
toolboxXML: PropTypes.string,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
@@ -264,4 +316,18 @@ Blocks.defaultProps = {
|
||||
options: Blocks.defaultOptions
|
||||
};
|
||||
|
||||
export default Blocks;
|
||||
const mapStateToProps = state => ({
|
||||
toolboxXML: state.toolbox.toolboxXML
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onActivateColorPicker: callback => dispatch(activateColorPicker(callback)),
|
||||
onExtensionAdded: toolboxXML => {
|
||||
dispatch(updateToolbox(toolboxXML));
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Blocks);
|
||||
@@ -29,7 +29,6 @@ class CostumeLibrary extends React.PureComponent {
|
||||
<LibraryComponent
|
||||
data={costumeLibraryContent}
|
||||
title="Costume Library"
|
||||
visible={this.props.visible}
|
||||
onItemSelected={this.handleItemSelected}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
/>
|
||||
@@ -39,7 +38,6 @@ class CostumeLibrary extends React.PureComponent {
|
||||
|
||||
CostumeLibrary.propTypes = {
|
||||
onRequestClose: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import bindAll from 'lodash.bindall';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import VM from 'scratch-vm';
|
||||
|
||||
import extensionLibraryContent from '../lib/libraries/extensions.json';
|
||||
|
||||
import LibraryComponent from '../components/library/library.jsx';
|
||||
import extensionIcon from '../components/sprite-selector/icon--sprite.svg';
|
||||
|
||||
class ExtensionLibrary extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleItemSelect'
|
||||
]);
|
||||
}
|
||||
handleItemSelect (item) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const extensionLibraryThumbnailData = extensionLibraryContent.map(extension => ({
|
||||
rawURL: extension.iconURL || extensionIcon,
|
||||
...extension
|
||||
}));
|
||||
return (
|
||||
<LibraryComponent
|
||||
data={extensionLibraryThumbnailData}
|
||||
title="Extension Library"
|
||||
visible={this.props.visible}
|
||||
onItemSelected={this.handleItemSelect}
|
||||
onRequestClose={this.props.onRequestClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExtensionLibrary.propTypes = {
|
||||
onRequestClose: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types
|
||||
};
|
||||
|
||||
export default ExtensionLibrary;
|
||||
+16
-1
@@ -3,6 +3,9 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import VM from 'scratch-vm';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {openExtensionLibrary} from '../reducers/modals.js';
|
||||
|
||||
import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
|
||||
|
||||
@@ -43,6 +46,7 @@ class GUI extends React.Component {
|
||||
} = this.props;
|
||||
return (
|
||||
<GUIComponent
|
||||
enableExtensions={window.location.search.includes('extensions')}
|
||||
tabIndex={this.state.tabIndex}
|
||||
vm={vm}
|
||||
onTabSelect={this.handleTabSelect}
|
||||
@@ -62,4 +66,15 @@ GUI.propTypes = {
|
||||
|
||||
GUI.defaultProps = GUIComponent.defaultProps;
|
||||
|
||||
export default vmListenerHOC(GUI);
|
||||
const mapStateToProps = () => ({});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onExtensionButtonClick: () => dispatch(openExtensionLibrary())
|
||||
});
|
||||
|
||||
const ConnectedGUI = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(GUI);
|
||||
|
||||
export default vmListenerHOC(ConnectedGUI);
|
||||
@@ -130,7 +130,6 @@ RecordModal.propTypes = {
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
visible: state.modals.soundRecorder,
|
||||
vm: state.vm
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import RecordingStepComponent from '../components/record-modal/recording-step.jsx';
|
||||
import AudioRecorder from '../lib/audio/audio-recorder.js';
|
||||
@@ -66,6 +67,10 @@ class RecordingStep extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
RecordingStep.propTypes = RecordingStepComponent.propTypes;
|
||||
RecordingStep.propTypes = {
|
||||
onRecord: PropTypes.func.isRequired,
|
||||
onStopRecording: PropTypes.func.isRequired,
|
||||
recording: PropTypes.bool
|
||||
};
|
||||
|
||||
export default RecordingStep;
|
||||
@@ -22,9 +22,8 @@ class SoundLibrary extends React.PureComponent {
|
||||
this.audioEngine = new AudioEngine();
|
||||
this.player = this.audioEngine.createPlayer();
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
// Stop playing sounds if the library closes without a mouseleave (e.g. by using the escape key)
|
||||
if (this.player && !newProps.visible) this.player.stopAllSounds();
|
||||
componentWillUnmount () {
|
||||
this.player.stopAllSounds();
|
||||
}
|
||||
handleItemMouseEnter (soundItem) {
|
||||
const md5ext = soundItem._md5;
|
||||
@@ -76,7 +75,6 @@ class SoundLibrary extends React.PureComponent {
|
||||
<LibraryComponent
|
||||
data={soundLibraryThumbnailData}
|
||||
title="Sound Library"
|
||||
visible={this.props.visible}
|
||||
onItemMouseEnter={this.handleItemMouseEnter}
|
||||
onItemMouseLeave={this.handleItemMouseLeave}
|
||||
onItemSelected={this.handleItemSelected}
|
||||
@@ -88,7 +86,6 @@ class SoundLibrary extends React.PureComponent {
|
||||
|
||||
SoundLibrary.propTypes = {
|
||||
onRequestClose: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ class SoundTab extends React.Component {
|
||||
onDeleteClick={this.handleDeleteSound}
|
||||
onItemClick={this.handleSelectSound}
|
||||
>
|
||||
{target.sounds && target.sounds.length > 0 ? (
|
||||
{editingTarget && target.sounds && target.sounds.length > 0 ? (
|
||||
<SoundEditor soundIndex={this.state.selectedSoundIndex} />
|
||||
) : null}
|
||||
{this.props.soundRecorderVisible ? (
|
||||
|
||||
@@ -24,8 +24,8 @@ class SpriteLibrary extends React.PureComponent {
|
||||
sprites: spriteLibraryContent
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (!newProps.visible) clearInterval(this.intervalId);
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
handleItemSelect (item) {
|
||||
this.props.vm.addSprite2(JSON.stringify(item.json));
|
||||
@@ -66,7 +66,6 @@ class SpriteLibrary extends React.PureComponent {
|
||||
<LibraryComponent
|
||||
data={this.state.sprites}
|
||||
title="Sprite Library"
|
||||
visible={this.props.visible}
|
||||
onItemMouseEnter={this.handleMouseEnter}
|
||||
onItemMouseLeave={this.handleMouseLeave}
|
||||
onItemSelected={this.handleItemSelect}
|
||||
@@ -78,7 +77,6 @@ class SpriteLibrary extends React.PureComponent {
|
||||
|
||||
SpriteLibrary.propTypes = {
|
||||
onRequestClose: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
vm: PropTypes.instanceOf(VM).isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -3,10 +3,19 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Renderer from 'scratch-render';
|
||||
import VM from 'scratch-vm';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
|
||||
import StageComponent from '../components/stage/stage.jsx';
|
||||
|
||||
import {
|
||||
activateColorPicker,
|
||||
deactivateColorPicker
|
||||
} from '../reducers/color-picker';
|
||||
|
||||
const colorPickerRadius = 20;
|
||||
|
||||
class Stage extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@@ -28,7 +37,8 @@ class Stage extends React.Component {
|
||||
mouseDownPosition: null,
|
||||
isDragging: false,
|
||||
dragOffset: null,
|
||||
dragId: null
|
||||
dragId: null,
|
||||
colorInfo: null
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
@@ -38,12 +48,31 @@ class Stage extends React.Component {
|
||||
this.renderer = new Renderer(this.canvas);
|
||||
this.props.vm.attachRenderer(this.renderer);
|
||||
}
|
||||
shouldComponentUpdate (nextProps) {
|
||||
return this.props.width !== nextProps.width || this.props.height !== nextProps.height;
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
return this.props.width !== nextProps.width ||
|
||||
this.props.height !== nextProps.height ||
|
||||
this.props.isColorPicking !== nextProps.isColorPicking ||
|
||||
this.state.colorInfo !== nextState.colorInfo;
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.isColorPicking && !prevProps.isColorPicking) {
|
||||
this.startColorPickingLoop();
|
||||
} else if (!this.props.isColorPicking && prevProps.isColorPicking) {
|
||||
this.stopColorPickingLoop();
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.detachMouseEvents(this.canvas);
|
||||
this.detachRectEvents();
|
||||
this.stopColorPickingLoop();
|
||||
}
|
||||
startColorPickingLoop () {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.setState({colorInfo: this.getColorInfo(this.pickX, this.pickY)});
|
||||
}, 30);
|
||||
}
|
||||
stopColorPickingLoop () {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
attachMouseEvents (canvas) {
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
@@ -79,6 +108,13 @@ class Stage extends React.Component {
|
||||
(nativeSize[1] / this.rect.height) * (y - (this.rect.height / 2))
|
||||
];
|
||||
}
|
||||
getColorInfo (x, y) {
|
||||
return {
|
||||
x: x,
|
||||
y: y,
|
||||
...this.renderer.extractColor(x, y, colorPickerRadius)
|
||||
};
|
||||
}
|
||||
handleDoubleClick (e) {
|
||||
const {x, y} = getEventXY(e);
|
||||
// Set editing target from cursor position, if clicking on a sprite.
|
||||
@@ -92,6 +128,11 @@ class Stage extends React.Component {
|
||||
onMouseMove (e) {
|
||||
const {x, y} = getEventXY(e);
|
||||
const mousePosition = [x - this.rect.left, y - this.rect.top];
|
||||
|
||||
// Set the pickX/Y for the color picker loop to pick up
|
||||
this.pickX = mousePosition[0];
|
||||
this.pickY = mousePosition[1];
|
||||
|
||||
if (this.state.mouseDownTimeoutId !== null) {
|
||||
this.cancelMouseDownTimeout();
|
||||
if (this.state.mouseDown && !this.state.isDragging) {
|
||||
@@ -157,6 +198,16 @@ class Stage extends React.Component {
|
||||
if (e.preventDefault) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if (this.props.isColorPicking) {
|
||||
const {r, g, b} = this.state.colorInfo.color;
|
||||
const componentToString = c => {
|
||||
const hex = c.toString(16);
|
||||
return hex.length === 1 ? `0${hex}` : hex;
|
||||
};
|
||||
const colorString = `#${componentToString(r)}${componentToString(g)}${componentToString(b)}`;
|
||||
this.props.onDeactivateColorPicker(colorString);
|
||||
this.setState({colorInfo: null});
|
||||
}
|
||||
}
|
||||
cancelMouseDownTimeout () {
|
||||
if (this.state.mouseDownTimeoutId !== null) {
|
||||
@@ -191,11 +242,13 @@ class Stage extends React.Component {
|
||||
render () {
|
||||
const {
|
||||
vm, // eslint-disable-line no-unused-vars
|
||||
onActivateColorPicker, // eslint-disable-line no-unused-vars
|
||||
...props
|
||||
} = this.props;
|
||||
return (
|
||||
<StageComponent
|
||||
canvasRef={this.setCanvas}
|
||||
colorInfo={this.state.colorInfo}
|
||||
onDoubleClick={this.handleDoubleClick}
|
||||
{...props}
|
||||
/>
|
||||
@@ -205,8 +258,23 @@ class Stage extends React.Component {
|
||||
|
||||
Stage.propTypes = {
|
||||
height: PropTypes.number,
|
||||
isColorPicking: PropTypes.bool,
|
||||
onActivateColorPicker: PropTypes.func,
|
||||
onDeactivateColorPicker: PropTypes.func,
|
||||
vm: PropTypes.instanceOf(VM).isRequired,
|
||||
width: PropTypes.number
|
||||
};
|
||||
|
||||
export default Stage;
|
||||
const mapStateToProps = state => ({
|
||||
isColorPicking: state.colorPicker.active
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onActivateColorPicker: () => dispatch(activateColorPicker()),
|
||||
onDeactivateColorPicker: color => dispatch(deactivateColorPicker(color))
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(Stage);
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
openSpriteLibrary,
|
||||
closeBackdropLibrary,
|
||||
closeCostumeLibrary,
|
||||
closeExtensionLibrary,
|
||||
closeSoundLibrary,
|
||||
closeSpriteLibrary
|
||||
} from '../reducers/modals';
|
||||
@@ -91,7 +92,8 @@ const mapStateToProps = state => ({
|
||||
soundLibraryVisible: state.modals.soundLibrary,
|
||||
spriteLibraryVisible: state.modals.spriteLibrary,
|
||||
costumeLibraryVisible: state.modals.costumeLibrary,
|
||||
backdropLibraryVisible: state.modals.backdropLibrary
|
||||
backdropLibraryVisible: state.modals.backdropLibrary,
|
||||
extensionLibraryVisible: state.modals.extensionLibrary
|
||||
});
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onNewSpriteClick: e => {
|
||||
@@ -104,6 +106,9 @@ const mapDispatchToProps = dispatch => ({
|
||||
onRequestCloseCostumeLibrary: () => {
|
||||
dispatch(closeCostumeLibrary());
|
||||
},
|
||||
onRequestCloseExtensionLibrary: () => {
|
||||
dispatch(closeExtensionLibrary());
|
||||
},
|
||||
onRequestCloseSoundLibrary: () => {
|
||||
dispatch(closeSoundLibrary());
|
||||
},
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
extends: ['scratch'], // no ES6
|
||||
env: {
|
||||
worker: true
|
||||
},
|
||||
globals: {
|
||||
Scratch: true
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,230 @@
|
||||
var ExampleExtension = function () {
|
||||
};
|
||||
|
||||
/**
|
||||
* @return {object} This extension's metadata.
|
||||
*/
|
||||
ExampleExtension.prototype.getInfo = function () {
|
||||
return {
|
||||
// Required: the machine-readable name of this extension.
|
||||
// Will be used as the extension's namespace. Must not contain a '.' character.
|
||||
id: 'someBlocks',
|
||||
|
||||
// Optional: the human-readable name of this extension as string.
|
||||
// This and any other string to be displayed in the Scratch UI may either be
|
||||
// a string or a call to `intlDefineMessage`; a plain string will not be
|
||||
// translated whereas a call to `intlDefineMessage` will connect the string
|
||||
// to the translation map (see below). The `intlDefineMessage` call is
|
||||
// similar to `defineMessages` from `react-intl` in form, but will actually
|
||||
// call some extension support code to do its magic. For example, we will
|
||||
// internally namespace the messages such that two extensions could have
|
||||
// messages with the same ID without colliding.
|
||||
// See also: https://github.com/yahoo/react-intl/wiki/API#definemessages
|
||||
name: 'Some Blocks',
|
||||
|
||||
// Optional: URI for an icon for this extension. Data URI OK.
|
||||
// If not present, use a generic icon.
|
||||
// TODO: what file types are OK? All web images? Just PNG?
|
||||
iconURI: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAAAAACyOJm3AAAAFklEQVQYV2P4DwMMEMgAI/+DE' +
|
||||
'UIMBgAEWB7i7uidhAAAAABJRU5ErkJggg==',
|
||||
|
||||
// Optional: Link to documentation content for this extension.
|
||||
// If not present, offer no link.
|
||||
docsURI: 'https://....',
|
||||
|
||||
// Required: the list of blocks implemented by this extension,
|
||||
// in the order intended for display.
|
||||
blocks: [
|
||||
{
|
||||
opcode: 'example-noop',
|
||||
blockType: Scratch.BlockType.COMMAND,
|
||||
blockAllThreads: false,
|
||||
text: 'do nothing',
|
||||
func: 'noop'
|
||||
},
|
||||
{
|
||||
opcode: 'example-conditional',
|
||||
blockType: Scratch.BlockType.CONDITIONAL,
|
||||
branchCount: 4,
|
||||
isTerminal: true,
|
||||
blockAllThreads: false,
|
||||
text: 'choose [BRANCH]',
|
||||
arguments: {
|
||||
BRANCH: {
|
||||
type: Scratch.ArgumentType.NUMBER,
|
||||
defaultValue: 1
|
||||
}
|
||||
},
|
||||
func: 'noop'
|
||||
},
|
||||
{
|
||||
// Required: the machine-readable name of this operation.
|
||||
// This will appear in project JSON. Must not contain a '.' character.
|
||||
opcode: 'myReporter', // becomes 'someBlocks.myReporter'
|
||||
|
||||
// Required: the kind of block we're defining, from a predefined list:
|
||||
// 'command' - a normal command block, like "move {} steps"
|
||||
// 'reporter' - returns a value, like "direction"
|
||||
// 'Boolean' - same as 'reporter' but returns a Boolean value
|
||||
// 'hat' - starts a stack if its value is truthy
|
||||
// 'conditional' - control flow, like "if {}" or "repeat {}"
|
||||
// A 'conditional' block may return the one-based index of a branch
|
||||
// to run, or it may return zero/falsy to run no branch. Each time a
|
||||
// child branch finishes, the block is called again. This is only a
|
||||
// slight change to the current model for control flow blocks, and is
|
||||
// also compatible with returning true/false for an "if" or "repeat"
|
||||
// block.
|
||||
// TODO: Consider Blockly-like nextStatement, previousStatement, and
|
||||
// output attributes as an alternative. Those are more flexible, but
|
||||
// allow bad combinations.
|
||||
blockType: Scratch.BlockType.REPORTER,
|
||||
|
||||
// Required for conditional blocks, ignored for others: the number of
|
||||
// child branches this block controls. An "if" or "repeat" block would
|
||||
// specify a branch count of 1; an "if-else" block would specify a
|
||||
// branch count of 2.
|
||||
// TODO: should we support dynamic branch count for "switch"-likes?
|
||||
branchCount: 0,
|
||||
|
||||
// Optional, default false: whether or not this block ends a stack.
|
||||
// The "forever" and "stop all" blocks would specify true here.
|
||||
isTerminal: true,
|
||||
|
||||
// Optional, default false: whether or not to block all threads while
|
||||
// this block is busy. This is for things like the "touching color"
|
||||
// block in compatibility mode, and is only needed if the VM runs in a
|
||||
// worker. We might even consider omitting it from extension docs...
|
||||
blockAllThreads: false,
|
||||
|
||||
// Required: the human-readable text on this block, including argument
|
||||
// placeholders. Argument placeholders should be in [MACRO_CASE] and
|
||||
// must be [ENCLOSED_WITHIN_SQUARE_BRACKETS].
|
||||
text: 'letter [LETTER_NUM] of [TEXT]',
|
||||
|
||||
// Required: describe each argument.
|
||||
// Note that this is an array: the order of arguments will be used
|
||||
arguments: {
|
||||
// Required: the ID of the argument, which will be the name in the
|
||||
// args object passed to the implementation function.
|
||||
LETTER_NUM: {
|
||||
// Required: type of the argument / shape of the block input
|
||||
type: Scratch.ArgumentType.NUMBER,
|
||||
|
||||
// Optional: the default value of the argument
|
||||
defaultValue: 1
|
||||
},
|
||||
|
||||
// Required: the ID of the argument, which will be the name in the
|
||||
// args object passed to the implementation function.
|
||||
TEXT: {
|
||||
// Required: type of the argument / shape of the block input
|
||||
type: Scratch.ArgumentType.STRING,
|
||||
|
||||
// Optional: the default value of the argument
|
||||
defaultValue: 'text'
|
||||
}
|
||||
},
|
||||
|
||||
// Optional: a string naming the function implementing this block.
|
||||
// If this is omitted, use the opcode string.
|
||||
func: 'myReporter',
|
||||
|
||||
// Optional: list of target types for which this block should appear.
|
||||
// If absent, assume it applies to all builtin targets -- that is:
|
||||
// ['sprite', 'stage']
|
||||
filter: ['someBlocks.wedo2', 'sprite', 'stage']
|
||||
},
|
||||
{
|
||||
opcode: 'example-Boolean',
|
||||
blockType: Scratch.BlockType.BOOLEAN,
|
||||
text: 'return true',
|
||||
func: 'returnTrue'
|
||||
},
|
||||
{
|
||||
opcode: 'example-hat',
|
||||
blockType: Scratch.BlockType.HAT,
|
||||
text: 'after forever',
|
||||
func: 'returnFalse'
|
||||
},
|
||||
{
|
||||
// Another block...
|
||||
}
|
||||
],
|
||||
|
||||
// Optional: define extension-specific menus here.
|
||||
menus: {
|
||||
// Required: an identifier for this menu, unique within this extension.
|
||||
menuA: [
|
||||
// Static menu: list items which should appear in the menu.
|
||||
{
|
||||
// Required: the value of the menu item when it is chosen.
|
||||
value: 'itemId1',
|
||||
|
||||
// Optional: the human-readable label for this item.
|
||||
// Use `value` as the text if this is absent.
|
||||
text: 'Item One'
|
||||
},
|
||||
|
||||
// The simplest form of a list item is a string which will be used as
|
||||
// both value and text.
|
||||
'itemId2'
|
||||
],
|
||||
|
||||
// Dynamic menu: a string naming a function which returns an array as above.
|
||||
// Called each time the menu is opened.
|
||||
menuB: 'getItemsForMenuB'
|
||||
},
|
||||
|
||||
// Optional: translations
|
||||
translation_map: {
|
||||
de: {
|
||||
'extensionName': 'Einige Blöcke',
|
||||
'myReporter': 'Buchstabe [LETTER_NUM] von [TEXT]',
|
||||
'myReporter.TEXT_default': 'Text',
|
||||
'menuA_item1': 'Artikel eins',
|
||||
|
||||
// Dynamic menus can be translated too
|
||||
'menuB_example': 'Beispiel',
|
||||
|
||||
// This message contains ICU placeholders (see `myReporter()` below)
|
||||
'myReporter.result': 'Buchstabe {LETTER_NUM} von {TEXT} ist {LETTER}.'
|
||||
},
|
||||
it: {
|
||||
// ...
|
||||
}
|
||||
},
|
||||
|
||||
// Optional: list new target type(s) provided by this extension.
|
||||
targetTypes: [
|
||||
'wedo2', // automatically transformed to 'someBlocks.wedo2'
|
||||
'speech' // automatically transformed to 'someBlocks.speech'
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Implement myReporter.
|
||||
* @param {object} args - the block's arguments.
|
||||
* @property {number} LETTER_NUM - the string value of the argument.
|
||||
* @property {string} TEXT - the string value of the argument.
|
||||
* @returns {string} a string which includes the block argument value.
|
||||
*/
|
||||
ExampleExtension.prototype.myReporter = function (args) {
|
||||
// Note: this implementation is not Unicode-clean; it's just here as an example.
|
||||
const result = args.TEXT.charAt(args.LETTER_NUM);
|
||||
|
||||
return ['Letter ', args.LETTER_NUM, ' of ', args.TEXT, ' is ', result, '.'].join('');
|
||||
};
|
||||
|
||||
ExampleExtension.prototype.noop = function () {
|
||||
};
|
||||
|
||||
ExampleExtension.prototype.returnTrue = function () {
|
||||
return true;
|
||||
};
|
||||
|
||||
ExampleExtension.prototype.returnFalse = function () {
|
||||
return false;
|
||||
};
|
||||
|
||||
Scratch.extensions.register(new ExampleExtension());
|
||||
+19
-6
@@ -29,19 +29,24 @@ export default function (vm) {
|
||||
};
|
||||
|
||||
const soundsMenu = function () {
|
||||
const sounds = vm.editingTarget.sprite.sounds;
|
||||
if (sounds.length === 0) {
|
||||
return [['', '']];
|
||||
if (vm.editingTarget && vm.editingTarget.sprite.sounds.length > 0) {
|
||||
return vm.editingTarget.sprite.sounds.map(sound => [sound.name, sound.name]);
|
||||
}
|
||||
return sounds.map(sound => [sound.name, sound.name]);
|
||||
return [['', '']];
|
||||
};
|
||||
|
||||
const costumesMenu = function () {
|
||||
return vm.editingTarget.sprite.costumes.map(costume => [costume.name, costume.name]);
|
||||
if (vm.editingTarget && vm.editingTarget.sprite.costumes.length > 0) {
|
||||
return vm.editingTarget.sprite.costumes.map(costume => [costume.name, costume.name]);
|
||||
}
|
||||
return [['', '']];
|
||||
};
|
||||
|
||||
const backdropsMenu = function () {
|
||||
return vm.runtime.targets[0].sprite.costumes.map(costume => [costume.name, costume.name]);
|
||||
if (vm.runtime.targets[0] && vm.runtime.targets[0].sprite.costumes.length > 0) {
|
||||
return vm.runtime.targets[0].sprite.costumes.map(costume => [costume.name, costume.name]);
|
||||
}
|
||||
return [['', '']];
|
||||
};
|
||||
|
||||
const spriteMenu = function () {
|
||||
@@ -100,6 +105,14 @@ export default function (vm) {
|
||||
this.jsonInit(json);
|
||||
};
|
||||
|
||||
ScratchBlocks.Blocks.motion_glideto_menu.init = function () {
|
||||
const json = jsonForMenuBlock('TO', spriteMenu, motionColors, [
|
||||
['mouse-pointer', '_mouse_'],
|
||||
['random position', '_random_']
|
||||
]);
|
||||
this.jsonInit(json);
|
||||
};
|
||||
|
||||
ScratchBlocks.Blocks.sensing_of_object_menu.init = function () {
|
||||
const json = jsonForMenuBlock('OBJECT', spriteMenu, sensingColors, [
|
||||
['Stage', '_stage_']
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"name": "Some Blocks",
|
||||
"extensionURL": "static/extensions/example-extension.js",
|
||||
"md5": "76f99773dd4eb59f86cd11bba6ad3cb9.svg"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,728 @@
|
||||
const separator = '<sep gap="45"/>';
|
||||
|
||||
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_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 motion = `
|
||||
<category name="Motion" colour="#4C97FF" secondaryColour="#3373CC">
|
||||
<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_turnleft">
|
||||
<value name="DEGREES">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">15</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_pointindirection">
|
||||
<value name="DIRECTION">
|
||||
<shadow type="math_angle">
|
||||
<field name="NUM">90</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_pointtowards">
|
||||
<value name="TOWARDS">
|
||||
<shadow type="motion_pointtowards_menu">
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_gotoxy">
|
||||
<value name="X">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="Y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_goto">
|
||||
<value name="TO">
|
||||
<shadow type="motion_goto_menu">
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_glidesecstoxy">
|
||||
<value name="SECS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="X">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="Y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_glideto" id="motion_glideto">
|
||||
<value name="SECS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="TO">
|
||||
<shadow type="motion_glideto_menu">
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_changexby">
|
||||
<value name="DX">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_setx">
|
||||
<value name="X">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_changeyby">
|
||||
<value name="DY">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_sety">
|
||||
<value name="Y">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="motion_ifonedgebounce"/>
|
||||
<block type="motion_setrotationstyle"/>
|
||||
<block type="motion_xposition"/>
|
||||
<block type="motion_yposition"/>
|
||||
<block type="motion_direction"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const looks = `
|
||||
<category name="Looks" colour="#9966FF" secondaryColour="#774DCB">
|
||||
<block type="looks_show"/>
|
||||
<block type="looks_hide"/>
|
||||
<block type="looks_switchcostumeto">
|
||||
<value name="COSTUME">
|
||||
<shadow type="looks_costume"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_nextcostume"/>
|
||||
<block type="looks_nextbackdrop"/>
|
||||
<block type="looks_switchbackdropto">
|
||||
<value name="BACKDROP">
|
||||
<shadow type="looks_backdrops"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_switchbackdroptoandwait">
|
||||
<value name="BACKDROP">
|
||||
<shadow type="looks_backdrops"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_changeeffectby">
|
||||
<value name="CHANGE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_seteffectto">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_cleargraphiceffects"/>
|
||||
<block type="looks_changesizeby">
|
||||
<value name="CHANGE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_setsizeto">
|
||||
<value name="SIZE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_gotofront"/>
|
||||
<block type="looks_gobacklayers">
|
||||
<value name="NUM">
|
||||
<shadow type="math_integer">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="looks_costumeorder"/>
|
||||
<block type="looks_backdroporder"/>
|
||||
<block type="looks_backdropname"/>
|
||||
<block type="looks_size"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const sound = `
|
||||
<category name="Sound" colour="#D65CD6" secondaryColour="#BD42BD">
|
||||
<block type="sound_play">
|
||||
<value name="SOUND_MENU">
|
||||
<shadow type="sound_sounds_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_playuntildone">
|
||||
<value name="SOUND_MENU">
|
||||
<shadow type="sound_sounds_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_stopallsounds"/>
|
||||
<block type="sound_playdrumforbeats">
|
||||
<value name="DRUM">
|
||||
<shadow type="sound_drums_menu"/>
|
||||
</value>
|
||||
<value name="BEATS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0.25</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_restforbeats">
|
||||
<value name="BEATS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0.25</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_playnoteforbeats">
|
||||
<value name="NOTE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">60</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="BEATS">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0.5</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_setinstrumentto">
|
||||
<value name="INSTRUMENT">
|
||||
<shadow type="sound_instruments_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_changeeffectby">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_seteffectto">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_cleareffects"/>
|
||||
<block type="sound_changevolumeby">
|
||||
<value name="VOLUME">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">-10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_setvolumeto">
|
||||
<value name="VOLUME">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">100</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_volume"/>
|
||||
<block type="sound_changetempoby">
|
||||
<value name="TEMPO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">20</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_settempotobpm">
|
||||
<value name="TEMPO">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">60</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sound_tempo"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const pen = `
|
||||
<category name="Pen" colour="#00B295" secondaryColour="#0B8E69">
|
||||
<block type="pen_clear"/>
|
||||
<block type="pen_stamp"/>
|
||||
<block type="pen_pendown"/>
|
||||
<block type="pen_penup"/>
|
||||
<block type="pen_setpencolortocolor">
|
||||
<value name="COLOR">
|
||||
<shadow type="colour_picker">
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_changepencolorby">
|
||||
<value name="COLOR">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_setpencolortonum">
|
||||
<value name="COLOR">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">0</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_changepenshadeby">
|
||||
<value name="SHADE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_setpenshadeto">
|
||||
<value name="SHADE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">50</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_changepensizeby">
|
||||
<value name="SIZE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_setpensizeto">
|
||||
<value name="SIZE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_changepentransparencyby" id="pen_changepentransparencyby">
|
||||
<value name="TRANSPARENCY">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="pen_setpentransparencyto" id="pen_setpentransparencyto">
|
||||
<value name="TRANSPARENCY">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">50</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const events = `
|
||||
<category name="Events" colour="#FFD500" secondaryColour="#CC9900">
|
||||
<block type="event_whenflagclicked"/>
|
||||
<block type="event_whenkeypressed">
|
||||
</block>
|
||||
<block type="event_whenthisspriteclicked"/>
|
||||
<block type="event_whenbackdropswitchesto">
|
||||
</block>
|
||||
<block type="event_whengreaterthan">
|
||||
<value name="VALUE">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM">10</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="event_whenbroadcastreceived">
|
||||
</block>
|
||||
<block type="event_broadcast">
|
||||
<value name="BROADCAST_OPTION">
|
||||
<shadow type="event_broadcast_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="event_broadcastandwait">
|
||||
<value name="BROADCAST_OPTION">
|
||||
<shadow type="event_broadcast_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const control = `
|
||||
<category name="Control" colour="#FFAB19" secondaryColour="#CF8B17">
|
||||
<block type="control_wait">
|
||||
<value name="DURATION">
|
||||
<shadow type="math_positive_number">
|
||||
<field name="NUM">1</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_forever"/>
|
||||
<block type="control_if"/>
|
||||
<block type="control_if_else"/>
|
||||
<block type="control_wait_until"/>
|
||||
<block type="control_repeat_until"/>
|
||||
<block type="control_stop"/>
|
||||
<block type="control_start_as_clone"/>
|
||||
<block type="control_create_clone_of">
|
||||
<value name="CLONE_OPTION">
|
||||
<shadow type="control_create_clone_of_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="control_delete_this_clone"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const sensing = `
|
||||
<category name="Sensing" colour="#4CBFE6" secondaryColour="#2E8EB8">
|
||||
<block type="sensing_touchingobject">
|
||||
<value name="TOUCHINGOBJECTMENU">
|
||||
<shadow type="sensing_touchingobjectmenu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_touchingcolor">
|
||||
<value name="COLOR">
|
||||
<shadow type="colour_picker"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_coloristouchingcolor">
|
||||
<value name="COLOR">
|
||||
<shadow type="colour_picker"/>
|
||||
</value>
|
||||
<value name="COLOR2">
|
||||
<shadow type="colour_picker"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_distanceto">
|
||||
<value name="DISTANCETOMENU">
|
||||
<shadow type="sensing_distancetomenu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_keypressed">
|
||||
<value name="KEY_OPTION">
|
||||
<shadow type="sensing_keyoptions"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_mousedown"/>
|
||||
<block type="sensing_mousex"/>
|
||||
<block type="sensing_mousey"/>
|
||||
<block type="sensing_loudness"/>
|
||||
<block type="sensing_timer"/>
|
||||
<block type="sensing_resettimer"/>
|
||||
<block type="sensing_of">
|
||||
<value name="PROPERTY">
|
||||
<shadow type="sensing_of_property_menu"/>
|
||||
</value>
|
||||
<value name="OBJECT">
|
||||
<shadow type="sensing_of_object_menu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_current">
|
||||
<value name="CURRENTMENU">
|
||||
<shadow type="sensing_currentmenu"/>
|
||||
</value>
|
||||
</block>
|
||||
<block type="sensing_dayssince2000"/>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const operators = `
|
||||
<category name="Operators" colour="#40BF4A" secondaryColour="#389438">
|
||||
<block type="operator_add">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_subtract">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_multiply">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_divide">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</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>
|
||||
<block type="operator_lt">
|
||||
<value name="OPERAND1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="OPERAND2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_equals">
|
||||
<value name="OPERAND1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="OPERAND2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_gt">
|
||||
<value name="OPERAND1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="OPERAND2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_and"/>
|
||||
<block type="operator_or"/>
|
||||
<block type="operator_not"/>
|
||||
<block type="operator_join">
|
||||
<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>
|
||||
<block type="operator_letter_of">
|
||||
<value name="LETTER">
|
||||
<shadow type="math_whole_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="STRING">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">world</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_length">
|
||||
<value name="STRING">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">world</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_mod">
|
||||
<value name="NUM1">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="NUM2">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_round">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_mathop">
|
||||
<value name="NUM">
|
||||
<shadow type="math_number">
|
||||
<field name="NUM"/>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</category>
|
||||
`;
|
||||
|
||||
const data = `
|
||||
<category name="Data" colour="#FF8C1A" secondaryColour="#DB6E00" custom="VARIABLE">
|
||||
</category>
|
||||
`;
|
||||
|
||||
const xmlOpen = '<xml style="display: none">';
|
||||
const xmlClose = '</xml>';
|
||||
|
||||
/**
|
||||
* @param {string?} categoriesXML - null for default toolbox, or an XML string with <category> elements.
|
||||
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
|
||||
*/
|
||||
const makeToolboxXML = function (categoriesXML) {
|
||||
const gap = [separator];
|
||||
|
||||
const everything = [
|
||||
xmlOpen,
|
||||
top, gap,
|
||||
motion, gap,
|
||||
looks, gap,
|
||||
sound, gap,
|
||||
events, gap,
|
||||
control, gap,
|
||||
sensing, gap,
|
||||
pen, gap,
|
||||
operators, gap,
|
||||
data
|
||||
];
|
||||
|
||||
if (categoriesXML) {
|
||||
everything.push(gap, categoriesXML);
|
||||
}
|
||||
|
||||
everything.push(xmlClose);
|
||||
|
||||
return everything.join('\n');
|
||||
};
|
||||
|
||||
export default makeToolboxXML;
|
||||
@@ -0,0 +1,40 @@
|
||||
const ACTIVATE_COLOR_PICKER = 'scratch-gui/color-picker/ACTIVATE_COLOR_PICKER';
|
||||
const DEACTIVATE_COLOR_PICKER = 'scratch-gui/color-picker/DEACTIVATE_COLOR_PICKER';
|
||||
const SET_CALLBACK = 'scratch-gui/color-picker/SET_CALLBACK';
|
||||
|
||||
const initialState = {
|
||||
active: false,
|
||||
callback: () => {
|
||||
throw new Error('Color picker callback not initialized');
|
||||
}
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case ACTIVATE_COLOR_PICKER:
|
||||
return Object.assign({}, state, {active: true, callback: action.callback});
|
||||
case DEACTIVATE_COLOR_PICKER:
|
||||
// Can be called without a string to deactivate without setting color
|
||||
// i.e. when clicking on the modal background
|
||||
if (typeof action.color === 'string') {
|
||||
state.callback(action.color);
|
||||
}
|
||||
return Object.assign({}, state, {active: false});
|
||||
case SET_CALLBACK:
|
||||
return Object.assign({}, state, {callback: action.callback});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const activateColorPicker = callback => ({type: ACTIVATE_COLOR_PICKER, callback: callback});
|
||||
const deactivateColorPicker = color => ({type: DEACTIVATE_COLOR_PICKER, color: color});
|
||||
const setCallback = callback => ({type: SET_CALLBACK, callback: callback});
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
activateColorPicker,
|
||||
deactivateColorPicker,
|
||||
setCallback
|
||||
};
|
||||
+4
-1
@@ -1,15 +1,18 @@
|
||||
import {combineReducers} from 'redux';
|
||||
import colorPickerReducer from './color-picker';
|
||||
import intlReducer from './intl';
|
||||
import modalReducer from './modals';
|
||||
import monitorReducer from './monitors';
|
||||
import targetReducer from './targets';
|
||||
import toolboxReducer from './toolbox';
|
||||
import vmReducer from './vm';
|
||||
|
||||
|
||||
export default combineReducers({
|
||||
colorPicker: colorPickerReducer,
|
||||
intl: intlReducer,
|
||||
modals: modalReducer,
|
||||
monitors: monitorReducer,
|
||||
targets: targetReducer,
|
||||
toolbox: toolboxReducer,
|
||||
vm: vmReducer
|
||||
});
|
||||
@@ -6,10 +6,12 @@ const MODAL_COSTUME_LIBRARY = 'costumeLibrary';
|
||||
const MODAL_SOUND_LIBRARY = 'soundLibrary';
|
||||
const MODAL_SPRITE_LIBRARY = 'spriteLibrary';
|
||||
const MODAL_SOUND_RECORDER = 'soundRecorder';
|
||||
const MODAL_EXTENSION_LIBRARY = 'extensionLibrary';
|
||||
|
||||
const initialState = {
|
||||
[MODAL_BACKDROP_LIBRARY]: false,
|
||||
[MODAL_COSTUME_LIBRARY]: false,
|
||||
[MODAL_EXTENSION_LIBRARY]: false,
|
||||
[MODAL_SOUND_LIBRARY]: false,
|
||||
[MODAL_SPRITE_LIBRARY]: false,
|
||||
[MODAL_SOUND_RECORDER]: false
|
||||
@@ -57,12 +59,18 @@ const openSpriteLibrary = function () {
|
||||
const openSoundRecorder = function () {
|
||||
return openModal(MODAL_SOUND_RECORDER);
|
||||
};
|
||||
const openExtensionLibrary = function () {
|
||||
return openModal(MODAL_EXTENSION_LIBRARY);
|
||||
};
|
||||
const closeBackdropLibrary = function () {
|
||||
return closeModal(MODAL_BACKDROP_LIBRARY);
|
||||
};
|
||||
const closeCostumeLibrary = function () {
|
||||
return closeModal(MODAL_COSTUME_LIBRARY);
|
||||
};
|
||||
const closeExtensionLibrary = function () {
|
||||
return closeModal(MODAL_EXTENSION_LIBRARY);
|
||||
};
|
||||
const closeSpriteLibrary = function () {
|
||||
return closeModal(MODAL_SPRITE_LIBRARY);
|
||||
};
|
||||
@@ -74,6 +82,7 @@ const closeSoundRecorder = function () {
|
||||
};
|
||||
export {
|
||||
reducer as default,
|
||||
openExtensionLibrary,
|
||||
openBackdropLibrary,
|
||||
openCostumeLibrary,
|
||||
openSoundLibrary,
|
||||
@@ -81,6 +90,7 @@ export {
|
||||
openSoundRecorder,
|
||||
closeBackdropLibrary,
|
||||
closeCostumeLibrary,
|
||||
closeExtensionLibrary,
|
||||
closeSpriteLibrary,
|
||||
closeSoundLibrary,
|
||||
closeSoundRecorder
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
const UPDATE_TOOLBOX = 'scratch-gui/toolbox/UPDATE_TOOLBOX';
|
||||
|
||||
import makeToolboxXML from '../lib/make-toolbox-xml';
|
||||
|
||||
const initialState = {
|
||||
toolboxXML: makeToolboxXML()
|
||||
};
|
||||
|
||||
const reducer = function (state, action) {
|
||||
if (typeof state === 'undefined') state = initialState;
|
||||
switch (action.type) {
|
||||
case UPDATE_TOOLBOX:
|
||||
return Object.assign({}, state, {
|
||||
toolboxXML: action.toolboxXML
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const updateToolbox = function (toolboxXML) {
|
||||
return {
|
||||
type: UPDATE_TOOLBOX,
|
||||
toolboxXML: toolboxXML
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
reducer as default,
|
||||
updateToolbox
|
||||
};
|
||||
@@ -33,7 +33,7 @@ class SeleniumHelper {
|
||||
}
|
||||
|
||||
clickText (text) {
|
||||
return this.clickXpath(`//*[contains(text(), '${text}')]`);
|
||||
return this.clickXpath(`//body//*[contains(text(), '${text}')]`);
|
||||
}
|
||||
|
||||
clickButton (text) {
|
||||
|
||||
@@ -99,6 +99,15 @@ module.exports = {
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'node_modules/scratch-blocks/media',
|
||||
to: 'static/blocks-media'
|
||||
}]),
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'extensions/**',
|
||||
to: 'static',
|
||||
context: 'src/examples'
|
||||
}]),
|
||||
new CopyWebpackPlugin([{
|
||||
from: 'extension-worker.{js,js.map}',
|
||||
context: 'node_modules/scratch-vm/dist/web'
|
||||
}])
|
||||
].concat(process.env.NODE_ENV === 'production' ? [
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
|
||||
Reference in new issue
Block a user