Merge pull request #2 from LLK/develop

update fork
This commit is contained in:
Jacob Bartles authored and GitHub committed 2018-02-27 13:28:56 -05:00
commit bc06dc2a78
57 files changed
+1825 -231

No files matched your search

+6 -6
View File
@@ -48,7 +48,7 @@
"eslint-config-scratch": "^5.0.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-react": "^7.5.1",
"file-loader": "1.1.6",
"file-loader": "1.1.9",
"get-float-time-domain-data": "0.1.0",
"get-user-media-promise": "1.1.1",
"gh-pages": "github:rschamp/gh-pages#publish-branch-to-subfolder",
@@ -77,8 +77,8 @@
"react-ga": "2.4.1",
"react-intl": "2.4.0",
"react-intl-redux": "0.7.0",
"react-modal": "3.1.13",
"react-redux": "5.0.6",
"react-modal": "3.3.1",
"react-redux": "5.0.7",
"react-responsive": "4.0.3",
"react-style-proptype": "3.2.0",
"react-tabs": "2.2.1",
@@ -89,12 +89,12 @@
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.1516198804",
"scratch-blocks": "0.1.0-prerelease.1518210548",
"scratch-blocks": "0.1.0-prerelease.1519256473",
"scratch-l10n": "2.0.20180108132626",
"scratch-paint": "0.1.0-prerelease.20180209163938",
"scratch-paint": "0.2.0-prerelease.20180227161624",
"scratch-render": "0.1.0-prerelease.1516837442",
"scratch-storage": "0.4.0",
"scratch-vm": "0.1.0-prerelease.1518098842-prerelease.1518098861",
"scratch-vm": "0.1.0-prerelease.1519681201-prerelease.1519681262",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.20.0",
+174
View File
@@ -0,0 +1,174 @@
@import "../../css/colors.css";
$main-button-size: 2.75rem;
$more-button-size: 2.25rem;
.menu-container {
display: flex;
flex-direction: column-reverse;
transition: 0.2s;
position: relative;
}
.button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: $motion-primary;
outline: none;
border: none;
transition: background-color 0.2s;
}
.button:hover {
background: $pen-primary;
}
.button.coming-soon:hover {
background: $data-primary;
}
.main-button {
border-radius: 100%;
width: $main-button-size;
height: $main-button-size;
z-index: 20; /* TODO reorder layout to prevent z-index need */
}
.main-icon {
width: calc($main-button-size - 1rem);
height: calc($main-button-size - 1rem);
}
.more-buttons-outer {
/*
Need to use two divs to set different overflow x/y
which is needed to get animation to look right while
allowing the tooltips to be visible.
*/
overflow-y: hidden;
background: $motion-tertiary;
border-top-left-radius: $more-button-size;
border-top-right-radius: $more-button-size;
width: $more-button-size;
margin-left: calc(($main-button-size - $more-button-size) / 2);
position: absolute;
bottom: calc($main-button-size);
margin-bottom: calc($main-button-size / -2);
padding-bottom: calc($main-button-size / 2);
}
.more-buttons {
max-height: 0;
transition: max-height 1s;
overflow-x: visible;
display: flex;
flex-direction: column;
z-index: 10; /* @todo justify */
}
.expanded .more-buttons {
max-height: 1000px; /* Arbitrary, needs to be a value in order for animation to run */
}
.force-hidden .more-buttons {
display: none; /* This property does not animate */
}
.more-buttons:first-child { /* Round off top button */
border-top-right-radius: $more-button-size;
border-top-left-radius: $more-button-size;
}
.more-button {
width: $more-button-size;
height: $more-button-size;
background: $motion-tertiary;
}
.more-icon {
width: calc($more-button-size - 1rem);
height: calc($more-button-size - 1rem);
}
.coming-soon .more-icon {
opacity: 0.5;
}
/*
@todo needs to be refactored with coming soon tooltip overrides.
The "!important"s are for the same reason as with coming soon, the library
is not very easy to style.
*/
.tooltip {
background-color: $pen-primary !important;
opacity: 1 !important;
border: 1px solid hsla(0, 0%, 0%, .1) !important;
box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
}
.tooltip:after {
background-color: $pen-primary;
}
.coming-soon-tooltip {
background-color: $data-primary !important;
}
.coming-soon-tooltip:after {
background-color: $data-primary !important;
}
.tooltip {
border: 1px solid hsla(0, 0%, 0%, .1) !important;
border-radius: .25rem !important;
box-shadow: 0 0 .5rem hsla(0, 0%, 0%, .25) !important;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif !important;
z-index: 100 !important;
}
$arrow-size: 0.5rem;
$arrow-inset: -0.25rem;
$arrow-rounding: 0.125rem;
.tooltip:after {
content: "";
border-top: 1px solid hsla(0, 0%, 0%, .1) !important;
border-left: 0 !important;
border-bottom: 0 !important;
border-right: 1px solid hsla(0, 0%, 0%, .1) !important;
border-radius: $arrow-rounding;
height: $arrow-size !important;
width: $arrow-size !important;
}
.tooltip:global(.place-left):after {
margin-top: $arrow-inset !important;
right: $arrow-inset !important;
transform: rotate(45deg) !important;
}
.tooltip:global(.place-right):after {
margin-top: $arrow-inset !important;
left: $arrow-inset !important;
transform: rotate(-135deg) !important;
}
.tooltip:global(.place-top):after {
margin-right: $arrow-inset !important;
bottom: $arrow-inset !important;
transform: rotate(135deg) !important;
}
.tooltip:global(.place-bottom):after {
margin-left: $arrow-inset !important;
top: $arrow-inset !important;
transform: rotate(-45deg) !important;
}
+189
View File
@@ -0,0 +1,189 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import bindAll from 'lodash.bindall';
import ReactTooltip from 'react-tooltip';
import styles from './action-menu.css';
const CLOSE_DELAY = 300; // ms
class ActionMenu extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'clickDelayer',
'handleClosePopover',
'handleToggleOpenState',
'handleTouchStart',
'handleTouchOutside',
'setButtonRef',
'setContainerRef'
]);
this.state = {
isOpen: false,
forceHide: false
};
}
componentDidMount () {
// Touch start on the main button is caught to trigger open and not click
this.buttonRef.addEventListener('touchstart', this.handleTouchStart);
// Touch start on document is used to trigger close if it is outside
document.addEventListener('touchstart', this.handleTouchOutside);
}
shouldComponentUpdate (newProps, newState) {
// This check prevents re-rendering while the project is updating.
// @todo check only the state and the title because it is enough to know
// if anything substantial has changed
// This is needed because of the sloppy way the props are passed as a new object,
// which should be refactored.
return newState.isOpen !== this.state.isOpen ||
newState.forceHide !== this.state.forceHide ||
newProps.title !== this.props.title;
}
componentWillUnmount () {
this.buttonRef.removeEventListener('touchstart', this.handleTouchStart);
document.removeEventListener('touchstart', this.handleTouchOutside);
}
handleClosePopover () {
this.closeTimeoutId = setTimeout(() => {
this.setState({isOpen: false});
this.closeTimeoutId = null;
}, CLOSE_DELAY);
}
handleToggleOpenState () {
// Mouse enter back in after timeout was started prevents it from closing.
if (this.closeTimeoutId) {
clearTimeout(this.closeTimeoutId);
this.closeTimeoutId = null;
} else if (!this.state.isOpen) {
this.setState({
isOpen: true,
forceHide: false
});
}
}
handleTouchOutside (e) {
if (this.state.isOpen && !this.containerRef.contains(e.target)) {
this.setState({isOpen: false});
}
}
clickDelayer (fn) {
// Return a wrapped action that manages the menu closing.
// @todo we may be able to use react-transition for this in the future
// for now all this work is to ensure the menu closes BEFORE the
// (possibly slow) action is started.
return event => {
this.setState({forceHide: true, isOpen: false}, () => {
if (fn) fn(event);
setTimeout(() => this.setState({forceHide: false}));
});
};
}
handleTouchStart (e) {
// Prevent this touch from becoming a click if menu is closed
if (!this.state.isOpen) {
e.preventDefault();
this.handleToggleOpenState();
}
}
setButtonRef (ref) {
this.buttonRef = ref;
}
setContainerRef (ref) {
this.containerRef = ref;
}
render () {
const {
className,
img: mainImg,
title: mainTitle,
moreButtons,
onClick
} = this.props;
const mainTooltipId = `tooltip-${Math.random()}`;
return (
<div
className={classNames(styles.menuContainer, className, {
[styles.expanded]: this.state.isOpen,
[styles.forceHidden]: this.state.forceHide
})}
ref={this.setContainerRef}
onMouseEnter={this.handleToggleOpenState}
onMouseLeave={this.handleClosePopover}
>
<button
aria-label={mainTitle}
className={classNames(styles.button, styles.mainButton)}
data-for={mainTooltipId}
data-tip={mainTitle}
ref={this.setButtonRef}
onClick={this.clickDelayer(onClick)}
>
<img
className={styles.mainIcon}
draggable={false}
src={mainImg}
/>
</button>
<ReactTooltip
className={styles.tooltip}
effect="solid"
id={mainTooltipId}
place="left"
/>
<div className={styles.moreButtonsOuter}>
<div className={styles.moreButtons}>
{(moreButtons || []).map(({img, title, onClick: handleClick}) => {
const isComingSoon = !handleClick;
const tooltipId = `tooltip-${Math.random()}`;
return (
<div key={tooltipId}>
<button
aria-label={title}
className={classNames(styles.button, styles.moreButton, {
[styles.comingSoon]: isComingSoon
})}
data-for={tooltipId}
data-tip={title}
onClick={this.clickDelayer(handleClick)}
>
<img
className={styles.moreIcon}
draggable={false}
src={img}
/>
</button>
<ReactTooltip
className={classNames(styles.tooltip, {
[styles.comingSoonTooltip]: isComingSoon
})}
effect="solid"
id={tooltipId}
place="left"
/>
</div>
);
})}
</div>
</div>
</div>
);
}
}
ActionMenu.propTypes = {
className: PropTypes.string,
img: PropTypes.string,
moreButtons: PropTypes.arrayOf(PropTypes.shape({
img: PropTypes.string,
title: PropTypes.node.isRequired,
onClick: PropTypes.func // Optional, "coming soon" if no callback provided
})),
onClick: PropTypes.func.isRequired,
title: PropTypes.node.isRequired
};
export default ActionMenu;
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>camera</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="camera" fill="#FFFFFF">
<path d="M10,14.5 C8.3485,14.5 7,13.1515 7,11.5 C7,9.8365 8.3485,8.5 10,8.5 C11.6635,8.5 13,9.8365 13,11.5 C13,13.1515 11.6635,14.5 10,14.5 M16,6.25 L14.95,6.25 C14.6635,6.25 14.425,6.1 14.2885,5.86 L13.2835,4.0315 C13.015,3.55 12.52,3.25 11.965,3.25 L8.05,3.25 C7.495,3.25 7,3.55 6.7285,4.0315 L5.71,5.86 C5.575,6.1 5.335,6.25 5.065,6.25 L4,6.25 C3.175,6.25 2.5,6.925 2.5,7.75 L2.5,15.25 C2.5,16.0765 3.175,16.75 4,16.75 L16,16.75 C16.825,16.75 17.5,16.0765 17.5,15.25 L17.5,7.75 C17.5,6.925 16.825,6.25 16,6.25" id="camera-icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>file-upload</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="file-upload" fill="#FFFFFF">
<path d="M5.91737098,6.88612463 L9.63888997,3.16460565 C9.85700097,2.94513145 10.1977994,2.94513145 10.4159104,3.16460565 L14.1374294,6.88612463 C14.4918598,7.24055501 14.232853,7.82536514 13.7421032,7.82536514 L11.8881597,7.82536514 L10.7839727,12.2012171 C10.6749172,12.6251704 10.2386952,12.882814 9.81610516,12.7737585 C9.51620253,12.706962 9.29809153,12.4752191 9.22993184,12.2012171 L8.12574489,7.82536514 L6.31269718,7.82536514 C5.82194742,7.82536514 5.5629406,7.24055501 5.91737098,6.88612463 Z M17,11.601753 L17,13.8510227 C17,15.5822788 15.5959104,17.0000003 13.8646543,17.0000003 L6.1489776,17.0000003 C4.40408958,17.0000003 3,15.5822788 3,13.8510227 L3,11.601753 C3,11.0292116 3.46348588,10.5793576 4.02239533,10.5793576 C4.58130477,10.5793576 5.04479065,11.0292116 5.04479065,11.601753 L5.04479065,13.8510227 C5.04479065,14.4508279 5.53554041,14.9552096 6.1489776,14.9552096 L13.8646543,14.9552096 C14.4644596,14.9552096 14.9552093,14.4508279 14.9552093,13.8510227 L14.9552093,11.601753 C14.9552093,11.0292116 15.4186952,10.5793576 15.9776047,10.5793576 C16.5501461,10.5793576 17,11.0292116 17,11.601753 Z" id="file-upload-icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>paint</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="paint" fill="#FFFFFF">
<path d="M16.1998226,6.58685277 C15.5417294,7.94213763 14.5662678,9.50539101 13.6457617,10.6638734 C12.8750097,11.639511 12.2567593,12.2117734 11.6659868,12.4909258 C11.5972923,12.5341944 11.5299717,12.5481521 11.4461644,12.5481521 C11.3912089,12.5481521 11.3362533,12.5341944 11.2675588,12.5048834 C11.1439087,12.4644063 11.0339976,12.366703 10.979042,12.2410844 C10.8141752,11.8628329 10.580614,11.5432035 10.2659932,11.2905705 C9.94862474,11.053291 9.59141344,10.871842 9.18062043,10.7741386 C9.05559648,10.7462234 8.93194641,10.6638734 8.86325193,10.5368591 C8.79455745,10.4251981 8.76845354,10.2856219 8.79455745,10.1474415 C8.9594242,9.50539101 9.38532999,8.75167956 10.1272304,7.81651905 C11.5849273,5.94480227 14.4975733,3.16863175 15.9113058,3.01370218 C16.2959949,2.9578717 16.5158172,3.08349028 16.6532062,3.19515123 C17.0117914,3.50221886 17.3690027,4.17358036 16.1998226,6.58685277 Z M10.2654437,13.9990466 C10.3478771,14.6969276 10.1692714,15.3808509 9.74199174,15.9251981 C9.37241543,16.412319 8.85033737,16.7486977 8.25956482,16.8882738 C8.23208703,16.9022315 8.19087034,16.9161891 8.16339255,16.9161891 L8.0246297,16.9301467 C7.76496455,16.9720196 7.51766442,16.9999348 7.2689904,16.9999348 C5.29196321,16.9999348 3.90296079,15.6586076 3.35477883,14.7806733 C3.14732149,14.4317328 2.83270076,13.7896823 3.10747869,13.3849114 C3.17617317,13.287208 3.36714383,13.0778437 3.77931072,13.1615894 C5.08450588,13.4407418 5.55162835,12.8545218 5.63543562,12.7428609 C6.51472499,11.5843784 8.14965365,11.3750141 9.27486926,12.2403866 C9.82579901,12.6730728 10.1816364,13.3011656 10.2654437,13.9990466 Z" id="Fill-4"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>surprise</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="surprise" fill="#FFFFFF">
<path d="M6.99375039,8.65327167 C7.80357478,8.43603525 8.43603525,7.80357478 8.65327167,6.99375039 L9.14824073,5.15274045 C9.38335104,4.28241985 10.6180239,4.28241985 10.8517593,5.15274045 L11.3481032,6.99375039 C11.5653397,7.80357478 12.1978001,8.43603525 13.0062496,8.65327167 L14.8472595,9.14824073 C15.7175802,9.38335104 15.7175802,10.6180239 14.8472595,10.8531342 L13.0062496,11.3481032 C12.1978001,11.5653397 11.5653397,12.1978001 11.3481032,13.0062496 L10.8517593,14.8472595 C10.6180239,15.7175802 9.38335104,15.7175802 9.14824073,14.8472595 L8.65327167,13.0062496 C8.43603525,12.1978001 7.80357478,11.5653397 6.99375039,11.3481032 L5.15274045,10.8531342 C4.28241985,10.6180239 4.28241985,9.38335104 5.15274045,9.14824073 L6.99375039,8.65327167 Z M3.9061823,14.5100817 C4.20079987,14.430095 4.4314281,14.2007999 4.51008165,13.9061823 L4.69005166,13.2369605 C4.77403766,12.9210132 5.22329612,12.9210132 5.30994834,13.2369605 L5.48858524,13.9061823 C5.5685719,14.2007999 5.79786702,14.430095 6.09248459,14.5100817 L6.76303949,14.6900517 C7.07898684,14.7740377 7.07898684,15.2232961 6.76303949,15.3099483 L6.09248459,15.4885852 C5.79786702,15.5685719 5.5685719,15.797867 5.48858524,16.0924846 L5.30994834,16.7617064 C5.22329612,17.0789868 4.77403766,17.0789868 4.69005166,16.7617064 L4.51008165,16.0924846 C4.4314281,15.797867 4.20079987,15.5685719 3.9061823,15.4885852 L3.23696051,15.3099483 C2.92101316,15.2232961 2.92101316,14.7740377 3.23696051,14.6900517 L3.9061823,14.5100817 Z M13.9064844,4.50925154 C14.2012002,4.42923821 14.4305718,4.2012002 14.5092515,3.90648441 L14.6906151,3.23703951 C14.7746291,2.92098683 15.2240373,2.92098683 15.3093849,3.23703951 L15.4894149,3.90648441 C15.5680947,4.2012002 15.7987998,4.42923821 16.0935156,4.50925154 L16.7629605,4.6906151 C17.0790132,4.7746291 17.0790132,5.22403734 16.7629605,5.3093849 L16.0935156,5.4894149 C15.7987998,5.56809468 15.5680947,5.7987998 15.4894149,6.09218203 L15.3093849,6.76162694 C15.2240373,7.07901317 14.7746291,7.07901317 14.6906151,6.76162694 L14.5092515,6.09218203 C14.4305718,5.7987998 14.2012002,5.56809468 13.9064844,5.4894149 L13.2370395,5.3093849 C12.9209868,5.22403734 12.9209868,4.7746291 13.2370395,4.6906151 L13.9064844,4.50925154 Z M5.75,5.75 C5.75,6.164 5.414,6.5 5,6.5 C4.586,6.5 4.25,6.164 4.25,5.75 C4.25,5.336 4.586,5 5,5 C5.414,5 5.75,5.336 5.75,5.75 Z M16,14.25 C16,14.9416667 15.44,15.5 14.75,15.5 C14.0583333,15.5 13.5,14.9416667 13.5,14.25 C13.5,13.56 14.0583333,13 14.75,13 C15.44,13 16,13.56 16,14.25 Z" id="surprise-icon"></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

@@ -1,21 +0,0 @@
@import "../../css/colors.css";
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background: $motion-primary;
border-radius: 100%;
width: 2.75rem;
height: 2.75rem;
outline: none;
border: none;
}
.icon {
width: 1.75rem;
height: 1.75rem;
}
@@ -1,32 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import styles from './asset-button.css';
const AssetButton = ({
img,
className,
title,
onClick
}) => (
<button
className={classNames(styles.container, className)}
title={title}
onClick={onClick}
>
<img
className={styles.icon}
draggable={false}
src={img}
/>
</button>
);
AssetButton.propTypes = {
className: PropTypes.string,
img: PropTypes.string,
onClick: PropTypes.func.isRequired,
title: PropTypes.node.isRequired
};
export default AssetButton;
+19 -1
View File
@@ -11,13 +11,31 @@
}
.new-buttons {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
margin: 0.75rem 0;
padding: 0.75rem 0;
color: $motion-primary;
text-align: center;
background: none;
}
$fade-out-distance: 100px;
.new-buttons:before {
content: "";
position: absolute;
bottom: 0;
left: 0;
background: linear-gradient(rgba(232,237,241, 0),rgba(232,237,241, 1));
height: $fade-out-distance;
width: 100%;
pointer-events: none;
}
.new-buttons > button + button {
+22 -11
View File
@@ -4,7 +4,7 @@ import React from 'react';
import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx';
import Box from '../box/box.jsx';
import AssetButton from '../asset-button/asset-button.jsx';
import ActionMenu from '../action-menu/action-menu.jsx';
import styles from './selector.css';
const Selector = props => {
@@ -13,9 +13,27 @@ const Selector = props => {
items,
selectedItemIndex,
onDeleteClick,
onDuplicateClick,
onItemClick
} = props;
let newButtonSection = null;
if (buttons.length > 0) {
const {img, title, onClick} = buttons[0];
const moreButtons = buttons.slice(1);
newButtonSection = (
<Box className={styles.newButtons}>
<ActionMenu
img={img}
moreButtons={moreButtons}
title={title}
onClick={onClick}
/>
</Box>
);
}
return (
<Box className={styles.wrapper}>
<Box className={styles.listArea}>
@@ -30,19 +48,11 @@ const Selector = props => {
selected={index === selectedItemIndex}
onClick={onItemClick}
onDeleteButtonClick={onDeleteClick}
onDuplicateButtonClick={onDuplicateClick}
/>
))}
</Box>
<Box className={styles.newButtons}>
{buttons.map(({message, img, onClick}, index) => (
<AssetButton
img={img}
key={index}
title={message}
onClick={onClick}
/>
))}
</Box>
{newButtonSection}
</Box>
);
};
@@ -58,6 +68,7 @@ Selector.propTypes = {
name: PropTypes.string.isRequired
})),
onDeleteClick: PropTypes.func,
onDuplicateClick: PropTypes.func,
onItemClick: PropTypes.func.isRequired,
selectedItemIndex: PropTypes.number.isRequired
};
@@ -49,3 +49,18 @@
width: 0.75rem;
height: 0.75rem;
}
.back-icon {
position: relative;
margin: 0.25rem;
user-select: none;
}
.small .back-icon {
width: 50%;
}
.large .back-icon {
width: 2rem;
height: 2rem;
}
+14 -5
View File
@@ -4,6 +4,7 @@ import classNames from 'classnames';
import styles from './close-button.css';
import closeIcon from './icon--close.svg';
import backIcon from './icon--back.svg';
const CloseButton = props => (
<div
@@ -20,10 +21,16 @@ const CloseButton = props => (
tabIndex="0"
onClick={props.onClick}
>
<img
className={styles.closeIcon}
src={closeIcon}
/>
{props.buttonType === 'back' ?
<img
className={styles.backIcon}
src={backIcon}
/> :
<img
className={styles.closeIcon}
src={closeIcon}
/>
}
</div>
);
@@ -31,13 +38,15 @@ CloseButton.SIZE_SMALL = 'small';
CloseButton.SIZE_LARGE = 'large';
CloseButton.propTypes = {
buttonType: PropTypes.oneOf(['back', 'close']),
className: PropTypes.string,
onClick: PropTypes.func.isRequired,
size: PropTypes.oneOf([CloseButton.SIZE_SMALL, CloseButton.SIZE_LARGE])
};
CloseButton.defaultProps = {
size: CloseButton.SIZE_LARGE
size: CloseButton.SIZE_LARGE,
buttonType: 'close'
};
export default CloseButton;
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="20px" height="20px" viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<title>back-icon</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="back-icon" fill="#FFFFFF">
<path d="M4.10804912,8.57765055 L9.33203638,3.35366329 C9.63820413,3.04558199 10.1165912,3.04558199 10.422759,3.35366329 L15.6467462,8.57765055 C16.1442688,9.07517314 15.7806946,9.89608543 15.0918172,9.89608543 L12.4893913,9.89608543 L10.9394171,16.0385759 C10.7863332,16.6336895 10.1739977,16.9953502 9.58079768,16.8422663 C9.15981702,16.7485024 8.85364927,16.4231992 8.75797184,16.0385759 L7.2079976,9.89608543 L4.66297817,9.89608543 C3.97410073,9.89608543 3.61052653,9.07517314 4.10804912,8.57765055" id="Fill-1" transform="translate(9.877398, 10.000000) rotate(-90.000000) translate(-9.877398, -10.000000) "></path>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -5,6 +5,7 @@
padding: 0.375rem;
border-radius: 0.25rem;
user-select: none;
user-drag: none;
cursor: pointer;
transition: 0.2s ease-out;
}
+15 -1
View File
@@ -140,7 +140,8 @@
For making the sprite-selector a scrollable pane
@todo: Not working in Safari
*/
overflow: hidden;
/* TODO this also breaks the thermometer menu */
/* overflow: hidden; */
}
.extension-button-container {
@@ -156,6 +157,19 @@
box-sizing: content-box; /* To match scratch-block vertical toolbox borders */
}
$fade-out-distance: 15px;
.extension-button-container:before {
content: "";
position: absolute;
top: calc(calc(-1 * $fade-out-distance) - 1px);
left: -1px;
background: linear-gradient(rgba(0, 0, 0, 0),rgba(0, 0, 0, 0.15));
height: $fade-out-distance;
width: calc(100% + 0.5px);
}
.extension-button {
background: none;
border: none;
+25 -15
View File
@@ -19,6 +19,7 @@ import Box from '../box/box.jsx';
import FeedbackForm from '../feedback-form/feedback-form.jsx';
import MenuBar from '../menu-bar/menu-bar.jsx';
import PreviewModal from '../../containers/preview-modal.jsx';
import ImportModal from '../../containers/import-modal.jsx';
import WebGlModal from '../../containers/webgl-modal.jsx';
import layout from '../../lib/layout-constants.js';
@@ -35,16 +36,19 @@ const messages = defineMessages({
const GUIComponent = props => {
const {
activeTabIndex,
basePath,
blocksTabVisible,
children,
enableExtensions,
intl,
costumesTabVisible,
feedbackFormVisible,
vm,
previewInfoVisible,
importInfoVisible,
intl,
onExtensionButtonClick,
onTabSelect,
tabIndex,
onActivateTab,
previewInfoVisible,
soundsTabVisible,
vm,
...componentProps
} = props;
if (children) {
@@ -74,6 +78,9 @@ const GUIComponent = props => {
{previewInfoVisible ? (
<PreviewModal />
) : null}
{importInfoVisible ? (
<ImportModal />
) : null}
{feedbackFormVisible ? (
<FeedbackForm />
) : null}
@@ -87,9 +94,10 @@ const GUIComponent = props => {
<Tabs
className={tabClassNames.tabs}
forceRenderTabPanel={true} // eslint-disable-line react/jsx-boolean-value
selectedIndex={activeTabIndex}
selectedTabClassName={tabClassNames.tabSelected}
selectedTabPanelClassName={tabClassNames.tabPanelSelected}
onSelect={onTabSelect}
onSelect={onActivateTab}
>
<TabList className={tabClassNames.tabList}>
<Tab className={tabClassNames.tab}>Blocks</Tab>
@@ -100,7 +108,7 @@ const GUIComponent = props => {
<Box className={styles.blocksWrapper}>
<Blocks
grow={1}
isVisible={tabIndex === 0} // Blocks tab
isVisible={blocksTabVisible}
options={{
media: `${basePath}static/blocks-media/`
}}
@@ -109,9 +117,7 @@ const GUIComponent = props => {
</Box>
<Box className={styles.extensionButtonContainer}>
<button
className={classNames(styles.extensionButton, {
[styles.hidden]: !enableExtensions
})}
className={styles.extensionButton}
title={intl.formatMessage(messages.addExtension)}
onClick={onExtensionButtonClick}
>
@@ -124,10 +130,10 @@ const GUIComponent = props => {
</Box>
</TabPanel>
<TabPanel className={tabClassNames.tabPanel}>
{tabIndex === 1 ? <CostumeTab vm={vm} /> : null}
{costumesTabVisible ? <CostumeTab vm={vm} /> : null}
</TabPanel>
<TabPanel className={tabClassNames.tabPanel}>
{tabIndex === 2 ? <SoundTab vm={vm} /> : null}
{soundsTabVisible ? <SoundTab vm={vm} /> : null}
</TabPanel>
</Tabs>
</Box>
@@ -162,15 +168,19 @@ const GUIComponent = props => {
);
};
GUIComponent.propTypes = {
activeTabIndex: PropTypes.number,
basePath: PropTypes.string,
blocksTabVisible: PropTypes.bool,
children: PropTypes.node,
enableExtensions: PropTypes.bool,
costumesTabVisible: PropTypes.bool,
feedbackFormVisible: PropTypes.bool,
importInfoVisible: PropTypes.bool,
intl: intlShape.isRequired,
onActivateTab: PropTypes.func,
onExtensionButtonClick: PropTypes.func,
onTabSelect: PropTypes.func,
previewInfoVisible: PropTypes.bool,
tabIndex: PropTypes.number,
soundsTabVisible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
GUIComponent.defaultProps = {
@@ -0,0 +1,185 @@
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/typography.css";
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
background-color: hsla(215, 100%, 65%, .9);
}
.modal-content {
margin: 100px auto;
outline: none;
border: .25rem solid hsla(0, 100%, 100%, .25);
padding: 0;
border-radius: $space;
user-select: none;
width: 500px;
color: $text-primary;
overflow: hidden;
}
/*
TODO figure out how to remove filter altogether
since it is null...
Modal header has 3 items:
|x title filter|
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: $import-primary;
}
.header-item {
display: flex;
align-items: center;
padding: 1rem;
text-decoration: none;
color: white;
user-select: none;
}
.header-item-filter {
display: flex;
flex-basis: $sides;
justify-content: flex-end;
}
.header-item-title {
flex-grow: 1;
flex-shrink: 0;
justify-content: center;
user-select: none;
letter-spacing: 0.4px;
cursor: default;
}
.header-item-title h2 {
font-size: 1.25rem;
}
.header-item-close {
display: flex;
flex-basis: $sides;
justify-content: flex-start;
}
.body {
background: $ui-pane-gray;
padding: 1.5rem 2.25rem;
text-align: center;
}
.input-row {
margin: 1.5rem 0;
font-weight: bolder;
text-align: right;
display: flex;
justify-content: center;
border: 1px solid;
border-radius: 0.25rem;
overflow: hidden;
}
.ok-input-container {
border-color: $motion-primary;
box-shadow: 0 0 0 0.2rem $motion-transparent;
}
.bad-input-container {
border-color: $data-primary;
box-shadow: 0 0 0 0.2rem hsla(30, 100%, 55%, 0.15);
}
.input-row input {
width: 100%;
padding: 0 1rem;
height: 3rem;
color: #6b6b6b;
font-size: .875rem;
outline: none;
border: none;
}
.input-row input::placeholder {
font-style: italic;
color: rgba(87,94,117,0.5);
}
.input-row button {
padding: 0.5rem 2rem;
font-weight: bold;
font-size: .875rem;
cursor: pointer;
border: 0px solid $import-primary;
outline: none;
}
.input-row button.ok-button {
background: $import-primary;
color: white;
}
.error-row {
margin: 1.5rem 0;
text-align: center;
display: flex;
justify-content: center;
background: hsla(30, 100%, 55%, 0.25);
color: $data-primary;
border: 1px solid $data-primary;
border-radius: 0.25rem;
}
.error-row p {
font-size: 0.875rem;
font-weight: bold;
}
/* Confirmation buttons at the bottom of the modal */
.button-row {
margin: 1.5rem 0;
font-weight: bolder;
text-align: right;
display: flex;
justify-content: center;
}
.button-row button {
border: 1px solid $motion-primary;
border-radius: 0.25rem;
padding: 0.5rem 1.5rem;
background: white;
font-weight: bold;
font-size: .875rem;
cursor: pointer;
color: $motion-primary;
}
.faq-link-text {
margin: 2rem 0 .5rem 0;
font-size: .875rem;
color: $text-primary;
}
.faq-link {
color: $motion-primary;
text-decoration: none;
}
@@ -0,0 +1,155 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactModal from 'react-modal';
import Box from '../box/box.jsx';
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import CloseButton from '../close-button/close-button.jsx';
import styles from './import-modal.css';
const messages = defineMessages({
title: {
id: 'gui.importInfo.title',
defaultMessage: 'View a Scratch 2.0 Project',
description: 'Scratch 2.0 import modal label - for accessibility'
},
formDescription: {
defaultMessage:
'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Preview will not be saved.',
description: 'Import project message',
id: 'gui.importInfo.message'
},
invalidFormatError: {
id: 'gui.importInfo.invalidFormatError',
defaultMessage: 'Uh oh, that project link or id doesn\'t look quite right.',
description: 'Invalid project link or id message'
}
});
const ImportModal = ({intl, ...props}) => (
<ReactModal
isOpen
className={styles.modalContent}
contentLabel={intl.formatMessage({...messages.title})}
overlayClassName={styles.modalOverlay}
onRequestClose={props.onCancel}
>
<Box>
<div className={styles.header}>
<div
className={classNames(
styles.headerItem,
styles.headerItemClose
)}
>
<CloseButton
buttonType="back"
size={CloseButton.SIZE_LARGE}
onClick={props.onGoBack}
/>
</div>
<div
className={classNames(
styles.headerItem,
styles.headerItemTitle
)}
>
<h2>
{intl.formatMessage({...messages.title})}
</h2>
</div>
<div className={classNames(styles.headerItem, styles.headerItemFilter)}>
{null}
</div>
</div>
</Box>
<Box className={styles.body}>
<p>
{intl.formatMessage({...messages.formDescription})}
</p>
<Box
className={classNames(styles.inputRow,
(props.hasValidationError ? styles.badInputContainer : styles.okInputContainer))
}
>
<input
autoFocus
placeholder={props.placeholder}
value={props.inputValue}
onChange={props.onChange}
onKeyPress={props.onKeyPress}
/>
<button
className={styles.okButton}
title="viewproject"
onClick={props.onViewProject}
>
<FormattedMessage
defaultMessage="View"
description="Label for button to load a scratch 2.0 project"
id="gui.importModal.viewproject"
/>
</button>
</Box>
{props.hasValidationError ?
<Box className={styles.errorRow}>
<p>
<FormattedMessage
{...messages[`${props.errorMessage}`]}
/>
</p>
</Box> : null
}
<Box className={styles.buttonRow}>
<button
onClick={props.onGoBack}
>
<FormattedMessage
defaultMessage="Go Back"
description="Label for button to back out of importing a project"
id="gui.importInfo.goback"
/>
</button>
</Box>
<Box className={styles.faqLinkText}>
<FormattedMessage
defaultMessage="To learn more, go to the {previewFaqLink}."
description="Invitation to try 3.0 preview"
id="gui.importInfo.previewfaq"
values={{
previewFaqLink: (
<a
className={styles.faqLink}
href="//scratch.mit.edu/preview-faq"
>
<FormattedMessage
defaultMessage="Preview FAQ"
description="link to Scratch 3.0 preview FAQ page"
id="gui.importInfo.previewfaqlink"
/>
</a>
)
}}
/>
</Box>
</Box>
</ReactModal>
);
ImportModal.propTypes = {
errorMessage: PropTypes.string.isRequired,
hasValidationError: PropTypes.bool.isRequired,
inputValue: PropTypes.string.isRequired,
intl: intlShape.isRequired,
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onGoBack: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
onViewProject: PropTypes.func.isRequired,
placeholder: PropTypes.string
};
export default injectIntl(ImportModal);
+2 -1
View File
@@ -9,7 +9,8 @@ const categories = {
sensing: '#5CB1D6',
sound: '#CF63CF',
looks: '#9966FF',
motion: '#4C97FF'
motion: '#4C97FF',
list: '#FC662C'
};
const MonitorComponent = props => (
@@ -51,7 +51,7 @@
.button-row button {
border: 1px solid $motion-primary;
border-radius: 0.25rem;
padding: 0.5rem 2rem;
padding: 0.5rem 1.5rem;
background: white;
font-weight: bold;
font-size: .875rem;
@@ -66,6 +66,13 @@
.button-row button.no-button {
color: $motion-primary;
}
.button-row button.view-project-button {
background: $import-primary;
border-color: $import-primary;
color: white;
}
.button-row button + button {
margin-left: 0.5rem;
}
+13 -1
View File
@@ -71,6 +71,17 @@ const PreviewModal = ({intl, ...props}) => (
}}
/>
</button>
<button
className={styles.viewProjectButton}
title="viewproject"
onClick={props.onViewProject}
>
<FormattedMessage
defaultMessage="View 2.0 Project"
description="Label for button to import a 2.0 project"
id="gui.previewModal.viewproject"
/>
</button>
</Box>
<Box className={styles.faqLinkText}>
<FormattedMessage
@@ -100,7 +111,8 @@ const PreviewModal = ({intl, ...props}) => (
PreviewModal.propTypes = {
intl: intlShape.isRequired,
onCancel: PropTypes.func.isRequired,
onTryIt: PropTypes.func.isRequired
onTryIt: PropTypes.func.isRequired,
onViewProject: PropTypes.func.isRequired
};
export default injectIntl(PreviewModal);
@@ -18,7 +18,9 @@ const SpriteSelectorItem = props => (
className: classNames(props.className, styles.spriteSelectorItem, {
[styles.isSelected]: props.selected
}),
onClick: props.onClick
onClick: props.onClick,
onMouseEnter: props.onMouseEnter,
onMouseLeave: props.onMouseLeave
}}
id={`${props.name}-${contextMenuId}`}
>
@@ -70,6 +72,8 @@ SpriteSelectorItem.propTypes = {
onClick: PropTypes.func,
onDeleteButtonClick: PropTypes.func,
onDuplicateButtonClick: PropTypes.func,
onMouseEnter: PropTypes.func,
onMouseLeave: PropTypes.func,
selected: PropTypes.bool.isRequired
};
@@ -2,7 +2,7 @@
.sprite-selector {
flex-grow: 1;
position: relative;
position: relative;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin-right: calc($space / 2);
background-color: #f9f9f9;
@@ -16,14 +16,14 @@
/* In prep for renaming sprite-selector-item to sprite */
.sprite {
/*
/*
Our goal is to fit sprites evenly in a row without leftover space.
Flexbox's `space between` property gets us close, but doesn't flow
Flexbox's `space between` property gets us close, but doesn't flow
well when the # of items per row > 1 and less than the max per row.
Solving by explicitly calc'ing the width of each sprite. Setting
`border-box` simplifies things, because content, padding and
border-width all are included in the width, leaving us only to subtract
Solving by explicitly calc'ing the width of each sprite. Setting
`border-box` simplifies things, because content, padding and
border-width all are included in the width, leaving us only to subtract
the left + right margins.
@todo: make room for the scrollbar
@@ -32,7 +32,7 @@
width: calc((100% / $sprites-per-row ) - $space);
min-width: 4rem;
min-height: 4rem; /* @todo: calc height same as width */
margin: calc($space / 2);
margin: calc($space / 2);
}
@@ -41,11 +41,11 @@
Sets the sprite-selector items as a scrollable pane
@todo: Safari: pane doesn't stretch to fill height;
@todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and
also introduces a new bug in Chrome when vertically resizing window down,
then back up, introduces white space in the outside the page container.
@todo: Adding `position: relative` still doesn't fix Safari scrolling pane, and
also introduces a new bug in Chrome when vertically resizing window down,
then back up, introduces white space in the outside the page container.
*/
height: calc(100% - $sprite-info-height);
height: calc(100% - $sprite-info-height);
overflow-y: scroll;
}
@@ -57,11 +57,90 @@
padding-top: calc($space / 2);
padding-left: calc($space / 2);
padding-right: calc($space / 2);
padding-bottom: $space;
padding-bottom: $space;
}
.add-button {
position: absolute;
bottom: 0.75rem;
right: 1rem;
z-index: 1; /* TODO overlaps the stage, this doesn't work, fix! */
}
.raised {
background-color: #cce1ff;
border-color: #8cbcff;
transition: all 0.25s ease;
}
.raised:hover {
background-color: #8cbcff;
transform: scale(1.05);
}
.raised:hover {
-webkit-animation-name: wiggle;
-ms-animation-name: wiggle;
-ms-animation-duration: 500ms;
-webkit-animation-duration: 500ms;
-webkit-animation-iteration-count: 1;
-ms-animation-iteration-count: 1;
-webkit-animation-timing-function: ease-in-out;
-ms-animation-timing-function: ease-in-out;
background-color: #8cbcff;
}
@-webkit-keyframes wiggle {
0% {-webkit-transform: rotate(3deg) scale(1.05);}
25% {-webkit-transform: rotate(-3deg) scale(1.05);}
50% {-webkit-transform: rotate(5deg) scale(1.05);}
75% {-webkit-transform: rotate(-2deg) scale(1.05);}
100% {-webkit-transform: rotate(0deg) scale(1.05);}
}
@-ms-keyframes wiggle {
0% {-ms-transform: rotate(3deg) scale(1.05);}
25% {-ms-transform: rotate(-3deg) scale(1.05);}
50% {-ms-transform: rotate(5deg) scale(1.05);}
75% {-ms-transform: rotate(-2deg) scale(1.05);}
100% {-ms-transform: rotate(0deg) scale(1.05);}
}
@keyframes wiggle {
0% {transform: rotate(3deg) scale(1.05);}
25% {transform: rotate(-3deg) scale(1.05);}
50% {transform: rotate(5deg) scale(1.05);}
75% {transform: rotate(-2deg) scale(1.05);}
100% {transform: rotate(0deg) scale(1.05);}
}
.receivedBlocks {
-webkit-animation: glowing 250ms;
-moz-animation: glowing 250ms;
-o-animation: glowing 250ms;
animation: glowing 250ms;
}
@-webkit-keyframes glowing {
10% { -webkit-box-shadow: 0 0 10px #7fff1e; }
90% { -webkit-box-shadow: 0 0 10px #7fff1e; }
100% { -webkit-box-shadow: none; }
}
@-moz-keyframes glowing {
10% { -moz-box-shadow: 0 0 10px #7fff1e; }
90% { -moz-box-shadow: 0 0 10px #7fff1e; }
100% { -moz-box-shadow: none; }
}
@-o-keyframes glowing {
10% { box-shadow: 0 0 10px #7fff1e; }
90% { box-shadow: 0 0 10px #7fff1e; }
100% { box-shadow: none; }
}
@keyframes glowing {
10% { box-shadow: 0 0 10px #7fff1e; }
90% { box-shadow: 0 0 10px #7fff1e; }
100% { box-shadow: none; }
}
@@ -1,25 +1,53 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import Box from '../box/box.jsx';
import SpriteInfo from '../../containers/sprite-info.jsx';
import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx';
import AssetButton from '../asset-button/asset-button.jsx';
import ActionMenu from '../action-menu/action-menu.jsx';
import styles from './sprite-selector.css';
import spriteIcon from './icon--sprite.svg';
import cameraIcon from '../action-menu/icon--camera.svg';
import fileUploadIcon from '../action-menu/icon--file-upload.svg';
import paintIcon from '../action-menu/icon--paint.svg';
import spriteIcon from '../action-menu/icon--sprite.svg';
import surpriseIcon from '../action-menu/icon--surprise.svg';
const messages = defineMessages({
addSprite: {
id: 'gui.spriteSelector.addSprite',
description: 'Button to add a sprite in the target pane',
defaultMessage: 'Add Sprite'
addSpriteFromLibrary: {
id: 'gui.spriteSelector.addSpriteFromLibrary',
description: 'Button to add a sprite in the target pane from library',
defaultMessage: 'Sprite Library'
},
addSpriteFromPaint: {
id: 'gui.spriteSelector.addSpriteFromPaint',
description: 'Button to add a sprite in the target pane from paint',
defaultMessage: 'Paint'
},
addSpriteFromSurprise: {
id: 'gui.spriteSelector.addSpriteFromSurprise',
description: 'Button to add a random sprite in the target pane',
defaultMessage: 'Surprise'
},
addSpriteFromFile: {
id: 'gui.spriteSelector.addSpriteFromFile',
description: 'Button to add a sprite in the target pane from file',
defaultMessage: 'Coming Soon'
},
addSpriteFromCamera: {
id: 'gui.spriteSelector.addSpriteFromCamera',
description: 'Button to add a sprite in the target pane from camera',
defaultMessage: 'Coming Soon'
}
});
const SpriteSelectorComponent = function (props) {
const {
editingTarget,
hoveredTarget,
intl,
onChangeSpriteDirection,
onChangeSpriteName,
@@ -30,7 +58,10 @@ const SpriteSelectorComponent = function (props) {
onDeleteSprite,
onDuplicateSprite,
onNewSpriteClick,
onSurpriseSpriteClick,
onPaintSpriteClick,
onSelectSprite,
raised,
selectedId,
sprites,
...componentProps
@@ -72,7 +103,12 @@ const SpriteSelectorComponent = function (props) {
.map(sprite => (
<SpriteSelectorItem
assetId={sprite.costume && sprite.costume.assetId}
className={styles.sprite}
className={hoveredTarget.sprite === sprite.id &&
sprite.id !== editingTarget &&
hoveredTarget.receivedBlocks ?
classNames(styles.sprite, styles.receivedBlocks) :
raised && sprite.id !== editingTarget ?
classNames(styles.sprite, styles.raised) : styles.sprite}
id={sprite.id}
key={sprite.id}
name={sprite.name}
@@ -85,10 +121,27 @@ const SpriteSelectorComponent = function (props) {
}
</Box>
</Box>
<AssetButton
<ActionMenu
className={styles.addButton}
img={spriteIcon}
title={intl.formatMessage(messages.addSprite)}
moreButtons={[
{
title: intl.formatMessage(messages.addSpriteFromCamera),
img: cameraIcon
}, {
title: intl.formatMessage(messages.addSpriteFromFile),
img: fileUploadIcon
}, {
title: intl.formatMessage(messages.addSpriteFromSurprise),
img: surpriseIcon,
onClick: onSurpriseSpriteClick // TODO need real function for this
}, {
title: intl.formatMessage(messages.addSpriteFromPaint),
img: paintIcon,
onClick: onPaintSpriteClick // TODO need real function for this
}
]}
title={intl.formatMessage(messages.addSpriteFromLibrary)}
onClick={onNewSpriteClick}
/>
</Box>
@@ -96,6 +149,11 @@ const SpriteSelectorComponent = function (props) {
};
SpriteSelectorComponent.propTypes = {
editingTarget: PropTypes.string,
hoveredTarget: PropTypes.shape({
hoveredSprite: PropTypes.string,
receivedBlocks: PropTypes.bool
}),
intl: intlShape.isRequired,
onChangeSpriteDirection: PropTypes.func,
onChangeSpriteName: PropTypes.func,
@@ -106,7 +164,10 @@ SpriteSelectorComponent.propTypes = {
onDeleteSprite: PropTypes.func,
onDuplicateSprite: PropTypes.func,
onNewSpriteClick: PropTypes.func,
onPaintSpriteClick: PropTypes.func,
onSelectSprite: PropTypes.func,
onSurpriseSpriteClick: PropTypes.func,
raised: PropTypes.bool,
selectedId: PropTypes.string,
sprites: PropTypes.shape({
id: PropTypes.shape({
@@ -4,16 +4,41 @@ import React from 'react';
import {defineMessages, intlShape, injectIntl, FormattedMessage} from 'react-intl';
import Box from '../box/box.jsx';
import AssetButton from '../asset-button/asset-button.jsx';
import ActionMenu from '../action-menu/action-menu.jsx';
import CostumeCanvas from '../costume-canvas/costume-canvas.jsx';
import styles from './stage-selector.css';
import backdropIcon from './icon--backdrop.svg';
import backdropIcon from '../action-menu/icon--backdrop.svg';
import cameraIcon from '../action-menu/icon--camera.svg';
import fileUploadIcon from '../action-menu/icon--file-upload.svg';
import paintIcon from '../action-menu/icon--paint.svg';
import surpriseIcon from '../action-menu/icon--surprise.svg';
const messages = defineMessages({
addBackdrop: {
id: 'gui.stageSelector.targetPaneAddBackdrop',
description: 'Button to add a backdrop in the target pane',
defaultMessage: 'Add Backdrop'
addBackdropFromLibrary: {
id: 'gui.spriteSelector.addBackdropFromLibrary',
description: 'Button to add a stage in the target pane from library',
defaultMessage: 'Backdrop Library'
},
addBackdropFromPaint: {
id: 'gui.stageSelector.addBackdropFromPaint',
description: 'Button to add a stage in the target pane from paint',
defaultMessage: 'Paint'
},
addBackdropFromSurprise: {
id: 'gui.stageSelector.addBackdropFromSurprise',
description: 'Button to add a random stage in the target pane',
defaultMessage: 'Surprise'
},
addBackdropFromFile: {
id: 'gui.stageSelector.addBackdropFromFile',
description: 'Button to add a stage in the target pane from file',
defaultMessage: 'Coming Soon'
},
addBackdropFromCamera: {
id: 'gui.stageSelector.addBackdropFromCamera',
description: 'Button to add a stage in the target pane from camera',
defaultMessage: 'Coming Soon'
}
});
@@ -25,6 +50,8 @@ const StageSelector = props => {
url,
onClick,
onNewBackdropClick,
onSurpriseBackdropClick,
onEmptyBackdropClick,
...componentProps
} = props;
return (
@@ -54,10 +81,28 @@ const StageSelector = props => {
/>
</div>
<div className={styles.count}>{backdropCount}</div>
<AssetButton
<ActionMenu
className={styles.addButton}
img={backdropIcon}
title={intl.formatMessage(messages.addBackdrop)}
moreButtons={[
{
title: intl.formatMessage(messages.addBackdropFromCamera),
img: cameraIcon
}, {
title: intl.formatMessage(messages.addBackdropFromFile),
img: fileUploadIcon
}, {
title: intl.formatMessage(messages.addBackdropFromSurprise),
img: surpriseIcon,
onClick: onSurpriseBackdropClick // TODO NEED REAL FUNCTION
}, {
title: intl.formatMessage(messages.addBackdropFromPaint),
img: paintIcon,
onClick: onEmptyBackdropClick // TODO NEED REAL FUNCTION
}
]}
title={intl.formatMessage(messages.addBackdropFromLibrary)}
onClick={onNewBackdropClick}
/>
</Box>
@@ -68,8 +113,11 @@ StageSelector.propTypes = {
backdropCount: PropTypes.number.isRequired,
intl: intlShape.isRequired,
onClick: PropTypes.func,
onEmptyBackdropClick: PropTypes.func,
onNewBackdropClick: PropTypes.func,
onSurpriseBackdropClick: PropTypes.func,
selected: PropTypes.bool.isRequired,
url: PropTypes.string
};
export default injectIntl(StageSelector);
@@ -19,6 +19,7 @@ import styles from './target-pane.css';
const TargetPane = ({
backdropLibraryVisible,
editingTarget,
hoveredTarget,
spriteLibraryVisible,
onChangeSpriteDirection,
onChangeSpriteName,
@@ -29,9 +30,12 @@ const TargetPane = ({
onDeleteSprite,
onDuplicateSprite,
onNewSpriteClick,
onSurpriseSpriteClick,
onPaintSpriteClick,
onRequestCloseSpriteLibrary,
onRequestCloseBackdropLibrary,
onSelectSprite,
raiseSprites,
stage,
sprites,
vm,
@@ -43,6 +47,9 @@ const TargetPane = ({
>
<SpriteSelectorComponent
editingTarget={editingTarget}
hoveredTarget={hoveredTarget}
raised={raiseSprites}
selectedId={editingTarget}
sprites={sprites}
onChangeSpriteDirection={onChangeSpriteDirection}
@@ -54,7 +61,9 @@ const TargetPane = ({
onDeleteSprite={onDeleteSprite}
onDuplicateSprite={onDuplicateSprite}
onNewSpriteClick={onNewSpriteClick}
onPaintSpriteClick={onPaintSpriteClick}
onSelectSprite={onSelectSprite}
onSurpriseSpriteClick={onSurpriseSpriteClick}
/>
<div className={styles.stageSelectorWrapper}>
{stage.id && <StageSelector
@@ -107,6 +116,10 @@ TargetPane.propTypes = {
backdropLibraryVisible: PropTypes.bool,
editingTarget: PropTypes.string,
extensionLibraryVisible: PropTypes.bool,
hoveredTarget: PropTypes.shape({
hoveredSprite: PropTypes.string,
receivedBlocks: PropTypes.bool
}),
onChangeSpriteDirection: PropTypes.func,
onChangeSpriteName: PropTypes.func,
onChangeSpriteSize: PropTypes.func,
@@ -116,10 +129,13 @@ TargetPane.propTypes = {
onDeleteSprite: PropTypes.func,
onDuplicateSprite: PropTypes.func,
onNewSpriteClick: PropTypes.func,
onPaintSpriteClick: PropTypes.func,
onRequestCloseBackdropLibrary: PropTypes.func,
onRequestCloseExtensionLibrary: PropTypes.func,
onRequestCloseSpriteLibrary: PropTypes.func,
onSelectSprite: PropTypes.func,
onSurpriseSpriteClick: PropTypes.func,
raiseSprites: PropTypes.bool,
spriteLibraryVisible: PropTypes.bool,
sprites: PropTypes.objectOf(spriteShape),
stage: spriteShape,
+4 -1
View File
@@ -252,7 +252,10 @@ class Blocks extends React.Component {
}
handleCustomProceduresClose (data) {
this.props.onRequestCloseCustomProcedures(data);
this.workspace.refreshToolboxSelection_();
const ws = this.workspace;
ws.refreshToolboxSelection_();
// @todo ensure this does not break on localization
ws.toolbox_.scrollToCategoryByName('My Blocks');
}
render () {
/* eslint-disable no-unused-vars */
+96 -36
View File
@@ -7,41 +7,54 @@ import VM from 'scratch-vm';
import AssetPanel from '../components/asset-panel/asset-panel.jsx';
import PaintEditorWrapper from './paint-editor-wrapper.jsx';
import CostumeLibrary from './costume-library.jsx';
import BackdropLibrary from './backdrop-library.jsx';
import {connect} from 'react-redux';
import {
closeCostumeLibrary,
closeBackdropLibrary,
openCostumeLibrary,
openBackdropLibrary
} from '../reducers/modals';
import addBlankCostumeIcon from '../components/asset-panel/icon--add-blank-costume.svg';
import addLibraryBackdropIcon from '../components/asset-panel/icon--add-backdrop-lib.svg';
import addLibraryCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg';
import fileUploadIcon from '../components/action-menu/icon--file-upload.svg';
import paintIcon from '../components/action-menu/icon--paint.svg';
import cameraIcon from '../components/action-menu/icon--camera.svg';
import surpriseIcon from '../components/action-menu/icon--surprise.svg';
import costumeLibraryContent from '../lib/libraries/costumes.json';
import backdropLibraryContent from '../lib/libraries/backdrops.json';
const messages = defineMessages({
addLibraryBackdropMsg: {
defaultMessage: 'Add Backdrop From Library',
defaultMessage: 'Backdrop Library',
description: 'Button to add a backdrop in the editor tab',
id: 'gui.costumeTab.addBackdrop'
id: 'gui.costumeTab.addBackdropFromLibrary'
},
addLibraryCostumeMsg: {
defaultMessage: 'Add Costume From Library',
defaultMessage: 'Costume Library',
description: 'Button to add a costume in the editor tab',
id: 'gui.costumeTab.addCostume'
},
addBlankBackdropMsg: {
defaultMessage: 'Add Blank Backdrop',
description: 'Button to add a blank backdrop in the editor tab',
id: 'gui.costumeTab.addBlankBackdrop'
id: 'gui.costumeTab.addCostumeFromLibrary'
},
addBlankCostumeMsg: {
defaultMessage: 'Add Blank Costume',
defaultMessage: 'Paint',
description: 'Button to add a blank costume in the editor tab',
id: 'gui.costumeTab.addBlankCostume'
},
addSurpriseCostumeMsg: {
defaultMessage: 'Surprise',
description: 'Button to add a surprise costume in the editor tab',
id: 'gui.costumeTab.addSurpriseCostume'
},
addFileCostumeMsg: {
defaultMessage: 'Coming Soon',
description: 'Button to add a file upload costume in the editor tab',
id: 'gui.costumeTab.addFileCostume'
},
addCameraCostumeMsg: {
defaultMessage: 'Coming Soon',
description: 'Button to use the camera to create a costume costume in the editor tab',
id: 'gui.costumeTab.addCameraCostume'
}
});
@@ -51,10 +64,23 @@ class CostumeTab extends React.Component {
bindAll(this, [
'handleSelectCostume',
'handleDeleteCostume',
'handleDuplicateCostume',
'handleNewCostume',
'handleNewBlankCostume'
'handleNewBlankCostume',
'handleSurpriseCostume',
'handleSurpriseBackdrop'
]);
this.state = {selectedCostumeIndex: 0};
const {
editingTarget,
sprites,
stage
} = props;
const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage;
if (target && target.currentCostume) {
this.state = {selectedCostumeIndex: target.currentCostume};
} else {
this.state = {selectedCostumeIndex: 0};
}
}
componentWillReceiveProps (nextProps) {
const {
@@ -75,9 +101,14 @@ class CostumeTab extends React.Component {
handleDeleteCostume (costumeIndex) {
this.props.vm.deleteCostume(costumeIndex);
}
handleDuplicateCostume (costumeIndex) {
this.props.vm.duplicateCostume(costumeIndex).then(() => {
this.setState({selectedCostumeIndex: costumeIndex + 1});
});
}
handleNewCostume () {
if (!this.props.vm.editingTarget) return;
const costumes = this.props.vm.editingTarget.sprite.costumes || [];
const costumes = this.props.vm.editingTarget.getCostumes() || [];
this.setState({selectedCostumeIndex: Math.max(costumes.length - 1, 0)});
}
handleNewBlankCostume () {
@@ -97,6 +128,32 @@ class CostumeTab extends React.Component {
this.handleNewCostume();
});
}
handleSurpriseCostume () {
const item = costumeLibraryContent[Math.floor(Math.random() * costumeLibraryContent.length)];
const vmCostume = {
name: item.name,
rotationCenterX: item.info[0],
rotationCenterY: item.info[1],
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
skinId: null
};
this.props.vm.addCostume(item.md5, vmCostume).then(() => {
this.handleNewCostume();
});
}
handleSurpriseBackdrop () {
const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
const vmCostume = {
name: item.name,
rotationCenterX: item.info[0] && item.info[0] / 2,
rotationCenterY: item.info[1] && item.info[1] / 2,
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
skinId: null
};
this.props.vm.addCostume(item.md5, vmCostume).then(() => {
this.handleNewCostume();
});
}
render () {
// For paint wrapper
const {
@@ -104,9 +161,7 @@ class CostumeTab extends React.Component {
onNewLibraryBackdropClick,
onNewLibraryCostumeClick,
costumeLibraryVisible,
backdropLibraryVisible,
onRequestCloseCostumeLibrary,
onRequestCloseBackdropLibrary,
...props
} = this.props;
@@ -124,7 +179,7 @@ class CostumeTab extends React.Component {
}
const addLibraryMessage = target.isStage ? messages.addLibraryBackdropMsg : messages.addLibraryCostumeMsg;
const addBlankMessage = target.isStage ? messages.addBlankBackdropMsg : messages.addBlankCostumeMsg;
const addSurpriseFunc = target.isStage ? this.handleSurpriseBackdrop : this.handleSurpriseCostume;
const addLibraryFunc = target.isStage ? onNewLibraryBackdropClick : onNewLibraryCostumeClick;
const addLibraryIcon = target.isStage ? addLibraryBackdropIcon : addLibraryCostumeIcon;
@@ -132,19 +187,33 @@ class CostumeTab extends React.Component {
<AssetPanel
buttons={[
{
message: intl.formatMessage(addBlankMessage),
img: addBlankCostumeIcon,
onClick: this.handleNewBlankCostume
},
{
message: intl.formatMessage(addLibraryMessage),
title: intl.formatMessage(addLibraryMessage),
img: addLibraryIcon,
onClick: addLibraryFunc
},
{
title: intl.formatMessage(messages.addCameraCostumeMsg),
img: cameraIcon
},
{
title: intl.formatMessage(messages.addFileCostumeMsg),
img: fileUploadIcon
},
{
title: intl.formatMessage(messages.addSurpriseCostumeMsg),
img: surpriseIcon,
onClick: addSurpriseFunc
},
{
title: intl.formatMessage(messages.addBlankCostumeMsg),
img: paintIcon,
onClick: this.handleNewBlankCostume
}
]}
items={target.costumes || []}
selectedItemIndex={this.state.selectedCostumeIndex}
onDeleteClick={target.costumes.length > 1 ? this.handleDeleteCostume : null}
onDuplicateClick={this.handleDuplicateCostume}
onItemClick={this.handleSelectCostume}
>
{target.costumes ?
@@ -161,13 +230,6 @@ class CostumeTab extends React.Component {
onRequestClose={onRequestCloseCostumeLibrary}
/>
) : null}
{backdropLibraryVisible ? (
<BackdropLibrary
vm={vm}
onNewBackdrop={this.handleNewCostume}
onRequestClose={onRequestCloseBackdropLibrary}
/>
) : null}
</AssetPanel>
);
}
@@ -186,7 +248,8 @@ CostumeTab.propTypes = {
id: PropTypes.shape({
costumes: PropTypes.arrayOf(PropTypes.shape({
url: PropTypes.string,
name: PropTypes.string.isRequired
name: PropTypes.string.isRequired,
skinId: PropTypes.number
}))
})
}),
@@ -215,9 +278,6 @@ const mapDispatchToProps = dispatch => ({
e.preventDefault();
dispatch(openCostumeLibrary());
},
onRequestCloseBackdropLibrary: () => {
dispatch(closeBackdropLibrary());
},
onRequestCloseCostumeLibrary: () => {
dispatch(closeCostumeLibrary());
}
+1 -1
View File
@@ -7,7 +7,7 @@ import extensionLibraryContent from '../lib/libraries/extensions/index';
import analytics from '../lib/analytics';
import LibraryComponent from '../components/library/library.jsx';
import extensionIcon from '../components/sprite-selector/icon--sprite.svg';
import extensionIcon from '../components/action-menu/icon--sprite.svg';
class ExtensionLibrary extends React.PureComponent {
constructor (props) {
+16 -17
View File
@@ -2,23 +2,21 @@ import AudioEngine from 'scratch-audio';
import PropTypes from 'prop-types';
import React from 'react';
import VM from 'scratch-vm';
import bindAll from 'lodash.bindall';
import {connect} from 'react-redux';
import {openExtensionLibrary} from '../reducers/modals.js';
import {openExtensionLibrary} from '../reducers/modals';
import {
activateTab,
BLOCKS_TAB_INDEX,
COSTUMES_TAB_INDEX,
SOUNDS_TAB_INDEX
} from '../reducers/editor-tab';
import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
import GUIComponent from '../components/gui/gui.jsx';
class GUI extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleTabSelect'
]);
this.state = {tabIndex: 0};
}
componentDidMount () {
this.audioEngine = new AudioEngine();
this.props.vm.attachAudioEngine(this.audioEngine);
@@ -34,9 +32,6 @@ class GUI extends React.Component {
componentWillUnmount () {
this.props.vm.stopAll();
}
handleTabSelect (tabIndex) {
this.setState({tabIndex});
}
render () {
const {
children,
@@ -46,10 +41,7 @@ class GUI extends React.Component {
} = this.props;
return (
<GUIComponent
enableExtensions={window.location.search.includes('extensions')}
tabIndex={this.state.tabIndex}
vm={vm}
onTabSelect={this.handleTabSelect}
{...componentProps}
>
{children}
@@ -61,6 +53,7 @@ class GUI extends React.Component {
GUI.propTypes = {
...GUIComponent.propTypes,
feedbackFormVisible: PropTypes.bool,
importInfoVisible: PropTypes.bool,
previewInfoVisible: PropTypes.bool,
projectData: PropTypes.string,
vm: PropTypes.instanceOf(VM)
@@ -69,12 +62,18 @@ GUI.propTypes = {
GUI.defaultProps = GUIComponent.defaultProps;
const mapStateToProps = state => ({
activeTabIndex: state.editorTab.activeTabIndex,
blocksTabVisible: state.editorTab.activeTabIndex === BLOCKS_TAB_INDEX,
costumesTabVisible: state.editorTab.activeTabIndex === COSTUMES_TAB_INDEX,
feedbackFormVisible: state.modals.feedbackForm,
previewInfoVisible: state.modals.previewInfo
importInfoVisible: state.modals.importInfo,
previewInfoVisible: state.modals.previewInfo,
soundsTabVisible: state.editorTab.activeTabIndex === SOUNDS_TAB_INDEX
});
const mapDispatchToProps = dispatch => ({
onExtensionButtonClick: () => dispatch(openExtensionLibrary())
onExtensionButtonClick: () => dispatch(openExtensionLibrary()),
onActivateTab: tab => dispatch(activateTab(tab))
});
const ConnectedGUI = connect(
+111
View File
@@ -0,0 +1,111 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import ImportModalComponent from '../components/import-modal/import-modal.jsx';
import {
closeImportInfo,
openPreviewInfo
} from '../reducers/modals';
class ImportModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleViewProject',
'handleCancel',
'handleChange',
'handleGoBack',
'handleKeyPress'
]);
this.state = {
inputValue: '',
hasValidationError: false,
errorMessage: ''
};
}
handleKeyPress (event) {
if (event.key === 'Enter') this.handleViewProject();
}
handleViewProject () {
const inputValue = this.state.inputValue;
const projectId = this.validate(inputValue);
if (projectId) {
const projectLink = document.createElement('a');
document.body.appendChild(projectLink);
projectLink.href = `#${projectId}`;
projectLink.click();
document.body.removeChild(projectLink);
this.props.onViewProject();
} else {
this.setState({
hasValidationError: true,
errorMessage: `invalidFormatError`});
}
}
handleChange (e) {
this.setState({inputValue: e.target.value, hasValidationError: false});
}
validate (input) {
const urlMatches = input.match(/^(?:https:\/\/)?scratch\.mit\.edu\/projects\/(\d+)/);
if (urlMatches && urlMatches.length > 0) {
return urlMatches[1];
}
const projectIdMatches = input.match(/^#?(\d+)$/);
if (projectIdMatches && projectIdMatches.length > 0) {
return projectIdMatches[1];
}
return null;
}
handleCancel () {
this.props.onCancel();
}
handleGoBack () {
this.props.onBack();
}
render () {
return (
<ImportModalComponent
errorMessage={this.state.errorMessage}
hasValidationError={this.state.hasValidationError}
inputValue={this.state.inputValue}
placeholder="scratch.mit.edu/projects/123456789"
onCancel={this.handleCancel}
onChange={this.handleChange}
onGoBack={this.handleGoBack}
onKeyPress={this.handleKeyPress}
onViewProject={this.handleViewProject}
/>
);
}
}
ImportModal.propTypes = {
onBack: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onViewProject: PropTypes.func
};
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch => ({
onBack: () => {
dispatch(closeImportInfo());
dispatch(openPreviewInfo());
},
onCancel: () => {
dispatch(closeImportInfo());
},
onViewProject: () => {
dispatch(closeImportInfo());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ImportModal);
+14 -3
View File
@@ -8,7 +8,8 @@ import PreviewModalComponent from '../components/preview-modal/preview-modal.jsx
import BrowserModalComponent from '../components/browser-modal/browser-modal.jsx';
import {
closePreviewInfo
closePreviewInfo,
openImportInfo
} from '../reducers/modals';
class PreviewModal extends React.Component {
@@ -16,7 +17,8 @@ class PreviewModal extends React.Component {
super(props);
bindAll(this, [
'handleTryIt',
'handleCancel'
'handleCancel',
'handleViewProject'
]);
this.state = {
@@ -30,6 +32,9 @@ class PreviewModal extends React.Component {
handleCancel () {
window.location.replace('https://scratch.mit.edu');
}
handleViewProject () {
this.props.onViewProject();
}
supportedBrowser () {
if (platform.name === 'IE') {
return false;
@@ -42,6 +47,7 @@ class PreviewModal extends React.Component {
previewing={this.state.previewing}
onCancel={this.handleCancel}
onTryIt={this.handleTryIt}
onViewProject={this.handleViewProject}
/> :
<BrowserModalComponent
onBack={this.handleCancel}
@@ -51,7 +57,8 @@ class PreviewModal extends React.Component {
}
PreviewModal.propTypes = {
onTryIt: PropTypes.func
onTryIt: PropTypes.func,
onViewProject: PropTypes.func
};
const mapStateToProps = () => ({});
@@ -59,6 +66,10 @@ const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch => ({
onTryIt: () => {
dispatch(closePreviewInfo());
},
onViewProject: () => {
dispatch(closePreviewInfo());
dispatch(openImportInfo());
}
});
+18 -2
View File
@@ -1,6 +1,7 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import WavEncoder from 'wav-encoder';
import {connect} from 'react-redux';
@@ -9,6 +10,7 @@ import {computeChunkedRMS} from '../lib/audio/audio-util.js';
import AudioEffects from '../lib/audio/audio-effects.js';
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
import log from '../lib/log.js';
const UNDO_STACK_SIZE = 99;
@@ -72,11 +74,25 @@ class SoundEditor extends React.Component {
}
this.undoStack.push(this.copyCurrentBuffer());
}
// Encode the new sound into a wav so that it can be stored
let wavBuffer = null;
try {
wavBuffer = WavEncoder.encode.sync({
sampleRate: sampleRate,
channelData: [samples]
});
} catch (e) {
// This error state is mostly for the mock sounds used during testing.
// Any incorrect sound buffer trying to get interpretd as a Wav file
// should yield this error.
log.error(`Encountered error while trying to encode sound update: ${e}`);
}
this.resetState(samples, sampleRate);
this.props.onUpdateSoundBuffer(
this.props.soundIndex,
this.audioBufferPlayer.buffer
);
this.audioBufferPlayer.buffer,
wavBuffer ? new Uint8Array(wavBuffer) : new Uint8Array());
}
handlePlay () {
this.audioBufferPlayer.play(
+57 -9
View File
@@ -8,10 +8,15 @@ import AssetPanel from '../components/asset-panel/asset-panel.jsx';
import soundIcon from '../components/asset-panel/icon--sound.svg';
import addSoundFromLibraryIcon from '../components/asset-panel/icon--add-sound-lib.svg';
import addSoundFromRecordingIcon from '../components/asset-panel/icon--add-sound-record.svg';
import fileUploadIcon from '../components/action-menu/icon--file-upload.svg';
import surpriseIcon from '../components/action-menu/icon--surprise.svg';
import RecordModal from './record-modal.jsx';
import SoundEditor from './sound-editor.jsx';
import SoundLibrary from './sound-library.jsx';
import soundLibraryContent from '../lib/libraries/sounds.json';
import {connect} from 'react-redux';
import {
@@ -26,7 +31,9 @@ class SoundTab extends React.Component {
bindAll(this, [
'handleSelectSound',
'handleDeleteSound',
'handleNewSound'
'handleDuplicateSound',
'handleNewSound',
'handleSurpriseSound'
]);
this.state = {selectedSoundIndex: 0};
}
@@ -51,6 +58,15 @@ class SoundTab extends React.Component {
handleDeleteSound (soundIndex) {
this.props.vm.deleteSound(soundIndex);
if (soundIndex >= this.state.selectedSoundIndex) {
this.setState({selectedSoundIndex: Math.max(0, soundIndex - 1)});
}
}
handleDuplicateSound (soundIndex) {
this.props.vm.duplicateSound(soundIndex).then(() => {
this.setState({selectedSoundIndex: soundIndex + 1});
});
}
handleNewSound () {
@@ -62,6 +78,20 @@ class SoundTab extends React.Component {
this.setState({selectedSoundIndex: Math.max(sounds.length - 1, 0)});
}
handleSurpriseSound () {
const soundItem = soundLibraryContent[Math.floor(Math.random() * soundLibraryContent.length)];
const vmSound = {
format: soundItem.format,
md5: soundItem.md5,
rate: soundItem.rate,
sampleCount: soundItem.sampleCount,
name: soundItem.name
};
this.props.vm.addSound(vmSound).then(() => {
this.handleNewSound();
});
}
render () {
const {
intl,
@@ -84,28 +114,45 @@ class SoundTab extends React.Component {
)) : [];
const messages = defineMessages({
fileUploadSound: {
defaultMessage: 'Coming Soon',
description: 'Button to upload sound from file in the editor tab',
id: 'gui.soundTab.fileUploadSound'
},
surpriseSound: {
defaultMessage: 'Surprise',
description: 'Button to get a random sound in the editor tab',
id: 'gui.soundTab.surpriseSound'
},
recordSound: {
defaultMessage: 'Record Sound',
defaultMessage: 'Record',
description: 'Button to record a sound in the editor tab',
id: 'gui.soundTab.recordSound'
},
addSound: {
defaultMessage: 'Add Sound',
defaultMessage: 'Sound Library',
description: 'Button to add a sound in the editor tab',
id: 'gui.soundTab.addSound'
id: 'gui.soundTab.addSoundFromLibrary'
}
});
return (
<AssetPanel
buttons={[{
message: intl.formatMessage(messages.recordSound),
img: addSoundFromRecordingIcon,
onClick: onNewSoundFromRecordingClick
}, {
message: intl.formatMessage(messages.addSound),
title: intl.formatMessage(messages.addSound),
img: addSoundFromLibraryIcon,
onClick: onNewSoundFromLibraryClick
}, {
title: intl.formatMessage(messages.fileUploadSound),
img: fileUploadIcon
}, {
title: intl.formatMessage(messages.surpriseSound),
img: surpriseIcon,
onClick: this.handleSurpriseSound
}, {
title: intl.formatMessage(messages.recordSound),
img: addSoundFromRecordingIcon,
onClick: onNewSoundFromRecordingClick
}]}
items={sounds.map(sound => ({
url: soundIcon,
@@ -113,6 +160,7 @@ class SoundTab extends React.Component {
}))}
selectedItemIndex={this.state.selectedSoundIndex}
onDeleteClick={this.handleDeleteSound}
onDuplicateClick={this.handleDuplicateSound}
onItemClick={this.handleSelectSound}
>
{sprite.sounds && sprite.sounds[this.state.selectedSoundIndex] ? (
+29 -6
View File
@@ -1,9 +1,10 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import {setHoveredSprite} from '../reducers/hovered-target';
import SpriteSelectorItemComponent from '../components/sprite-selector-item/sprite-selector-item.jsx';
class SpriteSelectorItem extends React.Component {
@@ -12,14 +13,17 @@ class SpriteSelectorItem extends React.Component {
bindAll(this, [
'handleClick',
'handleDelete',
'handleDuplicate'
'handleDuplicate',
'handleMouseEnter',
'handleMouseLeave'
]);
}
handleClick (e) {
e.preventDefault();
this.props.onClick(this.props.id);
}
handleDelete () {
handleDelete (e) {
e.stopPropagation(); // To prevent from bubbling back to handleClick
// @todo add i18n here
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure you want to delete this?')) {
@@ -30,6 +34,12 @@ class SpriteSelectorItem extends React.Component {
e.stopPropagation(); // To prevent from bubbling back to handleClick
this.props.onDuplicateButtonClick(this.props.id);
}
handleMouseLeave () {
this.props.dispatchSetHoveredSprite(null);
}
handleMouseEnter () {
this.props.dispatchSetHoveredSprite(this.props.id);
}
render () {
const {
/* eslint-disable no-unused-vars */
@@ -38,6 +48,7 @@ class SpriteSelectorItem extends React.Component {
onClick,
onDeleteButtonClick,
onDuplicateButtonClick,
receivedBlocks,
/* eslint-enable no-unused-vars */
...props
} = this.props;
@@ -46,6 +57,8 @@ class SpriteSelectorItem extends React.Component {
onClick={this.handleClick}
onDeleteButtonClick={onDeleteButtonClick ? this.handleDelete : null}
onDuplicateButtonClick={onDuplicateButtonClick ? this.handleDuplicate : null}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
{...props}
/>
);
@@ -55,18 +68,28 @@ class SpriteSelectorItem extends React.Component {
SpriteSelectorItem.propTypes = {
assetId: PropTypes.string,
costumeURL: PropTypes.string,
dispatchSetHoveredSprite: PropTypes.func.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
onClick: PropTypes.func,
onDeleteButtonClick: PropTypes.func,
onDuplicateButtonClick: PropTypes.func,
receivedBlocks: PropTypes.bool.isRequired,
selected: PropTypes.bool
};
const mapStateToProps = (state, {assetId, costumeURL}) => ({
costumeURL: costumeURL || (assetId && state.vm.runtime.storage.get(assetId).encodeDataURI())
const mapStateToProps = (state, {assetId, costumeURL, id}) => ({
costumeURL: costumeURL || (assetId && state.vm.runtime.storage.get(assetId).encodeDataURI()),
receivedBlocks: state.hoveredTarget.receivedBlocks &&
state.hoveredTarget.sprite === id
});
const mapDispatchToProps = dispatch => ({
dispatchSetHoveredSprite: spriteId => {
dispatch(setHoveredSprite(spriteId));
}
});
export default connect(
mapStateToProps
mapStateToProps,
mapDispatchToProps
)(SpriteSelectorItem);
+41 -2
View File
@@ -4,24 +4,57 @@ import React from 'react';
import {connect} from 'react-redux';
import {openBackdropLibrary} from '../reducers/modals';
import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab';
import StageSelectorComponent from '../components/stage-selector/stage-selector.jsx';
import backdropLibraryContent from '../lib/libraries/backdrops.json';
import costumeLibraryContent from '../lib/libraries/costumes.json';
class StageSelector extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleClick'
'handleClick',
'handleSurpriseBackdrop',
'handleEmptyBackdrop',
'addBackdropFromLibraryItem'
]);
}
addBackdropFromLibraryItem (item) {
const vmBackdrop = {
name: item.name,
rotationCenterX: item.info[0] && item.info[0] / 2,
rotationCenterY: item.info[1] && item.info[1] / 2,
bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
skinId: null
};
return this.props.vm.addBackdrop(item.md5, vmBackdrop);
}
handleClick (e) {
e.preventDefault();
this.props.onSelect(this.props.id);
}
handleSurpriseBackdrop () {
// @todo should this not add a backdrop you already have?
const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
this.addBackdropFromLibraryItem(item);
}
handleEmptyBackdrop () {
// @todo this is brittle, will need to be refactored for localized libraries
const emptyItem = costumeLibraryContent.find(item => item.name === 'Empty');
if (emptyItem) {
this.addBackdropFromLibraryItem(emptyItem).then(() => {
this.props.onActivateTab(COSTUMES_TAB_INDEX);
});
}
}
render () {
const {
/* eslint-disable no-unused-vars */
assetId,
id,
onActivateTab,
onSelect,
/* eslint-enable no-unused-vars */
...componentProps
@@ -29,6 +62,8 @@ class StageSelector extends React.Component {
return (
<StageSelectorComponent
onClick={this.handleClick}
onEmptyBackdropClick={this.handleEmptyBackdrop}
onSurpriseBackdropClick={this.handleSurpriseBackdrop}
{...componentProps}
/>
);
@@ -41,13 +76,17 @@ StageSelector.propTypes = {
};
const mapStateToProps = (state, {assetId}) => ({
url: assetId && state.vm.runtime.storage.get(assetId).encodeDataURI()
url: assetId && state.vm.runtime.storage.get(assetId).encodeDataURI(),
vm: state.vm
});
const mapDispatchToProps = dispatch => ({
onNewBackdropClick: e => {
e.preventDefault();
dispatch(openBackdropLibrary());
},
onActivateTab: tabIndex => {
dispatch(activateTab(tabIndex));
}
});
+10
View File
@@ -31,6 +31,7 @@ class Stage extends React.Component {
'onMouseDown',
'onStartDrag',
'onStopDrag',
'onWheel',
'updateRect',
'questionListener',
'setCanvas'
@@ -98,6 +99,7 @@ class Stage extends React.Component {
document.addEventListener('touchend', this.onMouseUp);
canvas.addEventListener('mousedown', this.onMouseDown);
canvas.addEventListener('touchstart', this.onMouseDown);
canvas.addEventListener('wheel', this.onWheel);
}
detachMouseEvents (canvas) {
document.removeEventListener('mousemove', this.onMouseMove);
@@ -106,6 +108,7 @@ class Stage extends React.Component {
document.removeEventListener('touchend', this.onMouseUp);
canvas.removeEventListener('mousedown', this.onMouseDown);
canvas.removeEventListener('touchstart', this.onMouseDown);
canvas.removeEventListener('wheel', this.onWheel);
}
attachRectEvents () {
window.addEventListener('resize', this.updateRect);
@@ -232,6 +235,13 @@ class Stage extends React.Component {
this.setState({colorInfo: null});
}
}
onWheel (e) {
const data = {
deltaX: e.deltaX,
deltaY: e.deltaY
};
this.props.vm.postIOData('mouseWheel', data);
}
cancelMouseDownTimeout () {
if (this.state.mouseDownTimeoutId !== null) {
clearTimeout(this.state.mouseDownTimeoutId);
+48 -2
View File
@@ -9,12 +9,17 @@ import {
closeSpriteLibrary
} from '../reducers/modals';
import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab';
import {setReceivedBlocks} from '../reducers/hovered-target';
import TargetPaneComponent from '../components/target-pane/target-pane.jsx';
import spriteLibraryContent from '../lib/libraries/sprites.json';
class TargetPane extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleBlockDragEnd',
'handleChangeSpriteDirection',
'handleChangeSpriteName',
'handleChangeSpriteSize',
@@ -23,9 +28,17 @@ class TargetPane extends React.Component {
'handleChangeSpriteY',
'handleDeleteSprite',
'handleDuplicateSprite',
'handleSelectSprite'
'handleSelectSprite',
'handleSurpriseSpriteClick',
'handlePaintSpriteClick'
]);
}
componentDidMount () {
this.props.vm.addListener('BLOCK_DRAG_END', this.handleBlockDragEnd);
}
componentWillUnmount () {
this.props.vm.removeListener('BLOCK_DRAG_END', this.handleBlockDragEnd);
}
handleChangeSpriteDirection (direction) {
this.props.vm.postSpriteInfo({direction});
}
@@ -53,10 +66,33 @@ class TargetPane extends React.Component {
handleSelectSprite (id) {
this.props.vm.setEditingTarget(id);
}
handleSurpriseSpriteClick () {
const item = spriteLibraryContent[Math.floor(Math.random() * spriteLibraryContent.length)];
this.props.vm.addSprite2(JSON.stringify(item.json));
}
handlePaintSpriteClick () {
// @todo this is brittle, will need to be refactored for localized libraries
const emptyItem = spriteLibraryContent.find(item => item.name === 'Empty');
if (emptyItem) {
this.props.vm.addSprite2(JSON.stringify(emptyItem.json)).then(() => {
this.props.onActivateTab(COSTUMES_TAB_INDEX);
});
}
}
handleBlockDragEnd (blocks) {
if (this.props.hoveredTarget.sprite && this.props.hoveredTarget.sprite !== this.props.editingTarget) {
this.props.vm.shareBlocksToTarget(blocks, this.props.hoveredTarget.sprite);
this.props.onReceivedBlocks(true);
}
}
render () {
const {
onActivateTab, // eslint-disable-line no-unused-vars
...componentProps
} = this.props;
return (
<TargetPaneComponent
{...this.props}
{...componentProps}
onChangeSpriteDirection={this.handleChangeSpriteDirection}
onChangeSpriteName={this.handleChangeSpriteName}
onChangeSpriteSize={this.handleChangeSpriteSize}
@@ -65,7 +101,9 @@ class TargetPane extends React.Component {
onChangeSpriteY={this.handleChangeSpriteY}
onDeleteSprite={this.handleDeleteSprite}
onDuplicateSprite={this.handleDuplicateSprite}
onPaintSpriteClick={this.handlePaintSpriteClick}
onSelectSprite={this.handleSelectSprite}
onSurpriseSpriteClick={this.handleSurpriseSpriteClick}
/>
);
}
@@ -82,6 +120,7 @@ TargetPane.propTypes = {
const mapStateToProps = state => ({
editingTarget: state.targets.editingTarget,
hoveredTarget: state.hoveredTarget,
sprites: Object.keys(state.targets.sprites).reduce((sprites, k) => {
let {direction, size, x, y, ...sprite} = state.targets.sprites[k];
if (typeof direction !== 'undefined') direction = Math.round(direction);
@@ -92,6 +131,7 @@ const mapStateToProps = state => ({
return sprites;
}, {}),
stage: state.targets.stage,
raiseSprites: state.blockDrag,
spriteLibraryVisible: state.modals.spriteLibrary,
backdropLibraryVisible: state.modals.backdropLibrary
});
@@ -105,6 +145,12 @@ const mapDispatchToProps = dispatch => ({
},
onRequestCloseBackdropLibrary: () => {
dispatch(closeBackdropLibrary());
},
onActivateTab: tabIndex => {
dispatch(activateTab(tabIndex));
},
onReceivedBlocks: receivedBlocks => {
dispatch(setReceivedBlocks(receivedBlocks));
}
});
+4
View File
@@ -18,4 +18,8 @@ $control-primary: #FFAB19;
$data-primary: #FF8C1A;
$pen-primary: #11B581;
$form-border: #E9EEF2;
$import-primary: #0FBD8C;
+4 -4
View File
@@ -36,15 +36,15 @@ export default function (vm) {
};
const costumesMenu = function () {
if (vm.editingTarget && vm.editingTarget.sprite.costumes.length > 0) {
return vm.editingTarget.sprite.costumes.map(costume => [costume.name, costume.name]);
if (vm.editingTarget && vm.editingTarget.getCostumes().length > 0) {
return vm.editingTarget.getCostumes().map(costume => [costume.name, costume.name]);
}
return [['', '']];
};
const backdropsMenu = function () {
if (vm.runtime.targets[0] && vm.runtime.targets[0].sprite.costumes.length > 0) {
return vm.runtime.targets[0].sprite.costumes.map(costume => [costume.name, costume.name])
if (vm.runtime.targets[0] && vm.runtime.targets[0].getCostumes().length > 0) {
return vm.runtime.targets[0].getCostumes().map(costume => [costume.name, costume.name])
.concat([['next backdrop', 'next backdrop'], ['previous backdrop', 'previous backdrop']]);
}
return [['', '']];
+2 -2
View File
@@ -37,10 +37,10 @@ const opcodeMap = {
labelFn: params => params.VARIABLE
},
data_listcontents: {
category: 'data',
category: 'list',
labelFn: params => params.LIST
},
// Sound
sound_volume: {
category: 'sound',
+12 -4
View File
@@ -6,6 +6,7 @@ import VM from 'scratch-vm';
import {connect} from 'react-redux';
import {updateEditingTarget, updateTargets} from '../reducers/targets';
import {updateBlockDrag} from '../reducers/block-drag';
import {updateMonitors} from '../reducers/monitors';
/*
@@ -29,7 +30,7 @@ const vmListenerHOC = function (WrappedComponent) {
// we need to start listening before mounting the wrapped component.
this.props.vm.on('targetsUpdate', this.props.onTargetsUpdate);
this.props.vm.on('MONITORS_UPDATE', this.props.onMonitorsUpdate);
this.props.vm.on('BLOCK_DRAG_UPDATE', this.props.onBlockDragUpdate);
}
componentDidMount () {
if (this.props.attachKeyboardEvents) {
@@ -74,6 +75,7 @@ const vmListenerHOC = function (WrappedComponent) {
const {
/* eslint-disable no-unused-vars */
attachKeyboardEvents,
onBlockDragUpdate,
onKeyDown,
onKeyUp,
onMonitorsUpdate,
@@ -86,17 +88,20 @@ const vmListenerHOC = function (WrappedComponent) {
}
VMListener.propTypes = {
attachKeyboardEvents: PropTypes.bool,
onBlockDragUpdate: PropTypes.func.isRequired,
onKeyDown: PropTypes.func,
onKeyUp: PropTypes.func,
onMonitorsUpdate: PropTypes.func,
onTargetsUpdate: PropTypes.func,
onMonitorsUpdate: PropTypes.func.isRequired,
onTargetsUpdate: PropTypes.func.isRequired,
vm: PropTypes.instanceOf(VM).isRequired
};
VMListener.defaultProps = {
attachKeyboardEvents: true
};
const mapStateToProps = state => ({
vm: state.vm
vm: state.vm,
hoveredSprite: state.hoveredTarget.sprite,
editingTarget: state.targets.editingTarget
});
const mapDispatchToProps = dispatch => ({
onTargetsUpdate: data => {
@@ -105,6 +110,9 @@ const vmListenerHOC = function (WrappedComponent) {
},
onMonitorsUpdate: monitorList => {
dispatch(updateMonitors(monitorList));
},
onBlockDragUpdate: areBlocksOverGui => {
dispatch(updateBlockDrag(areBlocksOverGui));
}
});
return connect(
+28
View File
@@ -0,0 +1,28 @@
const BLOCK_DRAG_UPDATE = 'scratch-gui/block-drag/BLOCK_DRAG_UPDATE';
const initialState = false;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case BLOCK_DRAG_UPDATE:
return action.areBlocksOverGui;
default:
return state;
}
};
const updateBlockDrag = function (areBlocksOverGui) {
return {
type: BLOCK_DRAG_UPDATE,
areBlocksOverGui: areBlocksOverGui,
meta: {
throttle: 30
}
};
};
export {
reducer as default,
updateBlockDrag
};
+37
View File
@@ -0,0 +1,37 @@
const ACTIVATE_TAB = 'scratch-gui/navigation/ACTIVATE_TAB';
// Constants use numbers to make it easier to work with react-tabs
const BLOCKS_TAB_INDEX = 0;
const COSTUMES_TAB_INDEX = 1;
const SOUNDS_TAB_INDEX = 2;
const initialState = {
activeTabIndex: BLOCKS_TAB_INDEX
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case ACTIVATE_TAB:
return Object.assign({}, state, {
activeTabIndex: action.activeTabIndex
});
default:
return state;
}
};
const activateTab = function (tab) {
return {
type: ACTIVATE_TAB,
activeTabIndex: tab
};
};
export {
reducer as default,
activateTab,
BLOCKS_TAB_INDEX,
COSTUMES_TAB_INDEX,
SOUNDS_TAB_INDEX
};
+6
View File
@@ -1,6 +1,9 @@
import {combineReducers} from 'redux';
import colorPickerReducer from './color-picker';
import customProceduresReducer from './custom-procedures';
import blockDragReducer from './block-drag';
import editorTabReducer from './editor-tab';
import hoveredTargetReducer from './hovered-target';
import intlReducer from './intl';
import modalReducer from './modals';
import monitorReducer from './monitors';
@@ -12,8 +15,11 @@ import stageSizeReducer from './stage-size';
import {ScratchPaintReducer} from 'scratch-paint';
export default combineReducers({
blockDrag: blockDragReducer,
colorPicker: colorPickerReducer,
customProcedures: customProceduresReducer,
editorTab: editorTabReducer,
hoveredTarget: hoveredTargetReducer,
intl: intlReducer,
stageSize: stageSizeReducer,
modals: modalReducer,
+48
View File
@@ -0,0 +1,48 @@
const SET_HOVERED_SPRITE = 'scratch-gui/hovered-target/SET_HOVERED_SPRITE';
const SET_RECEIVED_BLOCKS = 'scratch-gui/hovered-target/SET_RECEIVED_BLOCKS';
const initialState = {
sprite: null,
receivedBlocks: false
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case SET_HOVERED_SPRITE:
return {
sprite: action.spriteId,
receivedBlocks: false
};
case SET_RECEIVED_BLOCKS:
return {
sprite: state.sprite,
receivedBlocks: action.receivedBlocks
};
default:
return state;
}
};
const setHoveredSprite = function (spriteId) {
return {
type: SET_HOVERED_SPRITE,
spriteId: spriteId,
meta: {
throttle: 30
}
};
};
const setReceivedBlocks = function (receivedBlocks) {
return {
type: SET_RECEIVED_BLOCKS,
receivedBlocks: receivedBlocks
};
};
export {
reducer as default,
setHoveredSprite,
setReceivedBlocks
};
+15 -4
View File
@@ -7,6 +7,7 @@ const MODAL_BACKDROP_LIBRARY = 'backdropLibrary';
const MODAL_COSTUME_LIBRARY = 'costumeLibrary';
const MODAL_EXTENSION_LIBRARY = 'extensionLibrary';
const MODAL_FEEDBACK_FORM = 'feedbackForm';
const MODAL_IMPORT_INFO = 'importInfo';
const MODAL_PREVIEW_INFO = 'previewInfo';
const MODAL_SOUND_LIBRARY = 'soundLibrary';
const MODAL_SPRITE_LIBRARY = 'spriteLibrary';
@@ -18,6 +19,7 @@ const initialState = {
[MODAL_COSTUME_LIBRARY]: false,
[MODAL_EXTENSION_LIBRARY]: false,
[MODAL_FEEDBACK_FORM]: false,
[MODAL_IMPORT_INFO]: false,
[MODAL_PREVIEW_INFO]: true,
[MODAL_SOUND_LIBRARY]: false,
[MODAL_SPRITE_LIBRARY]: false,
@@ -67,6 +69,14 @@ const openFeedbackForm = function () {
analytics.pageview('/modals/feedback');
return openModal(MODAL_FEEDBACK_FORM);
};
const openImportInfo = function () {
analytics.pageview('modals/import');
return openModal(MODAL_IMPORT_INFO);
};
const openPreviewInfo = function () {
analytics.pageview('/modals/preview');
return openModal(MODAL_PREVIEW_INFO);
};
const openSoundLibrary = function () {
analytics.pageview('/libraries/sounds');
return openModal(MODAL_SOUND_LIBRARY);
@@ -79,10 +89,6 @@ const openSoundRecorder = function () {
analytics.pageview('/modals/microphone');
return openModal(MODAL_SOUND_RECORDER);
};
const openPreviewInfo = function () {
analytics.pageview('/modals/preview');
return openModal(MODAL_PREVIEW_INFO);
};
const closeBackdropLibrary = function () {
return closeModal(MODAL_BACKDROP_LIBRARY);
};
@@ -95,6 +101,9 @@ const closeExtensionLibrary = function () {
const closeFeedbackForm = function () {
return closeModal(MODAL_FEEDBACK_FORM);
};
const closeImportInfo = function () {
return closeModal(MODAL_IMPORT_INFO);
};
const closePreviewInfo = function () {
return closeModal(MODAL_PREVIEW_INFO);
};
@@ -113,6 +122,7 @@ export {
openCostumeLibrary,
openExtensionLibrary,
openFeedbackForm,
openImportInfo,
openPreviewInfo,
openSoundLibrary,
openSpriteLibrary,
@@ -121,6 +131,7 @@ export {
closeCostumeLibrary,
closeExtensionLibrary,
closeFeedbackForm,
closeImportInfo,
closePreviewInfo,
closeSpriteLibrary,
closeSoundLibrary,
+1
View File
@@ -25,6 +25,7 @@ class SeleniumHelper {
// List of useful xpath scopes for finding elements
return {
blocksTab: "*[@id='react-tabs-1']",
costumesTab: "*[@id='react-tabs-3']",
modal: '*[@class="ReactModalPortal"]',
reportedValue: '*[@class="blocklyDropDownContent"]',
soundsTab: "*[@id='react-tabs-5']",
+21 -3
View File
@@ -7,7 +7,9 @@ const {
findByXpath,
getDriver,
getLogs,
loadUri
loadUri,
rightClickText,
scope
} = new SeleniumHelper();
const uri = path.resolve(__dirname, '../../build/index.html');
@@ -27,7 +29,7 @@ describe('Working with costumes', () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
await clickText('Costumes');
await clickXpath('//button[@title="Add Costume"]');
await clickXpath('//button[@aria-label="Costume Library"]');
const el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('abb');
await clickText('Abby-a'); // Should close the modal, then click the costumes in the selector
@@ -36,10 +38,26 @@ describe('Working with costumes', () => {
await expect(logs).toEqual([]);
});
test('Duplicating a costume', async () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
await clickText('Costumes');
await rightClickText('costume1', scope.costumesTab);
await clickText('duplicate', scope.costumesTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for duplication to finish
// Make sure the duplicated costume is named correctly.
await clickText('costume3', scope.costumesTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Adding a backdrop', async () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
await clickXpath('//button[@title="Add Backdrop"]');
await clickXpath('//button[@aria-label="Backdrop Library"]');
const el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('blue');
await clickText('Blue Sky'); // Should close the modal
+35 -1
View File
@@ -4,6 +4,7 @@ import SeleniumHelper from '../helpers/selenium-helper';
const {
clickText,
clickXpath,
findByXpath,
getDriver,
getLogs,
loadUri
@@ -31,7 +32,40 @@ describe('Loading scratch gui', () => {
describe('Loading projects by ID', () => {
test('Load a project by ID', async () => {
test('Load 2.0 project using import modal', async () => {
await loadUri(uri);
await clickText('View 2.0 Project');
const el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']");
const projectId = '96708228';
await el.sendKeys(`scratch.mit.edu/projects/${projectId}`);
await clickXpath('//button[@title="viewproject"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Stop"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Invalid url when loading project through modal lets you try again', async () => {
await loadUri(uri);
await clickText('View 2.0 Project');
let el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']");
await el.sendKeys('thisisnotaurl');
await clickXpath('//button[@title="viewproject"]');
el = await findByXpath("//input[@placeholder='scratch.mit.edu/projects/123456789']");
await el.clear();
await el.sendKeys('scratch.mit.edu/projects/96708228');
await clickXpath('//button[@title="viewproject"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Go"]');
await new Promise(resolve => setTimeout(resolve, 2000));
await clickXpath('//img[@title="Stop"]');
const logs = await getLogs();
await expect(logs).toEqual([]);
});
test('Load a project by ID directly through url', async () => {
const projectId = '96708228';
await loadUri(`${uri}#${projectId}`);
await clickXpath('//button[@title="tryit"]');
+20 -4
View File
@@ -37,13 +37,13 @@ describe('Working with sounds', () => {
.accept();
// Add it back
await clickXpath('//button[@title="Add Sound"]');
await clickXpath('//button[@aria-label="Sound Library"]');
let el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('meow');
await clickText('Meow', scope.modal); // Should close the modal
// Add a new sound
await clickXpath('//button[@title="Add Sound"]');
await clickXpath('//button[@aria-label="Sound Library"]');
el = await findByXpath("//input[@placeholder='what are you looking for?']");
await el.sendKeys('chom');
await clickText('Chomp'); // Should close the modal, then click the sounds in the selector
@@ -63,6 +63,22 @@ describe('Working with sounds', () => {
await expect(logs).toEqual([]);
});
test('Duplicating a sound', async () => {
await loadUri(uri);
await clickXpath('//button[@title="tryit"]');
await clickText('Sounds');
await rightClickText('Meow', scope.soundsTab);
await clickText('duplicate', scope.soundsTab);
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error
// Make sure the duplicated sound is named correctly.
await clickText('Meow2', scope.soundsTab);
const logs = await getLogs();
await expect(logs).toEqual([]);
});
// Regression test for gui issue #1320
test('Switching sprites with different numbers of sounds', async () => {
await loadUri(uri);
@@ -70,11 +86,11 @@ describe('Working with sounds', () => {
// Add a sound so this sprite has 2 sounds.
await clickText('Sounds');
await clickXpath('//button[@title="Add Sound"]');
await clickXpath('//button[@aria-label="Sound Library"]');
await clickText('A Bass'); // Closes the modal
// Now add a sprite with only one sound.
await clickXpath('//button[@title="Add Sprite"]');
await clickXpath('//button[@aria-label="Sprite Library"]');
await clickText('Abby'); // Doing this used to crash the editor.
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error
@@ -6,6 +6,8 @@ exports[`SpriteSelectorItemComponent matches snapshot when selected 1`] = `
onClick={[Function]}
onContextMenu={[Function]}
onMouseDown={[Function]}
onMouseEnter={undefined}
onMouseLeave={undefined}
onMouseOut={[Function]}
onMouseUp={[Function]}
onTouchEnd={[Function]}
@@ -12,6 +12,7 @@ describe('SpriteSelectorItem Container', () => {
let costumeURL;
let name;
let onClick;
let dispatchSetHoveredSprite;
let onDeleteButtonClick;
let selected;
let id;
@@ -23,6 +24,7 @@ describe('SpriteSelectorItem Container', () => {
<SpriteSelectorItem
className={className}
costumeURL={costumeURL}
dispatchSetHoveredSprite={dispatchSetHoveredSprite}
id={id}
name={name}
selected={selected}
@@ -34,13 +36,14 @@ describe('SpriteSelectorItem Container', () => {
};
beforeEach(() => {
store = mockStore();
store = mockStore({hoveredTarget: {receivedBlocks: false, sprite: null}});
className = 'ponies';
costumeURL = 'https://scratch.mit.edu/foo/bar/pony';
id = 1337;
name = 'Pony sprite';
onClick = jest.fn();
onDeleteButtonClick = jest.fn();
dispatchSetHoveredSprite = jest.fn();
selected = true;
// Mock window.confirm() which is called when the close button is clicked.
global.confirm = jest.fn(() => true);
-4
View File
@@ -111,10 +111,6 @@ module.exports = {
from: 'node_modules/scratch-blocks/media',
to: 'static/blocks-media'
}]),
new CopyWebpackPlugin([{
from: 'node_modules/scratch-vm/dist/node/assets',
to: 'static/extension-assets'
}]),
new CopyWebpackPlugin([{
from: 'extensions/**',
to: 'static',