Merge branch 'develop' into editor-cloud-disconnect

This commit is contained in:
Karishma Chadha authored and GitHub committed 2018-11-28 18:27:32 -05:00
commit 9fc6d85ce6
70 files changed
+918 -360

No files matched your search

+4 -4
View File
@@ -102,12 +102,12 @@
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20181023202904",
"scratch-blocks": "0.1.0-prerelease.1542722191",
"scratch-l10n": "3.0.20181115134359",
"scratch-paint": "0.2.0-prerelease.20181120191526",
"scratch-render": "0.1.0-prerelease.20181126213837",
"scratch-l10n": "3.0.20181121224721",
"scratch-paint": "0.2.0-prerelease.20181128182047",
"scratch-render": "0.1.0-prerelease.20181127194508",
"scratch-storage": "1.2.0",
"scratch-svg-renderer": "0.2.0-prerelease.20181126212715",
"scratch-vm": "0.2.0-prerelease.20181121182825",
"scratch-vm": "0.2.0-prerelease.20181128224414",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
@@ -78,6 +78,9 @@ class ActionMenu extends React.Component {
return event => {
ReactTooltip.hide();
if (fn) fn(event);
// Blur the button so it does not keep focus after being clicked
// This prevents keyboard events from triggering the button
this.buttonRef.blur();
this.setState({forceHide: true, isOpen: false}, () => {
setTimeout(() => this.setState({forceHide: false}));
});
+15 -4
View File
@@ -7,7 +7,7 @@
display: flex;
flex-direction: row;
overflow: hidden;
align-items: left;
justify-content: space-between;
border-radius: $space;
padding: $space;
margin-bottom: 7px;
@@ -37,13 +37,17 @@
font-weight: bold;
font-size: .75rem;
line-height: 14px;
width: 100%;
display: flex;
align-items: center;
padding-left: .5rem;
padding-right: .5rem;
}
.alert-buttons {
display: flex;
flex-direction: row;
}
.alert-close-button {
outline-style:none;
}
@@ -55,7 +59,7 @@
align-self: center;
}
.connection-button {
.alert-connection-button {
min-height: 2rem;
padding: 0.55rem 0.9rem;
border-radius: 0.35rem;
@@ -68,6 +72,13 @@
display: flex;
align-items: center;
align-self: center;
margin-right: 13px;
outline-style:none;
}
[dir="ltr"] .alert-connection-button {
margin-right: 13px;
}
[dir="rtl"] .alert-connection-button {
margin-left: 13px;
}
+60 -26
View File
@@ -22,7 +22,11 @@ const AlertComponent = ({
iconSpinner,
iconURL,
level,
showDownload,
showSaveNow,
onCloseAlert,
onDownload,
onSaveNow,
onReconnect,
showReconnect
}) => (
@@ -42,7 +46,7 @@ const AlertComponent = ({
<div className={styles.alertMessage}>
{extensionName ? (
<FormattedMessage
defaultMessage="Scratch lost connection to {extensionName}"
defaultMessage="Scratch lost connection to {extensionName}."
description="Message indicating that an extension peripheral has been disconnected"
id="gui.alerts.lostPeripheralConnection"
values={{
@@ -53,30 +57,56 @@ const AlertComponent = ({
/>
) : content}
</div>
{showReconnect && (
<button
className={styles.connectionButton}
onClick={onReconnect}
>
<FormattedMessage
defaultMessage="Reconnect"
description="Button to reconnect the device"
id="gui.connection.reconnect"
/>
</button>
)}
{closeButton && (
<Box
className={styles.alertCloseButtonContainer}
>
<CloseButton
className={classNames(styles.alertCloseButton)}
color={closeButtonColors[level]}
size={CloseButton.SIZE_LARGE}
onClick={onCloseAlert}
/>
</Box>
)}
<div className={styles.alertButtons}>
{showSaveNow && (
<button
className={styles.alertConnectionButton}
onClick={onSaveNow}
>
<FormattedMessage
defaultMessage="Try Again"
description="Button to try saving again"
id="gui.alerts.tryAgain"
/>
</button>
)}
{showDownload && (
<button
className={styles.alertConnectionButton}
onClick={onDownload}
>
<FormattedMessage
defaultMessage="Download"
description="Button to download project locally"
id="gui.alerts.download"
/>
</button>
)}
{showReconnect && (
<button
className={styles.alertConnectionButton}
onClick={onReconnect}
>
<FormattedMessage
defaultMessage="Reconnect"
description="Button to reconnect the device"
id="gui.connection.reconnect"
/>
</button>
)}
{closeButton && (
<Box
className={styles.alertCloseButtonContainer}
>
<CloseButton
className={classNames(styles.alertCloseButton)}
color={closeButtonColors[level]}
size={CloseButton.SIZE_LARGE}
onClick={onCloseAlert}
/>
</Box>
)}
</div>
</Box>
);
@@ -88,8 +118,12 @@ AlertComponent.propTypes = {
iconURL: PropTypes.string,
level: PropTypes.string,
onCloseAlert: PropTypes.func.isRequired,
onDownload: PropTypes.func,
onReconnect: PropTypes.func,
showReconnect: PropTypes.bool
onSaveNow: PropTypes.func,
showDownload: PropTypes.func,
showReconnect: PropTypes.bool,
showSaveNow: PropTypes.bool
};
AlertComponent.defaultProps = {
+2
View File
@@ -25,7 +25,9 @@ const AlertsComponent = ({
key={index}
level={a.level}
message={a.message}
showDownload={a.showDownload}
showReconnect={a.showReconnect}
showSaveNow={a.showSaveNow}
onCloseAlert={onCloseAlert}
/>
))}
@@ -63,7 +63,7 @@ const AutoScanningStep = props => (
</div>
</Box>
<Box className={styles.bottomArea}>
<Box className={styles.instructions}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
{props.phase === PHASES.prescan && (
<FormattedMessage
defaultMessage="Have your device nearby, then begin searching."
@@ -80,60 +80,63 @@ const AutoScanningStep = props => (
)}
</Box>
<Dots
className={styles.bottomAreaItem}
counter={0}
total={3}
/>
{props.phase === PHASES.prescan && (
<button
className={styles.connectionButton}
onClick={props.onStartScan}
>
<FormattedMessage
defaultMessage="Start Searching"
description="Button in prompt for starting a search"
id="gui.connection.auto-scanning.start-search"
/>
</button>
)}
{props.phase === PHASES.pressbutton && (
<div className={styles.segmentedButton}>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
{props.phase === PHASES.prescan && (
<button
disabled
className={styles.connectionButton}
onClick={props.onStartScan}
>
<FormattedMessage
defaultMessage="Searching..."
description="Label indicating that search is in progress"
id="gui.connection.connecting-searchbutton"
defaultMessage="Start Searching"
description="Button in prompt for starting a search"
id="gui.connection.auto-scanning.start-search"
/>
</button>
)}
{props.phase === PHASES.pressbutton && (
<div className={styles.segmentedButton}>
<button
disabled
className={styles.connectionButton}
>
<FormattedMessage
defaultMessage="Searching..."
description="Label indicating that search is in progress"
id="gui.connection.connecting-searchbutton"
/>
</button>
<button
className={styles.connectionButton}
onClick={props.onRefresh}
>
<img
className={styles.abortConnectingIcon}
src={closeIcon}
/>
</button>
</div>
)}
{props.phase === PHASES.notfound && (
<button
className={styles.connectionButton}
onClick={props.onRefresh}
>
<img
className={styles.abortConnectingIcon}
src={closeIcon}
className={styles.buttonIconLeft}
src={backIcon}
/>
<FormattedMessage
defaultMessage="Try again"
description="Button in prompt for trying a device search again"
id="gui.connection.auto-scanning.try-again"
/>
</button>
</div>
)}
{props.phase === PHASES.notfound && (
<button
className={styles.connectionButton}
onClick={props.onRefresh}
>
<img
className={styles.buttonIconLeft}
src={backIcon}
/>
<FormattedMessage
defaultMessage="Try again"
description="Button in prompt for trying a device search again"
id="gui.connection.auto-scanning.try-again"
/>
</button>
)}
)}
</Box>
</Box>
</Box>
);
@@ -25,7 +25,7 @@ const ConnectedStep = props => (
</Box>
</Box>
<Box className={styles.bottomArea}>
<Box className={styles.instructions}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
<FormattedMessage
defaultMessage="Connected"
description="Message indicating that a device was connected"
@@ -34,9 +34,10 @@ const ConnectedStep = props => (
</Box>
<Dots
success
className={styles.bottomAreaItem}
total={3}
/>
<div className={styles.cornerButtons}>
<div className={classNames(styles.bottomAreaItem, styles.cornerButtons)}>
<button
className={classNames(styles.redButton, styles.connectionButton)}
onClick={props.onDisconnect}
@@ -1,6 +1,7 @@
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import Box from '../box/box.jsx';
import Dots from './dots.jsx';
@@ -27,14 +28,15 @@ const ConnectingStep = props => (
</Box>
</Box>
<Box className={styles.bottomArea}>
<Box className={styles.instructions}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
{props.connectingMessage}
</Box>
<Dots
className={styles.bottomAreaItem}
counter={1}
total={3}
/>
<div className={styles.segmentedButton}>
<div className={classNames(styles.bottomAreaItem, styles.segmentedButton)}>
<button
disabled
className={styles.connectionButton}
@@ -29,7 +29,6 @@
overflow-y: auto;
width: 100%;
height: 100%;
padding: 0.5rem;
}
.peripheral-tile {
@@ -225,6 +224,7 @@
display: flex;
justify-content: center;
align-items: center;
padding: .5rem;
}
.scratch-link-help {
@@ -232,7 +232,8 @@
flex-direction: column;
justify-content: space-around;
height: 100%;
padding: 1rem 0;
padding-top: .5rem;
padding-bottom: .5rem;
}
.scratch-link-help-step {
@@ -243,11 +244,11 @@
}
[dir="ltr"] .scratch-link-help-step {
margin-left: 3rem;
margin-left: 2.5rem;
}
[dir="rtl"] .scratch-link-help-step {
margin-right: 3rem;
margin-right: 2.5rem;
}
.scratch-link-icon {
@@ -364,12 +365,18 @@
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 12px;
padding-top: 1rem;
padding-bottom: .75rem;
padding-left: .75rem;
padding-right: .75rem;
}
.bottom-area .bottom-area-item+.bottom-area-item {
margin-top: 1rem;
}
.instructions {
text-align: center;
margin-top: 1rem;
}
.dots-row {
@@ -377,7 +384,6 @@
flex-direction: row;
justify-content: center;
align-items: center;
margin: 1rem;
}
.dots-holder {
+7 -1
View File
@@ -6,7 +6,12 @@ import Box from '../box/box.jsx';
import styles from './connection-modal.css';
const Dots = props => (
<Box className={styles.dotsRow}>
<Box
className={classNames(
props.className,
styles.dotsRow
)}
>
<div
className={classNames(
styles.dotsHolder,
@@ -32,6 +37,7 @@ const Dots = props => (
);
Dots.propTypes = {
className: PropTypes.string,
counter: PropTypes.number,
error: PropTypes.bool,
success: PropTypes.bool,
@@ -23,7 +23,7 @@ const ErrorStep = props => (
</Box>
</Box>
<Box className={styles.bottomArea}>
<div className={styles.instructions}>
<div className={classNames(styles.bottomAreaItem, styles.instructions)}>
<FormattedMessage
defaultMessage="Oops, looks like something went wrong."
description="The device connection process has encountered an error."
@@ -32,9 +32,10 @@ const ErrorStep = props => (
</div>
<Dots
error
className={styles.bottomAreaItem}
total={3}
/>
<Box className={styles.buttonRow}>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
<button
className={styles.connectionButton}
onClick={props.onScanning}
@@ -55,7 +55,7 @@ const ScanningStep = props => (
)}
</Box>
<Box className={styles.bottomArea}>
<Box className={styles.instructions}>
<Box className={classNames(styles.bottomAreaItem, styles.instructions)}>
<FormattedMessage
defaultMessage="Select your device in the list above."
description="Prompt for choosing a device to connect to"
@@ -63,11 +63,12 @@ const ScanningStep = props => (
/>
</Box>
<Dots
className={styles.bottomAreaItem}
counter={0}
total={3}
/>
<button
className={styles.connectionButton}
className={classNames(styles.bottomAreaItem, styles.connectionButton)}
onClick={props.onRefresh}
>
<FormattedMessage
@@ -57,9 +57,10 @@ const UnavailableStep = props => (
<Box className={styles.bottomArea}>
<Dots
error
className={styles.bottomAreaItem}
total={3}
/>
<Box className={styles.buttonRow}>
<Box className={classNames(styles.bottomAreaItem, styles.buttonRow)}>
<button
className={styles.connectionButton}
onClick={props.onScanning}
-13
View File
@@ -196,19 +196,6 @@
opacity: 0.5;
}
.save-in-progress {
animation: hue-rotate 3s linear infinite;
}
@keyframes hue-rotate {
from {
filter: hue-rotate();
}
to {
filter: hue-rotate(360deg);
}
}
.mystuff > a {
background-repeat: no-repeat;
background-position: center center;
+1 -2
View File
@@ -285,8 +285,7 @@ class MenuBar extends React.Component {
<Box
className={classNames(
this.props.className,
styles.menuBar,
{[styles.saveInProgress]: this.props.isUpdating}
styles.menuBar
)}
>
<div className={styles.mainMenu}>
+34 -1
View File
@@ -89,7 +89,10 @@ to adjust for the border using a different method */
padding-bottom: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width);
}
.monitor-wrapper, .color-picker-wrapper, .frame-wrapper {
.monitor-wrapper,
.color-picker-wrapper,
.frame-wrapper,
.green-flag-overlay-wrapper {
position: absolute;
top: 0;
left: 0;
@@ -138,6 +141,36 @@ to adjust for the border using a different method */
animation-fill-mode: forwards; /* Leave at 0 opacity after animation */
}
.green-flag-overlay-wrapper {
display: flex;
justify-content: center;
align-items: center;
background: rgba(0,0,0,0.25);
border-radius: 0.5rem;
}
.green-flag-overlay {
padding: 1rem;
border-radius: 100%;
background: rgba(255,255,255,0.75);
border: 3px solid $ui-white;
pointer-events: all;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
width: 5rem;
height: 5rem;
}
.green-flag-overlay > img {
width: 100%;
object-fit: contain;
}
@keyframes flash {
0% { opacity: 1; }
100% { opacity: 0; }
+10
View File
@@ -7,6 +7,7 @@ import DOMElementRenderer from '../../containers/dom-element-renderer.jsx';
import Loupe from '../loupe/loupe.jsx';
import MonitorList from '../../containers/monitor-list.jsx';
import TargetHighlight from '../../containers/target-highlight.jsx';
import GreenFlagOverlay from '../../containers/green-flag-overlay.jsx';
import Question from '../../containers/question.jsx';
import MicIndicator from '../mic-indicator/mic-indicator.jsx';
import {STAGE_DISPLAY_SIZES} from '../../lib/layout-constants.js';
@@ -19,6 +20,7 @@ const StageComponent = props => {
dragRef,
isColorPicking,
isFullScreen,
isStarted,
colorInfo,
micIndicator,
question,
@@ -71,6 +73,13 @@ const StageComponent = props => {
stageWidth={stageDimensions.width}
/>
</Box>
{isStarted ? null : (
<Box className={styles.greenFlagOverlayWrapper}>
<GreenFlagOverlay
className={styles.greenFlagOverlay}
/>
</Box>
)}
{isColorPicking && colorInfo ? (
<Box className={styles.colorPickerWrapper}>
<Loupe colorInfo={colorInfo} />
@@ -125,6 +134,7 @@ StageComponent.propTypes = {
dragRef: PropTypes.func,
isColorPicking: PropTypes.bool,
isFullScreen: PropTypes.bool.isRequired,
isStarted: PropTypes.bool,
micIndicator: PropTypes.bool,
onDeactivateColorPicker: PropTypes.func,
onDoubleClick: PropTypes.func,
+31 -15
View File
@@ -2,10 +2,11 @@ import React from 'react';
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import SB3Downloader from './sb3-downloader.jsx';
import AlertComponent from '../components/alerts/alert.jsx';
import {openConnectionModal} from '../reducers/modals';
import {setConnectionModalExtensionId} from '../reducers/connection-modal';
import {manualUpdateProject} from '../reducers/project-state';
class Alert extends React.Component {
constructor (props) {
@@ -32,21 +33,30 @@ class Alert extends React.Component {
iconSpinner,
iconURL,
message,
showReconnect
onSaveNow,
showDownload,
showReconnect,
showSaveNow
} = this.props;
return (
<AlertComponent
closeButton={closeButton}
content={content}
extensionName={extensionName}
iconSpinner={iconSpinner}
iconURL={iconURL}
level={level}
message={message}
showReconnect={showReconnect}
onCloseAlert={this.handleOnCloseAlert}
onReconnect={this.handleOnReconnect}
/>
<SB3Downloader>{(_, downloadProject) => (
<AlertComponent
closeButton={closeButton}
content={content}
extensionName={extensionName}
iconSpinner={iconSpinner}
iconURL={iconURL}
level={level}
message={message}
showDownload={showDownload}
showReconnect={showReconnect}
showSaveNow={showSaveNow}
onCloseAlert={this.handleOnCloseAlert}
onDownload={downloadProject}
onReconnect={this.handleOnReconnect}
onSaveNow={onSaveNow}
/>
)}</SB3Downloader>
);
}
}
@@ -57,6 +67,9 @@ const mapDispatchToProps = dispatch => ({
onOpenConnectionModal: id => {
dispatch(setConnectionModalExtensionId(id));
dispatch(openConnectionModal());
},
onSaveNow: () => {
dispatch(manualUpdateProject());
}
});
@@ -72,7 +85,10 @@ Alert.propTypes = {
message: PropTypes.string,
onCloseAlert: PropTypes.func.isRequired,
onOpenConnectionModal: PropTypes.func,
showReconnect: PropTypes.bool
onSaveNow: PropTypes.func,
showDownload: PropTypes.bool,
showReconnect: PropTypes.bool,
showSaveNow: PropTypes.bool
};
export default connect(
+4 -1
View File
@@ -328,7 +328,10 @@ class Blocks extends React.Component {
error.message = `Workspace Update Error: ${error.message}`;
log.error(error);
}
this.workspace.addChangeListener(this.props.vm.blockListener);
// All of the changes that happened during the load above are queued with
// timeouts, so re-enable the listener in the next tick, so it happens after
// the events are already fired.
setTimeout(() => this.workspace.addChangeListener(this.props.vm.blockListener));
if (this.props.vm.editingTarget && this.state.workspaceMetrics[this.props.vm.editingTarget.id]) {
const {scrollX, scrollY, scale} = this.state.workspaceMetrics[this.props.vm.editingTarget.id];
+6
View File
@@ -20,6 +20,9 @@ class Controls extends React.Component {
if (e.shiftKey) {
this.props.vm.setTurboMode(!this.props.turbo);
} else {
if (!this.props.isStarted) {
this.props.vm.start();
}
this.props.vm.greenFlag();
analytics.event({
category: 'general',
@@ -38,6 +41,7 @@ class Controls extends React.Component {
render () {
const {
vm, // eslint-disable-line no-unused-vars
isStarted, // eslint-disable-line no-unused-vars
projectRunning,
turbo,
...props
@@ -55,12 +59,14 @@ class Controls extends React.Component {
}
Controls.propTypes = {
isStarted: PropTypes.bool.isRequired,
projectRunning: PropTypes.bool.isRequired,
turbo: PropTypes.bool.isRequired,
vm: PropTypes.instanceOf(VM)
};
const mapStateToProps = state => ({
isStarted: state.scratchGui.vmStatus.running,
projectRunning: state.scratchGui.vmStatus.running,
turbo: state.scratchGui.vmStatus.turbo
});
+55
View File
@@ -0,0 +1,55 @@
import bindAll from 'lodash.bindall';
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import VM from 'scratch-vm';
import greenFlag from '../components/green-flag/icon--green-flag.svg';
class GreenFlagOverlay extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleClick'
]);
}
handleClick () {
this.props.vm.start();
this.props.vm.greenFlag();
}
render () {
if (this.props.isStarted) return null;
return (
<div
className={this.props.className}
onClick={this.handleClick}
>
<img
draggable={false}
src={greenFlag}
/>
</div>
);
}
}
GreenFlagOverlay.propTypes = {
className: PropTypes.string,
isStarted: PropTypes.bool,
vm: PropTypes.instanceOf(VM)
};
const mapStateToProps = state => ({
isStarted: state.scratchGui.vmStatus.started,
vm: state.scratchGui.vm
});
const mapDispatchToProps = () => ({});
export default connect(
mapStateToProps,
mapDispatchToProps
)(GreenFlagOverlay);
-60
View File
@@ -1,60 +0,0 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import VM from 'scratch-vm';
import GreenFlagComponent from '../components/green-flag/green-flag.jsx';
class GreenFlag extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleClick',
'onProjectRunStart',
'onProjectRunStop'
]);
this.state = {projectRunning: false};
}
componentDidMount () {
this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
}
componentWillUnmount () {
this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
}
onProjectRunStart () {
this.setState({projectRunning: true});
}
onProjectRunStop () {
this.setState({projectRunning: false});
}
handleClick (e) {
e.preventDefault();
if (e.shiftKey) {
this.props.vm.setTurboMode(!this.props.vm.runtime.turboMode);
} else {
this.props.vm.greenFlag();
}
}
render () {
const {
vm, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
<GreenFlagComponent
active={this.state.projectRunning}
onClick={this.handleClick}
{...props}
/>
);
}
}
GreenFlag.propTypes = {
vm: PropTypes.instanceOf(VM)
};
export default GreenFlag;
+9 -1
View File
@@ -59,6 +59,12 @@ class Stage extends React.Component {
this.canvas = document.createElement('canvas');
this.renderer = new Renderer(this.canvas);
this.props.vm.attachRenderer(this.renderer);
// Calling draw a single time before any project is loaded just makes
// the canvas white instead of solid black–needed because it is not
// possible to use CSS to style the canvas to have a different
// default color
this.props.vm.renderer.draw();
}
this.props.vm.attachV2SVGAdapter(new V2SVGAdapter());
this.props.vm.attachV2BitmapAdapter(new V2BitmapAdapter());
@@ -76,7 +82,8 @@ class Stage extends React.Component {
this.state.colorInfo !== nextState.colorInfo ||
this.props.isFullScreen !== nextProps.isFullScreen ||
this.state.question !== nextState.question ||
this.props.micIndicator !== nextProps.micIndicator;
this.props.micIndicator !== nextProps.micIndicator ||
this.props.isStarted !== nextProps.isStarted;
}
componentDidUpdate (prevProps) {
if (this.props.isColorPicking && !prevProps.isColorPicking) {
@@ -404,6 +411,7 @@ Stage.defaultProps = {
const mapStateToProps = state => ({
isColorPicking: state.scratchGui.colorPicker.active,
isFullScreen: state.scratchGui.mode.isFullScreen,
isStarted: state.scratchGui.vmStatus.started,
micIndicator: state.scratchGui.micIndicator,
// Do not use editor drag style in fullscreen or player mode.
useEditorDragStyle: !(state.scratchGui.mode.isFullScreen || state.scratchGui.mode.isPlayerOnly)
-55
View File
@@ -1,55 +0,0 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import VM from 'scratch-vm';
import StopAllComponent from '../components/stop-all/stop-all.jsx';
class StopAll extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleClick',
'onProjectRunStart',
'onProjectRunStop'
]);
this.state = {projectRunning: false};
}
componentDidMount () {
this.props.vm.addListener('PROJECT_RUN_START', this.onProjectRunStart);
this.props.vm.addListener('PROJECT_RUN_STOP', this.onProjectRunStop);
}
componentWillUnmount () {
this.props.vm.removeListener('PROJECT_RUN_START', this.onProjectRunStart);
this.props.vm.removeListener('PROJECT_RUN_STOP', this.onProjectRunStop);
}
onProjectRunStart () {
this.setState({projectRunning: true});
}
onProjectRunStop () {
this.setState({projectRunning: false});
}
handleClick (e) {
e.preventDefault();
this.props.vm.stopAll();
}
render () {
const {
vm, // eslint-disable-line no-unused-vars
...props
} = this.props;
return (
<StopAllComponent
active={!this.state.projectRunning}
onClick={this.handleClick}
{...props}
/>
);
}
}
StopAll.propTypes = {
vm: PropTypes.instanceOf(VM)
};
export default StopAll;
+7 -5
View File
@@ -61,11 +61,13 @@ const alerts = [
},
{
alertId: 'savingError',
clearList: ['saving', 'saveSuccess'],
closeButton: true,
clearList: ['saving', 'saveSuccess', 'savingError'],
showDownload: true,
showSaveNow: true,
closeButton: false,
content: (
<FormattedMessage
defaultMessage="Could not save the project. Please try again!"
defaultMessage="Project could not save."
description="Message indicating that project could not be saved"
id="gui.alerts.savingError"
/>
@@ -75,7 +77,7 @@ const alerts = [
{
alertId: 'saveSuccess',
alertType: AlertTypes.INLINE,
clearList: ['createSuccess', 'creating', 'saveSuccess', 'saving'],
clearList: ['createSuccess', 'creating', 'saveSuccess', 'saving', 'savingError'],
content: (
<FormattedMessage
defaultMessage="Successfully saved."
@@ -90,7 +92,7 @@ const alerts = [
{
alertId: 'saving',
alertType: AlertTypes.INLINE,
clearList: ['createSuccess', 'creating', 'saveSuccess', 'saving'],
clearList: ['createSuccess', 'creating', 'saveSuccess', 'saving', 'savingError'],
content: (
<FormattedMessage
defaultMessage="Saving..."
+17 -2
View File
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import bindAll from 'lodash.bindall';
import VM from 'scratch-vm';
import CloudProvider from '../lib/cloud-provider';
@@ -19,6 +20,11 @@ const cloudManagerHOC = function (WrappedComponent) {
constructor (props) {
super(props);
this.cloudProvider = null;
bindAll(this, [
'handleCloudDataUpdate'
]);
this.props.vm.on('HAS_CLOUD_DATA_UPDATE', this.handleCloudDataUpdate);
}
componentDidMount () {
if (this.shouldConnect(this.props)) {
@@ -51,12 +57,14 @@ const cloudManagerHOC = function (WrappedComponent) {
}
shouldConnect (props) {
return !this.isConnected() && this.canUseCloud(props) &&
props.isShowingWithId && !this.shouldNotModifyCloudData(props);
props.isShowingWithId && props.vm.runtime.hasCloudData() &&
!this.shouldNotModifyCloudData(props);
}
shouldDisconnect (props, prevProps) {
return this.isConnected() &&
( // Can no longer use cloud or cloud provider info is now stale
!this.canUseCloud(this.props) ||
!this.canUseCloud(props) ||
!props.vm.runtime.hasCloudData() ||
(props.projectId !== prevProps.projectId) ||
(props.username !== prevProps.username) ||
// Editing someone else's project
@@ -81,6 +89,13 @@ const cloudManagerHOC = function (WrappedComponent) {
this.props.vm.setCloudProvider(null);
}
}
handleCloudDataUpdate (projectHasCloudData) {
if (this.isConnected() && !projectHasCloudData) {
this.disconnectFromCloud();
} else if (!this.isConnected() && projectHasCloudData) {
this.connectToCloud();
}
}
render () {
const {
/* eslint-disable no-unused-vars */
+20 -7
View File
@@ -20,6 +20,10 @@ class CloudProvider {
this.connectionAttempts = 0;
// A queue of messages to send which were received before the
// connection was ready
this.queuedData = [];
this.openConnection();
}
@@ -67,6 +71,14 @@ class CloudProvider {
this.connectionAttempts = 1;
this.writeToServer('handshake');
log.info(`Successfully connected to clouddata server.`);
// Go through the queued data and send off messages that we weren't
// ready to send before
this.queuedData.forEach(data => {
this.sendCloudData(data);
});
// Reset the queue
this.queuedData = [];
}
onClose () {
@@ -91,12 +103,6 @@ class CloudProvider {
parseMessage (message) {
const varData = {};
switch (message.method) {
case 'ack': {
varData.varCreate = {
name: message.name
};
break;
}
case 'set': {
varData.varUpdate = {
name: message.name,
@@ -129,7 +135,14 @@ class CloudProvider {
if (typeof dataValue !== 'undefined' && dataValue !== null) msg.value = dataValue;
const dataToWrite = JSON.stringify(msg);
this.sendCloudData(dataToWrite);
if (this.connection && this.connection.readyState === WebSocket.OPEN) {
this.sendCloudData(dataToWrite);
} else if (msg.method === 'create' || msg.method === 'delete' || msg.method === 'rename') {
// Save data for sending when connection is open, iff the data
// is a create, rename, or delete
this.queuedData.push(dataToWrite);
}
}
/**
Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+293
View File
@@ -74,6 +74,37 @@ import stepRandom from './game/game-random-position.gif';
import stepGameChangeColor from './game/game-change-color.gif';
import stepResetScore from './game/game-reset-score.gif';
// Animate A Character
import libraryAnimateChar from './animate-char/lib_Animate_a_Character.jpg';
import stepAnimateCharPickBackdrop from './animate-char/animate-char-pick-backdrop.gif';
import stepAnimateCharPickSprite from './animate-char/animate-char-pick-sprite.gif';
import stepAnimateCharSaySomething from './animate-char/animate-char-say-something.gif';
import stepAnimateCharAddSound from './animate-char/animate-char-add-sound.gif';
import stepAnimateCharTalk from './animate-char/animate-char-talk.gif';
import stepAnimateCharMove from './animate-char/animate-char-move.gif';
import stepAnimateCharJump from './animate-char/animate-char-jump.gif';
import stepAnimateCharChangeColor from './animate-char/animate-char-change-color.gif';
// Tell A Story
import libraryStory from './story/lib-tell-a-story.jpg';
import stepStoryPickBackdrop from './story/story-pick-backdrop.gif';
import stepStoryPickSprite from './story/story-pick-sprite.gif';
import stepStoryCharSaySomething from './story/story-say-something.gif';
import stepStoryCharAddAnotherSprite from './story/story-pick-another-sprite.gif';
import stepStoryFlip from './story/story-flip.gif';
import stepStoryConversation from './story/story-conversation.gif';
import stepStoryAddAnotherBackdrop from './story/story-add-background-2.gif';
import stepStorySwitchBackdrop from './story/story-switch-backdrop.gif';
import stepStoryHide from './story/story-hide-character.gif';
import stepStoryShow from './story/story-show-character.gif';
// Video Sensing
import libraryVideoSens from './videosensing/lib_video_sensing.jpg';
import stepVideoSensAddExtension from './videosensing/videosens-add-extension.gif';
import stepVideoSensPet from './videosensing/videosens-pet-cat.gif';
import stepVideoSensAnimate from './videosensing/videosens-animate.gif';
import stepVideoSensPop from './videosensing/videosens-pop.gif';
// Videos
import recordASound from './videos/record-a-sound.jpg';
import glideAroundThumb from './videos/glide-around.jpg';
@@ -675,6 +706,268 @@ export default {
],
urlId: 'chase-game'
},
'Animate-A-Character': {
name: (
<FormattedMessage
defaultMessage="Animate A Character"
description="Name for the 'Animate A Character' how-to"
id="gui.howtos.animate-char.name"
/>
),
img: libraryAnimateChar,
steps: [{
video: 's228u3g5u9'
},
{
title: (
<FormattedMessage
defaultMessage="Add a Backdrop"
description="Step name for 'Add a Backdrop' step"
id="gui.howtos.animate-char.step_addbg"
/>
),
image: stepAnimateCharPickBackdrop
}, {
title: (
<FormattedMessage
defaultMessage="Add a Sprite"
description="Step name for 'Add a Sprite' step"
id="gui.howtos.animate-char.step_addsprite"
/>
),
image: stepAnimateCharPickSprite
}, {
title: (
<FormattedMessage
defaultMessage="Say Something"
description="Step name for 'Say Something' step"
id="gui.howtos.animate-char.step_saysomething"
/>
),
image: stepAnimateCharSaySomething
}, {
title: (
<FormattedMessage
defaultMessage="Add Sound"
description="Step name for 'Add Sound' step"
id="gui.howtos.animate-char.step_addsound"
/>
),
image: stepAnimateCharAddSound
}, {
title: (
<FormattedMessage
defaultMessage="Animate Talking"
description="Step name for 'Animate Talking' step"
id="gui.howtos.animate-char.step_animatetalking"
/>
),
image: stepAnimateCharTalk
}, {
title: (
<FormattedMessage
defaultMessage="Move Using Arrow Keys"
description="Step name for 'Move Using Arrow Keys' step"
id="gui.howtos.animate-char.step_arrowkeys"
/>
),
image: stepAnimateCharMove
}, {
title: (
<FormattedMessage
defaultMessage="Jump"
description="Step name for 'Jump' step"
id="gui.howtos.animate-char.step_jump"
/>
),
image: stepAnimateCharJump
}, {
title: (
<FormattedMessage
defaultMessage="Change Color"
description="Step name for 'Change Color' step"
id="gui.howtos.animate-char.step_changecolor"
/>
),
image: stepAnimateCharChangeColor
}, {
deckIds: [
'Chase-Game',
'Tell-A-Story'
]
}
],
urlId: 'animate-a-character'
},
'Tell-A-Story': {
name: (
<FormattedMessage
defaultMessage="Create A Story"
description="Name for the 'Create A Story' how-to"
id="gui.howtos.story.name"
/>
),
img: libraryStory,
steps: [{
video: 'stah7jjorp'
},
{
title: (
<FormattedMessage
defaultMessage="Add a Backdrop"
description="Step name for 'Add a Backdrop' step"
id="gui.howtos.story.step_addbg"
/>
),
image: stepStoryPickBackdrop
}, {
title: (
<FormattedMessage
defaultMessage="Add a Character"
description="Step name for 'Add a Character' step"
id="gui.howtos.story.step_addsprite"
/>
),
image: stepStoryPickSprite
}, {
title: (
<FormattedMessage
defaultMessage="Say Something"
description="Step name for 'Say Something' step"
id="gui.howtos.story.step_saysomething"
/>
),
image: stepStoryCharSaySomething
}, {
title: (
<FormattedMessage
defaultMessage="Add Another Character"
description="Step name for 'Add Another Character' step"
id="gui.howtos.story.step_addanothersprite"
/>
),
image: stepStoryCharAddAnotherSprite
}, {
title: (
<FormattedMessage
defaultMessage="Flip Direction"
description="Flip Direction' step"
id="gui.howtos.story.step_flip"
/>
),
image: stepStoryFlip
}, {
title: (
<FormattedMessage
defaultMessage="Have A Conversation"
description="Step name for 'Have A Conversation' step"
id="gui.howtos.story.step_conversation"
/>
),
image: stepStoryConversation
}, {
title: (
<FormattedMessage
defaultMessage="Add Another Backdrop"
description="Step name for 'Add Another Backdrop' step"
id="gui.howtos.story.addanotherbg"
/>
),
image: stepStoryAddAnotherBackdrop
}, {
title: (
<FormattedMessage
defaultMessage="Switch Backdrops"
description="Step name for 'Switch Backdrops' step"
id="gui.howtos.story.step_swithbg"
/>
),
image: stepStorySwitchBackdrop
}, {
title: (
<FormattedMessage
defaultMessage="Hide a Character"
description="Step name for 'Hide the Wizard' step"
id="gui.howtos.story.step_hidewizard"
/>
),
image: stepStoryHide
}, {
title: (
<FormattedMessage
defaultMessage="Show a Character"
description="Step name for 'Show the Wizard' step"
id="gui.howtos.story.step_showwizard"
/>
),
image: stepStoryShow
},
{
deckIds: [
'say-it-out-loud',
'record-a-sound'
]
}
],
urlId: 'tell-a-story'
},
'Video-Sensing': {
name: (
<FormattedMessage
defaultMessage="Video Sensing"
description="Name for the 'Video Sensing' how-to"
id="gui.howtos.videosens.name"
/>
),
img: libraryVideoSens,
steps: [{
video: '3pd1z110d6'
},
{
title: (
<FormattedMessage
defaultMessage="Add Extension"
description="Step name for 'Add Extension' step"
id="gui.howtos.videosens.step_addextension"
/>
),
image: stepVideoSensAddExtension
}, {
title: (
<FormattedMessage
defaultMessage="Pet the Cat"
description="Step name for 'Pet the Cat' step"
id="gui.howtos.videosens.step_pet"
/>
),
image: stepVideoSensPet
}, {
title: (
<FormattedMessage
defaultMessage="Animate"
description="Step name for 'Animate' step"
id="gui.howtos.videosens.step_animate"
/>
),
image: stepVideoSensAnimate
}, {
title: (
<FormattedMessage
defaultMessage="Pop a Balloon"
description="Step name for 'Pop a Balloon' step"
id="gui.howtos.videosens.step_pop"
/>
),
image: stepVideoSensPop
}, {
deckIds: [
'Make-Music',
'add-effects'
]
}
],
urlId: 'video-sensing'
},
'add-sprite': {
name: (
<FormattedMessage
Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1024 KiB

+8 -1
View File
@@ -4,10 +4,12 @@ import {intlShape, injectIntl} from 'react-intl';
import bindAll from 'lodash.bindall';
import {connect} from 'react-redux';
import {setProjectUnchanged} from '../reducers/project-changed';
import {
LoadingStates,
defaultProjectId,
getIsFetchingWithId,
getIsShowingProject,
onFetchedProjectData,
projectError,
setProjectId
@@ -54,6 +56,9 @@ const ProjectFetcherHOC = function (WrappedComponent) {
if (this.props.isFetchingWithId && !prevProps.isFetchingWithId) {
this.fetchProject(this.props.reduxProjectId, this.props.loadingState);
}
if (this.props.isShowingProject && !prevProps.isShowingProject) {
this.props.onProjectLoaded();
}
}
fetchProject (projectId, loadingState) {
return storage
@@ -123,6 +128,7 @@ const ProjectFetcherHOC = function (WrappedComponent) {
const mapStateToProps = state => ({
isFetchingWithId: getIsFetchingWithId(state.scratchGui.projectState.loadingState),
isShowingProject: getIsShowingProject(state.scratchGui.projectState.loadingState),
loadingState: state.scratchGui.projectState.loadingState,
reduxProjectId: state.scratchGui.projectState.projectId
});
@@ -130,7 +136,8 @@ const ProjectFetcherHOC = function (WrappedComponent) {
onError: error => dispatch(projectError(error)),
onFetchedProjectData: (projectData, loadingState) =>
dispatch(onFetchedProjectData(projectData, loadingState)),
setProjectId: projectId => dispatch(setProjectId(projectId))
setProjectId: projectId => dispatch(setProjectId(projectId)),
onProjectLoaded: () => dispatch(setProjectUnchanged())
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
+9 -5
View File
@@ -9,6 +9,7 @@ import {
showAlertWithTimeout,
showStandardAlert
} from '../reducers/alerts';
import {setProjectUnchanged} from '../reducers/project-changed';
import {
LoadingStates,
autoUpdateProject,
@@ -84,10 +85,9 @@ const ProjectSaverHOC = function (WrappedComponent) {
this.props.onShowSaveSuccessAlert();
})
.catch(err => {
// NOTE: should throw up a notice for user
if (this.props.isManualUpdating) {
this.props.onShowAlert('savingError');
}
// Always show the savingError alert because it gives the
// user the chance to download or retry the save manually.
this.props.onShowAlert('savingError');
this.props.onProjectError(err);
});
}
@@ -167,7 +167,10 @@ const ProjectSaverHOC = function (WrappedComponent) {
storage.DataFormat.JSON,
body,
projectId
);
).then(response => {
this.props.onSetProjectUnchanged();
return response;
});
})
.catch(err => {
log.error(err);
@@ -255,6 +258,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
onCreatedProject: (projectId, loadingState) => dispatch(doneCreatingProject(projectId, loadingState)),
onCreateProject: () => dispatch(createProject()),
onProjectError: error => dispatch(projectError(error)),
onSetProjectUnchanged: () => dispatch(setProjectUnchanged()),
onShowAlert: alertType => dispatch(showStandardAlert(alertType)),
onShowCreateSuccessAlert: () => showAlertWithTimeout(dispatch, 'createSuccess'),
onShowCreatingAlert: () => showAlertWithTimeout(dispatch, 'creating'),
+19 -1
View File
@@ -8,7 +8,8 @@ import {connect} from 'react-redux';
import {updateTargets} from '../reducers/targets';
import {updateBlockDrag} from '../reducers/block-drag';
import {updateMonitors} from '../reducers/monitors';
import {setRunningState, setTurboState} from '../reducers/vm-status';
import {setProjectChanged, setProjectUnchanged} from '../reducers/project-changed';
import {setRunningState, setTurboState, setStartedState} from '../reducers/vm-status';
import {showExtensionAlert} from '../reducers/alerts';
import {updateMicIndicator} from '../reducers/mic-indicator';
@@ -24,6 +25,7 @@ const vmListenerHOC = function (WrappedComponent) {
bindAll(this, [
'handleKeyDown',
'handleKeyUp',
'handleProjectChanged',
'handleTargetsUpdate'
]);
// We have to start listening to the vm here rather than in
@@ -39,6 +41,8 @@ const vmListenerHOC = function (WrappedComponent) {
this.props.vm.on('TURBO_MODE_OFF', this.props.onTurboModeOff);
this.props.vm.on('PROJECT_RUN_START', this.props.onProjectRunStart);
this.props.vm.on('PROJECT_RUN_STOP', this.props.onProjectRunStop);
this.props.vm.on('PROJECT_CHANGED', this.handleProjectChanged);
this.props.vm.on('RUNTIME_STARTED', this.props.onRuntimeStarted);
this.props.vm.on('PERIPHERAL_DISCONNECT_ERROR', this.props.onShowExtensionAlert);
this.props.vm.on('MIC_LISTENING', this.props.onMicListeningUpdate);
@@ -68,6 +72,9 @@ const vmListenerHOC = function (WrappedComponent) {
document.removeEventListener('keyup', this.handleKeyUp);
}
}
handleProjectChanged () {
this.props.onProjectChanged();
}
handleTargetsUpdate (data) {
if (this.props.shouldEmitTargetsUpdate) {
this.props.onTargetsUpdate(data);
@@ -82,6 +89,12 @@ const vmListenerHOC = function (WrappedComponent) {
key: e.key,
isDown: true
});
// Prevent space/arrow key from scrolling the page.
if (e.keyCode === 32 || // 32=space
(e.keyCode >= 37 && e.keyCode <= 40)) { // 37, 38, 39, 40 are arrows
e.preventDefault();
}
}
handleKeyUp (e) {
// Always capture up events,
@@ -110,6 +123,7 @@ const vmListenerHOC = function (WrappedComponent) {
onTargetsUpdate,
onProjectRunStart,
onProjectRunStop,
onRuntimeStarted,
onTurboModeOff,
onTurboModeOn,
onShowExtensionAlert,
@@ -128,6 +142,7 @@ const vmListenerHOC = function (WrappedComponent) {
onMonitorsUpdate: PropTypes.func.isRequired,
onProjectRunStart: PropTypes.func.isRequired,
onProjectRunStop: PropTypes.func.isRequired,
onRuntimeStarted: PropTypes.func.isRequired,
onShowExtensionAlert: PropTypes.func.isRequired,
onTargetsUpdate: PropTypes.func.isRequired,
onTurboModeOff: PropTypes.func.isRequired,
@@ -158,6 +173,9 @@ const vmListenerHOC = function (WrappedComponent) {
},
onProjectRunStart: () => dispatch(setRunningState(true)),
onProjectRunStop: () => dispatch(setRunningState(false)),
onProjectChanged: () => dispatch(setProjectChanged()),
onProjectSaved: () => dispatch(setProjectUnchanged()),
onRuntimeStarted: () => dispatch(setStartedState(true)),
onTurboModeOn: () => dispatch(setTurboState(true)),
onTurboModeOff: () => dispatch(setTurboState(false)),
onShowExtensionAlert: data => {
+35 -8
View File
@@ -6,6 +6,7 @@ import {connect} from 'react-redux';
import VM from 'scratch-vm';
import AudioEngine from 'scratch-audio';
import {setProjectUnchanged} from '../reducers/project-changed';
import {
LoadingStates,
getIsLoadingWithId,
@@ -27,12 +28,15 @@ const vmManagerHOC = function (WrappedComponent) {
]);
}
componentDidMount () {
if (this.props.vm.initialized) return;
this.audioEngine = new AudioEngine();
this.props.vm.attachAudioEngine(this.audioEngine);
this.props.vm.setCompatibilityMode(true);
this.props.vm.start();
this.props.vm.initialized = true;
if (!this.props.vm.initialized) {
this.audioEngine = new AudioEngine();
this.props.vm.attachAudioEngine(this.audioEngine);
this.props.vm.setCompatibilityMode(true);
this.props.vm.initialized = true;
}
if (!this.props.isPlayerOnly && !this.props.isStarted) {
this.props.vm.start();
}
}
componentDidUpdate (prevProps) {
// if project is in loading state, AND fonts are loaded,
@@ -41,11 +45,29 @@ const vmManagerHOC = function (WrappedComponent) {
(!prevProps.isLoadingWithId || !prevProps.fontsLoaded)) {
this.loadProject();
}
// Start the VM if entering editor mode with an unstarted vm
if (!this.props.isPlayerOnly && !this.props.isStarted) {
this.props.vm.start();
}
}
loadProject () {
return this.props.vm.loadProject(this.props.projectData)
.then(() => {
this.props.onLoadedProject(this.props.loadingState, this.props.canSave);
// Wrap in a setTimeout because skin loading in
// the renderer can be async.
setTimeout(() => this.props.onSetProjectUnchanged());
// If the vm is not running, call draw on the renderer manually
// This draws the state of the loaded project with no blocks running
// which closely matches the 2.0 behavior, except for monitors–
// 2.0 runs monitors and shows updates (e.g. timer monitor)
// before the VM starts running other hat blocks.
if (!this.props.isStarted) {
// Wrap in a setTimeout because skin loading in
// the renderer can be async.
setTimeout(() => this.props.vm.renderer.draw());
}
})
.catch(e => {
this.props.onError(e);
@@ -56,6 +78,7 @@ const vmManagerHOC = function (WrappedComponent) {
/* eslint-disable no-unused-vars */
fontsLoaded,
loadingState,
isStarted,
onError: onErrorProp,
onLoadedProject: onLoadedProjectProp,
projectData,
@@ -79,6 +102,7 @@ const vmManagerHOC = function (WrappedComponent) {
cloudHost: PropTypes.string,
fontsLoaded: PropTypes.bool,
isLoadingWithId: PropTypes.bool,
isPlayerOnly: PropTypes.bool,
loadingState: PropTypes.oneOf(LoadingStates),
onError: PropTypes.func,
onLoadedProject: PropTypes.func,
@@ -94,14 +118,17 @@ const vmManagerHOC = function (WrappedComponent) {
isLoadingWithId: getIsLoadingWithId(loadingState),
projectData: state.scratchGui.projectState.projectData,
projectId: state.scratchGui.projectState.projectId,
loadingState: loadingState
loadingState: loadingState,
isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
isStarted: state.scratchGui.vmStatus.started
};
};
const mapDispatchToProps = dispatch => ({
onError: error => dispatch(projectError(error)),
onLoadedProject: (loadingState, canSave) =>
dispatch(onLoadedProject(loadingState, canSave))
dispatch(onLoadedProject(loadingState, canSave)),
onSetProjectUnchanged: () => dispatch(setProjectUnchanged())
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
+2
View File
@@ -46,6 +46,8 @@ const reducer = function (state, action) {
newAlert.iconURL = alertData.iconURL;
newAlert.iconSpinner = alertData.iconSpinner;
newAlert.level = alertData.level;
newAlert.showDownload = alertData.showDownload;
newAlert.showSaveNow = alertData.showSaveNow;
newList.push(newAlert);
return Object.assign({}, state, {
+3
View File
@@ -14,6 +14,7 @@ import modalReducer, {modalsInitialState} from './modals';
import modeReducer, {modeInitialState} from './mode';
import monitorReducer, {monitorsInitialState} from './monitors';
import monitorLayoutReducer, {monitorLayoutInitialState} from './monitor-layout';
import projectChangedReducer, {projectChangedInitialState} from './project-changed';
import projectStateReducer, {projectStateInitialState} from './project-state';
import projectTitleReducer, {projectTitleInitialState} from './project-title';
import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion';
@@ -45,6 +46,7 @@ const guiInitialState = {
modals: modalsInitialState,
monitors: monitorsInitialState,
monitorLayout: monitorLayoutInitialState,
projectChanged: projectChangedInitialState,
projectState: projectStateInitialState,
projectTitle: projectTitleInitialState,
restoreDeletion: restoreDeletionInitialState,
@@ -126,6 +128,7 @@ const guiReducer = combineReducers({
modals: modalReducer,
monitors: monitorReducer,
monitorLayout: monitorLayoutReducer,
projectChanged: projectChangedReducer,
projectState: projectStateReducer,
projectTitle: projectTitleReducer,
restoreDeletion: restoreDeletionReducer,
+28
View File
@@ -0,0 +1,28 @@
const SET_PROJECT_CHANGED = 'scratch-gui/project-changed/SET_PROJECT_CHANGED';
const initialState = false;
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case SET_PROJECT_CHANGED:
return action.changed;
default:
return state;
}
};
const setProjectChanged = () => ({
type: SET_PROJECT_CHANGED,
changed: true
});
const setProjectUnchanged = () => ({
type: SET_PROJECT_CHANGED,
changed: false
});
export {
reducer as default,
initialState as projectChangedInitialState,
setProjectChanged,
setProjectUnchanged
};
+15
View File
@@ -1,14 +1,20 @@
const SET_RUNNING_STATE = 'scratch-gui/vm-status/SET_RUNNING_STATE';
const SET_TURBO_STATE = 'scratch-gui/vm-status/SET_TURBO_STATE';
const SET_STARTED_STATE = 'scratch-gui/vm-status/SET_STARTED_STATE';
const initialState = {
running: false,
started: false,
turbo: false
};
const reducer = function (state, action) {
if (typeof state === 'undefined') state = initialState;
switch (action.type) {
case SET_STARTED_STATE:
return Object.assign({}, state, {
started: action.started
});
case SET_RUNNING_STATE:
return Object.assign({}, state, {
running: action.running
@@ -22,6 +28,14 @@ const reducer = function (state, action) {
}
};
const setStartedState = function (started) {
return {
type: SET_STARTED_STATE,
started: started
};
};
const setRunningState = function (running) {
return {
type: SET_RUNNING_STATE,
@@ -40,5 +54,6 @@ export {
reducer as default,
initialState as vmStatusInitialState,
setRunningState,
setStartedState,
setTurboState
};
+4 -4
View File
@@ -31,24 +31,24 @@ describe('Menu bar settings', () => {
await findByXpath('//*[li[span[text()="New"]] and not(@data-tip="tooltip")]');
});
test('File->Upload should be enabled', async () => {
test('File->Load should be enabled', async () => {
await loadUri(uri);
await clickXpath('//button[@title="Try It"]');
await clickXpath(
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
);
await findByXpath('//*[li[span[text()="Upload from your computer"]] and not(@data-tip="tooltip")]');
await findByXpath('//*[li[span[text()="Load from your computer"]] and not(@data-tip="tooltip")]');
});
test('File->Download should be enabled', async () => {
test('File->Save should be enabled', async () => {
await loadUri(uri);
await clickXpath('//button[@title="Try It"]');
await clickXpath(
'//div[contains(@class, "menu-bar_menu-bar-item") and ' +
'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
);
await findByXpath('//*[li[span[text()="Download to your computer"]] and not(@data-tip="tooltip")]');
await findByXpath('//*[li[span[text()="Save to your computer"]] and not(@data-tip="tooltip")]');
});
test('Share button should NOT be enabled', async () => {
+1 -1
View File
@@ -24,7 +24,7 @@ describe('Working with shortcut to Tutorials library', () => {
test('opens with the Tutorial Library showing', async () => {
await loadUri(uri);
// make sure there is a tutorial visible that doesn't have a shortcut
await clickText('Switch costume');
await clickText('Make It Spin');
await findByXpath('//div[contains(@class, "step-video")]');
});
@@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GreenFlag Container renders active state 1`] = `
<img
className="undefined"
draggable={false}
onClick={[Function]}
src="test-file-stub"
title="Go"
/>
`;
exports[`GreenFlag Container renders inactive state 1`] = `
<img
className=""
draggable={false}
onClick={[Function]}
src="test-file-stub"
title="Go"
/>
`;
-49
View File
@@ -1,49 +0,0 @@
import React from 'react';
import {shallow} from 'enzyme';
import GreenFlag from '../../../src/containers/green-flag';
import renderer from 'react-test-renderer';
import VM from 'scratch-vm';
describe('GreenFlag Container', () => {
let vm;
beforeEach(() => {
vm = new VM();
});
test('renders active state', () => {
const component = renderer.create(
<GreenFlag
active
vm={vm}
/>
);
expect(component.toJSON()).toMatchSnapshot();
});
test('renders inactive state', () => {
const component = renderer.create(
<GreenFlag
active={false}
vm={vm}
/>
);
expect(component.toJSON()).toMatchSnapshot();
});
test('triggers onClick when active', () => {
const onClick = jest.fn();
const componentShallowWrapper = shallow(
<GreenFlag
active
vm={vm}
onClick={onClick}
/>
);
componentShallowWrapper.simulate('click');
expect(onClick).toHaveBeenCalled();
});
// @todo: Test for handles key events.
// @todo: Test project run start.
// @todo: Test project run stop.
});
+74 -2
View File
@@ -47,6 +47,9 @@ describe('CloudManagerHOC', () => {
});
vm = new VM();
vm.setCloudProvider = jest.fn();
vm.runtime = {
hasCloudData: jest.fn(() => true)
};
CloudProvider.mockClear();
mockCloudProviderInstance.requestCloseConnection.mockClear();
});
@@ -163,7 +166,7 @@ describe('CloudManagerHOC', () => {
expect(vm.setCloudProvider).toHaveBeenCalledWith(mockCloudProviderInstance);
});
test('when it unmounts, the cloud provider is set on the vm', () => {
test('when it unmounts, the cloud provider is reset to null on the vm', () => {
const Component = () => (<div />);
const WrappedComponent = cloudManagerHOC(Component);
const mounted = mount(
@@ -239,6 +242,76 @@ describe('CloudManagerHOC', () => {
expect(requestCloseConnection).toHaveBeenCalledTimes(1);
});
test('project without cloud data should not trigger cloud connection', () => {
// Mock the vm runtime function so that has cloud data is not
// initially true
vm.runtime.hasCloudData = jest.fn(() => false);
const Component = () => <div />;
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
cloudHost="nonEmpty"
store={store}
username="user"
vm={vm}
/>
);
expect(vm.setCloudProvider.mock.calls.length).toBe(0);
expect(CloudProvider).not.toHaveBeenCalled();
});
test('projectHasCloudData becoming true should trigger a cloud connection', () => {
// Mock the vm runtime function so that has cloud data is not
// initially true
vm.runtime.hasCloudData = jest.fn(() => false);
const Component = () => <div />;
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
cloudHost="nonEmpty"
store={store}
username="user"
vm={vm}
/>
);
expect(vm.setCloudProvider.mock.calls.length).toBe(0);
expect(CloudProvider).not.toHaveBeenCalled();
// Mock VM hasCloudData becoming true and emitting an update
vm.runtime.hasCloudData = jest.fn(() => true);
vm.emit('HAS_CLOUD_DATA_UPDATE', true);
expect(vm.setCloudProvider.mock.calls.length).toBe(1);
expect(CloudProvider).toHaveBeenCalledTimes(1);
expect(vm.setCloudProvider).toHaveBeenCalledWith(mockCloudProviderInstance);
});
test('projectHasCloudDataUpdate becoming false should trigger cloudProvider disconnection', () => {
const Component = () => <div />;
const WrappedComponent = cloudManagerHOC(Component);
mount(
<WrappedComponent
canSave
cloudHost="nonEmpty"
store={store}
username="user"
vm={vm}
/>
);
vm.runtime.hasCloudData = jest.fn(() => false);
vm.emit('HAS_CLOUD_DATA_UPDATE', false);
expect(vm.setCloudProvider.mock.calls.length).toBe(2);
expect(vm.setCloudProvider).toHaveBeenCalledWith(null);
expect(requestCloseConnection).toHaveBeenCalledTimes(1);
});
// Editor Mode Connection/Disconnection Tests
test('Entering editor mode and can\'t save project should disconnect cloud provider # 1', () => {
@@ -268,7 +341,6 @@ describe('CloudManagerHOC', () => {
});
// Editor Mode Connection/Disconnection Tests
test('Entering editor mode and can\'t save project should disconnect cloud provider # 2', () => {
const Component = () => <div />;
const WrappedComponent = cloudManagerHOC(Component);
+4 -11
View File
@@ -84,14 +84,6 @@ describe('CloudProvider', () => {
expect(typeof obj.value).toEqual('undefined');
});
test('onMessage ack', () => {
const msg = JSON.stringify({
method: 'ack',
name: 'name'
});
cloudProvider.connection._receive({data: msg});
expect(vmIOData[0].varCreate.name).toEqual('name');
});
test('onMessage set', () => {
const msg = JSON.stringify({
@@ -121,12 +113,13 @@ describe('CloudProvider', () => {
value: 'value'
});
const msg2 = JSON.stringify({
method: 'ack',
name: 'name2'
method: 'set',
name: 'name2',
value: 'value2'
});
cloudProvider.connection._receive({data: `${msg1}\n${msg2}`});
expect(vmIOData[0].varUpdate.name).toEqual('name1');
expect(vmIOData[1].varCreate.name).toEqual('name2');
expect(vmIOData[1].varUpdate.name).toEqual('name2');
});
test('connnection attempts set back to 1 when socket is opened', () => {
+68 -5
View File
@@ -16,7 +16,9 @@ describe('VMManagerHOC', () => {
beforeEach(() => {
store = mockStore({
scratchGui: {
projectState: {}
projectState: {},
mode: {},
vmStatus: {}
}
});
vm = new VM();
@@ -24,34 +26,95 @@ describe('VMManagerHOC', () => {
vm.setCompatibilityMode = jest.fn();
vm.start = jest.fn();
});
test('when it mounts, the vm is initialized', () => {
test('when it mounts in player mode, the vm is initialized but not started', () => {
const Component = () => (<div />);
const WrappedComponent = vmManagerHOC(Component);
mount(
<WrappedComponent
isPlayerOnly
isStarted={false}
store={store}
vm={vm}
/>
);
expect(vm.attachAudioEngine.mock.calls.length).toBe(1);
expect(vm.setCompatibilityMode.mock.calls.length).toBe(1);
expect(vm.start.mock.calls.length).toBe(1);
expect(vm.initialized).toBe(true);
// But vm should not be started automatically
expect(vm.start).not.toHaveBeenCalled();
});
test('if it mounts with an initialized vm, it does not reinitialize the vm', () => {
test('when it mounts in editor mode, the vm is initialized and started', () => {
const Component = () => (<div />);
const WrappedComponent = vmManagerHOC(Component);
mount(
<WrappedComponent
isPlayerOnly={false}
isStarted={false}
store={store}
vm={vm}
/>
);
expect(vm.attachAudioEngine.mock.calls.length).toBe(1);
expect(vm.setCompatibilityMode.mock.calls.length).toBe(1);
expect(vm.initialized).toBe(true);
expect(vm.start).toHaveBeenCalled();
});
test('if it mounts with an initialized vm, it does not reinitialize the vm but will start it', () => {
const Component = () => <div />;
const WrappedComponent = vmManagerHOC(Component);
vm.initialized = true;
mount(
<WrappedComponent
isPlayerOnly={false}
isStarted={false}
store={store}
vm={vm}
/>
);
expect(vm.attachAudioEngine.mock.calls.length).toBe(0);
expect(vm.setCompatibilityMode.mock.calls.length).toBe(0);
expect(vm.start.mock.calls.length).toBe(0);
expect(vm.initialized).toBe(true);
expect(vm.start).toHaveBeenCalled();
});
test('if it mounts without starting the VM, it can be started by switching to editor mode', () => {
const Component = () => <div />;
const WrappedComponent = vmManagerHOC(Component);
vm.initialized = true;
const mounted = mount(
<WrappedComponent
isPlayerOnly
isStarted={false}
store={store}
vm={vm}
/>
);
expect(vm.start).not.toHaveBeenCalled();
mounted.setProps({
isPlayerOnly: false
});
expect(vm.start).toHaveBeenCalled();
});
test('if it mounts with an initialized and started VM, it does not start again', () => {
const Component = () => <div />;
const WrappedComponent = vmManagerHOC(Component);
vm.initialized = true;
const mounted = mount(
<WrappedComponent
isPlayerOnly
isStarted
store={store}
vm={vm}
/>
);
expect(vm.start).not.toHaveBeenCalled();
mounted.setProps({
isPlayerOnly: false
});
expect(vm.start).not.toHaveBeenCalled();
});
test('if the isLoadingWithId prop becomes true, it loads project data into the vm', () => {
vm.loadProject = jest.fn(() => Promise.resolve());