Merge branch 'develop' into feature/checked-callback

This commit is contained in:
Paul Kaplan authored and GitHub committed 2017-05-30 12:33:56 -04:00
commit 7414ead04a
23 files changed
+577 -130

No files matched your search

+1
View File
@@ -39,6 +39,7 @@
"gh-pages": "^1.0.0",
"html-webpack-plugin": "2.28.0",
"lodash.bindall": "4.4.0",
"lodash.debounce": "4.0.8",
"lodash.defaultsdeep": "4.6.0",
"lodash.isequal": "4.5.0",
"lodash.omit": "4.5.0",
+25 -19
View File
@@ -6,40 +6,46 @@
align-items: center;
justify-content: center;
color: gray;
background-color: $blue;
overflow: hidden; /* Mask the icon animation */
background-color: rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-color: #dbdbdb;
border-style: solid;
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.1);
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
cursor: pointer;
user-select: none;
transition: all 0.15s ease-out; /* @todo: standardize with var */
cursor: pointer;
transition: all 0.15s ease-out;
}
.close-button.large:hover {
transform: scale(1.1, 1.1);
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.15);
}
.small {
width: 1rem;
height: 1rem;
border-width: 1px;
color: #FFF;
background-color: $blue;
}
.large {
width: 2.75rem;
height: 2.75rem;
border-width: 2px;
width: 1.75rem;
height: 1.75rem;
}
.close-button:hover {
transform: scale(1.1, 1.1);
}
/* Same icon as Sprite Selector Add button, but rotated.
@todo: reuse?
*/
.close-icon {
position: relative;
margin: 0.25rem;
user-select: none;
transform-origin: 50%;
transform: rotate(45deg);
}
.small .close-icon {
width: 40%;
}
.large .close-icon {
width: 0.75rem;
height: 0.75rem;
}
+4 -4
View File
@@ -11,8 +11,8 @@ const CloseButton = props => (
styles.closeButton,
props.className,
{
[styles.large]: props.size === CloseButton.SIZE_LARGE,
[styles.small]: props.size === CloseButton.SIZE_SMALL
[styles.small]: props.size === CloseButton.SIZE_SMALL,
[styles.large]: props.size === CloseButton.SIZE_LARGE
}
)}
onClick={props.onClick}
@@ -24,13 +24,13 @@ const CloseButton = props => (
</div>
);
CloseButton.SIZE_LARGE = 'large';
CloseButton.SIZE_SMALL = 'small';
CloseButton.SIZE_LARGE = 'large';
CloseButton.propTypes = {
className: PropTypes.string,
onClick: PropTypes.func.isRequired,
size: PropTypes.oneOf([CloseButton.SIZE_LARGE, CloseButton.SIZE_SMALL])
size: PropTypes.oneOf([CloseButton.SIZE_SMALL, CloseButton.SIZE_LARGE])
};
CloseButton.defaultProps = {
+103
View File
@@ -0,0 +1,103 @@
@import "../../css/units.css";
@import "../../css/colors.css";
.filter {
display: flex;
flex-direction: row;
align-items: center;
/*
Should we use width 100% instead, for cases when component's
parent is not a flexbox container?
*/
flex-grow: 1;
padding: 0.3rem 0.5rem;
background: rgba(0, 0, 0, 0.10);
border: 1px solid rgba(255, 255, 255, 0.10);
border-radius: 10rem;
user-select: none;
}
.filter:hover {
background: rgba(0, 0, 0, 0.15);
}
.filter-icon {
height: 0.9rem;
width: 0.9rem;
margin: 0 0.5rem 0 0.75rem;
}
/*
Hidden state
*/
.x-icon-wrapper {
opacity: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden; /* Mask the icon animation */
height: 1.25rem;
width: 1.25rem;
margin: 0 0.25rem 0 0.5rem; /* @todo: move to parent to make component*/
border-radius: 50%;
pointer-events: none;
cursor: default;
transition: opacity 0.05s linear;
}
/*
Shown state
*/
.filter.is-active .x-icon-wrapper {
pointer-events: auto;
cursor: pointer;
opacity: 1;
transition: opacity 0.05s linear;
}
.filter.is-active .x-icon-wrapper:hover {
transform: scale(1.2, 1.2);
}
/*
Hidden state
*/
.x-icon {
position: relative;
margin: 0.25rem;
user-select: none;
transform: translateX(0.5rem);
transition: transform 0.085s cubic-bezier(0.78, 1, 1, 1);
}
/*
Shown state
*/
.filter.is-active .x-icon-wrapper .x-icon {
transform: translateX(0);
}
.filter-input {
flex-grow: 1;
line-height: 1.25rem;
background-color: transparent;
-webkit-appearance: none;
outline: none;
border: 0;
color: white;
font-size: 0.75rem;
letter-spacing: 0.15px;
cursor: text;
}
.filter-input::placeholder {
opacity: 0.75;
padding: 0 0 0 0.25rem;
color: white;
font-size: 0.75rem;
letter-spacing: 0.15px;
}
+57
View File
@@ -0,0 +1,57 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
const filterIcon = require('./icon--filter.svg');
const xIcon = require('./icon--x.svg');
const styles = require('./filter.css');
const FilterComponent = props => {
const {
onChange,
onClear,
placeholderText,
filterQuery
} = props;
return (
<div
className={classNames({
[styles.filter]: true,
[styles.isActive]: filterQuery.length > 0
})}
>
<img
className={styles.filterIcon}
src={filterIcon}
/>
<input
autoFocus
className={styles.filterInput}
placeholder={placeholderText}
type="text"
value={filterQuery}
onChange={onChange}
/>
<div
className={styles.xIconWrapper}
onClick={onClear}
>
<img
className={styles.xIcon}
src={xIcon}
/>
</div>
</div>
);
};
FilterComponent.propTypes = {
filterQuery: PropTypes.string,
onChange: PropTypes.func,
onClear: PropTypes.func,
placeholderText: PropTypes.string
};
FilterComponent.defaultProps = {
placeholderText: 'what are you looking for?'
};
module.exports = FilterComponent;
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="14.5" height="14.6" viewBox="0 0 14.5 14.6">
<path fill="#FFFFFF" d="M10.4 9.2l-.8-.1-.2-.2c.5-.5.8-1 1-1.6.2-.6.3-1.2.3-1.9 0-1-.2-1.9-.7-2.7-.4-.9-1.1-1.5-1.9-2C7.3.2 6.3 0 5.4 0c-.9 0-1.9.2-2.7.7-.9.5-1.5 1.2-2 2S0 4.4 0 5.4s.2 1.9.7 2.8c.5.8 1.2 1.4 2 1.9s1.7.7 2.7.7c.7 0 1.3-.1 1.9-.3.6-.3 1.1-.6 1.6-1l.2.3v.6l4.1 4.2 1.3-1.3-4.1-4.1zM8.7 7.3c-.4.6-.8 1.1-1.4 1.4-.6.3-1.2.5-1.9.5-.6 0-1.3-.2-1.8-.5-.6-.4-1.1-.8-1.4-1.4-.3-.6-.5-1.2-.5-1.9s.2-1.4.5-1.9.8-1 1.4-1.3c.5-.4 1.1-.5 1.8-.5s1.3.1 1.9.5c.6.3 1 .8 1.4 1.3.3.6.5 1.2.5 1.9S9 6.7 8.7 7.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 613 B

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="9" height="8" viewBox="0 0 9 8">
<path fill="none" stroke="#FFF" stroke-width="2" d="M1 7.333L7.333 1M1 1L7.333 7.333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 224 B

+10 -9
View File
@@ -6,8 +6,9 @@
align-items: center;
justify-content: center;
flex-basis: 160px;
max-width: 160px;
height: 160px;
margin: calc($space / 2);
margin: $space;
padding: 1rem 1rem 0 1rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #575e75;
@@ -23,18 +24,18 @@
.library-item:hover {
border-width: 2px;
border-color: #1dacf4;
transition: 0.1s ease-out;
transform: scale(1.02, 1.02);
}
.library-item.is-selected {
border-width: 2px;
border-color: #1dacf4;
transition: 0.25s ease-out;
.library-item-image-container-wrapper {
height: 100px;
width: 100%;
position: relative;
}
.library-item-image-container {
position: absolute;
height: 100px;
width: 100%;
}
.library-item-image {
@@ -47,12 +48,12 @@
margin: 0.25rem 0;
text-align: center;
/*
/*
For truncating overflowing text gracefully
Min-width is for a bug: https://css-tricks.com/flexbox-truncated-text
*/
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
min-width: 0;
}
+28 -13
View File
@@ -1,4 +1,3 @@
const classNames = require('classnames');
const bindAll = require('lodash.bindall');
const PropTypes = require('prop-types');
const React = require('react');
@@ -9,26 +8,41 @@ const styles = require('./library-item.css');
class LibraryItem extends React.Component {
constructor (props) {
super(props);
bindAll(this, ['handleClick']);
bindAll(this, [
'handleClick',
'handleMouseEnter',
'handleMouseLeave'
]);
}
shouldComponentUpdate (nextProps) {
return this.props.iconURL !== nextProps.iconURL;
}
handleClick (e) {
this.props.onSelect(this.props.id);
e.preventDefault();
}
handleMouseEnter () {
this.props.onMouseEnter(this.props.id);
}
handleMouseLeave () {
this.props.onMouseLeave(this.props.id);
}
render () {
return (
<Box
className={classNames({
[styles.libraryItem]: true,
[styles.isSelected]: this.props.selected
})}
className={styles.libraryItem}
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<Box className={styles.libraryItemImageContainer}>
<img
className={styles.libraryItemImage}
src={this.props.iconURL}
/>
{/* Layers of wrapping is to prevent layout thrashing on animation */}
<Box className={styles.libraryItemImageContainerWrapper}>
<Box className={styles.libraryItemImageContainer}>
<img
className={styles.libraryItemImage}
src={this.props.iconURL}
/>
</Box>
</Box>
<span className={styles.libraryItemName}>{this.props.name}</span>
</Box>
@@ -40,8 +54,9 @@ LibraryItem.propTypes = {
iconURL: PropTypes.string.isRequired,
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
onSelect: PropTypes.func.isRequired,
selected: PropTypes.bool.isRequired
onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired
};
module.exports = LibraryItem;
+8 -18
View File
@@ -1,24 +1,14 @@
@import "../../css/units.css";
@import "../../css/colors.css";
.library-scroll-grid {
display: flex;
justify-content: center;
align-content: flex-start;
background: $ui-pane-gray;
flex-grow: 1;
justify-content: space-between;
flex-wrap: wrap;
overflow-y: scroll;
height: calc(100% - 6rem); /* @todo: currently estimate, fix precision */
/*
Gives sprites a bit of room so they don't get cut off when they grow on hover
@todo: sync as a var, with the transform defined on .library-item:hover
*/
padding: 0.15rem;
}
.modal-header {
width: 100%;
margin-bottom: 2rem;
overflow-y: auto;
height: calc(100% - $library-header-height);
padding: 0.5rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1.5rem;
font-weight: normal;
color: #8e8f95;
}
+40 -18
View File
@@ -10,42 +10,62 @@ const styles = require('./library.css');
class LibraryComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, ['handleSelect']);
this.state = {selectedItem: null};
bindAll(this, [
'handleFilterChange',
'handleFilterClear',
'handleMouseEnter',
'handleMouseLeave',
'handleSelect'
]);
this.state = {
selectedItem: null,
filterQuery: ''
};
}
handleSelect (id) {
if (this.state.selectedItem === id) {
// Double select: select as the library's value.
this.props.onRequestClose();
this.props.onItemSelected(this.props.data[id]);
} else {
if (this.props.onItemChosen) {
this.props.onItemChosen(this.props.data[id]);
}
}
this.setState({selectedItem: id});
this.props.onRequestClose();
this.props.onItemSelected(this.getFilteredData()[id]);
}
handleMouseEnter (id) {
if (this.props.onItemMouseEnter) this.props.onItemMouseEnter(this.getFilteredData()[id]);
}
handleMouseLeave (id) {
if (this.props.onItemMouseLeave) this.props.onItemMouseLeave(this.getFilteredData()[id]);
}
handleFilterChange (event) {
this.setState({filterQuery: event.target.value});
}
handleFilterClear () {
this.setState({filterQuery: ''});
}
getFilteredData () {
return this.props.data.filter(dataItem =>
dataItem.name.toLowerCase().indexOf(this.state.filterQuery.toLowerCase()) !== -1);
}
render () {
if (!this.props.visible) return null;
return (
<ModalComponent
contentLabel={this.props.title}
filterQuery={this.state.filterQuery}
visible={this.props.visible}
onFilterChange={this.handleFilterChange}
onFilterClear={this.handleFilterClear}
onRequestClose={this.props.onRequestClose}
>
<h1 className={styles.modalHeader}>{this.props.title}</h1>
<div className={styles.libraryScrollGrid}>
{this.props.data.map((dataItem, itemId) => {
{this.getFilteredData().map((dataItem, index) => {
const scratchURL = dataItem.md5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` :
dataItem.rawURL;
return (
<LibraryItem
iconURL={scratchURL}
id={itemId}
key={`item_${itemId}`}
id={index}
key={`item_${index}`}
name={dataItem.name}
selected={this.state.selectedItem === itemId}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
);
@@ -59,6 +79,7 @@ class LibraryComponent extends React.Component {
LibraryComponent.propTypes = {
data: PropTypes.arrayOf(
/* eslint-disable react/no-unused-prop-types, lines-around-comment */
// An item in the library
PropTypes.shape({
// @todo remove md5/rawURL prop from library, refactor to use storage
md5: PropTypes.string,
@@ -67,7 +88,8 @@ LibraryComponent.propTypes = {
})
/* eslint-enable react/no-unused-prop-types, lines-around-comment */
),
onItemChosen: PropTypes.func,
onItemMouseEnter: PropTypes.func,
onItemMouseLeave: PropTypes.func,
onItemSelected: PropTypes.func,
onRequestClose: PropTypes.func,
title: PropTypes.string.isRequired,
+57 -20
View File
@@ -11,32 +11,69 @@
background-color: rgba(0, 0, 0, .75);
}
.modal-content {
/* @todo: extract to type: full ? */
.full-modal-content {
position: absolute;
outline: none;
overflow-y: scroll;
overflow-y: auto;
-webkit-overflow-scrolling: 'touch';
border: 1px solid #ccc;
padding: 0;
top: 5%;
right: 5%;
bottom: 5%;
left: 5%;
border-radius: $space;
user-select: none;
height: 100%;
width: 100%;
display: flex;
}
/*
Modal header has 3 items:
|filter title x|
Use the same width for both side item containers,
so that title remains centered
*/
$sides: 20rem;
.header {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
height: $library-header-height;
box-sizing: border-box;
width: 100%;
background-color: $blue;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1rem;
font-weight: normal;
}
.header-item {
display: flex;
align-items: center;
padding: 1rem;
text-decoration: none;
color: white;
user-select: none;
}
.modal-children {
overflow: hidden;
height: 100%;
z-index: 0;
padding: 2rem;
background: $ui-pane-gray;
.header-item-filter {
display: flex;
flex-basis: $sides;
justify-content: flex-start;
}
.close-button {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 2;
.header-item-title {
flex-grow: 1;
flex-shrink: 0;
justify-content: center;
user-select: none;
letter-spacing: 0.4px;
cursor: default;
}
.header-item-close {
flex-basis: $sides;
justify-content: flex-end;
}
+36 -8
View File
@@ -1,9 +1,11 @@
const classNames = require('classnames');
const PropTypes = require('prop-types');
const React = require('react');
const ReactModal = require('react-modal');
const Box = require('../box/box.jsx');
const CloseButton = require('../close-button/close-button.jsx');
const Filter = require('../filter/filter.jsx');
const styles = require('./modal.css');
@@ -11,21 +13,44 @@ class ModalComponent extends React.Component {
render () {
return (
<ReactModal
className={styles.modalContent}
className={styles.fullModalContent}
contentLabel={this.props.contentLabel}
isOpen={this.props.visible}
overlayClassName={styles.modalOverlay}
ref={m => (this.modal = m)}
onRequestClose={this.props.onRequestClose}
>
<CloseButton
className={styles.closeButton}
onClick={this.props.onRequestClose}
/>
<Box
className={styles.modalChildren}
direction="column"
grow={1}
>
<div className={styles.header}>
<div className={classNames(styles.headerItem, styles.headerItemFilter)}>
<Filter
filterQuery={this.props.filterQuery}
onChange={this.props.onFilterChange}
onClear={this.props.onFilterClear}
/>
</div>
<div
className={classNames(
styles.headerItem,
styles.headerItemTitle
)}
>
{this.props.contentLabel}
</div>
<div
className={classNames(
styles.headerItem,
styles.headerItemClose
)}
>
<CloseButton
size={CloseButton.SIZE_LARGE}
onClick={this.props.onRequestClose}
/>
</div>
</div>
{this.props.children}
</Box>
</ReactModal>
@@ -36,7 +61,10 @@ class ModalComponent extends React.Component {
ModalComponent.propTypes = {
children: PropTypes.node,
contentLabel: PropTypes.string.isRequired,
onRequestClose: PropTypes.func.isRequired,
filterQuery: PropTypes.string,
onFilterChange: PropTypes.func,
onFilterClear: PropTypes.func,
onRequestClose: PropTypes.func,
visible: PropTypes.bool.isRequired
};
+1 -1
View File
@@ -7,7 +7,7 @@ const styles = require('./monitor.css');
const categories = {
data: '#FF8C1A',
sensing: '#5CB1D6',
sounds: '#CF63CF',
sound: '#CF63CF',
looks: '#9966FF',
motion: '#4C97FF'
};
+2 -2
View File
@@ -4,7 +4,7 @@ const React = require('react');
const VM = require('scratch-vm');
const backdropLibraryContent = require('../lib/libraries/backdrops.json');
const LibaryComponent = require('../components/library/library.jsx');
const LibraryComponent = require('../components/library/library.jsx');
class BackdropLibrary extends React.Component {
@@ -26,7 +26,7 @@ class BackdropLibrary extends React.Component {
}
render () {
return (
<LibaryComponent
<LibraryComponent
data={backdropLibraryContent}
title="Backdrop Library"
visible={this.props.visible}
+23 -2
View File
@@ -1,4 +1,5 @@
const bindAll = require('lodash.bindall');
const debounce = require('lodash.debounce');
const defaultsDeep = require('lodash.defaultsdeep');
const PropTypes = require('prop-types');
const React = require('react');
@@ -30,6 +31,7 @@ class Blocks extends React.Component {
'onScriptGlowOff',
'onBlockGlowOn',
'onBlockGlowOff',
'onTargetsUpdate',
'onVisualReport',
'onWorkspaceUpdate',
'onWorkspaceMetricsChange',
@@ -40,6 +42,7 @@ class Blocks extends React.Component {
workspaceMetrics: {},
prompt: null
};
this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
}
componentDidMount () {
const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options);
@@ -85,6 +88,7 @@ class Blocks extends React.Component {
this.props.vm.addListener('BLOCK_GLOW_OFF', this.onBlockGlowOff);
this.props.vm.addListener('VISUAL_REPORT', this.onVisualReport);
this.props.vm.addListener('workspaceUpdate', this.onWorkspaceUpdate);
this.props.vm.addListener('targetsUpdate', this.onTargetsUpdate);
}
detachVM () {
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
@@ -93,8 +97,25 @@ class Blocks extends React.Component {
this.props.vm.removeListener('BLOCK_GLOW_OFF', this.onBlockGlowOff);
this.props.vm.removeListener('VISUAL_REPORT', this.onVisualReport);
this.props.vm.removeListener('workspaceUpdate', this.onWorkspaceUpdate);
this.props.vm.removeListener('targetsUpdate', this.onTargetsUpdate);
}
updateToolboxBlockValue (id, value) {
this.workspace
.getFlyout()
.getWorkspace()
.getBlockById(id)
.inputList[0]
.fieldRow[0]
.setValue(value);
}
onTargetsUpdate () {
if (this.props.vm.editingTarget) {
['glide', 'move', 'set'].forEach(prefix => {
this.updateToolboxBlockValue(`${prefix}x`, this.props.vm.editingTarget.x.toFixed(0));
this.updateToolboxBlockValue(`${prefix}y`, this.props.vm.editingTarget.y.toFixed(0));
});
}
}
onWorkspaceMetricsChange () {
const target = this.props.vm.editingTarget;
if (target && target.id) {
@@ -127,7 +148,6 @@ class Blocks extends React.Component {
if (this.props.vm.editingTarget && !this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
this.onWorkspaceMetricsChange();
}
this.ScratchBlocks.Events.disable();
this.workspace.clear();
@@ -152,6 +172,7 @@ class Blocks extends React.Component {
}
handlePromptCallback (data) {
this.state.prompt.callback(data);
this.props.vm.createVariable(data);
this.handlePromptClose();
}
handlePromptClose () {
+3 -3
View File
@@ -4,10 +4,10 @@ const React = require('react');
const VM = require('scratch-vm');
const costumeLibraryContent = require('../lib/libraries/costumes.json');
const LibaryComponent = require('../components/library/library.jsx');
const LibraryComponent = require('../components/library/library.jsx');
class CostumeLibrary extends React.Component {
class CostumeLibrary extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
@@ -26,7 +26,7 @@ class CostumeLibrary extends React.Component {
}
render () {
return (
<LibaryComponent
<LibraryComponent
data={costumeLibraryContent}
title="Costume Library"
visible={this.props.visible}
+15 -6
View File
@@ -3,25 +3,30 @@ const PropTypes = require('prop-types');
const React = require('react');
const VM = require('scratch-vm');
const AudioEngine = require('scratch-audio');
const LibaryComponent = require('../components/library/library.jsx');
const LibraryComponent = require('../components/library/library.jsx');
const soundIcon = require('../components/asset-panel/icon--sound.svg');
const soundLibraryContent = require('../lib/libraries/sounds.json');
class SoundLibrary extends React.Component {
class SoundLibrary extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
'handleItemSelected',
'handleItemChosen'
'handleItemMouseEnter',
'handleItemMouseLeave'
]);
}
componentDidMount () {
this.audioEngine = new AudioEngine();
this.player = this.audioEngine.createPlayer();
}
handleItemChosen (soundItem) {
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();
}
handleItemMouseEnter (soundItem) {
const md5ext = soundItem._md5;
const idParts = md5ext.split('.');
const md5 = idParts[0];
@@ -39,6 +44,9 @@ class SoundLibrary extends React.Component {
this.player.playSound(soundItem._md5);
});
}
handleItemMouseLeave () {
this.player.stopAllSounds();
}
handleItemSelected (soundItem) {
const vmSound = {
format: soundItem.format,
@@ -64,11 +72,12 @@ class SoundLibrary extends React.Component {
});
return (
<LibaryComponent
<LibraryComponent
data={soundLibraryThumbnailData}
title="Sound Library"
visible={this.props.visible}
onItemChosen={this.handleItemChosen}
onItemMouseEnter={this.handleItemMouseEnter}
onItemMouseLeave={this.handleItemMouseLeave}
onItemSelected={this.handleItemSelected}
onRequestClose={this.props.onRequestClose}
/>
+51 -5
View File
@@ -5,24 +5,70 @@ const VM = require('scratch-vm');
const spriteLibraryContent = require('../lib/libraries/sprites.json');
const LibaryComponent = require('../components/library/library.jsx');
const LibraryComponent = require('../components/library/library.jsx');
class SpriteLibrary extends React.Component {
class SpriteLibrary extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
'handleItemSelect'
'handleItemSelect',
'handleMouseEnter',
'handleMouseLeave',
'rotateCostume',
'startRotatingCostumes',
'stopRotatingCostumes'
]);
this.state = {
activeSprite: null,
costumeIndex: 0,
sprites: spriteLibraryContent
};
}
componentWillReceiveProps (newProps) {
if (!newProps.visible) clearInterval(this.intervalId);
}
handleItemSelect (item) {
this.props.vm.addSprite2(JSON.stringify(item.json));
}
handleMouseEnter (item) {
this.stopRotatingCostumes();
this.setState({activeSprite: item}, this.startRotatingCostumes);
}
handleMouseLeave () {
this.stopRotatingCostumes();
}
startRotatingCostumes () {
if (!this.state.activeSprite) return;
this.rotateCostume();
this.intervalId = setInterval(this.rotateCostume, 300);
}
stopRotatingCostumes () {
this.intervalId = clearInterval(this.intervalId);
}
rotateCostume () {
const costumes = this.state.activeSprite.json.costumes;
const nextCostumeIndex = (this.state.costumeIndex + 1) % costumes.length;
this.setState({
costumeIndex: nextCostumeIndex,
sprites: this.state.sprites.map(sprite => {
if (sprite.name === this.state.activeSprite.name) {
return {
...sprite,
md5: sprite.json.costumes[nextCostumeIndex].baseLayerMD5
};
}
return sprite;
})
});
}
render () {
return (
<LibaryComponent
data={spriteLibraryContent}
<LibraryComponent
data={this.state.sprites}
title="Sprite Library"
visible={this.props.visible}
onItemMouseEnter={this.handleMouseEnter}
onItemMouseLeave={this.handleMouseLeave}
onItemSelected={this.handleItemSelect}
onRequestClose={this.props.onRequestClose}
/>
+3 -1
View File
@@ -3,5 +3,7 @@ $space: 0.5rem;
$sprites-per-row: 5;
$menu-bar-height: 3rem;
$sprite-info-height: 5.25rem; /* TODO: SpriteInfo isn't explicitly set to this height yet */
$sprite-info-height: 5.25rem; /* @todo: SpriteInfo isn't explicitly set to this height yet */
$stage-menu-height: 3rem;
$library-header-height: 4.375rem;
+26
View File
@@ -0,0 +1,26 @@
/**
* Convert monitors from VM format to what the GUI needs to render.
* - Convert opcode to a label and a category
* - Add missing XY position data if needed
*/
const OpcodeLabels = require('../lib/opcode-labels.js');
const PADDING = 5;
const MONITOR_HEIGHT = 23;
const isUndefined = a => typeof a === 'undefined';
module.exports = function ({id, opcode, params, value, x, y}, monitorIndex) {
let {label, category, labelFn} = OpcodeLabels(opcode);
// Use labelFn if provided for dynamic labelling (e.g. variables)
if (!isUndefined(labelFn)) label = labelFn(params);
// Simple layout if x or y are undefined
// @todo scratch2 has a more complex layout behavior we may want to adopt
// @todo e.g. this does not work well when monitors have already been moved
if (isUndefined(x)) x = PADDING;
if (isUndefined(y)) y = PADDING + (monitorIndex * (PADDING + MONITOR_HEIGHT));
return {id, label, category, value, x, y};
};
+75
View File
@@ -0,0 +1,75 @@
const opcodeMap = {
// Motion
motion_direction: {
category: 'motion',
label: 'direction'
},
motion_xposition: {
category: 'motion',
label: 'x position'
},
motion_yposition: {
category: 'motion',
label: 'y position'
},
// Looks
looks_size: {
category: 'looks',
label: 'size'
},
looks_costumeorder: {
category: 'looks',
label: 'costume #'
},
looks_backdroporder: {
category: 'looks',
label: 'backdrop #'
},
looks_backdropname: {
category: 'looks',
label: 'backdrop name'
},
// Data
data_variable: {
category: 'data',
labelFn: params => params.VARIABLE
},
// Sound
sound_volume: {
category: 'sound',
label: 'volume'
},
sound_tempo: {
category: 'sound',
label: 'tempo'
},
// Sensing
sensing_loudness: {
category: 'sensing',
label: 'loudness'
},
sensing_of: {
category: 'sensing',
labelFn: params => `${params.PROPERTY} of ${params.OBJECT}`
},
sensing_current: {
category: 'sensing',
labelFn: params => params.CURRENTMENU.toLowerCase()
},
sensing_timer: {
category: 'sensing',
label: 'timer'
}
};
module.exports = function (opcode) {
if (opcode in opcodeMap) return opcodeMap[opcode];
return {
category: 'data',
label: opcode
};
};
+3 -1
View File
@@ -1,3 +1,5 @@
const monitorAdapter = require('../lib/monitor-adapter.js');
const UPDATE_MONITORS = 'scratch-gui/monitors/UPDATE_MONITORS';
const initialState = [];
@@ -6,7 +8,7 @@ const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case UPDATE_MONITORS:
return [...action.monitors];
return action.monitors.map(monitorAdapter);
default:
return state;
}