Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4e71024fb | ||
|
|
a83d9369dc | ||
|
|
012bb612ad | ||
|
|
b9e7bde676 | ||
|
|
992f012ff3 | ||
|
|
c53b88dbad |
No files matched your search
@@ -4,6 +4,7 @@
|
||||
# NPM
|
||||
/node_modules
|
||||
npm-*
|
||||
/package-lock.json
|
||||
|
||||
# Testing
|
||||
/.nyc_output
|
||||
|
||||
@@ -14,18 +14,12 @@ env:
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
before_install:
|
||||
# package-lock.json was introduced in npm@5
|
||||
- '[[ $(node -v) =~ ^v9.*$ ]] || npm install -g npm@latest' # skipped when using node 9
|
||||
- npm install -g greenkeeper-lockfile
|
||||
install:
|
||||
- npm --production=false install
|
||||
- npm --production=false update
|
||||
before_script: greenkeeper-lockfile-update
|
||||
script:
|
||||
- npm test
|
||||
- if [ "$TRAVIS_EVENT_TYPE" == "pull_request" ] && [ "$TRAVIS_BRANCH" == "master" ]; then npm run test:smoke; fi
|
||||
after_script: greenkeeper-lockfile-upload
|
||||
before_deploy:
|
||||
- >
|
||||
if [ -z "$BEFORE_DEPLOY_RAN" ]; then
|
||||
|
||||
@@ -100,18 +100,19 @@
|
||||
"react-test-renderer": "16.2.0",
|
||||
"react-tooltip": "3.8.0",
|
||||
"react-virtualized": "9.20.1",
|
||||
"react-visibility-sensor": "5.0.2",
|
||||
"redux": "3.7.2",
|
||||
"redux-mock-store": "^1.2.3",
|
||||
"redux-throttle": "0.1.1",
|
||||
"rimraf": "^2.6.1",
|
||||
"scratch-audio": "0.1.0-prerelease.20190114210212",
|
||||
"scratch-l10n": "3.3.20190711005050",
|
||||
"scratch-blocks": "0.1.0-prerelease.1561987297",
|
||||
"scratch-paint": "0.2.0-prerelease.20190624133806",
|
||||
"scratch-blocks": "0.1.0-prerelease.1559136797",
|
||||
"scratch-l10n": "3.3.20190605143948",
|
||||
"scratch-paint": "0.2.0-prerelease.20190524133325",
|
||||
"scratch-render": "0.1.0-prerelease.20190605151415",
|
||||
"scratch-storage": "1.3.2",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20190523193400",
|
||||
"scratch-vm": "0.2.0-prerelease.20190628161443",
|
||||
"scratch-vm": "0.2.0-prerelease.20190604212312",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"startaudiocontext": "1.2.1",
|
||||
"style-loader": "^0.23.0",
|
||||
|
||||
@@ -170,9 +170,6 @@
|
||||
align-items: center;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
/* Min height prevents layout changing when images change */
|
||||
min-height: 256px;
|
||||
}
|
||||
|
||||
.step-video {
|
||||
|
||||
@@ -104,9 +104,6 @@ class VideoStep extends React.Component {
|
||||
// We use the Wistia API here to update or pause the video dynamically:
|
||||
// https://wistia.com/support/developers/player-api
|
||||
componentDidUpdate (prevProps) {
|
||||
// Ensure the wistia API is loaded and available
|
||||
if (!(window.Wistia && window.Wistia.api)) return;
|
||||
|
||||
// Get a handle on the currently loaded video
|
||||
const video = window.Wistia.api(prevProps.video);
|
||||
|
||||
@@ -158,7 +155,6 @@ const ImageStep = ({title, image}) => (
|
||||
<img
|
||||
className={styles.stepImage}
|
||||
draggable={false}
|
||||
key={image} /* Use src as key to prevent hanging around on slow connections */
|
||||
src={image}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
import ScratchImage from '../scratch-image/scratch-image.jsx';
|
||||
import styles from './library-item.css';
|
||||
import classNames from 'classnames';
|
||||
|
||||
@@ -35,10 +36,12 @@ class LibraryItemComponent extends React.PureComponent {
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<img
|
||||
className={styles.featuredImage}
|
||||
src={this.props.iconURL}
|
||||
/>
|
||||
{this.props.iconSource ? (
|
||||
<ScratchImage
|
||||
className={styles.featuredImage}
|
||||
imageSource={this.props.iconSource}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{this.props.insetIconURL ? (
|
||||
<div className={styles.libraryItemInsetImageContainer}>
|
||||
@@ -121,9 +124,9 @@ class LibraryItemComponent extends React.PureComponent {
|
||||
{/* Layers of wrapping is to prevent layout thrashing on animation */}
|
||||
<Box className={styles.libraryItemImageContainerWrapper}>
|
||||
<Box className={styles.libraryItemImageContainer}>
|
||||
<img
|
||||
<ScratchImage
|
||||
className={styles.libraryItemImage}
|
||||
src={this.props.iconURL}
|
||||
imageSource={this.props.iconSource}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -146,7 +149,7 @@ LibraryItemComponent.propTypes = {
|
||||
extensionId: PropTypes.string,
|
||||
featured: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
iconURL: PropTypes.string,
|
||||
iconSource: ScratchImage.ImageSourcePropType,
|
||||
insetIconURL: PropTypes.string,
|
||||
internetConnectionRequired: PropTypes.bool,
|
||||
name: PropTypes.oneOfType([
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
overflow-y: auto;
|
||||
height: auto;
|
||||
padding: 0.5rem;
|
||||
height: calc(100% - $library-header-height);
|
||||
}
|
||||
|
||||
.library-scroll-grid.withFilterBar {
|
||||
@@ -60,11 +59,3 @@
|
||||
height: $library-filter-bar-height;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.spinner-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import Modal from '../../containers/modal.jsx';
|
||||
import Divider from '../divider/divider.jsx';
|
||||
import Filter from '../filter/filter.jsx';
|
||||
import TagButton from '../../containers/tag-button.jsx';
|
||||
import Spinner from '../spinner/spinner.jsx';
|
||||
import storage from '../../lib/storage';
|
||||
|
||||
import styles from './library.css';
|
||||
|
||||
@@ -29,6 +29,47 @@ const messages = defineMessages({
|
||||
const ALL_TAG = {tag: 'all', intlLabel: messages.allTag};
|
||||
const tagListPrefix = [ALL_TAG];
|
||||
|
||||
/**
|
||||
* Find the AssetType which corresponds to a particular file extension. For example, 'png' => AssetType.ImageBitmap.
|
||||
* @param {string} fileExtension - the file extension to look up.
|
||||
* @returns {AssetType} - the AssetType corresponding to the extension, if any.
|
||||
*/
|
||||
const getAssetTypeForFileExtension = function (fileExtension) {
|
||||
const compareOptions = {
|
||||
sensitivity: 'accent',
|
||||
usage: 'search'
|
||||
};
|
||||
for (const assetTypeId in storage.AssetType) {
|
||||
const assetType = storage.AssetType[assetTypeId];
|
||||
if (fileExtension.localeCompare(assetType.runtimeFormat, compareOptions) === 0) {
|
||||
return assetType;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Figure out an `imageSource` (URI or asset ID & type) for a library item's icon.
|
||||
* @param {object} item - either a library item or one of a library item's costumes.
|
||||
* @returns {object} - an `imageSource` ready to be passed to a `ScratchImage`.
|
||||
*/
|
||||
const getItemImageSource = function (item) {
|
||||
if (item.rawURL) {
|
||||
return {
|
||||
uri: item.rawURL
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: adjust libraries to be more storage-friendly; don't use split() here.
|
||||
const md5 = item.md5 || item.baseLayerMD5;
|
||||
if (md5) {
|
||||
const [assetId, fileExtension] = md5.split('.');
|
||||
return {
|
||||
assetId: assetId,
|
||||
assetType: getAssetTypeForFileExtension(fileExtension)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
class LibraryComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
@@ -45,16 +86,9 @@ class LibraryComponent extends React.Component {
|
||||
this.state = {
|
||||
selectedItem: null,
|
||||
filterQuery: '',
|
||||
selectedTag: ALL_TAG.tag,
|
||||
loaded: false
|
||||
selectedTag: ALL_TAG.tag
|
||||
};
|
||||
}
|
||||
componentDidMount () {
|
||||
// Allow the spinner to display before loading the content
|
||||
setTimeout(() => {
|
||||
this.setState({loaded: true});
|
||||
});
|
||||
}
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
if (prevState.filterQuery !== this.state.filterQuery ||
|
||||
prevState.selectedTag !== this.state.selectedTag) {
|
||||
@@ -170,8 +204,10 @@ class LibraryComponent extends React.Component {
|
||||
})}
|
||||
ref={this.setFilteredDataRef}
|
||||
>
|
||||
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => (
|
||||
<LibraryItem
|
||||
{this.getFilteredData().map((dataItem, index) => {
|
||||
const iconSource = getItemImageSource(dataItem);
|
||||
const icons = dataItem.json && dataItem.json.costumes.map(getItemImageSource);
|
||||
return (<LibraryItem
|
||||
bluetoothRequired={dataItem.bluetoothRequired}
|
||||
collaborator={dataItem.collaborator}
|
||||
description={dataItem.description}
|
||||
@@ -179,26 +215,18 @@ class LibraryComponent extends React.Component {
|
||||
extensionId={dataItem.extensionId}
|
||||
featured={dataItem.featured}
|
||||
hidden={dataItem.hidden}
|
||||
iconMd5={dataItem.md5}
|
||||
iconRawURL={dataItem.rawURL}
|
||||
icons={dataItem.json && dataItem.json.costumes}
|
||||
iconSource={iconSource}
|
||||
icons={icons}
|
||||
id={index}
|
||||
insetIconURL={dataItem.insetIconURL}
|
||||
internetConnectionRequired={dataItem.internetConnectionRequired}
|
||||
key={typeof dataItem.name === 'string' ? dataItem.name : dataItem.rawURL}
|
||||
key={`item_${index}`}
|
||||
name={dataItem.name}
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
onSelect={this.handleSelect}
|
||||
/>
|
||||
)) : (
|
||||
<div className={styles.spinnerWrapper}>
|
||||
<Spinner
|
||||
large
|
||||
level="primary"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>);
|
||||
})}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -91,8 +91,7 @@ LoupeComponent.propTypes = {
|
||||
color: PropTypes.shape({
|
||||
r: PropTypes.number,
|
||||
g: PropTypes.number,
|
||||
b: PropTypes.number,
|
||||
a: PropTypes.number
|
||||
b: PropTypes.number
|
||||
}),
|
||||
data: PropTypes.instanceOf(Uint8Array),
|
||||
width: PropTypes.number,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
|
||||
import storage from '../../lib/storage';
|
||||
|
||||
class ScratchImage extends React.PureComponent {
|
||||
static init () {
|
||||
this._maxParallelism = 6;
|
||||
this._currentJobs = 0;
|
||||
this._pendingImages = new Set();
|
||||
}
|
||||
|
||||
static loadPendingImages () {
|
||||
if (this._currentJobs >= this._maxParallelism) {
|
||||
// already busy
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the first visible image. If there aren't any, find the first non-visible image.
|
||||
let nextImage;
|
||||
for (const image of this._pendingImages) {
|
||||
if (image.isVisible) {
|
||||
nextImage = image;
|
||||
break;
|
||||
} else {
|
||||
nextImage = nextImage || image;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found an image to load:
|
||||
// 1) Remove it from the queue
|
||||
// 2) Load the image
|
||||
// 3) Pump the queue again
|
||||
if (nextImage) {
|
||||
this._pendingImages.delete(nextImage);
|
||||
const imageSource = nextImage.props.imageSource;
|
||||
++this._currentJobs;
|
||||
storage
|
||||
.load(imageSource.assetType, imageSource.assetId)
|
||||
.then(asset => {
|
||||
if (!nextImage.wasUnmounted) {
|
||||
const dataURI = asset.encodeDataURI();
|
||||
|
||||
nextImage.setState({
|
||||
imageURI: dataURI
|
||||
});
|
||||
}
|
||||
--this._currentJobs;
|
||||
this.loadPendingImages();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
Object.assign(this.state, this._loadImageSource(props.imageSource));
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
const newState = this._loadImageSource(nextProps.imageSource);
|
||||
this.setState(newState);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.wasUnmounted = true;
|
||||
ScratchImage._pendingImages.delete(this);
|
||||
}
|
||||
/**
|
||||
* Calculate the state changes necessary to load the image specified in the provided source info. If the component
|
||||
* is mounted, call setState() with the return value of this function. If the component has not yet mounted, use
|
||||
* the return value of this function as initial state for the component.
|
||||
*
|
||||
* @param {object} imageSource - the new source for the image, including either assetId or URI
|
||||
* @returns {object} - the new state values, if any.
|
||||
*/
|
||||
_loadImageSource (imageSource) {
|
||||
if (imageSource) {
|
||||
if (imageSource.uri) {
|
||||
ScratchImage._pendingImages.delete(this);
|
||||
return {
|
||||
imageURI: imageSource.uri,
|
||||
lastRequestedAsset: null
|
||||
};
|
||||
}
|
||||
if (this.state.lastRequestedAsset !== imageSource.assetId) {
|
||||
ScratchImage._pendingImages.add(this);
|
||||
return {
|
||||
lastRequestedAsset: imageSource.assetId
|
||||
};
|
||||
}
|
||||
}
|
||||
// Nothing to do - don't change any state.
|
||||
return {};
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
src: _src,
|
||||
imageSource: _imageSource,
|
||||
...imgProps
|
||||
} = this.props;
|
||||
return (
|
||||
<VisibilitySensor
|
||||
intervalCheck
|
||||
scrollCheck
|
||||
>
|
||||
{
|
||||
({isVisible}) => {
|
||||
this.isVisible = isVisible;
|
||||
ScratchImage.loadPendingImages();
|
||||
return (
|
||||
<img
|
||||
src={this.state.imageURI}
|
||||
{...imgProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
</VisibilitySensor>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScratchImage.ImageSourcePropType = PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
assetId: PropTypes.string.isRequired,
|
||||
assetType: PropTypes.oneOf(Object.values(storage.AssetType)).isRequired
|
||||
}),
|
||||
PropTypes.shape({
|
||||
uri: PropTypes.string.isRequired
|
||||
})
|
||||
]);
|
||||
|
||||
ScratchImage.propTypes = {
|
||||
imageSource: ScratchImage.ImageSourcePropType.isRequired
|
||||
};
|
||||
|
||||
ScratchImage.init();
|
||||
|
||||
export default ScratchImage;
|
||||
@@ -39,16 +39,6 @@
|
||||
height: .5rem;
|
||||
}
|
||||
|
||||
.large {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.large::before, .large::after {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
@@ -81,10 +71,3 @@
|
||||
.spinner.info::after {
|
||||
border-top-color: $ui-white;
|
||||
}
|
||||
|
||||
.spinner.primary {
|
||||
border-color: $motion-transparent;
|
||||
}
|
||||
.spinner.primary::after {
|
||||
border-top-color: $motion-primary;
|
||||
}
|
||||
@@ -8,8 +8,7 @@ const SpinnerComponent = function (props) {
|
||||
const {
|
||||
className,
|
||||
level,
|
||||
small,
|
||||
large
|
||||
small
|
||||
} = props;
|
||||
return (
|
||||
<div
|
||||
@@ -17,24 +16,18 @@ const SpinnerComponent = function (props) {
|
||||
className,
|
||||
styles.spinner,
|
||||
styles[level],
|
||||
{
|
||||
[styles.small]: small,
|
||||
[styles.large]: large
|
||||
}
|
||||
{[styles.small]: small}
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
SpinnerComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
large: PropTypes.bool,
|
||||
level: PropTypes.string,
|
||||
small: PropTypes.bool
|
||||
};
|
||||
SpinnerComponent.defaultProps = {
|
||||
className: '',
|
||||
large: false,
|
||||
level: 'info',
|
||||
small: false
|
||||
level: 'info'
|
||||
};
|
||||
export default SpinnerComponent;
|
||||
@@ -20,6 +20,7 @@
|
||||
cursor: pointer;
|
||||
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.sprite-selector-item.is-selected {
|
||||
|
||||
@@ -23,9 +23,8 @@ const SpriteSelectorItem = props => (
|
||||
onMouseDown: props.onMouseDown,
|
||||
onTouchStart: props.onMouseDown
|
||||
}}
|
||||
disable={props.preventContextMenu}
|
||||
disable={props.dragging}
|
||||
id={`${props.name}-${contextMenuId}`}
|
||||
ref={props.componentRef}
|
||||
>
|
||||
{typeof props.number === 'undefined' ? null : (
|
||||
<div className={styles.number}>{props.number}</div>
|
||||
@@ -90,9 +89,9 @@ const SpriteSelectorItem = props => (
|
||||
|
||||
SpriteSelectorItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
componentRef: PropTypes.func,
|
||||
costumeURL: PropTypes.string,
|
||||
details: PropTypes.string,
|
||||
dragging: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
number: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
@@ -102,7 +101,6 @@ SpriteSelectorItem.propTypes = {
|
||||
onMouseDown: PropTypes.func,
|
||||
onMouseEnter: PropTypes.func,
|
||||
onMouseLeave: PropTypes.func,
|
||||
preventContextMenu: PropTypes.bool,
|
||||
selected: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -110,8 +110,7 @@ SpriteList.propTypes = {
|
||||
editingTarget: PropTypes.string,
|
||||
hoveredTarget: PropTypes.shape({
|
||||
hoveredSprite: PropTypes.string,
|
||||
receivedBlocks: PropTypes.bool,
|
||||
sprite: PropTypes.string
|
||||
receivedBlocks: PropTypes.bool
|
||||
}),
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
costume: PropTypes.shape({
|
||||
|
||||
@@ -68,8 +68,6 @@
|
||||
padding-left: calc($space / 2);
|
||||
padding-right: calc($space / 2);
|
||||
padding-bottom: $space;
|
||||
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
|
||||
@@ -17,7 +17,6 @@ import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
|
||||
import {STAGE_DISPLAY_SIZES} from '../lib/layout-constants';
|
||||
import DropAreaHOC from '../lib/drop-area-hoc.jsx';
|
||||
import DragConstants from '../lib/drag-constants';
|
||||
import defineDynamicBlock from '../lib/define-dynamic-block';
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {updateToolbox} from '../reducers/toolbox';
|
||||
@@ -394,50 +393,20 @@ class Blocks extends React.Component {
|
||||
// workspace to be 'undone' here.
|
||||
this.workspace.clearUndo();
|
||||
}
|
||||
handleExtensionAdded (categoryInfo) {
|
||||
const defineBlocks = blockInfoArray => {
|
||||
if (blockInfoArray && blockInfoArray.length > 0) {
|
||||
const staticBlocksJson = [];
|
||||
const dynamicBlocksInfo = [];
|
||||
blockInfoArray.forEach(blockInfo => {
|
||||
if (blockInfo.info && blockInfo.info.isDynamic) {
|
||||
dynamicBlocksInfo.push(blockInfo);
|
||||
} else if (blockInfo.json) {
|
||||
staticBlocksJson.push(blockInfo.json);
|
||||
}
|
||||
// otherwise it's a non-block entry such as '---'
|
||||
});
|
||||
handleExtensionAdded (blocksInfo) {
|
||||
// select JSON from each block info object then reject the pseudo-blocks which don't have JSON, like separators
|
||||
// this actually defines blocks and MUST run regardless of the UI state
|
||||
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x));
|
||||
|
||||
this.ScratchBlocks.defineBlocksWithJsonArray(staticBlocksJson);
|
||||
dynamicBlocksInfo.forEach(blockInfo => {
|
||||
// This is creating the block factory / constructor -- NOT a specific instance of the block.
|
||||
// The factory should only know static info about the block: the category info and the opcode.
|
||||
// Anything else will be picked up from the XML attached to the block instance.
|
||||
const extendedOpcode = `${categoryInfo.id}_${blockInfo.info.opcode}`;
|
||||
const blockDefinition =
|
||||
defineDynamicBlock(this.ScratchBlocks, categoryInfo, blockInfo, extendedOpcode);
|
||||
this.ScratchBlocks.Blocks[extendedOpcode] = blockDefinition;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// scratch-blocks implements a menu or custom field as a special kind of block ("shadow" block)
|
||||
// these actually define blocks and MUST run regardless of the UI state
|
||||
defineBlocks(
|
||||
Object.getOwnPropertyNames(categoryInfo.customFieldTypes)
|
||||
.map(fieldTypeName => categoryInfo.customFieldTypes[fieldTypeName].scratchBlocksDefinition));
|
||||
defineBlocks(categoryInfo.menus);
|
||||
defineBlocks(categoryInfo.blocks);
|
||||
|
||||
// Update the toolbox with new blocks if possible
|
||||
// Update the toolbox with new blocks
|
||||
const toolboxXML = this.getToolboxXML();
|
||||
if (toolboxXML) {
|
||||
this.props.updateToolboxState(toolboxXML);
|
||||
}
|
||||
}
|
||||
handleBlocksInfoUpdate (categoryInfo) {
|
||||
handleBlocksInfoUpdate (blocksInfo) {
|
||||
// @todo Later we should replace this to avoid all the warnings from redefining blocks.
|
||||
this.handleExtensionAdded(categoryInfo);
|
||||
this.handleExtensionAdded(blocksInfo);
|
||||
}
|
||||
handleCategorySelected (categoryId) {
|
||||
const extension = extensionData.find(ext => ext.extensionId === categoryId);
|
||||
|
||||
@@ -52,7 +52,6 @@ class GUI extends React.Component {
|
||||
setIsScratchDesktop(this.props.isScratchDesktop);
|
||||
this.setReduxTitle(this.props.projectTitle);
|
||||
this.props.onStorageInit(storage);
|
||||
this.props.onVmInit(this.props.vm);
|
||||
}
|
||||
componentDidUpdate (prevProps) {
|
||||
if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
|
||||
@@ -93,7 +92,6 @@ class GUI extends React.Component {
|
||||
onStorageInit,
|
||||
onUpdateProjectId,
|
||||
onUpdateReduxProjectTitle,
|
||||
onVmInit,
|
||||
projectHost,
|
||||
projectId,
|
||||
projectTitle,
|
||||
@@ -133,7 +131,6 @@ GUI.propTypes = {
|
||||
onUpdateProjectId: PropTypes.func,
|
||||
onUpdateProjectTitle: PropTypes.func,
|
||||
onUpdateReduxProjectTitle: PropTypes.func,
|
||||
onVmInit: PropTypes.func,
|
||||
projectHost: PropTypes.string,
|
||||
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
projectTitle: PropTypes.string,
|
||||
@@ -145,8 +142,7 @@ GUI.defaultProps = {
|
||||
isScratchDesktop: false,
|
||||
onStorageInit: storageInstance => storageInstance.addOfficialScratchWebStores(),
|
||||
onProjectLoaded: () => {},
|
||||
onUpdateProjectId: () => {},
|
||||
onVmInit: (/* vm */) => {}
|
||||
onUpdateProjectId: () => {}
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
|
||||
@@ -75,21 +75,17 @@ class LibraryItem extends React.PureComponent {
|
||||
const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length;
|
||||
this.setState({iconIndex: nextIconIndex});
|
||||
}
|
||||
curIconMd5 () {
|
||||
curIconSource () {
|
||||
if (this.props.icons &&
|
||||
this.state.isRotatingIcon &&
|
||||
this.state.iconIndex < this.props.icons.length &&
|
||||
this.props.icons[this.state.iconIndex] &&
|
||||
this.props.icons[this.state.iconIndex].baseLayerMD5) {
|
||||
return this.props.icons[this.state.iconIndex].baseLayerMD5;
|
||||
this.props.icons[this.state.iconIndex]) {
|
||||
return this.props.icons[this.state.iconIndex];
|
||||
}
|
||||
return this.props.iconMd5;
|
||||
return this.props.iconSource;
|
||||
}
|
||||
render () {
|
||||
const iconMd5 = this.curIconMd5();
|
||||
const iconURL = iconMd5 ?
|
||||
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` :
|
||||
this.props.iconRawURL;
|
||||
const iconSource = this.curIconSource();
|
||||
return (
|
||||
<LibraryItemComponent
|
||||
bluetoothRequired={this.props.bluetoothRequired}
|
||||
@@ -99,8 +95,7 @@ class LibraryItem extends React.PureComponent {
|
||||
extensionId={this.props.extensionId}
|
||||
featured={this.props.featured}
|
||||
hidden={this.props.hidden}
|
||||
iconURL={iconURL}
|
||||
icons={this.props.icons}
|
||||
iconSource={iconSource}
|
||||
id={this.props.id}
|
||||
insetIconURL={this.props.insetIconURL}
|
||||
internetConnectionRequired={this.props.internetConnectionRequired}
|
||||
@@ -127,13 +122,8 @@ LibraryItem.propTypes = {
|
||||
extensionId: PropTypes.string,
|
||||
featured: PropTypes.bool,
|
||||
hidden: PropTypes.bool,
|
||||
iconMd5: PropTypes.string,
|
||||
iconRawURL: PropTypes.string,
|
||||
icons: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
baseLayerMD5: PropTypes.string
|
||||
})
|
||||
),
|
||||
iconSource: LibraryItemComponent.propTypes.iconSource, // single icon
|
||||
icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons
|
||||
id: PropTypes.number.isRequired,
|
||||
insetIconURL: PropTypes.string,
|
||||
internetConnectionRequired: PropTypes.bool,
|
||||
|
||||
@@ -6,19 +6,19 @@ import {connect} from 'react-redux';
|
||||
import {setHoveredSprite} from '../reducers/hovered-target';
|
||||
import {updateAssetDrag} from '../reducers/asset-drag';
|
||||
import storage from '../lib/storage';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
import VM from 'scratch-vm';
|
||||
import getCostumeUrl from '../lib/get-costume-url';
|
||||
import DragRecognizer from '../lib/drag-recognizer';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
|
||||
import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
|
||||
|
||||
const dragThreshold = 3; // Same as the block drag threshold
|
||||
|
||||
class SpriteSelectorItem extends React.PureComponent {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'getCostumeData',
|
||||
'setRef',
|
||||
'handleClick',
|
||||
'handleDelete',
|
||||
'handleDuplicate',
|
||||
@@ -26,22 +26,9 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handleMouseDown',
|
||||
'handleDragEnd',
|
||||
'handleDrag',
|
||||
'handleTouchEnd'
|
||||
'handleMouseMove',
|
||||
'handleMouseUp'
|
||||
]);
|
||||
|
||||
this.dragRecognizer = new DragRecognizer({
|
||||
onDrag: this.handleDrag,
|
||||
onDragEnd: this.handleDragEnd
|
||||
});
|
||||
}
|
||||
componentDidMount () {
|
||||
document.addEventListener('touchend', this.handleTouchEnd);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('touchend', this.handleTouchEnd);
|
||||
this.dragRecognizer.reset();
|
||||
}
|
||||
getCostumeData () {
|
||||
if (this.props.costumeURL) return this.props.costumeURL;
|
||||
@@ -49,7 +36,12 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
|
||||
return getCostumeUrl(this.props.asset);
|
||||
}
|
||||
handleDragEnd () {
|
||||
handleMouseUp () {
|
||||
this.initialOffset = null;
|
||||
window.removeEventListener('mouseup', this.handleMouseUp);
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
window.removeEventListener('touchend', this.handleMouseUp);
|
||||
window.removeEventListener('touchmove', this.handleMouseMove);
|
||||
if (this.props.dragging) {
|
||||
this.props.onDrag({
|
||||
img: null,
|
||||
@@ -63,26 +55,29 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
this.noClick = false;
|
||||
});
|
||||
}
|
||||
handleDrag (currentOffset) {
|
||||
this.props.onDrag({
|
||||
img: this.getCostumeData(),
|
||||
currentOffset: currentOffset,
|
||||
dragging: true,
|
||||
dragType: this.props.dragType,
|
||||
index: this.props.index,
|
||||
payload: this.props.dragPayload
|
||||
});
|
||||
this.noClick = true;
|
||||
}
|
||||
handleTouchEnd (e) {
|
||||
const {x, y} = getEventXY(e);
|
||||
const {top, left, bottom, right} = this.ref.getBoundingClientRect();
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
this.handleMouseEnter();
|
||||
handleMouseMove (e) {
|
||||
const currentOffset = getEventXY(e);
|
||||
const dx = currentOffset.x - this.initialOffset.x;
|
||||
const dy = currentOffset.y - this.initialOffset.y;
|
||||
if (Math.sqrt((dx * dx) + (dy * dy)) > dragThreshold) {
|
||||
this.props.onDrag({
|
||||
img: this.getCostumeData(),
|
||||
currentOffset: currentOffset,
|
||||
dragging: true,
|
||||
dragType: this.props.dragType,
|
||||
index: this.props.index,
|
||||
payload: this.props.dragPayload
|
||||
});
|
||||
this.noClick = true;
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
handleMouseDown (e) {
|
||||
this.dragRecognizer.start(e);
|
||||
this.initialOffset = getEventXY(e);
|
||||
window.addEventListener('mouseup', this.handleMouseUp);
|
||||
window.addEventListener('mousemove', this.handleMouseMove);
|
||||
window.addEventListener('touchend', this.handleMouseUp);
|
||||
window.addEventListener('touchmove', this.handleMouseMove);
|
||||
}
|
||||
handleClick (e) {
|
||||
e.preventDefault();
|
||||
@@ -108,10 +103,6 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
handleMouseEnter () {
|
||||
this.props.dispatchSetHoveredSprite(this.props.id);
|
||||
}
|
||||
setRef (component) {
|
||||
// Access the DOM node using .elem because it is going through ContextMenuTrigger
|
||||
this.ref = component && component.elem;
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
/* eslint-disable no-unused-vars */
|
||||
@@ -131,9 +122,7 @@ class SpriteSelectorItem extends React.PureComponent {
|
||||
} = this.props;
|
||||
return (
|
||||
<SpriteSelectorItemComponent
|
||||
componentRef={this.setRef}
|
||||
costumeURL={this.getCostumeData()}
|
||||
preventContextMenu={this.dragRecognizer.gestureInProgress()}
|
||||
onClick={this.handleClick}
|
||||
onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null}
|
||||
onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null}
|
||||
|
||||
@@ -15,7 +15,6 @@ import ThrottledPropertyHOC from '../lib/throttled-property-hoc.jsx';
|
||||
import {emptyCostume} from '../lib/empty-assets';
|
||||
import sharedMessages from '../lib/shared-messages';
|
||||
import {fetchCode} from '../lib/backpack-api';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
|
||||
import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx';
|
||||
|
||||
@@ -47,25 +46,10 @@ class StageSelector extends React.Component {
|
||||
'handleBackdropUpload',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handleTouchEnd',
|
||||
'handleDrop',
|
||||
'setFileInput',
|
||||
'setRef'
|
||||
'setFileInput'
|
||||
]);
|
||||
}
|
||||
componentDidMount () {
|
||||
document.addEventListener('touchend', this.handleTouchEnd);
|
||||
}
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('touchend', this.handleTouchEnd);
|
||||
}
|
||||
handleTouchEnd (e) {
|
||||
const {x, y} = getEventXY(e);
|
||||
const {top, left, bottom, right} = this.ref.getBoundingClientRect();
|
||||
if (x >= left && x <= right && y >= top && y <= bottom) {
|
||||
this.handleMouseEnter();
|
||||
}
|
||||
}
|
||||
addBackdropFromLibraryItem (item, shouldActivateTab = true) {
|
||||
const vmBackdrop = {
|
||||
name: item.name,
|
||||
@@ -153,16 +137,12 @@ class StageSelector extends React.Component {
|
||||
setFileInput (input) {
|
||||
this.fileInput = input;
|
||||
}
|
||||
setRef (ref) {
|
||||
this.ref = ref;
|
||||
}
|
||||
render () {
|
||||
const componentProps = omit(this.props, [
|
||||
'asset', 'dispatchSetHoveredSprite', 'id', 'intl',
|
||||
'onActivateTab', 'onSelect', 'onShowImporting', 'onCloseImporting']);
|
||||
return (
|
||||
<DroppableThrottledStage
|
||||
componentRef={this.setRef}
|
||||
fileInputRef={this.setFileInput}
|
||||
onBackdropFileUpload={this.handleBackdropUpload}
|
||||
onBackdropFileUploadClick={this.handleFileUploadClick}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
// TODO: access `BlockType` and `ArgumentType` without reaching into VM
|
||||
// Should we move these into a new extension support module or something?
|
||||
import ArgumentType from 'scratch-vm/src/extension-support/argument-type';
|
||||
import BlockType from 'scratch-vm/src/extension-support/block-type';
|
||||
|
||||
/**
|
||||
* Define a block using extension info which has the ability to dynamically determine (and update) its layout.
|
||||
* This functionality is used for extension blocks which can change its properties based on different state
|
||||
* information. For example, the `control_stop` block changes its shape based on which menu item is selected
|
||||
* and a variable block changes its text to reflect the variable name without using an editable field.
|
||||
* @param {object} ScratchBlocks - The ScratchBlocks name space.
|
||||
* @param {object} categoryInfo - Information about this block's extension category, including any menus and icons.
|
||||
* @param {object} staticBlockInfo - The base block information before any dynamic changes.
|
||||
* @param {string} extendedOpcode - The opcode for the block (including the extension ID).
|
||||
*/
|
||||
// TODO: grow this until it can fully replace `_convertForScratchBlocks` in the VM runtime
|
||||
const defineDynamicBlock = (ScratchBlocks, categoryInfo, staticBlockInfo, extendedOpcode) => ({
|
||||
init: function () {
|
||||
const blockJson = {
|
||||
type: extendedOpcode,
|
||||
inputsInline: true,
|
||||
category: categoryInfo.name,
|
||||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3
|
||||
};
|
||||
// There is a scratch-blocks / Blockly extension called "scratch_extension" which adjusts the styling of
|
||||
// blocks to allow for an icon, a feature of Scratch extension blocks. However, Scratch "core" extension
|
||||
// blocks don't have icons and so they should not use 'scratch_extension'. Adding a scratch-blocks / Blockly
|
||||
// extension after `jsonInit` isn't fully supported (?), so we decide now whether there will be an icon.
|
||||
if (staticBlockInfo.blockIconURI || categoryInfo.blockIconURI) {
|
||||
blockJson.extensions = ['scratch_extension'];
|
||||
}
|
||||
// initialize the basics of the block, to be overridden & extended later by `domToMutation`
|
||||
this.jsonInit(blockJson);
|
||||
// initialize the cached block info used to carry block info from `domToMutation` to `mutationToDom`
|
||||
this.blockInfoText = '{}';
|
||||
// we need a block info update (through `domToMutation`) before we have a completely initialized block
|
||||
this.needsBlockInfoUpdate = true;
|
||||
},
|
||||
mutationToDom: function () {
|
||||
const container = document.createElement('mutation');
|
||||
container.setAttribute('blockInfo', this.blockInfoText);
|
||||
return container;
|
||||
},
|
||||
domToMutation: function (xmlElement) {
|
||||
const blockInfoText = xmlElement.getAttribute('blockInfo');
|
||||
if (!blockInfoText) return;
|
||||
if (!this.needsBlockInfoUpdate) {
|
||||
throw new Error('Attempted to update block info twice');
|
||||
}
|
||||
delete this.needsBlockInfoUpdate;
|
||||
this.blockInfoText = blockInfoText;
|
||||
const blockInfo = JSON.parse(blockInfoText);
|
||||
|
||||
switch (blockInfo.blockType) {
|
||||
case BlockType.COMMAND:
|
||||
case BlockType.CONDITIONAL:
|
||||
case BlockType.LOOP:
|
||||
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
|
||||
this.setPreviousStatement(true);
|
||||
this.setNextStatement(!blockInfo.isTerminal);
|
||||
break;
|
||||
case BlockType.REPORTER:
|
||||
this.setOutput(true);
|
||||
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_ROUND);
|
||||
if (!blockInfo.disableMonitor) {
|
||||
this.setCheckboxInFlyout(true);
|
||||
}
|
||||
break;
|
||||
case BlockType.BOOLEAN:
|
||||
this.setOutput(true);
|
||||
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_HEXAGONAL);
|
||||
break;
|
||||
case BlockType.HAT:
|
||||
case BlockType.EVENT:
|
||||
this.setOutputShape(ScratchBlocks.OUTPUT_SHAPE_SQUARE);
|
||||
this.setNextStatement(true);
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockInfo.color1 || blockInfo.color2 || blockInfo.color3) {
|
||||
// `setColour` handles undefined parameters by adjusting defined colors
|
||||
this.setColour(blockInfo.color1, blockInfo.color2, blockInfo.color3);
|
||||
}
|
||||
|
||||
// Layout block arguments
|
||||
// TODO handle E/C Blocks
|
||||
const blockText = blockInfo.text;
|
||||
const args = [];
|
||||
let argCount = 0;
|
||||
const scratchBlocksStyleText = blockText.replace(/\[(.+?)]/g, (match, argName) => {
|
||||
const arg = blockInfo.arguments[argName];
|
||||
switch (arg.type) {
|
||||
case ArgumentType.STRING:
|
||||
args.push({type: 'input_value', name: argName});
|
||||
break;
|
||||
case ArgumentType.BOOLEAN:
|
||||
args.push({type: 'input_value', name: argName, check: 'Boolean'});
|
||||
break;
|
||||
}
|
||||
return `%${++argCount}`;
|
||||
});
|
||||
this.interpolate_(scratchBlocksStyleText, args);
|
||||
}
|
||||
});
|
||||
|
||||
export default defineDynamicBlock;
|
||||
@@ -1,125 +0,0 @@
|
||||
import bindAll from 'lodash.bindall';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
|
||||
class DragRecognizer {
|
||||
/* Gesture states */
|
||||
static get STATE_UNIDENTIFIED () {
|
||||
return 'unidentified';
|
||||
}
|
||||
static get STATE_SCROLL () {
|
||||
return 'scroll';
|
||||
}
|
||||
static get STATE_DRAG () {
|
||||
return 'drag';
|
||||
}
|
||||
|
||||
constructor ({
|
||||
onDrag = (() => {}),
|
||||
onDragEnd = (() => {}),
|
||||
touchDragAngle = 70, // Angle and distance thresholds are the same as scratch-blocks
|
||||
distanceThreshold = 3
|
||||
}) {
|
||||
this._onDrag = onDrag;
|
||||
this._onDragEnd = onDragEnd;
|
||||
this._touchDragAngle = touchDragAngle;
|
||||
this._distanceThreshold = distanceThreshold;
|
||||
|
||||
this._initialOffset = null;
|
||||
this._gestureState = DragRecognizer.STATE_UNIDENTIFIED;
|
||||
|
||||
bindAll(this, [
|
||||
'start',
|
||||
'gestureInProgress',
|
||||
'reset',
|
||||
'_handleMove',
|
||||
'_handleEnd'
|
||||
]);
|
||||
}
|
||||
|
||||
start (event) {
|
||||
this._initialOffset = getEventXY(event);
|
||||
this._bindListeners();
|
||||
}
|
||||
|
||||
gestureInProgress () {
|
||||
return this._gestureState !== DragRecognizer.STATE_UNIDENTIFIED;
|
||||
}
|
||||
|
||||
reset () {
|
||||
this._unbindListeners();
|
||||
this._initialOffset = null;
|
||||
this._gestureState = DragRecognizer.STATE_UNIDENTIFIED;
|
||||
}
|
||||
|
||||
//
|
||||
// Internal functions
|
||||
//
|
||||
|
||||
_bindListeners () {
|
||||
window.addEventListener('mouseup', this._handleEnd);
|
||||
window.addEventListener('mousemove', this._handleMove);
|
||||
window.addEventListener('touchend', this._handleEnd);
|
||||
// touchmove must be marked as non-passive, or else it cannot prevent scrolling
|
||||
window.addEventListener('touchmove', this._handleMove, {passive: false});
|
||||
}
|
||||
|
||||
_unbindListeners () {
|
||||
window.removeEventListener('mouseup', this._handleEnd);
|
||||
window.removeEventListener('mousemove', this._handleMove);
|
||||
window.removeEventListener('touchend', this._handleEnd);
|
||||
window.removeEventListener('touchmove', this._handleMove, {passive: false});
|
||||
}
|
||||
|
||||
_handleMove (event) {
|
||||
// For gestures identified as vertical scrolls, do not process movement events
|
||||
if (this._isScroll()) return;
|
||||
|
||||
const currentOffset = getEventXY(event);
|
||||
|
||||
// Try to identify this gesture if it hasn't been identified already
|
||||
if (!this.gestureInProgress()) {
|
||||
const dx = currentOffset.x - this._initialOffset.x;
|
||||
const dy = currentOffset.y - this._initialOffset.y;
|
||||
const dragDistance = Math.sqrt((dx * dx) + (dy * dy));
|
||||
if (dragDistance < this._distanceThreshold) return;
|
||||
|
||||
// For touch moves, additionally check if the angle suggests drag vs. scroll
|
||||
if (event.type === 'touchmove') {
|
||||
// Direction goes from -180 to 180, with 0 toward the right.
|
||||
let angle = Math.atan2(dy, dx) / Math.PI * 180;
|
||||
// Fold over horizontal axis, range now 0 to 180
|
||||
angle = Math.abs(angle);
|
||||
// Fold over vertical axis, range now 0 to 90
|
||||
if (angle > 90) angle = 180 - angle;
|
||||
if (angle > this._touchDragAngle) {
|
||||
this._gestureState = DragRecognizer.STATE_SCROLL;
|
||||
} else {
|
||||
this._gestureState = DragRecognizer.STATE_DRAG;
|
||||
}
|
||||
} else {
|
||||
// Mouse moves are always considered drags
|
||||
this._gestureState = DragRecognizer.STATE_DRAG;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._isDrag()) {
|
||||
this._onDrag(currentOffset, this._initialOffset);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
_handleEnd () {
|
||||
this._onDragEnd();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
_isDrag () {
|
||||
return this._gestureState === DragRecognizer.STATE_DRAG;
|
||||
}
|
||||
|
||||
_isScroll () {
|
||||
return this._gestureState === DragRecognizer.STATE_SCROLL;
|
||||
}
|
||||
}
|
||||
|
||||
export default DragRecognizer;
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Andie-a",
|
||||
"md5": "d2c433e02481b10584e34ccdcf5a046a.svg",
|
||||
"md5": "419ff5a76adbb4b076c072f68267fdeb.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -87,33 +87,14 @@
|
||||
"alex eben meyer"
|
||||
],
|
||||
"info": [
|
||||
80,
|
||||
65,
|
||||
86,
|
||||
81,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Andie-b",
|
||||
"md5": "045f92ad80d72521098ede6d7c80549a.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
"basketball",
|
||||
"people",
|
||||
"wheelchair",
|
||||
"handicap",
|
||||
"handicapable",
|
||||
"alex eben meyer"
|
||||
],
|
||||
"info": [
|
||||
40,
|
||||
91,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Andie-c",
|
||||
"md5": "c9bad70ad371b8d6d97daed7ab9def98.svg",
|
||||
"md5": "a6a766912b4e43064ee3f389c72925b4.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -126,13 +107,13 @@
|
||||
],
|
||||
"info": [
|
||||
46,
|
||||
49,
|
||||
106,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Andie-d",
|
||||
"md5": "7c9db638cd671898ff07771112faff0f.svg",
|
||||
"name": "Andie-c",
|
||||
"md5": "fac1025857d2c28391cb31f39a80df54.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -144,8 +125,27 @@
|
||||
"alex eben meyer"
|
||||
],
|
||||
"info": [
|
||||
71,
|
||||
57,
|
||||
52,
|
||||
65,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Andie-d",
|
||||
"md5": "7af50835e05d43e5e07cd2d641e7401e.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
"basketball",
|
||||
"people",
|
||||
"wheelchair",
|
||||
"handicap",
|
||||
"handicapable",
|
||||
"alex eben meyer"
|
||||
],
|
||||
"info": [
|
||||
87,
|
||||
72,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -1069,7 +1069,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Ben-a",
|
||||
"md5": "322e13bd17d02acfdc048a5ef2167be8.svg",
|
||||
"md5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -1079,14 +1079,14 @@
|
||||
"alex eben meyer"
|
||||
],
|
||||
"info": [
|
||||
54,
|
||||
53,
|
||||
69,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ben-b",
|
||||
"md5": "59ba1e80ba75b1d06c37db4b9b4fde27.svg",
|
||||
"md5": "90db137cd0743204d02fdc9be1c8eb94.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -1096,14 +1096,14 @@
|
||||
"alex eben meyer"
|
||||
],
|
||||
"info": [
|
||||
54,
|
||||
69,
|
||||
53,
|
||||
67,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ben-c",
|
||||
"md5": "940ec2fa85d666dde95a51bcbccd266e.svg",
|
||||
"md5": "5f05d41ede773b3bb379286e86a5b869.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -1113,14 +1113,14 @@
|
||||
"alex eben meyer"
|
||||
],
|
||||
"info": [
|
||||
73,
|
||||
71,
|
||||
72,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Ben-d",
|
||||
"md5": "3043a50a91b9d484a8cd32eb82f2d88f.svg",
|
||||
"md5": "6f6e246567c0639c758ed4f6930be76f.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -1130,8 +1130,8 @@
|
||||
"alex eben meyer"
|
||||
],
|
||||
"info": [
|
||||
44,
|
||||
71,
|
||||
42,
|
||||
72,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -3628,17 +3628,16 @@
|
||||
},
|
||||
{
|
||||
"name": "Earth",
|
||||
"md5": "64220361b80d2812e9f27fc88ef39027.svg",
|
||||
"md5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"space",
|
||||
"planet",
|
||||
"earth",
|
||||
"globe"
|
||||
"earth"
|
||||
],
|
||||
"info": [
|
||||
55,
|
||||
55,
|
||||
41,
|
||||
40,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -6179,7 +6178,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Jordyn-a",
|
||||
"md5": "2f59adebc2c0feb9ebfabd7ee9642f2b.svg",
|
||||
"md5": "8355df83cca2baa0fdf6269d1311f1ea.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -6190,13 +6189,13 @@
|
||||
],
|
||||
"info": [
|
||||
51,
|
||||
62,
|
||||
63,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Jordyn-b",
|
||||
"md5": "075f382883e60dba8094b3dc30918f60.svg",
|
||||
"md5": "2e2c43bbd7b18524fb132f1426ca3134.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -6213,7 +6212,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Jordyn-c",
|
||||
"md5": "fbe81c40d4a880c8799c458359758629.svg",
|
||||
"md5": "5c0d24c53805e16358a2f6f6cc4a456e.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -6224,13 +6223,13 @@
|
||||
],
|
||||
"info": [
|
||||
68,
|
||||
62,
|
||||
61,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Jordyn-d",
|
||||
"md5": "68a784d4d0340475a6e6feff3b28b470.svg",
|
||||
"md5": "ebe2b20b99c1e1b373fd2b02be48aab9.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -6241,7 +6240,7 @@
|
||||
],
|
||||
"info": [
|
||||
40,
|
||||
62,
|
||||
61,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -9661,7 +9660,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Sun",
|
||||
"md5": "14b0508da0dc30e83b2bede13c1be1e9.svg",
|
||||
"md5": "55c931c65456822c0c56a2b30e3e550d.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"space",
|
||||
@@ -9672,8 +9671,8 @@
|
||||
"nuclear"
|
||||
],
|
||||
"info": [
|
||||
54,
|
||||
54,
|
||||
72,
|
||||
72,
|
||||
1
|
||||
]
|
||||
},
|
||||
@@ -10583,24 +10582,6 @@
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wizard Girl",
|
||||
"md5": "dd40d3a2565990bcc24d2b36cd446394.svg",
|
||||
"type": "costume",
|
||||
"tags": [
|
||||
"people",
|
||||
"female",
|
||||
"girl",
|
||||
"wizard",
|
||||
"magic",
|
||||
"fantasy"
|
||||
],
|
||||
"info": [
|
||||
80,
|
||||
91,
|
||||
1
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Wizard Hat",
|
||||
"md5": "2da561a7e5cbfcda613f4750020d4aa5.svg",
|
||||
@@ -12701,4 +12682,4 @@
|
||||
1
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -90,86 +90,6 @@ import videoPet from './steps/video-pet.en.gif';
|
||||
import videoAnimate from './steps/video-animate.en.gif';
|
||||
import videoPop from './steps/video-pop.en.gif';
|
||||
|
||||
// Make it Fly
|
||||
import flyChooseBackdrop from './steps/fly-choose-backdrop.en.gif';
|
||||
import flyChooseCharacter from './steps/fly-choose-character.en.gif';
|
||||
import flySaySomething from './steps/fly-say-something.en.gif';
|
||||
import flyMoveArrows from './steps/fly-make-interactive.en.gif';
|
||||
import flyChooseObject from './steps/fly-object-to-collect.en.gif';
|
||||
import flyFlyingObject from './steps/fly-flying-heart.en.gif';
|
||||
import flySelectFlyingSprite from './steps/fly-select-flyer.en.jpg';
|
||||
import flyAddScore from './steps/fly-add-score.en.gif';
|
||||
import flyKeepScore from './steps/fly-keep-score.en.gif';
|
||||
import flyAddScenery from './steps/fly-choose-scenery.en.gif';
|
||||
import flyMoveScenery from './steps/fly-move-scenery.en.gif';
|
||||
import flySwitchLooks from './steps/fly-switch-costume.en.gif';
|
||||
|
||||
// Pong
|
||||
import pongAddBackdrop from './steps/pong-add-backdrop.en.png';
|
||||
import pongAddBallSprite from './steps/pong-add-ball-sprite.en.png';
|
||||
import pongBounceAround from './steps/pong-bounce-around.en.gif';
|
||||
import pongAddPaddle from './steps/pong-add-a-paddle.en.gif';
|
||||
import pongMoveThePaddle from './steps/pong-move-the-paddle.en.gif';
|
||||
import pongSelectBallSprite from './steps/pong-select-ball.en.png';
|
||||
import pongAddMoreCodeToBall from './steps/pong-add-code-to-ball.en.png';
|
||||
import pongAddAScore from './steps/pong-add-score.en.gif';
|
||||
import pongChooseScoreFromMenu from './steps/pong-choose-score.en.png';
|
||||
import pongInsertChangeScoreBlock from './steps/pong-insert-change-score.en.png';
|
||||
import pongResetScore from './steps/pong-reset-score.en.png';
|
||||
import pongAddLineSprite from './steps/pong-add-line.en.gif';
|
||||
import pongGameOver from './steps/pong-game-over.en.png';
|
||||
|
||||
// Imagine If
|
||||
import imagineTypeWhatYouWant from './steps/imagine-type-what-you-want.en.png';
|
||||
import imagineClickGreenFlag from './steps/imagine-click-green-flag.en.png';
|
||||
import imagineChooseBackdrop from './steps/imagine-choose-backdrop.en.png';
|
||||
import imagineChooseSprite from './steps/imagine-choose-any-sprite.en.png';
|
||||
import imagineFlyAround from './steps/imagine-fly-around.en.png';
|
||||
import imagineChooseAnotherSprite from './steps/imagine-choose-another-sprite.en.png';
|
||||
import imagineLeftRight from './steps/imagine-left-right.en.png';
|
||||
import imagineUpDown from './steps/imagine-up-down.en.png';
|
||||
import imagineChangeCostumes from './steps/imagine-change-costumes.en.png';
|
||||
import imagineGlideToPoint from './steps/imagine-glide-to-point.en.png';
|
||||
import imagineGrowShrink from './steps/imagine-grow-shrink.en.png';
|
||||
import imagineChooseAnotherBackdrop from './steps/imagine-choose-another-backdrop.en.png';
|
||||
import imagineSwitchBackdrops from './steps/imagine-switch-backdrops.en.gif';
|
||||
import imagineRecordASound from './steps/imagine-record-a-sound.en.gif';
|
||||
import imagineChooseSound from './steps/imagine-choose-sound.en.png';
|
||||
|
||||
// Add a Backdrop
|
||||
import addBackdrop from './steps/add-backdrop.en.png';
|
||||
|
||||
// Add Effectsf
|
||||
import addEffects from './steps/add-effects.en.png';
|
||||
|
||||
// Hide and Show
|
||||
import hideAndShow from './steps/hide-show.en.png';
|
||||
|
||||
// Switch Costumes
|
||||
import switchCostumes from './steps/switch-costumes.en.png';
|
||||
|
||||
// Change Size
|
||||
import changeSize from './steps/change-size.en.png';
|
||||
|
||||
// Spin
|
||||
import spinTurn from './steps/spin-turn.en.png';
|
||||
import spinPointInDirection from './steps/spin-point-in-direction.en.png';
|
||||
|
||||
// Record a Sound
|
||||
import recordASoundSoundsTab from './steps/record-a-sound-sounds-tab.en.png';
|
||||
import recordASoundClickRecord from './steps/record-a-sound-click-record.en.png';
|
||||
import recordASoundPressRecordButton from './steps/record-a-sound-press-record-button.en.png';
|
||||
import recordASoundChooseSound from './steps/record-a-sound-choose-sound.en.png';
|
||||
import recordASoundPlayYourSound from './steps/record-a-sound-play-your-sound.en.png';
|
||||
|
||||
// Use Arrow Keys
|
||||
import moveArrowKeysLeftRight from './steps/move-arrow-keys-left-right.en.png';
|
||||
import moveArrowKeysUpDown from './steps/move-arrow-keys-up-down.en.png';
|
||||
|
||||
// Glide Around
|
||||
import glideAroundBackAndForth from './steps/glide-around-back-and-forth.en.png';
|
||||
import glideAroundPoint from './steps/glide-around-point.en.png';
|
||||
|
||||
const enImages = {
|
||||
// Intro
|
||||
introMove: introMove,
|
||||
@@ -261,89 +181,8 @@ const enImages = {
|
||||
videoAddExtension: videoAddExtension,
|
||||
videoPet: videoPet,
|
||||
videoAnimate: videoAnimate,
|
||||
videoPop: videoPop,
|
||||
videoPop: videoPop
|
||||
|
||||
// Make it Fly
|
||||
flyChooseBackdrop: flyChooseBackdrop,
|
||||
flyChooseCharacter: flyChooseCharacter,
|
||||
flySaySomething: flySaySomething,
|
||||
flyMoveArrows: flyMoveArrows,
|
||||
flyChooseObject: flyChooseObject,
|
||||
flyFlyingObject: flyFlyingObject,
|
||||
flySelectFlyingSprite: flySelectFlyingSprite,
|
||||
flyAddScore: flyAddScore,
|
||||
flyKeepScore: flyKeepScore,
|
||||
flyAddScenery: flyAddScenery,
|
||||
flyMoveScenery: flyMoveScenery,
|
||||
flySwitchLooks: flySwitchLooks,
|
||||
|
||||
// Pong
|
||||
pongAddBackdrop: pongAddBackdrop,
|
||||
pongAddBallSprite: pongAddBallSprite,
|
||||
pongBounceAround: pongBounceAround,
|
||||
pongAddPaddle: pongAddPaddle,
|
||||
pongMoveThePaddle: pongMoveThePaddle,
|
||||
pongSelectBallSprite: pongSelectBallSprite,
|
||||
pongAddMoreCodeToBall: pongAddMoreCodeToBall,
|
||||
pongAddAScore: pongAddAScore,
|
||||
pongChooseScoreFromMenu: pongChooseScoreFromMenu,
|
||||
pongInsertChangeScoreBlock: pongInsertChangeScoreBlock,
|
||||
pongResetScore: pongResetScore,
|
||||
pongAddLineSprite: pongAddLineSprite,
|
||||
pongGameOver: pongGameOver,
|
||||
|
||||
// Imagine If
|
||||
imagineTypeWhatYouWant: imagineTypeWhatYouWant,
|
||||
imagineClickGreenFlag: imagineClickGreenFlag,
|
||||
imagineChooseBackdrop: imagineChooseBackdrop,
|
||||
imagineChooseSprite: imagineChooseSprite,
|
||||
imagineFlyAround: imagineFlyAround,
|
||||
imagineChooseAnotherSprite: imagineChooseAnotherSprite,
|
||||
imagineLeftRight: imagineLeftRight,
|
||||
imagineUpDown: imagineUpDown,
|
||||
imagineChangeCostumes: imagineChangeCostumes,
|
||||
imagineGlideToPoint: imagineGlideToPoint,
|
||||
imagineGrowShrink: imagineGrowShrink,
|
||||
imagineChooseAnotherBackdrop: imagineChooseAnotherBackdrop,
|
||||
imagineSwitchBackdrops: imagineSwitchBackdrops,
|
||||
imagineRecordASound: imagineRecordASound,
|
||||
imagineChooseSound: imagineChooseSound,
|
||||
|
||||
// Add a Backdrop
|
||||
addBackdrop: addBackdrop,
|
||||
|
||||
// Add Effects
|
||||
addEffects: addEffects,
|
||||
|
||||
// Hide and Show
|
||||
hideAndShow: hideAndShow,
|
||||
|
||||
// Switch Costumes
|
||||
switchCostumes: switchCostumes,
|
||||
|
||||
// Change Size
|
||||
changeSize: changeSize,
|
||||
|
||||
// Spin
|
||||
spinTurn: spinTurn,
|
||||
spinPointInDirection: spinPointInDirection,
|
||||
|
||||
// Record a Sound
|
||||
recordASoundSoundsTab: recordASoundSoundsTab,
|
||||
recordASoundClickRecord: recordASoundClickRecord,
|
||||
recordASoundPressRecordButton: recordASoundPressRecordButton,
|
||||
recordASoundChooseSound: recordASoundChooseSound,
|
||||
recordASoundPlayYourSound: recordASoundPlayYourSound,
|
||||
|
||||
// Use Arrow Keys
|
||||
moveArrowKeysLeftRight: moveArrowKeysLeftRight,
|
||||
moveArrowKeysUpDown: moveArrowKeysUpDown,
|
||||
|
||||
// Glide Around
|
||||
glideAroundBackAndForth: glideAroundBackAndForth,
|
||||
glideAroundPoint: glideAroundPoint
|
||||
|
||||
};
|
||||
|
||||
|
||||
export {enImages};
|
||||
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 219 KiB |
|
Before Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 473 KiB |
|
Before Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 69 KiB |
@@ -74,12 +74,6 @@ const videos = {
|
||||
'en': '3jvl8zgjo2',
|
||||
'ja': 'q7zq9vu0wb',
|
||||
'ja-Hira': 'q7zq9vu0wb'
|
||||
},
|
||||
'make-it-fly': {
|
||||
en: 'zbtdx2dem9'
|
||||
},
|
||||
'pong-game': {
|
||||
en: '8m48dv0ens'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Andie",
|
||||
"md5": "d2c433e02481b10584e34ccdcf5a046a.svg",
|
||||
"md5": "419ff5a76adbb4b076c072f68267fdeb.svg",
|
||||
"type": "sprite",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -138,7 +138,7 @@
|
||||
"objName": "Andie",
|
||||
"sounds": [
|
||||
{
|
||||
"soundName": "Basketball Bounce",
|
||||
"soundName": "basketball bounce",
|
||||
"soundID": -1,
|
||||
"md5": "1727f65b5f22d151685b8e5917456a60.wav",
|
||||
"sampleCount": 8099,
|
||||
@@ -149,40 +149,40 @@
|
||||
"costumes": [
|
||||
{
|
||||
"costumeName": "andie-a",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "d2c433e02481b10584e34ccdcf5a046a.svg",
|
||||
"baseLayerID": 0,
|
||||
"baseLayerMD5": "419ff5a76adbb4b076c072f68267fdeb.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 80,
|
||||
"rotationCenterY": 65
|
||||
"rotationCenterX": 86,
|
||||
"rotationCenterY": 81
|
||||
},
|
||||
{
|
||||
"costumeName": "andie-b",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "045f92ad80d72521098ede6d7c80549a.svg",
|
||||
"baseLayerMD5": "a6a766912b4e43064ee3f389c72925b4.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 40,
|
||||
"rotationCenterY": 91
|
||||
"rotationCenterX": 46,
|
||||
"rotationCenterY": 106
|
||||
},
|
||||
{
|
||||
"costumeName": "andie-c",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "c9bad70ad371b8d6d97daed7ab9def98.svg",
|
||||
"baseLayerMD5": "fac1025857d2c28391cb31f39a80df54.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 46,
|
||||
"rotationCenterY": 49
|
||||
"rotationCenterX": 52,
|
||||
"rotationCenterY": 65
|
||||
},
|
||||
{
|
||||
"costumeName": "andie-d",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "7c9db638cd671898ff07771112faff0f.svg",
|
||||
"baseLayerMD5": "7af50835e05d43e5e07cd2d641e7401e.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 71,
|
||||
"rotationCenterY": 57
|
||||
"rotationCenterX": 87,
|
||||
"rotationCenterY": 72
|
||||
}
|
||||
],
|
||||
"currentCostumeIndex": 0,
|
||||
"scratchX": -150,
|
||||
"scratchY": 90,
|
||||
"scratchX": -129,
|
||||
"scratchY": -73,
|
||||
"scale": 1,
|
||||
"direction": 90,
|
||||
"rotationStyle": "normal",
|
||||
@@ -1442,7 +1442,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Ben",
|
||||
"md5": "322e13bd17d02acfdc048a5ef2167be8.svg",
|
||||
"md5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg",
|
||||
"type": "sprite",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -1460,18 +1460,18 @@
|
||||
"objName": "Ben",
|
||||
"sounds": [
|
||||
{
|
||||
"soundName": "Goal Cheer",
|
||||
"soundName": "pop",
|
||||
"soundID": -1,
|
||||
"md5": "a434069c58e79d42f5d21abb1c318919.wav",
|
||||
"sampleCount": 84096,
|
||||
"rate": 22050,
|
||||
"format": "adpcm"
|
||||
"md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav",
|
||||
"sampleCount": 258,
|
||||
"rate": 11025,
|
||||
"format": ""
|
||||
},
|
||||
{
|
||||
"soundName": "Referee Whistle",
|
||||
"soundName": "basketball bounce",
|
||||
"soundID": -1,
|
||||
"md5": "8468b9b3f11a665ee4d215afd8463b97.wav",
|
||||
"sampleCount": 14034,
|
||||
"md5": "1727f65b5f22d151685b8e5917456a60.wav",
|
||||
"sampleCount": 8099,
|
||||
"rate": 22050,
|
||||
"format": "adpcm"
|
||||
}
|
||||
@@ -1480,39 +1480,39 @@
|
||||
{
|
||||
"costumeName": "ben-a",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "322e13bd17d02acfdc048a5ef2167be8.svg",
|
||||
"baseLayerMD5": "4386d05d5abd6e1aa0a93b7b447d1b39.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 54,
|
||||
"rotationCenterX": 53,
|
||||
"rotationCenterY": 69
|
||||
},
|
||||
{
|
||||
"costumeName": "ben-b",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "59ba1e80ba75b1d06c37db4b9b4fde27.svg",
|
||||
"baseLayerMD5": "90db137cd0743204d02fdc9be1c8eb94.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 54,
|
||||
"rotationCenterY": 69
|
||||
"rotationCenterX": 53,
|
||||
"rotationCenterY": 67
|
||||
},
|
||||
{
|
||||
"costumeName": "ben-c",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "940ec2fa85d666dde95a51bcbccd266e.svg",
|
||||
"baseLayerMD5": "5f05d41ede773b3bb379286e86a5b869.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 73,
|
||||
"rotationCenterY": 71
|
||||
"rotationCenterX": 71,
|
||||
"rotationCenterY": 72
|
||||
},
|
||||
{
|
||||
"costumeName": "ben-d",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "3043a50a91b9d484a8cd32eb82f2d88f.svg",
|
||||
"baseLayerMD5": "6f6e246567c0639c758ed4f6930be76f.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 44,
|
||||
"rotationCenterY": 71
|
||||
"rotationCenterX": 42,
|
||||
"rotationCenterY": 72
|
||||
}
|
||||
],
|
||||
"currentCostumeIndex": 0,
|
||||
"scratchX": -15,
|
||||
"scratchY": 93,
|
||||
"scratchX": -63,
|
||||
"scratchY": -94,
|
||||
"scale": 1,
|
||||
"direction": 90,
|
||||
"rotationStyle": "normal",
|
||||
@@ -5434,13 +5434,12 @@
|
||||
},
|
||||
{
|
||||
"name": "Earth",
|
||||
"md5": "64220361b80d2812e9f27fc88ef39027.svg",
|
||||
"md5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg",
|
||||
"type": "sprite",
|
||||
"tags": [
|
||||
"space",
|
||||
"planet",
|
||||
"earth",
|
||||
"globe"
|
||||
"earth"
|
||||
],
|
||||
"info": [
|
||||
0,
|
||||
@@ -5461,17 +5460,17 @@
|
||||
],
|
||||
"costumes": [
|
||||
{
|
||||
"costumeName": "earth",
|
||||
"costumeName": "Earth",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "64220361b80d2812e9f27fc88ef39027.svg",
|
||||
"baseLayerMD5": "d9df2cf4f6eb2460d0de3e4f1e05f40b.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 55,
|
||||
"rotationCenterY": 55
|
||||
"rotationCenterX": 41,
|
||||
"rotationCenterY": 40
|
||||
}
|
||||
],
|
||||
"currentCostumeIndex": 0,
|
||||
"scratchX": 142,
|
||||
"scratchY": -53,
|
||||
"scratchX": 57,
|
||||
"scratchY": -39,
|
||||
"scale": 1,
|
||||
"direction": 90,
|
||||
"rotationStyle": "normal",
|
||||
@@ -8843,7 +8842,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Jordyn",
|
||||
"md5": "2f59adebc2c0feb9ebfabd7ee9642f2b.svg",
|
||||
"md5": "8355df83cca2baa0fdf6269d1311f1ea.svg",
|
||||
"type": "sprite",
|
||||
"tags": [
|
||||
"sports",
|
||||
@@ -8855,24 +8854,16 @@
|
||||
"info": [
|
||||
0,
|
||||
4,
|
||||
2
|
||||
1
|
||||
],
|
||||
"json": {
|
||||
"objName": "Jordyn",
|
||||
"sounds": [
|
||||
{
|
||||
"soundName": "Goal Cheer",
|
||||
"soundName": "basketball bounce",
|
||||
"soundID": -1,
|
||||
"md5": "199b30c8b4fe0642e849924bd1e1b463.wav",
|
||||
"sampleCount": 84411,
|
||||
"rate": 22050,
|
||||
"format": "adpcm"
|
||||
},
|
||||
{
|
||||
"soundName": "Referee Whistle",
|
||||
"soundID": -1,
|
||||
"md5": "7d91d95d841dc6cf1282914306a4674a.wav",
|
||||
"sampleCount": 14238,
|
||||
"md5": "1727f65b5f22d151685b8e5917456a60.wav",
|
||||
"sampleCount": 8099,
|
||||
"rate": 22050,
|
||||
"format": "adpcm"
|
||||
}
|
||||
@@ -8881,15 +8872,15 @@
|
||||
{
|
||||
"costumeName": "jordyn-a",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "2f59adebc2c0feb9ebfabd7ee9642f2b.svg",
|
||||
"baseLayerMD5": "8355df83cca2baa0fdf6269d1311f1ea.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 51,
|
||||
"rotationCenterY": 62
|
||||
"rotationCenterY": 63
|
||||
},
|
||||
{
|
||||
"costumeName": "jordyn-b",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "075f382883e60dba8094b3dc30918f60.svg",
|
||||
"baseLayerMD5": "2e2c43bbd7b18524fb132f1426ca3134.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 51,
|
||||
"rotationCenterY": 63
|
||||
@@ -8897,23 +8888,23 @@
|
||||
{
|
||||
"costumeName": "jordyn-c",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "fbe81c40d4a880c8799c458359758629.svg",
|
||||
"baseLayerMD5": "5c0d24c53805e16358a2f6f6cc4a456e.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 68,
|
||||
"rotationCenterY": 62
|
||||
"rotationCenterY": 61
|
||||
},
|
||||
{
|
||||
"costumeName": "jordyn-d",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "68a784d4d0340475a6e6feff3b28b470.svg",
|
||||
"baseLayerMD5": "ebe2b20b99c1e1b373fd2b02be48aab9.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 40,
|
||||
"rotationCenterY": 62
|
||||
"rotationCenterY": 61
|
||||
}
|
||||
],
|
||||
"currentCostumeIndex": 0,
|
||||
"scratchX": 61,
|
||||
"scratchY": 93,
|
||||
"scratchX": -164,
|
||||
"scratchY": -41,
|
||||
"scale": 1,
|
||||
"direction": 90,
|
||||
"rotationStyle": "normal",
|
||||
@@ -14381,7 +14372,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Sun",
|
||||
"md5": "14b0508da0dc30e83b2bede13c1be1e9.svg",
|
||||
"md5": "55c931c65456822c0c56a2b30e3e550d.svg",
|
||||
"type": "sprite",
|
||||
"tags": [
|
||||
"space",
|
||||
@@ -14412,15 +14403,15 @@
|
||||
{
|
||||
"costumeName": "sun",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "14b0508da0dc30e83b2bede13c1be1e9.svg",
|
||||
"baseLayerMD5": "55c931c65456822c0c56a2b30e3e550d.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 54,
|
||||
"rotationCenterY": 54
|
||||
"rotationCenterX": 72,
|
||||
"rotationCenterY": 72
|
||||
}
|
||||
],
|
||||
"currentCostumeIndex": 0,
|
||||
"scratchX": -3,
|
||||
"scratchY": -42,
|
||||
"scratchX": 16,
|
||||
"scratchY": -28,
|
||||
"scale": 1,
|
||||
"direction": 90,
|
||||
"rotationStyle": "normal",
|
||||
@@ -15787,56 +15778,6 @@
|
||||
"spriteInfo": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wizard Girl",
|
||||
"md5": "dd40d3a2565990bcc24d2b36cd446394.svg",
|
||||
"type": "sprite",
|
||||
"tags": [
|
||||
"people",
|
||||
"female",
|
||||
"girl",
|
||||
"wizard",
|
||||
"magic",
|
||||
"fantasy"
|
||||
],
|
||||
"info": [
|
||||
0,
|
||||
1,
|
||||
1
|
||||
],
|
||||
"json": {
|
||||
"objName": "Wizard Girl",
|
||||
"sounds": [
|
||||
{
|
||||
"soundName": "Magic Spell",
|
||||
"soundID": -1,
|
||||
"md5": "1cb60ecdb1075c8769cb346d5c2a22c7.wav",
|
||||
"sampleCount": 43077,
|
||||
"rate": 22050,
|
||||
"format": "adpcm"
|
||||
}
|
||||
],
|
||||
"costumes": [
|
||||
{
|
||||
"costumeName": "wizard girl",
|
||||
"baseLayerID": -1,
|
||||
"baseLayerMD5": "dd40d3a2565990bcc24d2b36cd446394.svg",
|
||||
"bitmapResolution": 1,
|
||||
"rotationCenterX": 80,
|
||||
"rotationCenterY": 91
|
||||
}
|
||||
],
|
||||
"currentCostumeIndex": 0,
|
||||
"scratchX": -147,
|
||||
"scratchY": -71,
|
||||
"scale": 1,
|
||||
"direction": 90,
|
||||
"rotationStyle": "normal",
|
||||
"isDraggable": false,
|
||||
"visible": true,
|
||||
"spriteInfo": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Wizard Hat",
|
||||
"md5": "2da561a7e5cbfcda613f4750020d4aa5.svg",
|
||||
@@ -20471,4 +20412,4 @@
|
||||
"spriteInfo": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -716,16 +716,13 @@ const xmlClose = '</xml>';
|
||||
/**
|
||||
* @param {!boolean} isStage - Whether the toolbox is for a stage-type target.
|
||||
* @param {?string} targetId - The current editing target
|
||||
* @param {?Array.<object>} categoriesXML - optional array of `{id,xml}` for categories. This can include both core
|
||||
* and other extensions: core extensions will be placed in the normal Scratch order; others will go at the bottom.
|
||||
* @property {string} id - the extension / category ID.
|
||||
* @property {string} xml - the `<category>...</category>` XML for this extension / category.
|
||||
* @param {?string} categoriesXML - null for default toolbox, or an XML string with <category> elements.
|
||||
* @param {?string} costumeName - The name of the default selected costume dropdown.
|
||||
* @param {?string} backdropName - The name of the default selected backdrop dropdown.
|
||||
* @param {?string} soundName - The name of the default selected sound dropdown.
|
||||
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
|
||||
*/
|
||||
const makeToolboxXML = function (isStage, targetId, categoriesXML = [],
|
||||
const makeToolboxXML = function (isStage, targetId, categoriesXML,
|
||||
costumeName = '', backdropName = '', soundName = '') {
|
||||
const gap = [categorySeparator];
|
||||
|
||||
@@ -733,41 +730,21 @@ const makeToolboxXML = function (isStage, targetId, categoriesXML = [],
|
||||
backdropName = xmlEscape(backdropName);
|
||||
soundName = xmlEscape(soundName);
|
||||
|
||||
categoriesXML = categoriesXML.slice();
|
||||
const moveCategory = categoryId => {
|
||||
const index = categoriesXML.findIndex(categoryInfo => categoryInfo.id === categoryId);
|
||||
if (index >= 0) {
|
||||
// remove the category from categoriesXML and return its XML
|
||||
const [categoryInfo] = categoriesXML.splice(index, 1);
|
||||
return categoryInfo.xml;
|
||||
}
|
||||
// return `undefined`
|
||||
};
|
||||
const motionXML = moveCategory('motion') || motion(isStage, targetId);
|
||||
const looksXML = moveCategory('looks') || looks(isStage, targetId, costumeName, backdropName);
|
||||
const soundXML = moveCategory('sound') || sound(isStage, targetId, soundName);
|
||||
const eventsXML = moveCategory('event') || events(isStage, targetId);
|
||||
const controlXML = moveCategory('control') || control(isStage, targetId);
|
||||
const sensingXML = moveCategory('sensing') || sensing(isStage, targetId);
|
||||
const operatorsXML = moveCategory('operators') || operators(isStage, targetId);
|
||||
const variablesXML = moveCategory('data') || variables(isStage, targetId);
|
||||
const myBlocksXML = moveCategory('procedures') || myBlocks(isStage, targetId);
|
||||
|
||||
const everything = [
|
||||
xmlOpen,
|
||||
motionXML, gap,
|
||||
looksXML, gap,
|
||||
soundXML, gap,
|
||||
eventsXML, gap,
|
||||
controlXML, gap,
|
||||
sensingXML, gap,
|
||||
operatorsXML, gap,
|
||||
variablesXML, gap,
|
||||
myBlocksXML
|
||||
motion(isStage, targetId), gap,
|
||||
looks(isStage, targetId, costumeName, backdropName), gap,
|
||||
sound(isStage, targetId, soundName), gap,
|
||||
events(isStage, targetId), gap,
|
||||
control(isStage, targetId), gap,
|
||||
sensing(isStage, targetId), gap,
|
||||
operators(isStage, targetId), gap,
|
||||
variables(isStage, targetId), gap,
|
||||
myBlocks(isStage, targetId)
|
||||
];
|
||||
|
||||
for (const extensionCategory of categoriesXML) {
|
||||
everything.push(gap, extensionCategory.xml);
|
||||
if (categoriesXML) {
|
||||
everything.push(gap, categoriesXML);
|
||||
}
|
||||
|
||||
everything.push(xmlClose);
|
||||
|
||||
@@ -234,14 +234,9 @@ const ProjectSaverHOC = function (WrappedComponent) {
|
||||
asset.dataFormat,
|
||||
asset.data,
|
||||
asset.assetId
|
||||
).then(response => {
|
||||
// Asset servers respond with {status: ok} for successful POSTs
|
||||
if (response.status !== 'ok') {
|
||||
// Errors include a `code` property, e.g. "Forbidden"
|
||||
return Promise.reject(response.code);
|
||||
}
|
||||
asset.clean = true;
|
||||
})
|
||||
).then(
|
||||
() => (asset.clean = true)
|
||||
)
|
||||
)
|
||||
)
|
||||
.then(() => this.props.onUpdateProjectData(projectId, savedVMState, requestParams))
|
||||
|
||||
@@ -45,7 +45,6 @@ export default function (projectId, vmState, params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
xhr(opts, (err, response) => {
|
||||
if (err) return reject(err);
|
||||
if (response.statusCode !== 200) return reject(response.statusCode);
|
||||
let body;
|
||||
try {
|
||||
// Since we didn't set json: true, we have to parse manually
|
||||
|
||||
@@ -193,7 +193,11 @@ describe('Working with costumes', () => {
|
||||
.mouseMove(abbyElement)
|
||||
.perform();
|
||||
// wait for one of Abby's alternate costumes to appear
|
||||
await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/b6e23922f23b49ddc6f62f675e77417c.svg/get/"]');
|
||||
const src1 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
|
||||
await driver.sleep(300);
|
||||
const src2 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
|
||||
const sourcesMatch = (src1 === src2);
|
||||
await expect(sourcesMatch).toBeFalsy(); // 'src' attribute should have changed by now
|
||||
const logs = await getLogs();
|
||||
await expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -75,8 +75,7 @@ describe('blocks example', () => {
|
||||
await expect(projectRequests).toEqual(['https://projects.scratch.mit.edu/96708228']);
|
||||
});
|
||||
|
||||
// skipping per https://github.com/LLK/scratch-gui/issues/4902 until we have better approach
|
||||
test.skip('Change categories', async () => {
|
||||
test('Change categories', async () => {
|
||||
await loadUri(`${uri}`);
|
||||
await clickText('Looks');
|
||||
await clickText('Sound');
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import defineDynamicBlock from '../../../src/lib/define-dynamic-block';
|
||||
|
||||
import BlockType from 'scratch-vm/src/extension-support/block-type';
|
||||
|
||||
const MockScratchBlocks = {
|
||||
OUTPUT_SHAPE_HEXAGONAL: 1,
|
||||
OUTPUT_SHAPE_ROUND: 2,
|
||||
OUTPUT_SHAPE_SQUARE: 3
|
||||
};
|
||||
|
||||
const categoryInfo = {
|
||||
name: 'test category',
|
||||
color1: '#111',
|
||||
color2: '#222',
|
||||
color3: '#333'
|
||||
};
|
||||
|
||||
const penIconURI = 'data:image/svg+xml;base64,fake_pen_icon_svg_base64_data';
|
||||
|
||||
const testBlockInfo = {
|
||||
commandWithIcon: {
|
||||
blockType: BlockType.COMMAND,
|
||||
blockIconURI: penIconURI,
|
||||
text: 'command with icon'
|
||||
},
|
||||
commandWithoutIcon: {
|
||||
blockType: BlockType.COMMAND,
|
||||
text: 'command without icon'
|
||||
},
|
||||
terminalCommand: {
|
||||
blockType: BlockType.COMMAND,
|
||||
isTerminal: true,
|
||||
text: 'terminal command'
|
||||
},
|
||||
reporter: {
|
||||
blockType: BlockType.REPORTER,
|
||||
text: 'reporter'
|
||||
},
|
||||
boolean: {
|
||||
blockType: BlockType.BOOLEAN,
|
||||
text: 'Boolean'
|
||||
},
|
||||
hat: {
|
||||
blockType: BlockType.HAT,
|
||||
text: 'hat'
|
||||
}
|
||||
};
|
||||
|
||||
// similar to goog.mixin from the Closure library
|
||||
const mixin = function (target, source) {
|
||||
for (const x in source) {
|
||||
target[x] = source[x];
|
||||
}
|
||||
};
|
||||
|
||||
class MockBlock {
|
||||
constructor (blockInfo, extendedOpcode) {
|
||||
// mimic Closure-style inheritance by mixing in `defineDynamicBlock` output as this instance's prototype
|
||||
// see also the `Blockly.Block` constructor
|
||||
const prototype = defineDynamicBlock(MockScratchBlocks, categoryInfo, blockInfo, extendedOpcode);
|
||||
mixin(this, prototype);
|
||||
this.init();
|
||||
|
||||
// bootstrap the mutation<->DOM cycle
|
||||
this.blockInfoText = JSON.stringify(blockInfo);
|
||||
const xmlElement = this.mutationToDom();
|
||||
|
||||
// parse blockInfo from XML to fill dynamic properties
|
||||
this.domToMutation(xmlElement);
|
||||
}
|
||||
|
||||
jsonInit (json) {
|
||||
this.result = Object.assign({}, json);
|
||||
}
|
||||
interpolate_ () {
|
||||
// TODO: add tests for this?
|
||||
}
|
||||
setCheckboxInFlyout (isEnabled) {
|
||||
this.result.checkboxInFlyout_ = isEnabled;
|
||||
}
|
||||
setOutput (isEnabled) {
|
||||
this.result.outputConnection = isEnabled; // Blockly calls `makeConnection_` here
|
||||
}
|
||||
setOutputShape (outputShape) {
|
||||
this.result.outputShape_ = outputShape;
|
||||
}
|
||||
setNextStatement (isEnabled) {
|
||||
this.result.nextConnection = isEnabled; // Blockly calls `makeConnection_` here
|
||||
}
|
||||
setPreviousStatement (isEnabled) {
|
||||
this.result.previousConnection = isEnabled; // Blockly calls `makeConnection_` here
|
||||
}
|
||||
}
|
||||
|
||||
describe('defineDynamicBlock', () => {
|
||||
test('is a function', () => {
|
||||
expect(typeof defineDynamicBlock).toBe('function');
|
||||
});
|
||||
test('can define a command block with an icon', () => {
|
||||
const extendedOpcode = 'test.commandWithIcon';
|
||||
const block = new MockBlock(testBlockInfo.commandWithIcon, extendedOpcode);
|
||||
expect(block.result).toEqual({
|
||||
category: categoryInfo.name,
|
||||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3,
|
||||
extensions: ['scratch_extension'],
|
||||
inputsInline: true,
|
||||
nextConnection: true,
|
||||
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
|
||||
previousConnection: true,
|
||||
type: extendedOpcode
|
||||
});
|
||||
});
|
||||
test('can define a command block without an icon', () => {
|
||||
const extendedOpcode = 'test.commandWithoutIcon';
|
||||
const block = new MockBlock(testBlockInfo.commandWithoutIcon, extendedOpcode);
|
||||
expect(block.result).toEqual({
|
||||
category: categoryInfo.name,
|
||||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3,
|
||||
// extensions: undefined, // no icon means no extension
|
||||
inputsInline: true,
|
||||
nextConnection: true,
|
||||
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
|
||||
previousConnection: true,
|
||||
type: extendedOpcode
|
||||
});
|
||||
});
|
||||
test('can define a terminal command', () => {
|
||||
const extendedOpcode = 'test.terminal';
|
||||
const block = new MockBlock(testBlockInfo.terminalCommand, extendedOpcode);
|
||||
expect(block.result).toEqual({
|
||||
category: categoryInfo.name,
|
||||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3,
|
||||
// extensions: undefined, // no icon means no extension
|
||||
inputsInline: true,
|
||||
nextConnection: false, // terminal
|
||||
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
|
||||
previousConnection: true,
|
||||
type: extendedOpcode
|
||||
});
|
||||
});
|
||||
test('can define a reporter', () => {
|
||||
const extendedOpcode = 'test.reporter';
|
||||
const block = new MockBlock(testBlockInfo.reporter, extendedOpcode);
|
||||
expect(block.result).toEqual({
|
||||
category: categoryInfo.name,
|
||||
checkboxInFlyout_: true,
|
||||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3,
|
||||
// extensions: undefined, // no icon means no extension
|
||||
inputsInline: true,
|
||||
// nextConnection: undefined, // reporter
|
||||
outputConnection: true, // reporter
|
||||
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_ROUND, // reporter
|
||||
// previousConnection: undefined, // reporter
|
||||
type: extendedOpcode
|
||||
});
|
||||
});
|
||||
test('can define a Boolean', () => {
|
||||
const extendedOpcode = 'test.boolean';
|
||||
const block = new MockBlock(testBlockInfo.boolean, extendedOpcode);
|
||||
expect(block.result).toEqual({
|
||||
category: categoryInfo.name,
|
||||
// checkboxInFlyout_: undefined,
|
||||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3,
|
||||
// extensions: undefined, // no icon means no extension
|
||||
inputsInline: true,
|
||||
// nextConnection: undefined, // reporter
|
||||
outputConnection: true, // reporter
|
||||
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_HEXAGONAL, // Boolean
|
||||
// previousConnection: undefined, // reporter
|
||||
type: extendedOpcode
|
||||
});
|
||||
});
|
||||
test('can define a hat', () => {
|
||||
const extendedOpcode = 'test.hat';
|
||||
const block = new MockBlock(testBlockInfo.hat, extendedOpcode);
|
||||
expect(block.result).toEqual({
|
||||
category: categoryInfo.name,
|
||||
colour: categoryInfo.color1,
|
||||
colourSecondary: categoryInfo.color2,
|
||||
colourTertiary: categoryInfo.color3,
|
||||
// extensions: undefined, // no icon means no extension
|
||||
inputsInline: true,
|
||||
nextConnection: true,
|
||||
outputShape_: MockScratchBlocks.OUTPUT_SHAPE_SQUARE,
|
||||
// previousConnection: undefined, // hat
|
||||
type: extendedOpcode
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
import DragRecognizer from '../../../src/lib/drag-recognizer';
|
||||
|
||||
describe('DragRecognizer', () => {
|
||||
let onDrag;
|
||||
let onDragEnd;
|
||||
let dragRecognizer;
|
||||
|
||||
beforeEach(() => {
|
||||
onDrag = jest.fn();
|
||||
onDragEnd = jest.fn();
|
||||
dragRecognizer = new DragRecognizer({onDrag, onDragEnd});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dragRecognizer.reset();
|
||||
});
|
||||
|
||||
test('start -> small drag', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
window.dispatchEvent(new MouseEvent('mousemove', {clientX: 101, clientY: 101}));
|
||||
expect(onDrag).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('start -> large vertical touch move -> scroll, not drag', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 106, clientY: 150}));
|
||||
expect(onDrag).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('start -> large vertical mouse move -> mouse moves always drag)', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
window.dispatchEvent(new MouseEvent('mousemove', {clientX: 100, clientY: 150}));
|
||||
expect(onDrag).toBeCalled();
|
||||
});
|
||||
|
||||
test('start -> large horizontal touch move -> drag', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
|
||||
expect(onDrag).toBeCalled();
|
||||
});
|
||||
|
||||
test('after starting a scroll, it cannot become a drag', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 100, clientY: 110}));
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 100, clientY: 100}));
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 110, clientY: 100}));
|
||||
expect(onDrag).not.toBeCalled();
|
||||
});
|
||||
|
||||
test('start -> end unbinds', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
|
||||
expect(onDrag).toHaveBeenCalledTimes(1);
|
||||
window.dispatchEvent(new MouseEvent('touchend', {clientX: 150, clientY: 106}));
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
|
||||
expect(onDrag).toHaveBeenCalledTimes(1); // Still 1
|
||||
});
|
||||
|
||||
test('start -> reset unbinds', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
|
||||
expect(onDrag).toHaveBeenCalledTimes(1);
|
||||
dragRecognizer.reset();
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 150, clientY: 106}));
|
||||
expect(onDrag).toHaveBeenCalledTimes(1); // Still 1
|
||||
});
|
||||
|
||||
test('scrolls do not call prevent default', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
const event = new MouseEvent('touchmove', {clientX: 100, clientY: 110});
|
||||
event.preventDefault = jest.fn();
|
||||
window.dispatchEvent(event);
|
||||
expect(event.preventDefault).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test('confirmed drags have preventDefault called on them', () => {
|
||||
dragRecognizer.start({clientX: 100, clientY: 100});
|
||||
const event = new MouseEvent('touchmove', {clientX: 150, clientY: 106});
|
||||
event.preventDefault = jest.fn();
|
||||
window.dispatchEvent(event);
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('multiple horizontal drag angles', () => {
|
||||
// +45 from horizontal => drag
|
||||
dragRecognizer.start({clientX: 0, clientY: 0});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 10, clientY: 10}));
|
||||
expect(onDrag).toHaveBeenCalledTimes(1);
|
||||
dragRecognizer.reset();
|
||||
|
||||
// -45 from horizontal => drag
|
||||
dragRecognizer.start({clientX: 0, clientY: 0});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: 10, clientY: -10}));
|
||||
expect(onDrag).toHaveBeenCalledTimes(2);
|
||||
dragRecognizer.reset();
|
||||
|
||||
// +135 from horizontal => drag
|
||||
dragRecognizer.start({clientX: 0, clientY: 0});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: -10, clientY: 10}));
|
||||
expect(onDrag).toHaveBeenCalledTimes(3);
|
||||
dragRecognizer.reset();
|
||||
|
||||
// -135 from horizontal => drag
|
||||
dragRecognizer.start({clientX: 0, clientY: 0});
|
||||
window.dispatchEvent(new MouseEvent('touchmove', {clientX: -10, clientY: -10}));
|
||||
expect(onDrag).toHaveBeenCalledTimes(4);
|
||||
dragRecognizer.reset();
|
||||
});
|
||||
});
|
||||