Merge branch 'develop' into editor-cloud-disconnect
No files matched your search
@@ -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}));
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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..."
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 520 KiB |
|
After Width: | Height: | Size: 282 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 109 KiB |
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 311 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 303 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 307 KiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 319 KiB |
|
After Width: | Height: | Size: 404 KiB |
|
After Width: | Height: | Size: 293 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 635 KiB |
|
After Width: | Height: | Size: 477 KiB |
|
After Width: | Height: | Size: 1024 KiB |
@@ -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,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'),
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
`;
|
||||
@@ -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.
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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());
|
||||
|
||||