Merge branch 'develop' into scratch-desktop

Conflicts:
	src/components/library-item/library-item.jsx
	src/components/library/library.jsx
	src/components/menu-bar/menu-bar.jsx
	src/containers/library-item.jsx
	src/lib/project-saver-hoc.jsx
This commit is contained in:
Christopher Willis-Ford committed 2019-03-12 16:13:40 -07:00
commit f6b9f11d3b
276 files changed
+2420 -1774

No files matched your search

+12 -7
View File
@@ -10,6 +10,7 @@ env:
- NODE_ENV=production
- SMOKE_URL=https://llk.github.io/scratch-gui/$TRAVIS_PULL_REQUEST_BRANCH
- NPM_TAG=latest
- RELEASE_VERSION="0.1.0-prerelease.$(date +'%Y%m%d%H%M%S')"
cache:
directories:
- node_modules
@@ -22,8 +23,8 @@ script:
before_deploy:
- >
if [ -z "$BEFORE_DEPLOY_RAN" ]; then
npm --no-git-tag-version version 0.1.0-prerelease.$(date +%Y%m%d%H%M%S)
if [ "$TRAVIS_BRANCH" == "develop" ]; then export NPM_TAG=develop; fi
npm --no-git-tag-version version $RELEASE_VERSION
if [ "$TRAVIS_BRANCH" == "master" ]; then export NPM_TAG=stable; fi
git config --global user.email $(git log --pretty=format:"%ae" -n1)
git config --global user.name $(git log --pretty=format:"%an" -n1)
export BEFORE_DEPLOY_RAN=true
@@ -40,6 +41,15 @@ deploy:
email: $NPM_EMAIL
api_key: $NPM_TOKEN
tag: $NPM_TAG
- provider: script
on:
branch:
- master
- develop
- smoke
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
script: if npm info scratch-gui | grep -q $RELEASE_VERSION; then git tag $RELEASE_VERSION && git push https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git $RELEASE_VERSION; fi
- provider: s3
on:
branch:
@@ -58,11 +68,6 @@ deploy:
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
- provider: script
on:
all_branches: true
condition: $TRAVIS_EVENT_TYPE != cron
script: npm run prune -- https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
- provider: script
on:
branch: develop
+8 -7
View File
@@ -76,6 +76,7 @@
"lodash.throttle": "4.0.1",
"minilog": "3.1.0",
"mkdirp": "^0.5.1",
"omggif": "1.0.9",
"papaparse": "4.6.2",
"postcss-import": "^12.0.0",
"postcss-loader": "^3.0.0",
@@ -103,14 +104,14 @@
"redux-mock-store": "^1.2.3",
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20190108181031",
"scratch-blocks": "0.1.0-prerelease.1545364693",
"scratch-l10n": "3.1.20181220222259",
"scratch-paint": "0.2.0-prerelease.20181220194927",
"scratch-render": "0.1.0-prerelease.20190109201300",
"scratch-audio": "0.1.0-prerelease.20190114210212",
"scratch-blocks": "0.1.0-prerelease.1549990124",
"scratch-l10n": "3.1.20190220143732",
"scratch-paint": "0.2.0-prerelease.20190114205252",
"scratch-render": "0.1.0-prerelease.20190213183713",
"scratch-storage": "1.2.2",
"scratch-svg-renderer": "0.2.0-prerelease.20190109201344",
"scratch-vm": "0.2.0-prerelease.20190109200835",
"scratch-svg-renderer": "0.2.0-prerelease.20190125192231",
"scratch-vm": "0.2.0-prerelease.20190226023539",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
+4 -2
View File
@@ -142,7 +142,7 @@ class ActionMenu extends React.Component {
<div className={styles.moreButtonsOuter}>
<div className={styles.moreButtons}>
{(moreButtons || []).map(({img, title, onClick: handleClick,
fileAccept, fileChange, fileInput}, keyId) => {
fileAccept, fileChange, fileInput, fileMultiple}, keyId) => {
const isComingSoon = !handleClick;
const hasFileInput = fileInput;
const tooltipId = `${this.mainTooltipId}-${title}`;
@@ -166,6 +166,7 @@ class ActionMenu extends React.Component {
<input
accept={fileAccept}
className={styles.fileInput}
multiple={fileMultiple}
ref={fileInput}
type="file"
onChange={fileChange}
@@ -198,7 +199,8 @@ ActionMenu.propTypes = {
onClick: PropTypes.func, // Optional, "coming soon" if no callback provided
fileAccept: PropTypes.string, // Optional, only for file upload
fileChange: PropTypes.func, // Optional, only for file upload
fileInput: PropTypes.func // Optional, only for file upload
fileInput: PropTypes.func, // Optional, only for file upload
fileMultiple: PropTypes.bool // Optional, only for file upload
})),
onClick: PropTypes.func.isRequired,
title: PropTypes.node.isRequired,
+2 -2
View File
@@ -56,7 +56,7 @@ $fade-out-distance: 100px;
.list-item {
width: 5rem;
min-height: 5rem;
height: 5rem;
margin: 0.5rem auto;
}
@@ -72,6 +72,6 @@ $fade-out-distance: 100px;
.list-item.placeholder {
background: black;
background: white;
filter: opacity(15%) brightness(0%);
}
+21 -1
View File
@@ -12,6 +12,9 @@ $hover-scale: 2;
left: 0;
width: 100%;
height: 100%;
/* Force the browser to paint separately to avoid composite cost with waveform */
transform: translateZ(0);
}
.trim-background {
@@ -50,8 +53,25 @@ $hover-scale: 2;
border: 1px solid $red-tertiary;
}
.playhead-container {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
overflow: hidden;
}
.playhead {
border: 1px solid $motion-primary;
/*
Even though playhead is just a line, it is 100% width (the width of the waveform)
so that we can use transform: translateX() using percentages.
*/
width: 100%;
border-left: 1px solid $motion-primary;
border-top: none;
border-bottom: none;
border-right: none;
}
.start-trim-line {
@@ -32,12 +32,14 @@ const AudioTrimmer = props => (
)}
{props.playhead ? (
<Box
className={classNames(styles.trimLine, styles.playhead)}
style={{
left: `${100 * props.playhead}%`
}}
/>
<div className={styles.playheadContainer}>
<div
className={classNames(styles.trimLine, styles.playhead)}
style={{
transform: `translateX(${100 * props.playhead}%)`
}}
/>
</div>
) : null}
{props.trimEnd === null ? null : (
+11 -2
View File
@@ -69,9 +69,18 @@
}
.backpack-item {
min-width: 4rem;
max-width: 6rem;
width: 4rem;
height: 4.5rem;
margin: 0 0.25rem;
flex-shrink: 0;
/* Need to hide overflow because of background setting below */
overflow: hidden;
}
.backpack-item > div {
/* Need to set the background to get blend-mode below to work */
background: $ui-primary;
}
.backpack-item img {
+2
View File
@@ -1,5 +1,6 @@
@import "../../css/units.css";
@import "../../css/colors.css";
@import "../../css/z-index.css";
.blocks {
height: 100%;
@@ -80,6 +81,7 @@
This does not prevent user interaction on the blocks themselves.
*/
pointer-events: none;
z-index: $z-index-drag-layer; /* make blocks match gui drag layer */
}
/*
+2 -1
View File
@@ -12,6 +12,7 @@ import helpIcon from '../../lib/assets/icon--tutorials.svg';
import closeIcon from './icon--close.svg';
import {translateVideo} from '../../lib/libraries/decks/translate-video.js';
import {translateImage} from '../../lib/libraries/decks/translate-image.js';
const CardHeader = ({onCloseCards, onShowAll, totalSteps, step}) => (
<div className={styles.headerButtons}>
@@ -275,7 +276,7 @@ const Cards = props => {
/>
) : (
<ImageStep
image={steps[step].image}
image={translateImage(steps[step].image, locale)}
title={steps[step].title}
/>
)
@@ -28,3 +28,11 @@
background: $motion-primary;
color: white;
}
.menu-item-bordered {
border-top: 1px solid $ui-black-transparent;
}
.menu-item-bordered:hover {
background: $error-primary;
}
@@ -1,5 +1,6 @@
import React from 'react';
import {ContextMenu, MenuItem} from 'react-contextmenu';
import classNames from 'classnames';
import styles from './context-menu.css';
@@ -17,7 +18,16 @@ const StyledMenuItem = props => (
/>
);
const BorderedMenuItem = props => (
<MenuItem
{...props}
attributes={{className: classNames(styles.menuItem, styles.menuItemBordered)}}
/>
);
export {
BorderedMenuItem,
StyledContextMenu as ContextMenu,
StyledMenuItem as MenuItem
};
+1 -2
View File
@@ -8,11 +8,10 @@
user-select: none;
user-drag: none;
cursor: pointer;
transition: 0.2s ease-out;
}
.green-flag:hover {
transform: scale(1.2);
background-color: $motion-light-transparent;
}
.green-flag.is-active {
+4 -12
View File
@@ -23,8 +23,6 @@ import BackdropLibrary from '../../containers/backdrop-library.jsx';
import Watermark from '../../containers/watermark.jsx';
import Backpack from '../../containers/backpack.jsx';
import PreviewModal from '../../containers/preview-modal.jsx';
import ImportModal from '../../containers/import-modal.jsx';
import WebGlModal from '../../containers/webgl-modal.jsx';
import TipsLibrary from '../../containers/tips-library.jsx';
import Cards from '../../containers/cards.jsx';
@@ -80,9 +78,9 @@ const GUIComponent = props => {
costumeLibraryVisible,
costumesTabVisible,
enableCommunity,
importInfoVisible,
intl,
isCreating,
isFullScreen,
isPlayerOnly,
isRtl,
isShared,
@@ -108,7 +106,6 @@ const GUIComponent = props => {
onTelemetryModalCancel,
onTelemetryModalOptIn,
onTelemetryModalOptOut,
previewInfoVisible,
showComingSoon,
soundsTabVisible,
stageSizeMode,
@@ -140,6 +137,7 @@ const GUIComponent = props => {
return isPlayerOnly ? (
<StageWrapper
isFullScreen={isFullScreen}
isRendererSupported={isRendererSupported}
isRtl={isRtl}
loading={loading}
@@ -156,9 +154,6 @@ const GUIComponent = props => {
dir={isRtl ? 'rtl' : 'ltr'}
{...componentProps}
>
{previewInfoVisible ? (
<PreviewModal />
) : null}
{telemetryModalVisible ? (
<TelemetryModal
onCancel={onTelemetryModalCancel}
@@ -173,9 +168,6 @@ const GUIComponent = props => {
{isCreating ? (
<Loader messageId="gui.loader.creating" />
) : null}
{importInfoVisible ? (
<ImportModal />
) : null}
{isRendererSupported ? null : (
<WebGlModal isRtl={isRtl} />
)}
@@ -337,6 +329,7 @@ const GUIComponent = props => {
<Box className={classNames(styles.stageAndTargetWrapper, styles[stageSize])}>
<StageWrapper
isRendererSupported={isRendererSupported}
isRtl={isRtl}
stageSize={stageSize}
vm={vm}
/>
@@ -378,9 +371,9 @@ GUIComponent.propTypes = {
costumeLibraryVisible: PropTypes.bool,
costumesTabVisible: PropTypes.bool,
enableCommunity: PropTypes.bool,
importInfoVisible: PropTypes.bool,
intl: intlShape.isRequired,
isCreating: PropTypes.bool,
isFullScreen: PropTypes.bool,
isPlayerOnly: PropTypes.bool,
isRtl: PropTypes.bool,
isShared: PropTypes.bool,
@@ -405,7 +398,6 @@ GUIComponent.propTypes = {
onTelemetryModalOptOut: PropTypes.func,
onToggleLoginOpen: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
previewInfoVisible: PropTypes.bool,
renderLogin: PropTypes.func,
showComingSoon: PropTypes.bool,
soundsTabVisible: PropTypes.bool,
@@ -5,16 +5,11 @@
flex-direction: column;
align-items: center;
cursor: pointer;
transition: 0.2s;
font-size: 0.75rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: $motion-primary;
}
.container:hover {
transform: scale(1.1);
}
.container + .container {
margin-top: 1.25rem;
}
@@ -1,190 +0,0 @@
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/typography.css";
@import "../../css/z-index.css";
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: $z-index-modal;
background-color: $ui-modal-overlay;
}
.modal-content {
margin: 100px auto;
outline: none;
border: .25rem solid $ui-white-transparent;
padding: 0;
border-radius: $space;
user-select: none;
width: 500px;
color: $text-primary;
overflow: hidden;
}
/*
TODO figure out how to remove filter altogether
since it is null...
Modal header has 3 items:
|x title filter|
Use the same width for both side item containers,
so that title remains centered
*/
$sides: 20rem;
.header {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
height: $library-header-height;
box-sizing: border-box;
width: 100%;
background-color: $extensions-primary;
}
.header-item {
display: flex;
align-items: center;
padding: 1rem;
text-decoration: none;
color: white;
user-select: none;
}
.header-item-filter {
display: flex;
flex-basis: $sides;
justify-content: flex-end;
}
.header-item-title {
flex-grow: 1;
flex-shrink: 0;
justify-content: center;
user-select: none;
letter-spacing: 0.4px;
cursor: default;
}
.header-item-title h2 {
font-size: 1.25rem;
}
.header-item-close {
display: flex;
flex-basis: $sides;
justify-content: flex-start;
}
[dir="rtl"] .header-item-close img {
transform: scaleX(-1);
}
.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
text-align: center;
}
.input-row {
margin: 1.5rem 0;
font-weight: bolder;
text-align: right;
display: flex;
justify-content: center;
border: 1px solid;
border-radius: 0.25rem;
overflow: hidden;
}
.ok-input-container {
border-color: $motion-primary;
box-shadow: 0 0 0 0.2rem $motion-transparent;
}
.bad-input-container {
border-color: $data-primary;
box-shadow: 0 0 0 0.2rem hsla(30, 100%, 55%, 0.15);
}
.input-row input {
width: 100%;
padding: 0 1rem;
height: 3rem;
color: $text-primary;
font-size: .875rem;
outline: none;
border: none;
}
.input-row input::placeholder {
font-style: italic;
color: $text-primary-transparent;
}
.input-row button {
padding: 0.5rem 2rem;
font-weight: bold;
font-size: .875rem;
cursor: pointer;
border: 0px solid $extensions-primary;
outline: none;
}
.input-row button.ok-button {
background: $extensions-primary;
color: white;
}
.error-row {
margin: 1.5rem 0;
text-align: center;
display: flex;
justify-content: center;
background: hsla(30, 100%, 55%, 0.25);
color: $data-primary;
border: 1px solid $data-primary;
border-radius: 0.25rem;
}
.error-row p {
font-size: 0.875rem;
font-weight: bold;
}
/* Confirmation buttons at the bottom of the modal */
.button-row {
margin: 1.5rem 0;
font-weight: bolder;
text-align: right;
display: flex;
justify-content: center;
}
.button-row button {
border: 1px solid $motion-primary;
border-radius: 0.25rem;
padding: 0.5rem 1.5rem;
background: white;
font-weight: bold;
font-size: .875rem;
cursor: pointer;
color: $motion-primary;
}
.faq-link-text {
margin: 2rem 0 .5rem 0;
font-size: .875rem;
color: $text-primary;
}
.faq-link {
color: $motion-primary;
text-decoration: none;
}
@@ -1,168 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactModal from 'react-modal';
import Box from '../box/box.jsx';
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import CloseButton from '../close-button/close-button.jsx';
import styles from './import-modal.css';
const messages = defineMessages({
title: {
id: 'gui.importInfo.title',
defaultMessage: 'View a Scratch 2.0 Project',
description: 'Scratch 2.0 import modal label - for accessibility'
},
formDescription: {
defaultMessage:
'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Beta will not be saved.',
description: 'Import project message',
id: 'gui.importInfo.betamessage'
},
previewFormDescription: {
defaultMessage:
'Enter a link to one of your shared Scratch projects. Changes made in this 3.0 Preview will not be saved.',
description: 'Import project message',
id: 'gui.importInfo.message'
},
invalidFormatError: {
id: 'gui.importInfo.invalidFormatError',
defaultMessage: 'Uh oh, that project link or id doesn\'t look quite right.',
description: 'Invalid project link or id message'
}
});
const ImportModal = ({intl, ...props}) => (
<ReactModal
isOpen
className={styles.modalContent}
contentLabel={intl.formatMessage({...messages.title})}
overlayClassName={styles.modalOverlay}
onRequestClose={props.onCancel}
>
<div dir={props.isRtl ? 'rtl' : 'ltr'} >
<Box>
<div className={styles.header}>
<div
className={classNames(
styles.headerItem,
styles.headerItemClose
)}
>
<CloseButton
buttonType="back"
size={CloseButton.SIZE_LARGE}
onClick={props.onGoBack}
/>
</div>
<div
className={classNames(
styles.headerItem,
styles.headerItemTitle
)}
>
<h2>
{intl.formatMessage({...messages.title})}
</h2>
</div>
<div className={classNames(styles.headerItem, styles.headerItemFilter)}>
{null}
</div>
</div>
</Box>
<Box className={styles.body}>
<p>
{intl.formatMessage({...messages.formDescription})}
</p>
<Box
className={classNames(styles.inputRow,
(props.hasValidationError ? styles.badInputContainer : styles.okInputContainer))
}
>
<input
autoFocus
placeholder={props.placeholder}
value={props.inputValue}
onChange={props.onChange}
onKeyPress={props.onKeyPress}
/>
<button
className={styles.okButton}
title={intl.formatMessage({
defaultMessage: 'View Project',
description: 'Tooltip for View button',
id: 'gui.importModal.viewprojecttooltip'
})}
onClick={props.onViewProject}
>
<FormattedMessage
defaultMessage="View"
description="Label for button to load a scratch 2.0 project"
id="gui.importModal.viewproject"
/>
</button>
</Box>
{props.hasValidationError ?
<Box className={styles.errorRow}>
<p>
<FormattedMessage
{...messages[`${props.errorMessage}`]}
/>
</p>
</Box> : null
}
<Box className={styles.buttonRow}>
<button
onClick={props.onGoBack}
>
<FormattedMessage
defaultMessage="Go Back"
description="Label for button to back out of importing a project"
id="gui.importInfo.goback"
/>
</button>
</Box>
<Box className={styles.faqLinkText}>
<FormattedMessage
defaultMessage="To learn more, go to the {previewFaqLink}."
description="Invitation to try 3.0 preview"
id="gui.importInfo.previewfaq"
values={{
previewFaqLink: (
<a
className={styles.faqLink}
href="//scratch.mit.edu/3faq"
>
<FormattedMessage
defaultMessage="FAQ"
description="link to Scratch 3.0 FAQ page"
id="gui.importInfo.previewfaqlinktext"
/>
</a>
)
}}
/>
</Box>
</Box>
</div>
</ReactModal>
);
ImportModal.propTypes = {
errorMessage: PropTypes.string.isRequired,
hasValidationError: PropTypes.bool.isRequired,
inputValue: PropTypes.string.isRequired,
intl: intlShape.isRequired,
isRtl: PropTypes.bool,
onCancel: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
onGoBack: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
onViewProject: PropTypes.func.isRequired,
placeholder: PropTypes.string
};
export default injectIntl(ImportModal);
@@ -8,6 +8,7 @@
justify-content: flex-start;
flex-basis: 160px;
height: 160px;
max-width: 160px;
margin: $space;
padding: 1rem 1rem 0 1rem;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+19 -52
View File
@@ -1,4 +1,3 @@
import bindAll from 'lodash.bindall';
import {FormattedMessage} from 'react-intl';
import PropTypes from 'prop-types';
import React from 'react';
@@ -10,42 +9,8 @@ import classNames from 'classnames';
import bluetoothIconURL from './bluetooth.svg';
import internetConnectionIconURL from './internet-connection.svg';
class LibraryItem extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
'handleBlur',
'handleClick',
'handleFocus',
'handleKeyPress',
'handleMouseEnter',
'handleMouseLeave'
]);
}
handleBlur () {
this.props.onBlur(this.props.id);
}
handleFocus () {
this.props.onFocus(this.props.id);
}
handleClick (e) {
if (!this.props.disabled) {
this.props.onSelect(this.props.id);
}
e.preventDefault();
}
handleKeyPress (e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this.props.onSelect(this.props.id);
}
}
handleMouseEnter () {
this.props.onMouseEnter(this.props.id);
}
handleMouseLeave () {
this.props.onMouseLeave(this.props.id);
}
/* eslint-disable react/prefer-stateless-function */
class LibraryItemComponent extends React.PureComponent {
render () {
return this.props.featured ? (
<div
@@ -58,7 +23,7 @@ class LibraryItem extends React.PureComponent {
this.props.extensionId ? styles.libraryItemExtension : null,
this.props.hidden ? styles.hidden : null
)}
onClick={this.handleClick}
onClick={this.props.onClick}
>
<div className={styles.featuredImageContainer}>
{this.props.disabled ? (
@@ -146,12 +111,12 @@ class LibraryItem extends React.PureComponent {
)}
role="button"
tabIndex="0"
onBlur={this.handleBlur}
onClick={this.handleClick}
onFocus={this.handleFocus}
onKeyPress={this.handleKeyPress}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onBlur={this.props.onBlur}
onClick={this.props.onClick}
onFocus={this.props.onFocus}
onKeyPress={this.props.onKeyPress}
onMouseEnter={this.props.onMouseEnter}
onMouseLeave={this.props.onMouseLeave}
>
{/* Layers of wrapping is to prevent layout thrashing on animation */}
<Box className={styles.libraryItemImageContainerWrapper}>
@@ -167,8 +132,10 @@ class LibraryItem extends React.PureComponent {
);
}
}
/* eslint-enable react/prefer-stateless-function */
LibraryItem.propTypes = {
LibraryItemComponent.propTypes = {
bluetoothRequired: PropTypes.bool,
collaborator: PropTypes.string,
description: PropTypes.oneOfType([
@@ -180,22 +147,22 @@ LibraryItem.propTypes = {
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconURL: PropTypes.string,
id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
name: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
onBlur: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func.isRequired,
onClick: PropTypes.func.isRequired,
onFocus: PropTypes.func.isRequired,
onKeyPress: PropTypes.func.isRequired,
onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired
onMouseLeave: PropTypes.func.isRequired
};
LibraryItem.defaultProps = {
LibraryItemComponent.defaultProps = {
disabled: false
};
export default LibraryItem;
export default LibraryItemComponent;
+23 -36
View File
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import LibraryItem from '../library-item/library-item.jsx';
import LibraryItem from '../../containers/library-item.jsx';
import Modal from '../../containers/modal.jsx';
import Divider from '../divider/divider.jsx';
import Filter from '../filter/filter.jsx';
@@ -33,11 +33,9 @@ class LibraryComponent extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleBlur',
'handleClose',
'handleFilterChange',
'handleFilterClear',
'handleFocus',
'handleMouseEnter',
'handleMouseLeave',
'handleSelect',
@@ -56,12 +54,6 @@ class LibraryComponent extends React.Component {
this.scrollToTop();
}
}
handleBlur (id) {
this.handleMouseLeave(id);
}
handleFocus (id) {
this.handleMouseEnter(id);
}
handleSelect (id) {
this.handleClose();
this.props.onItemSelected(this.getFilteredData()[id]);
@@ -172,33 +164,28 @@ class LibraryComponent extends React.Component {
})}
ref={this.setFilteredDataRef}
>
{this.getFilteredData().map((dataItem, index) => {
const scratchURL = dataItem.md5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${dataItem.md5}/get/` :
dataItem.rawURL;
return (
<LibraryItem
bluetoothRequired={dataItem.bluetoothRequired}
collaborator={dataItem.collaborator}
description={dataItem.description}
disabled={dataItem.disabled}
extensionId={dataItem.extensionId}
featured={dataItem.featured}
hidden={dataItem.hidden}
iconURL={scratchURL}
id={index}
insetIconURL={dataItem.insetIconURL}
internetConnectionRequired={dataItem.internetConnectionRequired}
key={`item_${index}`}
name={dataItem.name}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
);
})}
{this.getFilteredData().map((dataItem, index) => (
<LibraryItem
bluetoothRequired={dataItem.bluetoothRequired}
collaborator={dataItem.collaborator}
description={dataItem.description}
disabled={dataItem.disabled}
extensionId={dataItem.extensionId}
featured={dataItem.featured}
hidden={dataItem.hidden}
iconMd5={dataItem.md5}
iconRawURL={dataItem.rawURL}
icons={dataItem.json && dataItem.json.costumes}
id={index}
insetIconURL={dataItem.insetIconURL}
internetConnectionRequired={dataItem.internetConnectionRequired}
key={`item_${index}`}
name={dataItem.name}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
))}
</div>
</Modal>
);
+7
View File
@@ -17,6 +17,13 @@
color: white;
}
.fullscreen {
/* Break out of the layout using position: fixed to cover the whole screen */
position: fixed;
/* Use the fullscreen stage z-index to allow covering full-screen mode */
z-index: $z-index-stage-wrapper-overlay;
}
.block-animation {
width: 125px;
height: 150px;
+11 -6
View File
@@ -1,5 +1,6 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import styles from './loader.css';
import PropTypes from 'prop-types';
@@ -119,15 +120,13 @@ class LoaderComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
messageNumber: 0
messageNumber: this.chooseRandomMessage()
};
}
componentDidMount () {
this.chooseRandomMessage();
// Start an interval to choose a new message every 5 seconds
this.intervalId = setInterval(() => {
this.chooseRandomMessage();
this.setState({messageNumber: this.chooseRandomMessage()});
}, 5000);
}
componentWillUnmount () {
@@ -144,11 +143,15 @@ class LoaderComponent extends React.Component {
break;
}
}
this.setState({messageNumber});
return messageNumber;
}
render () {
return (
<div className={styles.background}>
<div
className={classNames(styles.background, {
[styles.fullscreen]: this.props.isFullScreen
})}
>
<div className={styles.container}>
<div className={styles.blockAnimation}>
<img
@@ -189,9 +192,11 @@ class LoaderComponent extends React.Component {
}
LoaderComponent.propTypes = {
isFullScreen: PropTypes.bool,
messageId: PropTypes.string
};
LoaderComponent.defaultProps = {
isFullScreen: false,
messageId: 'gui.loader.headline'
};
+18 -8
View File
@@ -3,6 +3,7 @@ import {connect} from 'react-redux';
import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl';
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
import bowser from 'bowser';
import React from 'react';
import VM from 'scratch-vm';
@@ -70,13 +71,8 @@ import languageIcon from '../language-selector/language-icon.svg';
import scratchLogo from './scratch-logo.svg';
const messages = defineMessages({
confirmNav: {
id: 'gui.menuBar.confirmNewWithoutSaving',
defaultMessage: 'Replace contents of the current project?',
description: 'message for prompting user to confirm that they want to create new project without saving'
}
});
import sharedMessages from '../../lib/shared-messages';
const ariaMessages = defineMessages({
language: {
id: 'gui.menuBar.LanguageSelector',
@@ -155,12 +151,19 @@ class MenuBar extends React.Component {
'handleClickSeeCommunity',
'handleClickShare',
'handleCloseFileMenuAndThen',
'handleKeyPress',
'handleLanguageMouseUp',
'handleRestoreOption',
'handleSaveToComputer',
'restoreOptionMessage'
]);
}
componentDidMount () {
document.addEventListener('keydown', this.handleKeyPress);
}
componentWillUnmount () {
document.removeEventListener('keydown', this.handleKeyPress);
}
handleClickNew () {
let readyToReplaceProject = true;
// if the project is dirty, and user owns the project, we will autosave.
@@ -170,7 +173,7 @@ class MenuBar extends React.Component {
// they'll lose their work.
if (this.props.projectChanged && !this.props.canCreateNew) {
readyToReplaceProject = confirm( // eslint-disable-line no-alert
this.props.intl.formatMessage(messages.confirmNav)
this.props.intl.formatMessage(sharedMessages.replaceProjectWarning)
);
}
this.props.onRequestCloseFile();
@@ -224,6 +227,13 @@ class MenuBar extends React.Component {
fn();
};
}
handleKeyPress (event) {
const modifier = bowser.mac ? event.metaKey : event.ctrlKey;
if (modifier && event.key === 's') {
this.props.onClickSave();
event.preventDefault();
}
}
handleSaveToComputer (downloadProject) {
return () => {
this.props.onRequestCloseFile();
+18
View File
@@ -1,3 +1,5 @@
@import "../../css/colors.css";
.green {
fill: rgb(171, 220, 170);
stroke: rgb(174, 211, 168);
@@ -12,3 +14,19 @@
fill: rgb(251, 194, 142);
stroke: rgb(235, 189, 142);
}
.mask-container {
position: relative;
}
.mask {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
transform-origin: top;
will-change: transform;
background: $ui-primary;
opacity: 0.75;
}
+29 -25
View File
@@ -20,36 +20,40 @@ const Meter = props => {
const barHeight = (height - (barSpacing * (nBars + 1))) / nBars;
const nBarsToMask = nBars - Math.floor(level * nBars);
const scale = ((nBarsToMask * (barHeight + barSpacing)) + (barSpacing / 2)) / height;
return (
<svg
className={styles.container}
height={height}
width={width}
<div
className={styles.maskContainer}
style={{height: `${height}px`}}
>
{Array(nBars).fill(0)
.map((value, index) => (
<rect
className={index < nGreen ? styles.green :
(index < nGreen + nYellow ? styles.yellow : styles.red)}
height={barHeight}
key={index}
rx={barRounding}
ry={barRounding}
width={width - 2}
x={1}
y={height - ((barSpacing + barHeight) * (index + 1))}
/>
))}
<rect
fill="hsla(215, 100%, 95%, 1)"
height={(nBarsToMask * (barHeight + barSpacing)) + (barSpacing / 2)}
opacity="0.75"
<svg
className={styles.container}
height={height}
width={width}
x={0}
y={0}
>
{Array(nBars).fill(0)
.map((value, index) => (
<rect
className={index < nGreen ? styles.green :
(index < nGreen + nYellow ? styles.yellow : styles.red)}
height={barHeight}
key={index}
rx={barRounding}
ry={barRounding}
width={width - 2}
x={1}
y={height - ((barSpacing + barHeight) * (index + 1))}
/>
))}
</svg>
<div
className={styles.mask}
style={{
transform: `scaleY(${scale})`
}}
/>
</svg>
</div>
);
};
@@ -27,6 +27,7 @@ const MonitorList = props => (
draggable={props.draggable}
height={monitorData.height}
id={monitorData.id}
isDiscrete={monitorData.isDiscrete}
key={monitorData.id}
max={monitorData.sliderMax}
min={monitorData.sliderMin}
+4 -1
View File
@@ -4,7 +4,7 @@ import classNames from 'classnames';
import styles from './monitor.css';
const SliderMonitor = ({categoryColor, label, min, max, value, onSliderUpdate}) => (
const SliderMonitor = ({categoryColor, isDiscrete, label, min, max, value, onSliderUpdate}) => (
<div className={styles.defaultMonitor}>
<div className={styles.row}>
<div className={styles.label}>
@@ -22,6 +22,7 @@ const SliderMonitor = ({categoryColor, label, min, max, value, onSliderUpdate})
className={classNames(styles.slider, 'no-drag')} // Class used on parent Draggable to prevent drags
max={max}
min={min}
step={isDiscrete ? 1 : 0.01}
type="range"
value={value}
onChange={onSliderUpdate}
@@ -33,6 +34,7 @@ const SliderMonitor = ({categoryColor, label, min, max, value, onSliderUpdate})
SliderMonitor.propTypes = {
categoryColor: PropTypes.string.isRequired,
isDiscrete: PropTypes.bool,
label: PropTypes.string.isRequired,
max: PropTypes.number,
min: PropTypes.number,
@@ -44,6 +46,7 @@ SliderMonitor.propTypes = {
};
SliderMonitor.defaultProps = {
isDiscrete: true,
min: 0,
max: 100
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

@@ -1,108 +0,0 @@
@import "../../css/colors.css";
@import "../../css/units.css";
@import "../../css/typography.css";
@import "../../css/z-index.css";
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: $z-index-modal;
background-color: $ui-modal-overlay;
}
.modal-content {
margin: 100px auto;
outline: none;
border: .25rem solid $ui-white-transparent;
padding: 0;
border-radius: $space;
user-select: none;
width: 500px;
color: $text-primary;
overflow: hidden;
}
.illustration {
width: 100%;
height: 208px;
background-color: $motion-primary;
background-image: url('./welcome.png');
background-size: cover;
}
.body {
background: $ui-white;
padding: 1.5rem 2.25rem;
text-align: center;
}
/* Confirmation buttons at the bottom of the modal */
.button-row {
margin: 1.5rem 0;
font-weight: bolder;
text-align: right;
display: flex;
justify-content: center;
}
.button-row button {
border: 1px solid $motion-primary;
border-radius: 0.25rem;
padding: 0.5rem 1.5rem;
background: white;
font-weight: bold;
font-size: .875rem;
cursor: pointer;
}
.button-row button.ok-button {
background: $motion-primary;
color: white;
}
.button-row button.no-button {
color: $motion-primary;
}
.button-row button.view-project-button {
background: $extensions-primary;
border-color: $extensions-primary;
color: white;
}
[dir="ltr"] .button-row button + button {
margin-left: 0.5rem;
}
[dir="rtl"] .button-row button + button {
margin-right: 0.5rem;
}
.cat-icon {
width: 1.5rem;
height: 1.5rem;
vertical-align: middle;
}
[dir="ltr"] .cat-icon {
margin-left: .125rem;
}
[dir="rtl"] .cat-icon {
margin-right: .125rem;
}
.faq-link-text {
margin: 2rem 0 .5rem 0;
font-size: .875rem;
color: $text-primary;
}
.faq-link {
color: $motion-primary;
text-decoration: none;
}
@@ -1,144 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ReactModal from 'react-modal';
import Box from '../box/box.jsx';
import {defineMessages, injectIntl, intlShape, FormattedMessage} from 'react-intl';
import styles from './preview-modal.css';
import catIcon from './happy-cat.png';
const messages = defineMessages({
label: {
id: 'gui.previewInfo.label',
defaultMessage: 'Try Scratch 3.0',
description: 'Scratch 3.0 modal label - for accessibility'
},
previewWelcome: {
defaultMessage: 'Welcome to the Scratch 3.0 Beta',
description: 'Header for Preview Info Modal',
id: 'gui.previewInfo.welcome'
},
notNowTooltip: {
defaultMessage: 'Not Now',
description: 'Tooltip for Not Now button',
id: 'gui.previewModal.notnowtooltip'
},
tryItTooltip: {
defaultMessage: 'Try It',
description: 'Tooltip for Try It button',
id: 'gui.previewModal.tryittooltip'
},
viewProjectTooltip: {
defaultMessage: 'View 2.0 Project',
description: 'Tooltip for View 2.0 Project button',
id: 'gui.previewModal.viewprojecttooltip'
}
});
const PreviewModal = ({intl, ...props}) => (
<ReactModal
isOpen
className={styles.modalContent}
contentLabel={intl.formatMessage({...messages.label})}
overlayClassName={styles.modalOverlay}
onRequestClose={props.onTryIt}
>
<div dir={props.isRtl ? 'rtl' : 'ltr'} >
<Box className={styles.illustration} />
<Box className={styles.body}>
<h2>
<FormattedMessage
defaultMessage="Welcome to the Scratch 3.0 Beta"
description="Header for Beta Info Modal"
id="gui.previewInfo.betawelcome"
/>
</h2>
<p>
{ /* eslint-disable max-len */ }
<FormattedMessage
defaultMessage="We're working on the next generation of Scratch. We're excited for you to try it!"
description="Invitation to try 3.0 Beta"
id="gui.previewInfo.invitation"
/>
{ /* eslint-enable max-len */ }
</p>
<Box className={styles.buttonRow}>
<button
className={styles.noButton}
title={intl.formatMessage(messages.notNowTooltip)}
onClick={props.onCancel}
>
<FormattedMessage
defaultMessage="Not Now"
description="Label for button to back out of trying Scratch 3.0 Beta"
id="gui.previewInfo.notnow"
/>
</button>
<button
className={styles.okButton}
title={intl.formatMessage(messages.tryItTooltip)}
onClick={props.onTryIt}
>
<FormattedMessage
defaultMessage="Try It! {caticon}"
description="Label for button to try Scratch 3.0 Beta"
id="gui.previewModal.tryit"
values={{
caticon: (
<img
className={styles.catIcon}
src={catIcon}
/>
)
}}
/>
</button>
<button
className={styles.viewProjectButton}
title={intl.formatMessage(messages.viewProjectTooltip)}
onClick={props.onViewProject}
>
<FormattedMessage
defaultMessage="View 2.0 Project"
description="Label for button to import a 2.0 project"
id="gui.previewModal.viewproject"
/>
</button>
</Box>
<Box className={styles.faqLinkText}>
<FormattedMessage
defaultMessage="To learn more, go to the {previewFaqLink}."
description="Invitation to try 3.0 Beta"
id="gui.previewInfo.previewfaq"
values={{
previewFaqLink: (
<a
className={styles.faqLink}
href="//scratch.mit.edu/3faq"
>
<FormattedMessage
defaultMessage="FAQ"
description="link to Scratch 3.0 FAQ page"
id="gui.previewInfo.previewfaqlinktext"
/>
</a>
)
}}
/>
</Box>
</Box>
</div>
</ReactModal>
);
PreviewModal.propTypes = {
intl: intlShape.isRequired,
isRtl: PropTypes.bool,
onCancel: PropTypes.func.isRequired,
onTryIt: PropTypes.func.isRequired,
onViewProject: PropTypes.func.isRequired
};
export default injectIntl(PreviewModal);
Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

+9 -8
View File
@@ -34,14 +34,15 @@ class SpriteInfo extends React.Component {
shouldComponentUpdate (nextProps) {
return (
this.props.rotationStyle !== nextProps.rotationStyle ||
this.props.direction !== nextProps.direction ||
this.props.disabled !== nextProps.disabled ||
this.props.name !== nextProps.name ||
this.props.size !== nextProps.size ||
this.props.stageSize !== nextProps.stageSize ||
this.props.visible !== nextProps.visible ||
this.props.x !== nextProps.x ||
this.props.y !== nextProps.y
// Only update these if rounded value has changed
Math.round(this.props.direction) !== Math.round(nextProps.direction) ||
Math.round(this.props.size) !== Math.round(nextProps.size) ||
Math.round(this.props.x) !== Math.round(nextProps.x) ||
Math.round(this.props.y) !== Math.round(nextProps.y)
);
}
render () {
@@ -110,7 +111,7 @@ class SpriteInfo extends React.Component {
placeholder="x"
tabIndex="0"
type="text"
value={this.props.disabled ? '' : this.props.x}
value={this.props.disabled ? '' : Math.round(this.props.x)}
onSubmit={this.props.onChangeX}
/>
</Label>
@@ -137,7 +138,7 @@ class SpriteInfo extends React.Component {
placeholder="y"
tabIndex="0"
type="text"
value={this.props.disabled ? '' : this.props.y}
value={this.props.disabled ? '' : Math.round(this.props.y)}
onSubmit={this.props.onChangeY}
/>
</Label>
@@ -237,14 +238,14 @@ class SpriteInfo extends React.Component {
label={sizeLabel}
tabIndex="0"
type="text"
value={this.props.disabled ? '' : this.props.size}
value={this.props.disabled ? '' : Math.round(this.props.size)}
onSubmit={this.props.onChangeSize}
/>
</Label>
</div>
<div className={classNames(styles.group, styles.largerInput)}>
<DirectionPicker
direction={this.props.direction}
direction={Math.round(this.props.direction)}
disabled={this.props.disabled}
labelAbove={labelAbove}
rotationStyle={this.props.rotationStyle}
@@ -18,7 +18,6 @@
text-align: center;
cursor: pointer;
transition: 0.25s ease-out;
user-select: none;
touch-action: none;
@@ -39,8 +38,24 @@
filter: drop-shadow(0px 0px 2px $ui-black-transparent);
}
/* Outer/Inner chicanery is to prevent layouts when sprite image changes */
.sprite-image-outer {
position: relative;
width: 100%;
height: 100%;
transform: translateZ(0);
}
.sprite-image-inner {
position: absolute;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.sprite-image {
margin: auto;
user-select: none;
pointer-events: none;
max-width: 32px;
@@ -5,7 +5,7 @@ import React from 'react';
import CloseButton from '../close-button/close-button.jsx';
import styles from './sprite-selector-item.css';
import {ContextMenuTrigger} from 'react-contextmenu';
import {ContextMenu, MenuItem} from '../context-menu/context-menu.jsx';
import {BorderedMenuItem, ContextMenu, MenuItem} from '../context-menu/context-menu.jsx';
import {FormattedMessage} from 'react-intl';
// react-contextmenu requires unique id to match trigger and context menu
@@ -26,22 +26,19 @@ const SpriteSelectorItem = props => (
disable={props.dragging}
id={`${props.name}-${contextMenuId}`}
>
{(props.selected && props.onDeleteButtonClick) ? (
<CloseButton
className={styles.deleteButton}
size={CloseButton.SIZE_SMALL}
onClick={props.onDeleteButtonClick}
/>
) : null }
{typeof props.number === 'undefined' ? null : (
<div className={styles.number}>{props.number}</div>
)}
{props.costumeURL ? (
<img
className={styles.spriteImage}
draggable={false}
src={props.costumeURL}
/>
<div className={styles.spriteImageOuter}>
<div className={styles.spriteImageInner}>
<img
className={styles.spriteImage}
draggable={false}
src={props.costumeURL}
/>
</div>
</div>
) : null}
<div className={styles.spriteInfo}>
<div className={styles.spriteName}>{props.name}</div>
@@ -49,6 +46,13 @@ const SpriteSelectorItem = props => (
<div className={styles.spriteDetails}>{props.details}</div>
) : null}
</div>
{(props.selected && props.onDeleteButtonClick) ? (
<CloseButton
className={styles.deleteButton}
size={CloseButton.SIZE_SMALL}
onClick={props.onDeleteButtonClick}
/>
) : null }
{props.onDuplicateButtonClick || props.onDeleteButtonClick || props.onExportButtonClick ? (
<ContextMenu id={`${props.name}-${contextMenuId++}`}>
{props.onDuplicateButtonClick ? (
@@ -60,15 +64,6 @@ const SpriteSelectorItem = props => (
/>
</MenuItem>
) : null}
{props.onDeleteButtonClick ? (
<MenuItem onClick={props.onDeleteButtonClick}>
<FormattedMessage
defaultMessage="delete"
description="Menu item to delete in the right click menu"
id="gui.spriteSelectorItem.contextMenuDelete"
/>
</MenuItem>
) : null }
{props.onExportButtonClick ? (
<MenuItem onClick={props.onExportButtonClick}>
<FormattedMessage
@@ -78,6 +73,15 @@ const SpriteSelectorItem = props => (
/>
</MenuItem>
) : null }
{props.onDeleteButtonClick ? (
<BorderedMenuItem onClick={props.onDeleteButtonClick}>
<FormattedMessage
defaultMessage="delete"
description="Menu item to delete in the right click menu"
id="gui.spriteSelectorItem.contextMenuDelete"
/>
</BorderedMenuItem>
) : null }
</ContextMenu>
) : null}
</ContextMenuTrigger>
@@ -8,9 +8,12 @@ import Box from '../box/box.jsx';
import SpriteSelectorItem from '../../containers/sprite-selector-item.jsx';
import SortableHOC from '../../lib/sortable-hoc.jsx';
import SortableAsset from '../asset-panel/sortable-asset.jsx';
import ThrottledPropertyHOC from '../../lib/throttled-property-hoc.jsx';
import styles from './sprite-selector.css';
const ThrottledSpriteSelectorItem = ThrottledPropertyHOC('asset', 500)(SpriteSelectorItem);
const SpriteList = function (props) {
const {
containerRef,
@@ -74,13 +77,13 @@ const SpriteList = function (props) {
onAddSortable={onAddSortable}
onRemoveSortable={onRemoveSortable}
>
<SpriteSelectorItem
<ThrottledSpriteSelectorItem
asset={sprite.costume && sprite.costume.asset}
className={classNames(styles.sprite, {
[styles.raised]: isRaised,
[styles.receivedBlocks]: receivedBlocks
})}
dragPayload={sprite}
dragPayload={sprite.id}
dragType={DragConstants.SPRITE}
id={sprite.id}
index={index}
@@ -120,9 +120,10 @@ const SpriteSelectorComponent = function (props) {
title: intl.formatMessage(messages.addSpriteFromFile),
img: fileUploadIcon,
onClick: onFileUploadClick,
fileAccept: '.svg, .png, .jpg, .jpeg, .sprite2, .sprite3',
fileAccept: '.svg, .png, .jpg, .jpeg, .sprite2, .sprite3, .gif',
fileChange: onSpriteUpload,
fileInput: spriteFileInput
fileInput: spriteFileInput,
fileMultiple: true
}, {
title: intl.formatMessage(messages.addSpriteFromSurprise),
img: surpriseIcon,
@@ -102,9 +102,10 @@ const StageSelector = props => {
title: intl.formatMessage(messages.addBackdropFromFile),
img: fileUploadIcon,
onClick: onBackdropFileUploadClick,
fileAccept: '.svg, .png, .jpg, .jpeg', // Bitmap coming soon
fileAccept: '.svg, .png, .jpg, .jpeg, .gif',
fileChange: onBackdropFileUpload,
fileInput: fileInputRef
fileInput: fileInputRef,
fileMultiple: true
}, {
title: intl.formatMessage(messages.addBackdropFromSurprise),
img: surpriseIcon,
@@ -12,6 +12,7 @@ import styles from './stage-wrapper.css';
const StageWrapperComponent = function (props) {
const {
isFullScreen,
isRtl,
isRendererSupported,
loading,
@@ -41,15 +42,16 @@ const StageWrapperComponent = function (props) {
}
</Box>
{loading ? (
<Loader />
<Loader isFullScreen={isFullScreen} />
) : null}
</Box>
);
};
StageWrapperComponent.propTypes = {
isFullScreen: PropTypes.bool,
isRendererSupported: PropTypes.bool.isRequired,
isRtl: PropTypes.bool,
isRtl: PropTypes.bool.isRequired,
loading: PropTypes.bool,
stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired,
vm: PropTypes.instanceOf(VM).isRequired
+3 -2
View File
@@ -128,6 +128,7 @@ to adjust for the border using a different method */
}
.question-wrapper {
z-index: $z-index-stage-question;
pointer-events: auto;
}
@@ -147,6 +148,8 @@ to adjust for the border using a different method */
align-items: center;
background: rgba(0,0,0,0.25);
border-radius: 0.5rem;
pointer-events: all;
cursor: pointer;
}
.green-flag-overlay {
@@ -154,8 +157,6 @@ to adjust for the border using a different method */
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;
+4 -5
View File
@@ -81,11 +81,10 @@ const StageComponent = props => {
/>
</Box>
{isStarted ? null : (
<Box className={styles.greenFlagOverlayWrapper}>
<GreenFlagOverlay
className={styles.greenFlagOverlay}
/>
</Box>
<GreenFlagOverlay
className={styles.greenFlagOverlay}
wrapperClass={styles.greenFlagOverlayWrapper}
/>
)}
{isColorPicking && colorInfo ? (
<Box className={styles.colorPickerWrapper}>
+6 -5
View File
@@ -1,3 +1,5 @@
@import "../../css/colors.css";
.stop-all {
width: 2rem;
height: 2rem;
@@ -5,17 +7,16 @@
border-radius: 0.25rem;
user-select: none;
cursor: pointer;
transition: 0.2s ease-out;
}
.stop-all:hover {
background-color: $motion-light-transparent;
}
.stop-all {
opacity: 0.5;
}
.stop-all:hover {
transform: scale(1.2);
}
.stop-all.is-active {
opacity: 1;
}
+54 -41
View File
@@ -2,52 +2,65 @@ import React from 'react';
import PropTypes from 'prop-types';
import styles from './waveform.css';
const Waveform = props => {
const {
width,
height,
data
} = props;
// Waveform is expensive to compute, make sure it only updates when data does
// by using PureComponent. In future can be changed back to function with React.memo
// eslint-disable-next-line react/prefer-stateless-function
class Waveform extends React.PureComponent {
render () {
const {
width,
height,
data
} = this.props;
const cappedData = [0, ...data, 0];
// Never want a density of points higher than the number of pixels
// This is very conservative, could be far fewer points because of curve smoothing.
// Drawing too many points seems to cause an explosion in browser
// composite time when animating the playhead
const takeEveryN = Math.ceil(data.length / width);
const points = [
...cappedData.map((v, i) =>
[width * i / cappedData.length, height * v / 2]
),
...cappedData.reverse().map((v, i) =>
[width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2]
)
];
const filteredData = takeEveryN === 1 ? data :
data.filter((_, i) => i % takeEveryN === 0);
const pathComponents = points.map(([x, y], i) => {
const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0];
return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`;
});
const cappedData = [0, ...filteredData, 0];
return (
<svg
className={styles.container}
viewBox={`-1 0 ${width} ${height}`}
>
<line
className={styles.baseline}
x1={-1}
x2={width}
y1={height / 2}
y2={height / 2}
/>
<g transform={`scale(1, -1) translate(0, -${height / 2})`}>
<path
className={styles.waveformPath}
d={`M0 0${pathComponents.join(' ')}Z`}
strokeLinejoin={'round'}
strokeWidth={1}
const points = [
...cappedData.map((v, i) =>
[width * i / cappedData.length, height * v / 2]
),
...cappedData.reverse().map((v, i) =>
[width * (cappedData.length - i - 1) / cappedData.length, -height * v / 2]
)
];
const pathComponents = points.map(([x, y], i) => {
const [nx, ny] = points[i < points.length - 1 ? i + 1 : 0];
return `Q${x} ${y} ${(x + nx) / 2} ${(y + ny) / 2}`;
});
return (
<svg
className={styles.container}
viewBox={`-1 0 ${width} ${height}`}
>
<line
className={styles.baseline}
x1={-1}
x2={width}
y1={height / 2}
y2={height / 2}
/>
</g>
</svg>
);
};
<g transform={`scale(1, -1) translate(0, -${height / 2})`}>
<path
className={styles.waveformPath}
d={`M0 0${pathComponents.join(' ')}Z`}
strokeLinejoin={'round'}
strokeWidth={1}
/>
</g>
</svg>
);
}
}
Waveform.propTypes = {
data: PropTypes.arrayOf(PropTypes.number),
+60 -26
View File
@@ -50,6 +50,7 @@ class Blocks extends React.Component {
bindAll(this, [
'attachVM',
'detachVM',
'getToolboxXML',
'handleCategorySelected',
'handleConnectionModalStart',
'handleDrop',
@@ -86,6 +87,7 @@ class Blocks extends React.Component {
componentDidMount () {
this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker;
this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures;
this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
const workspaceConfig = defaultsDeep({},
Blocks.defaultOptions,
@@ -94,6 +96,11 @@ class Blocks extends React.Component {
);
this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
// Store the xml of the toolbox that is actually rendered.
// This is used in componentDidUpdate instead of prevProps, because
// the xml can change while e.g. on the costumes tab.
this._renderedToolboxXML = this.props.toolboxXML;
// we actually never want the workspace to enable "refresh toolbox" - this basically re-renders the
// entire toolbox every time we reset the workspace. We call updateToolbox as a part of
// componentDidUpdate so the toolbox will still correctly be updated
@@ -117,7 +124,7 @@ class Blocks extends React.Component {
return (
this.state.prompt !== nextState.prompt ||
this.props.isVisible !== nextProps.isVisible ||
this.props.toolboxXML !== nextProps.toolboxXML ||
this._renderedToolboxXML !== nextProps.toolboxXML ||
this.props.extensionLibraryVisible !== nextProps.extensionLibraryVisible ||
this.props.customProceduresVisible !== nextProps.customProceduresVisible ||
this.props.locale !== nextProps.locale ||
@@ -131,13 +138,13 @@ class Blocks extends React.Component {
this.ScratchBlocks.hideChaff();
}
if (prevProps.toolboxXML !== this.props.toolboxXML) {
// rather than update the toolbox "sync" -- update it in the next frame
clearTimeout(this.toolboxUpdateTimeout);
this.toolboxUpdateTimeout = setTimeout(() => {
this.updateToolbox();
}, 0);
// Only rerender the toolbox when the blocks are visible and the xml is
// different from the previously rendered toolbox xml.
// Do not check against prevProps.toolboxXML because that may not have been rendered.
if (this.props.isVisible && this.props.toolboxXML !== this._renderedToolboxXML) {
this.requestToolboxUpdate();
}
if (this.props.isVisible === prevProps.isVisible) {
if (this.props.stageSize !== prevProps.stageSize) {
// force workspace to redraw for the new stage size
@@ -155,7 +162,7 @@ class Blocks extends React.Component {
this.setLocale();
} else {
this.props.vm.refreshWorkspace();
this.updateToolbox();
this.requestToolboxUpdate();
}
window.dispatchEvent(new Event('resize'));
@@ -168,15 +175,22 @@ class Blocks extends React.Component {
this.workspace.dispose();
clearTimeout(this.toolboxUpdateTimeout);
}
requestToolboxUpdate () {
clearTimeout(this.toolboxUpdateTimeout);
this.toolboxUpdateTimeout = setTimeout(() => {
this.updateToolbox();
}, 0);
}
setLocale () {
this.workspace.getFlyout().setRecyclingEnabled(false);
this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
this.props.vm.setLocale(this.props.locale, this.props.messages)
.then(() => {
this.workspace.getFlyout().setRecyclingEnabled(false);
this.props.vm.refreshWorkspace();
this.updateToolbox();
this.workspace.getFlyout().setRecyclingEnabled(true);
this.requestToolboxUpdate();
this.withToolboxUpdates(() => {
this.workspace.getFlyout().setRecyclingEnabled(true);
});
});
}
@@ -186,6 +200,8 @@ class Blocks extends React.Component {
const categoryId = this.workspace.toolbox_.getSelectedCategoryId();
const offset = this.workspace.toolbox_.getCategoryScrollOffset();
this.workspace.updateToolbox(this.props.toolboxXML);
this._renderedToolboxXML = this.props.toolboxXML;
// In order to catch any changes that mutate the toolbox during "normal runtime"
// (variable changes/etc), re-enable toolbox refresh.
// Using the setter function will rerender the entire toolbox which we just rendered.
@@ -230,7 +246,7 @@ class Blocks extends React.Component {
this.props.vm.addListener('EXTENSION_ADDED', this.handleExtensionAdded);
this.props.vm.addListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate);
this.props.vm.addListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate);
this.props.vm.addListener('PERIPHERAL_DISCONNECT_ERROR', this.handleStatusButtonUpdate);
this.props.vm.addListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate);
}
detachVM () {
this.props.vm.removeListener('SCRIPT_GLOW_ON', this.onScriptGlowOn);
@@ -243,7 +259,7 @@ class Blocks extends React.Component {
this.props.vm.removeListener('EXTENSION_ADDED', this.handleExtensionAdded);
this.props.vm.removeListener('BLOCKSINFO_UPDATE', this.handleBlocksInfoUpdate);
this.props.vm.removeListener('PERIPHERAL_CONNECTED', this.handleStatusButtonUpdate);
this.props.vm.removeListener('PERIPHERAL_DISCONNECT_ERROR', this.handleStatusButtonUpdate);
this.props.vm.removeListener('PERIPHERAL_DISCONNECTED', this.handleStatusButtonUpdate);
}
updateToolboxBlockValue (id, value) {
@@ -259,7 +275,7 @@ class Blocks extends React.Component {
}
onTargetsUpdate () {
if (this.props.vm.editingTarget) {
if (this.props.vm.editingTarget && this.workspace.getFlyout()) {
['glide', 'move', 'set'].forEach(prefix => {
this.updateToolboxBlockValue(`${prefix}x`, Math.round(this.props.vm.editingTarget.x).toString());
this.updateToolboxBlockValue(`${prefix}y`, Math.round(this.props.vm.editingTarget.y).toString());
@@ -294,12 +310,32 @@ class Blocks extends React.Component {
onVisualReport (data) {
this.workspace.reportValue(data.id, data.value);
}
getToolboxXML () {
// Use try/catch because this requires digging pretty deep into the VM
// Code inside intentionally ignores several error situations (no stage, etc.)
// Because they would get caught by this try/catch
try {
let {editingTarget: target, runtime} = this.props.vm;
const stage = runtime.getTargetForStage();
if (!target) target = stage; // If no editingTarget, use the stage
const stageCostumes = stage.getCostumes();
const targetCostumes = target.getCostumes();
const targetSounds = target.getSounds();
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML,
targetCostumes[0].name,
stageCostumes[0].name,
targetSounds.length > 0 ? targetSounds[0].name : ''
);
} catch {
return null;
}
}
onWorkspaceUpdate (data) {
// When we change sprites, update the toolbox to have the new sprite's blocks
if (this.props.vm.editingTarget) {
const target = this.props.vm.editingTarget;
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML);
const toolboxXML = this.getToolboxXML();
if (toolboxXML) {
this.props.updateToolboxState(toolboxXML);
}
@@ -347,12 +383,9 @@ class Blocks extends React.Component {
// this actually defines blocks and MUST run regardless of the UI state
this.ScratchBlocks.defineBlocksWithJsonArray(blocksInfo.map(blockInfo => blockInfo.json).filter(x => x));
// update the toolbox view: this can be skipped if we're not looking at a target, etc.
const runtime = this.props.vm.runtime;
const target = runtime.getEditingTarget() || runtime.getTargetForStage();
if (target) {
const dynamicBlocksXML = runtime.getBlocksXML();
const toolboxXML = makeToolboxXML(target.isStage, target.id, dynamicBlocksXML);
// Update the toolbox with new blocks
const toolboxXML = this.getToolboxXML();
if (toolboxXML) {
this.props.updateToolboxState(toolboxXML);
}
}
@@ -423,6 +456,7 @@ class Blocks extends React.Component {
.then(blocks => this.props.vm.shareBlocksToTarget(blocks, this.props.vm.editingTarget.id))
.then(() => {
this.props.vm.refreshWorkspace();
this.updateToolbox(); // To show new variables/custom blocks
});
}
render () {
@@ -495,7 +529,7 @@ Blocks.propTypes = {
extensionLibraryVisible: PropTypes.bool,
isRtl: PropTypes.bool,
isVisible: PropTypes.bool,
locale: PropTypes.string,
locale: PropTypes.string.isRequired,
messages: PropTypes.objectOf(PropTypes.string),
onActivateColorPicker: PropTypes.func,
onActivateCustomProcedures: PropTypes.func,
+26 -1
View File
@@ -1,4 +1,6 @@
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import React from 'react';
import {
activateDeck,
@@ -15,6 +17,29 @@ import {
} from '../reducers/modals';
import CardsComponent from '../components/cards/cards.jsx';
import {loadImageData} from '../lib/libraries/decks/translate-image.js';
class Cards extends React.Component {
componentDidMount () {
if (this.props.locale !== 'en') {
loadImageData(this.props.locale);
}
}
componentDidUpdate (prevProps) {
if (this.props.locale !== prevProps.locale) {
loadImageData(this.props.locale);
}
}
render () {
return (
<CardsComponent {...this.props} />
);
}
}
Cards.propTypes = {
locale: PropTypes.string.isRequired
};
const mapStateToProps = state => ({
visible: state.scratchGui.cards.visible,
@@ -45,4 +70,4 @@ const mapDispatchToProps = dispatch => ({
export default connect(
mapStateToProps,
mapDispatchToProps
)(CardsComponent);
)(Cards);
+42 -13
View File
@@ -13,6 +13,7 @@ import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import DragConstants from '../lib/drag-constants';
import {emptyCostume} from '../lib/empty-assets';
import sharedMessages from '../lib/shared-messages';
import downloadBlob from '../lib/download-blob';
import {
closeCameraCapture,
@@ -27,6 +28,7 @@ import {
} from '../reducers/editor-tab';
import {setRestore} from '../reducers/restore-deletion';
import {showStandardAlert, closeAlertWithId} from '../reducers/alerts';
import addLibraryBackdropIcon from '../components/asset-panel/icon--add-backdrop-lib.svg';
import addLibraryCostumeIcon from '../components/asset-panel/icon--add-costume-lib.svg';
@@ -86,6 +88,7 @@ class CostumeTab extends React.Component {
'handleSelectCostume',
'handleDeleteCostume',
'handleDuplicateCostume',
'handleExportCostume',
'handleNewCostume',
'handleNewBlankCostume',
'handleSurpriseCostume',
@@ -150,12 +153,20 @@ class CostumeTab extends React.Component {
handleDuplicateCostume (costumeIndex) {
this.props.vm.duplicateCostume(costumeIndex);
}
handleExportCostume (costumeIndex) {
const item = this.props.vm.editingTarget.sprite.costumes[costumeIndex];
const blob = new Blob([item.asset.data], {type: item.asset.assetType.contentType});
downloadBlob(`${item.name}.${item.asset.dataFormat}`, blob);
}
handleNewCostume (costume, fromCostumeLibrary) {
if (fromCostumeLibrary) {
this.props.vm.addCostumeFromLibrary(costume.md5, costume);
} else {
this.props.vm.addCostume(costume.md5, costume);
}
const costumes = Array.isArray(costume) ? costume : [costume];
return Promise.all(costumes.map(c => {
if (fromCostumeLibrary) {
return this.props.vm.addCostumeFromLibrary(c.md5, c);
}
return this.props.vm.addCostume(c.md5, c);
}));
}
handleNewBlankCostume () {
const name = this.props.vm.editingTarget.isStage ?
@@ -193,14 +204,26 @@ class CostumeTab extends React.Component {
}
handleCostumeUpload (e) {
const storage = this.props.vm.runtime.storage;
handleFileUpload(e.target, (buffer, fileType, fileName) => {
costumeUpload(buffer, fileType, fileName, storage, this.handleNewCostume);
});
this.props.onShowImporting();
handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => {
costumeUpload(buffer, fileType, storage, vmCostumes => {
vmCostumes.forEach((costume, i) => {
costume.name = `${fileName}${i ? i + 1 : ''}`;
});
this.handleNewCostume(vmCostumes).then(() => {
if (fileIndex === fileCount - 1) {
this.props.onCloseImporting();
}
});
}, this.props.onCloseImporting);
}, this.props.onCloseImporting);
}
handleCameraBuffer (buffer) {
const storage = this.props.vm.runtime.storage;
const name = this.props.intl.formatMessage(messages.costume, {index: 1});
costumeUpload(buffer, 'image/png', name, storage, this.handleNewCostume);
costumeUpload(buffer, 'image/png', storage, vmCostumes => {
vmCostumes[0].name = this.props.intl.formatMessage(messages.costume, {index: 1});
this.handleNewCostume(vmCostumes);
});
}
handleFileUploadClick () {
this.fileInput.click();
@@ -284,9 +307,10 @@ class CostumeTab extends React.Component {
title: intl.formatMessage(addFileMessage),
img: fileUploadIcon,
onClick: this.handleFileUploadClick,
fileAccept: '.svg, .png, .jpg, .jpeg',
fileAccept: '.svg, .png, .jpg, .jpeg, .gif',
fileChange: this.handleCostumeUpload,
fileInput: this.setFileInput
fileInput: this.setFileInput,
fileMultiple: true
},
{
title: intl.formatMessage(messages.addSurpriseCostumeMsg),
@@ -312,6 +336,7 @@ class CostumeTab extends React.Component {
this.handleDeleteCostume : null}
onDrop={this.handleDrop}
onDuplicateClick={this.handleDuplicateCostume}
onExportClick={this.handleExportCostume}
onItemClick={this.handleSelectCostume}
>
{target.costumes ?
@@ -338,10 +363,12 @@ CostumeTab.propTypes = {
intl: intlShape,
isRtl: PropTypes.bool,
onActivateSoundsTab: PropTypes.func.isRequired,
onCloseImporting: PropTypes.func.isRequired,
onNewCostumeFromCameraClick: PropTypes.func.isRequired,
onNewLibraryBackdropClick: PropTypes.func.isRequired,
onNewLibraryCostumeClick: PropTypes.func.isRequired,
onRequestCloseCameraModal: PropTypes.func.isRequired,
onShowImporting: PropTypes.func.isRequired,
sprites: PropTypes.shape({
id: PropTypes.shape({
costumes: PropTypes.arrayOf(PropTypes.shape({
@@ -386,7 +413,9 @@ const mapDispatchToProps = dispatch => ({
},
dispatchUpdateRestore: restoreState => {
dispatch(setRestore(restoreState));
}
},
onCloseImporting: () => dispatch(closeAlertWithId('importingAsset')),
onShowImporting: () => dispatch(showStandardAlert('importingAsset'))
});
export default errorBoundaryHOC('Costume Tab')(
+13 -8
View File
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import VM from 'scratch-vm';
import Box from '../components/box/box.jsx';
import greenFlag from '../components/green-flag/icon--green-flag.svg';
class GreenFlagOverlay extends React.Component {
@@ -23,15 +24,18 @@ class GreenFlagOverlay extends React.Component {
if (this.props.isStarted) return null;
return (
<div
className={this.props.className}
<Box
className={this.props.wrapperClass}
onClick={this.handleClick}
>
<img
draggable={false}
src={greenFlag}
/>
</div>
<div className={this.props.className}>
<img
draggable={false}
src={greenFlag}
/>
</div>
</Box>
);
}
}
@@ -39,7 +43,8 @@ class GreenFlagOverlay extends React.Component {
GreenFlagOverlay.propTypes = {
className: PropTypes.string,
isStarted: PropTypes.bool,
vm: PropTypes.instanceOf(VM)
vm: PropTypes.instanceOf(VM),
wrapperClass: PropTypes.string
};
const mapStateToProps = state => ({
+9 -4
View File
@@ -60,6 +60,11 @@ class GUI extends React.Component {
if (this.props.projectTitle !== prevProps.projectTitle) {
this.setReduxTitle(this.props.projectTitle);
}
if (this.props.isShowingProject && !prevProps.isShowingProject) {
// this only notifies container when a project changes from not yet loaded to loaded
// At this time the project view in www doesn't need to know when a project is unloaded
this.props.onProjectLoaded();
}
}
setReduxTitle (newTitle) {
if (newTitle === null || typeof newTitle === 'undefined') {
@@ -83,6 +88,7 @@ class GUI extends React.Component {
isError,
isScratchDesktop,
isShowingProject,
onProjectLoaded,
onStorageInit,
onUpdateProjectId,
onUpdateReduxProjectTitle,
@@ -113,19 +119,18 @@ GUI.propTypes = {
cloudHost: PropTypes.string,
error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
fetchingProject: PropTypes.bool,
importInfoVisible: PropTypes.bool,
intl: intlShape,
isError: PropTypes.bool,
isLoading: PropTypes.bool,
isScratchDesktop: PropTypes.bool,
isShowingProject: PropTypes.bool,
loadingStateVisible: PropTypes.bool,
onProjectLoaded: PropTypes.func,
onSeeCommunity: PropTypes.func,
onStorageInit: PropTypes.func,
onUpdateProjectId: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
onUpdateReduxProjectTitle: PropTypes.func,
previewInfoVisible: PropTypes.bool,
projectHost: PropTypes.string,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
projectTitle: PropTypes.string,
@@ -136,6 +141,7 @@ GUI.propTypes = {
GUI.defaultProps = {
isScratchDesktop: false,
onStorageInit: storageInstance => storageInstance.addOfficialScratchWebStores(),
onProjectLoaded: () => {},
onUpdateProjectId: () => {}
};
@@ -151,13 +157,12 @@ const mapStateToProps = state => {
costumeLibraryVisible: state.scratchGui.modals.costumeLibrary,
costumesTabVisible: state.scratchGui.editorTab.activeTabIndex === COSTUMES_TAB_INDEX,
error: state.scratchGui.projectState.error,
importInfoVisible: state.scratchGui.modals.importInfo,
isError: getIsError(loadingState),
isFullScreen: state.scratchGui.mode.isFullScreen,
isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
isRtl: state.locales.isRtl,
isShowingProject: getIsShowingProject(loadingState),
loadingStateVisible: state.scratchGui.modals.loadingProject,
previewInfoVisible: state.scratchGui.modals.previewInfo,
projectId: state.scratchGui.projectState.projectId,
soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX,
targetIsStage: (
-115
View File
@@ -1,115 +0,0 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import ImportModalComponent from '../components/import-modal/import-modal.jsx';
import {
closeImportInfo,
openPreviewInfo
} from '../reducers/modals';
class ImportModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleViewProject',
'handleCancel',
'handleChange',
'handleGoBack',
'handleKeyPress'
]);
this.state = {
inputValue: '',
hasValidationError: false,
errorMessage: ''
};
}
handleKeyPress (event) {
if (event.key === 'Enter') this.handleViewProject();
}
handleViewProject () {
const inputValue = this.state.inputValue;
const projectId = this.validate(inputValue);
if (projectId) {
const projectLink = document.createElement('a');
document.body.appendChild(projectLink);
projectLink.href = `#${projectId}`;
projectLink.click();
document.body.removeChild(projectLink);
this.props.onViewProject();
} else {
this.setState({
hasValidationError: true,
errorMessage: `invalidFormatError`});
}
}
handleChange (e) {
this.setState({inputValue: e.target.value, hasValidationError: false});
}
validate (input) {
const urlMatches = input.match(/^(?:https?:\/\/)?scratch\.mit\.edu\/projects\/(\d+)/);
if (urlMatches && urlMatches.length > 0) {
return urlMatches[1];
}
const projectIdMatches = input.match(/^#?(\d+)$/);
if (projectIdMatches && projectIdMatches.length > 0) {
return projectIdMatches[1];
}
return null;
}
handleCancel () {
this.props.onCancel();
}
handleGoBack () {
this.props.onBack();
}
render () {
return (
<ImportModalComponent
errorMessage={this.state.errorMessage}
hasValidationError={this.state.hasValidationError}
inputValue={this.state.inputValue}
isRtl={this.props.isRtl}
placeholder="scratch.mit.edu/projects/123456789"
onCancel={this.handleCancel}
onChange={this.handleChange}
onGoBack={this.handleGoBack}
onKeyPress={this.handleKeyPress}
onViewProject={this.handleViewProject}
/>
);
}
}
ImportModal.propTypes = {
isRtl: PropTypes.bool,
onBack: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onViewProject: PropTypes.func
};
const mapStateToProps = state => ({
isRtl: state.locales.isRtl
});
const mapDispatchToProps = dispatch => ({
onBack: () => {
dispatch(closeImportInfo());
dispatch(openPreviewInfo());
},
onCancel: () => {
dispatch(closeImportInfo());
},
onViewProject: () => {
dispatch(closeImportInfo());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(ImportModal);
+149
View File
@@ -0,0 +1,149 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {injectIntl} from 'react-intl';
import LibraryItemComponent from '../components/library-item/library-item.jsx';
class LibraryItem extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
'handleBlur',
'handleClick',
'handleFocus',
'handleKeyPress',
'handleMouseEnter',
'handleMouseLeave',
'rotateIcon',
'startRotatingIcons',
'stopRotatingIcons'
]);
this.state = {
iconIndex: 0,
isRotatingIcon: false
};
}
componentWillUnmount () {
clearInterval(this.intervalId);
}
handleBlur (id) {
this.handleMouseLeave(id);
}
handleClick (e) {
if (!this.props.disabled) {
this.props.onSelect(this.props.id);
}
e.preventDefault();
}
handleFocus (id) {
this.handleMouseEnter(id);
}
handleKeyPress (e) {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
this.props.onSelect(this.props.id);
}
}
handleMouseEnter () {
this.props.onMouseEnter(this.props.id);
if (this.props.icons && this.props.icons.length) {
this.stopRotatingIcons();
this.setState({
isRotatingIcon: true
}, this.startRotatingIcons);
}
}
handleMouseLeave () {
this.props.onMouseLeave(this.props.id);
if (this.props.icons && this.props.icons.length) {
this.setState({
isRotatingIcon: false
}, this.stopRotatingIcons);
}
}
startRotatingIcons () {
this.rotateIcon();
this.intervalId = setInterval(this.rotateIcon, 300);
}
stopRotatingIcons () {
if (this.intervalId) {
this.intervalId = clearInterval(this.intervalId);
}
}
rotateIcon () {
const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length;
this.setState({iconIndex: nextIconIndex});
}
curIconMd5 () {
if (this.props.icons &&
this.state.isRotatingIcon &&
this.state.iconIndex < this.props.icons.length &&
this.props.icons[this.state.iconIndex] &&
this.props.icons[this.state.iconIndex].baseLayerMD5) {
return this.props.icons[this.state.iconIndex].baseLayerMD5;
}
return this.props.iconMd5;
}
render () {
const iconMd5 = this.curIconMd5();
const iconURL = iconMd5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` :
this.props.iconRawURL;
return (
<LibraryItemComponent
bluetoothRequired={this.props.bluetoothRequired}
collaborator={this.props.collaborator}
description={this.props.description}
disabled={this.props.disabled}
extensionId={this.props.extensionId}
featured={this.props.featured}
hidden={this.props.hidden}
iconURL={iconURL}
icons={this.props.icons}
id={this.props.id}
insetIconURL={this.props.insetIconURL}
internetConnectionRequired={this.props.internetConnectionRequired}
name={this.props.name}
onBlur={this.handleBlur}
onClick={this.handleClick}
onFocus={this.handleFocus}
onKeyPress={this.handleKeyPress}
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
/>
);
}
}
LibraryItem.propTypes = {
bluetoothRequired: PropTypes.bool,
collaborator: PropTypes.string,
description: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
disabled: PropTypes.bool,
extensionId: PropTypes.string,
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconMd5: PropTypes.string,
iconRawURL: PropTypes.string,
icons: PropTypes.arrayOf(
PropTypes.shape({
baseLayerMD5: PropTypes.string
})
),
id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
name: PropTypes.oneOfType([
PropTypes.string,
PropTypes.node
]),
onMouseEnter: PropTypes.func.isRequired,
onMouseLeave: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired
};
export default injectIntl(LibraryItem);
+6 -2
View File
@@ -8,7 +8,7 @@ import MonitorComponent, {monitorModes} from '../components/monitor/monitor.jsx'
import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect} from '../reducers/monitor-layout';
import {getVariable, setVariableValue} from '../lib/variable-utils';
import importCSV from '../lib/import-csv';
import downloadText from '../lib/download-text';
import downloadBlob from '../lib/download-blob';
import {connect} from 'react-redux';
import {Map} from 'immutable';
@@ -155,7 +155,9 @@ class Monitor extends React.Component {
handleExport () {
const {vm, targetId, id: variableId} = this.props;
const variable = getVariable(vm, targetId, variableId);
downloadText(`${variable.name}.txt`, variable.value.join('\r\n'));
const text = variable.value.join('\r\n');
const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
downloadBlob(`${variable.name}.txt`, blob);
}
render () {
const monitorProps = monitorAdapter(this.props);
@@ -167,6 +169,7 @@ class Monitor extends React.Component {
{...monitorProps}
draggable={this.props.draggable}
height={this.props.height}
isDiscrete={this.props.isDiscrete}
max={this.props.max}
min={this.props.min}
mode={this.props.mode}
@@ -190,6 +193,7 @@ Monitor.propTypes = {
height: PropTypes.number,
id: PropTypes.string.isRequired,
intl: intlShape,
isDiscrete: PropTypes.bool,
max: PropTypes.number,
min: PropTypes.number,
mode: PropTypes.oneOf(['default', 'slider', 'large', 'list']),
-76
View File
@@ -1,76 +0,0 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import tabletFullScreen from '../lib/tablet-full-screen';
import PreviewModalComponent from '../components/preview-modal/preview-modal.jsx';
import {
closePreviewInfo,
openImportInfo
} from '../reducers/modals';
class PreviewModal extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleTryIt',
'handleCancel',
'handleViewProject'
]);
this.state = {
previewing: false
};
}
handleTryIt () {
this.setState({previewing: true});
// try to run in fullscreen mode on tablets.
tabletFullScreen();
this.props.onTryIt();
}
handleCancel () {
window.location.replace('https://scratch.mit.edu');
}
handleViewProject () {
this.props.onViewProject();
}
render () {
return (
<PreviewModalComponent
isRtl={this.props.isRtl}
previewing={this.state.previewing}
onCancel={this.handleCancel}
onTryIt={this.handleTryIt}
onViewProject={this.handleViewProject}
/>
);
}
}
PreviewModal.propTypes = {
isRtl: PropTypes.bool,
onTryIt: PropTypes.func,
onViewProject: PropTypes.func
};
const mapStateToProps = state => ({
isRtl: state.locales.isRtl
});
const mapDispatchToProps = dispatch => ({
onTryIt: () => {
dispatch(closePreviewInfo());
},
onViewProject: () => {
dispatch(closePreviewInfo());
dispatch(openImportInfo());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(PreviewModal);
+3 -1
View File
@@ -43,7 +43,9 @@ class RecordModal extends React.Component {
this.setState({recording: true});
}
handleStopRecording (samples, sampleRate, levels, trimStart, trimEnd) {
this.setState({samples, sampleRate, levels, trimStart, trimEnd, recording: false});
if (samples.length > 0) {
this.setState({samples, sampleRate, levels, trimStart, trimEnd, recording: false});
}
}
handlePlay () {
this.setState({playing: true});
+1 -1
View File
@@ -41,7 +41,7 @@ class RecordingStep extends React.Component {
this.setState({listening: true});
}
handleRecordingError () {
alert(this.props.intl.formatMessage(messages.extensionUrl)); // eslint-disable-line no-alert
alert(this.props.intl.formatMessage(messages.alertMsg)); // eslint-disable-line no-alert
}
handleLevelUpdate (level) {
this.setState({level});
+28 -2
View File
@@ -6,9 +6,12 @@ import {defineMessages, injectIntl, intlShape} from 'react-intl';
import analytics from '../lib/analytics';
import log from '../lib/log';
import sharedMessages from '../lib/shared-messages';
import {
LoadingStates,
getIsLoadingUpload,
getIsShowingWithoutId,
onLoadedProject,
requestProjectUpload
} from '../reducers/project-state';
@@ -88,10 +91,25 @@ class SBFileUploader extends React.Component {
}
// called when user has finished selecting a file to upload
handleChange (e) {
const {
intl,
isShowingWithoutId,
loadingState,
projectChanged
} = this.props;
const thisFileInput = e.target;
if (thisFileInput.files) { // Don't attempt to load if no file was selected
this.fileToUpload = thisFileInput.files[0];
this.props.requestProjectUpload(this.props.loadingState);
// Allow upload to continue only after confirmation if the project
// has changed and is not showing with ID. If it has an ID, this operation
// does not currently overwrite that project, so it is safe to do without confirmation.
const uploadAllowed = (isShowingWithoutId && projectChanged) ?
confirm(intl.formatMessage(sharedMessages.replaceProjectWarning)) : // eslint-disable-line no-alert
true;
if (uploadAllowed) this.props.requestProjectUpload(loadingState);
}
}
// called when file upload raw data is available in the reader
@@ -107,7 +125,11 @@ class SBFileUploader extends React.Component {
nonInteraction: true
});
// Remove the hash if any (without triggering a hash change event or a reload)
history.replaceState({}, document.title, '.');
try { // Can fail e.g. when GUI is loaded from static file (integration tests)
history.replaceState({}, document.title, '.');
} catch {
// No fallback, just do not trigger promise catch below
}
this.props.onLoadingFinished(this.props.loadingState, true);
// Reset the file input after project is loaded
// This is necessary in case the user wants to reload a project
@@ -156,10 +178,12 @@ SBFileUploader.propTypes = {
className: PropTypes.string,
intl: intlShape.isRequired,
isLoadingUpload: PropTypes.bool,
isShowingWithoutId: PropTypes.bool,
loadingState: PropTypes.oneOf(LoadingStates),
onLoadingFinished: PropTypes.func,
onLoadingStarted: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectChanged: PropTypes.bool,
requestProjectUpload: PropTypes.func,
vm: PropTypes.shape({
loadProject: PropTypes.func
@@ -172,7 +196,9 @@ const mapStateToProps = state => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
isLoadingUpload: getIsLoadingUpload(loadingState),
isShowingWithoutId: getIsShowingWithoutId(loadingState),
loadingState: loadingState,
projectChanged: state.scratchGui.projectChanged,
vm: state.scratchGui.vm
};
};
+2 -16
View File
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import {projectTitleInitialState} from '../reducers/project-title';
import downloadBlob from '../lib/download-blob';
/**
* Project saver component passes a downloadProject function to its child.
* It expects this child to be a function with the signature
@@ -26,25 +26,11 @@ class SB3Downloader extends React.Component {
]);
}
downloadProject () {
const downloadLink = document.createElement('a');
document.body.appendChild(downloadLink);
this.props.saveProjectSb3().then(content => {
if (this.props.onSaveFinished) {
this.props.onSaveFinished();
}
// Use special ms version if available to get it working on Edge.
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(content, this.props.projectFilename);
return;
}
const url = window.URL.createObjectURL(content);
downloadLink.href = url;
downloadLink.download = this.props.projectFilename;
downloadLink.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(downloadLink);
downloadBlob(this.props.projectFilename, content);
});
}
render () {
+38 -17
View File
@@ -5,7 +5,7 @@ import WavEncoder from 'wav-encoder';
import {connect} from 'react-redux';
import {computeChunkedRMS} from '../lib/audio/audio-util.js';
import {computeChunkedRMS, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
import AudioEffects from '../lib/audio/audio-effects.js';
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
@@ -65,13 +65,6 @@ class SoundEditor extends React.Component {
});
}
submitNewSamples (samples, sampleRate, skipUndo) {
if (!skipUndo) {
this.redoStack = [];
if (this.undoStack.length >= UNDO_STACK_SIZE) {
this.undoStack.shift(); // Drop the first element off the array
}
this.undoStack.push(this.copyCurrentBuffer());
}
// Encode the new sound into a wav so that it can be stored
let wavBuffer = null;
try {
@@ -79,18 +72,38 @@ class SoundEditor extends React.Component {
sampleRate: sampleRate,
channelData: [samples]
});
if (wavBuffer.byteLength > SOUND_BYTE_LIMIT) {
// Cancel the sound update by setting to null
wavBuffer = null;
log.error(`Refusing to encode sound larger than ${SOUND_BYTE_LIMIT} bytes`);
}
} catch (e) {
// This error state is mostly for the mock sounds used during testing.
// Any incorrect sound buffer trying to get interpretd as a Wav file
// should yield this error.
// This can also happen if the sound is too be allocated in memory.
log.error(`Encountered error while trying to encode sound update: ${e}`);
}
this.resetState(samples, sampleRate);
this.props.vm.updateSoundBuffer(
this.props.soundIndex,
this.audioBufferPlayer.buffer,
wavBuffer ? new Uint8Array(wavBuffer) : new Uint8Array());
// Do not submit sound if it could not be encoded (i.e. if too large)
if (wavBuffer) {
if (!skipUndo) {
this.redoStack = [];
if (this.undoStack.length >= UNDO_STACK_SIZE) {
this.undoStack.shift(); // Drop the first element off the array
}
this.undoStack.push(this.copyCurrentBuffer());
}
this.resetState(samples, sampleRate);
this.props.vm.updateSoundBuffer(
this.props.soundIndex,
this.audioBufferPlayer.buffer,
new Uint8Array(wavBuffer));
return true; // Update succeeded
}
return false; // Update failed
}
handlePlay () {
this.audioBufferPlayer.play(
@@ -120,8 +133,16 @@ class SoundEditor extends React.Component {
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
const clippedSamples = samples.slice(startIndex, endIndex);
this.submitNewSamples(clippedSamples, sampleRate);
if (endIndex > startIndex) { // Strictly greater to prevent 0 sample sounds
const clippedSamples = samples.slice(startIndex, endIndex);
this.submitNewSamples(clippedSamples, sampleRate);
} else {
// Just clear the trim state, it cannot be completed
this.setState({
trimStart: null,
trimEnd: null
});
}
}
}
handleUpdateTrimEnd (trimEnd) {
@@ -145,8 +166,8 @@ class SoundEditor extends React.Component {
effects.process(({renderedBuffer}) => {
const samples = renderedBuffer.getChannelData(0);
const sampleRate = renderedBuffer.sampleRate;
this.submitNewSamples(samples, sampleRate);
this.handlePlay();
const success = this.submitNewSamples(samples, sampleRate);
if (success) this.handlePlay();
});
}
handleUndo () {
+29 -8
View File
@@ -21,6 +21,7 @@ import soundLibraryContent from '../lib/libraries/sounds.json';
import {handleFileUpload, soundUpload} from '../lib/file-uploader.js';
import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import DragConstants from '../lib/drag-constants';
import downloadBlob from '../lib/download-blob';
import {connect} from 'react-redux';
@@ -36,6 +37,7 @@ import {
} from '../reducers/editor-tab';
import {setRestore} from '../reducers/restore-deletion';
import {showStandardAlert, closeAlertWithId} from '../reducers/alerts';
class SoundTab extends React.Component {
constructor (props) {
@@ -44,6 +46,7 @@ class SoundTab extends React.Component {
'handleSelectSound',
'handleDeleteSound',
'handleDuplicateSound',
'handleExportSound',
'handleNewSound',
'handleSurpriseSound',
'handleFileUploadClick',
@@ -86,6 +89,12 @@ class SoundTab extends React.Component {
this.props.dispatchUpdateRestore({restoreFun, deletedItem: 'Sound'});
}
handleExportSound (soundIndex) {
const item = this.props.vm.editingTarget.sprite.sounds[soundIndex];
const blob = new Blob([item.asset.data], {type: item.asset.assetType.contentType});
downloadBlob(`${item.name}.${item.asset.dataFormat}`, blob);
}
handleDuplicateSound (soundIndex) {
this.props.vm.duplicateSound(soundIndex).then(() => {
this.setState({selectedSoundIndex: soundIndex + 1});
@@ -121,12 +130,18 @@ class SoundTab extends React.Component {
handleSoundUpload (e) {
const storage = this.props.vm.runtime.storage;
const handleSound = newSound => this.props.vm.addSound(newSound)
.then(() => this.handleNewSound());
handleFileUpload(e.target, (buffer, fileType, fileName) => {
soundUpload(buffer, fileType, fileName, storage, handleSound);
});
this.props.onShowImporting();
handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => {
soundUpload(buffer, fileType, storage, newSound => {
newSound.name = fileName;
this.props.vm.addSound(newSound).then(() => {
this.handleNewSound();
if (fileIndex === fileCount - 1) {
this.props.onCloseImporting();
}
});
});
}, this.props.onCloseImporting);
}
handleDrop (dropInfo) {
@@ -215,7 +230,8 @@ class SoundTab extends React.Component {
onClick: this.handleFileUploadClick,
fileAccept: '.wav, .mp3',
fileChange: this.handleSoundUpload,
fileInput: this.setFileInput
fileInput: this.setFileInput,
fileMultiple: true
}, {
title: intl.formatMessage(messages.surpriseSound),
img: surpriseIcon,
@@ -236,6 +252,7 @@ class SoundTab extends React.Component {
onDeleteClick={this.handleDeleteSound}
onDrop={this.handleDrop}
onDuplicateClick={this.handleDuplicateSound}
onExportClick={this.handleExportSound}
onItemClick={this.handleSelectSound}
>
{sprite.sounds && sprite.sounds[this.state.selectedSoundIndex] ? (
@@ -264,9 +281,11 @@ SoundTab.propTypes = {
intl: intlShape,
isRtl: PropTypes.bool,
onActivateCostumesTab: PropTypes.func.isRequired,
onCloseImporting: PropTypes.func.isRequired,
onNewSoundFromLibraryClick: PropTypes.func.isRequired,
onNewSoundFromRecordingClick: PropTypes.func.isRequired,
onRequestCloseSoundLibrary: PropTypes.func.isRequired,
onShowImporting: PropTypes.func.isRequired,
soundLibraryVisible: PropTypes.bool,
soundRecorderVisible: PropTypes.bool,
sprites: PropTypes.shape({
@@ -307,7 +326,9 @@ const mapDispatchToProps = dispatch => ({
},
dispatchUpdateRestore: restoreState => {
dispatch(setRestore(restoreState));
}
},
onCloseImporting: () => dispatch(closeAlertWithId('importingAsset')),
onShowImporting: () => dispatch(showStandardAlert('importingAsset'))
});
export default errorBoundaryHOC('Sound Tab')(
+2 -48
View File
@@ -23,21 +23,8 @@ class SpriteLibrary extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
'handleItemSelect',
'handleMouseEnter',
'handleMouseLeave',
'rotateCostume',
'startRotatingCostumes',
'stopRotatingCostumes'
'handleItemSelect'
]);
this.state = {
activeSprite: null,
costumeIndex: 0,
sprites: spriteLibraryContent
};
}
componentWillUnmount () {
clearInterval(this.intervalId);
}
handleItemSelect (item) {
// Randomize position of library sprite
@@ -51,46 +38,13 @@ class SpriteLibrary extends React.PureComponent {
label: item.name
});
}
handleMouseEnter (item) {
this.stopRotatingCostumes();
this.setState({activeSprite: item}, this.startRotatingCostumes);
}
handleMouseLeave () {
this.stopRotatingCostumes();
}
startRotatingCostumes () {
if (!this.state.activeSprite) return;
this.rotateCostume();
this.intervalId = setInterval(this.rotateCostume, 300);
}
stopRotatingCostumes () {
this.intervalId = clearInterval(this.intervalId);
}
rotateCostume () {
const costumes = this.state.activeSprite.json.costumes;
const nextCostumeIndex = (this.state.costumeIndex + 1) % costumes.length;
this.setState({
costumeIndex: nextCostumeIndex,
sprites: this.state.sprites.map(sprite => {
if (sprite.name === this.state.activeSprite.name) {
return {
...sprite,
md5: sprite.json.costumes[nextCostumeIndex].baseLayerMD5
};
}
return sprite;
})
});
}
render () {
return (
<LibraryComponent
data={this.state.sprites}
data={spriteLibraryContent}
id="spriteLibrary"
tags={spriteTags}
title={this.props.intl.formatMessage(messages.libraryTitle)}
onItemMouseEnter={this.handleMouseEnter}
onItemMouseLeave={this.handleMouseLeave}
onItemSelected={this.handleItemSelect}
onRequestClose={this.props.onRequestClose}
/>
+2 -18
View File
@@ -14,7 +14,7 @@ import SpriteSelectorItemComponent from '../components/sprite-selector-item/spri
const dragThreshold = 3; // Same as the block drag threshold
class SpriteSelectorItem extends React.Component {
class SpriteSelectorItem extends React.PureComponent {
constructor (props) {
super(props);
bindAll(this, [
@@ -29,19 +29,6 @@ class SpriteSelectorItem extends React.Component {
'handleMouseMove',
'handleMouseUp'
]);
// Asset ID of the current decoded costume
this.decodedAssetId = null;
}
shouldComponentUpdate (nextProps) {
// Ignore dragPayload due to https://github.com/LLK/scratch-gui/issues/3172.
// This function should be removed once the issue is fixed.
for (const property in nextProps) {
if (property !== 'dragPayload' && this.props[property] !== nextProps[property]) {
return true;
}
}
return false;
}
getCostumeData () {
if (this.props.costumeURL) return this.props.costumeURL;
@@ -153,10 +140,7 @@ SpriteSelectorItem.propTypes = {
asset: PropTypes.instanceOf(storage.Asset),
costumeURL: PropTypes.string,
dispatchSetHoveredSprite: PropTypes.func.isRequired,
dragPayload: PropTypes.shape({
name: PropTypes.string,
body: PropTypes.string
}),
dragPayload: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
dragType: PropTypes.string,
dragging: PropTypes.bool,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+34 -11
View File
@@ -7,9 +7,11 @@ import {intlShape, injectIntl} from 'react-intl';
import {connect} from 'react-redux';
import {openBackdropLibrary} from '../reducers/modals';
import {activateTab, COSTUMES_TAB_INDEX} from '../reducers/editor-tab';
import {showStandardAlert, closeAlertWithId} from '../reducers/alerts';
import {setHoveredSprite} from '../reducers/hovered-target';
import DragConstants from '../lib/drag-constants';
import DropAreaHOC from '../lib/drop-area-hoc.jsx';
import ThrottledPropertyHOC from '../lib/throttled-property-hoc.jsx';
import {emptyCostume} from '../lib/empty-assets';
import sharedMessages from '../lib/shared-messages';
import {fetchCode} from '../lib/backpack-api';
@@ -27,7 +29,9 @@ const dragTypes = [
DragConstants.BACKPACK_CODE
];
const DroppableStage = DropAreaHOC(dragTypes)(StageSelectorComponent);
const DroppableThrottledStage = DropAreaHOC(dragTypes)(
ThrottledPropertyHOC('url', 500)(StageSelectorComponent)
);
class StageSelector extends React.Component {
constructor (props) {
@@ -60,9 +64,13 @@ class StageSelector extends React.Component {
handleClick () {
this.props.onSelect(this.props.id);
}
handleNewBackdrop (backdrop) {
this.props.vm.addBackdrop(backdrop.md5, backdrop).then(() =>
this.props.onActivateTab(COSTUMES_TAB_INDEX));
handleNewBackdrop (backdrops_) {
const backdrops = Array.isArray(backdrops_) ? backdrops_ : [backdrops_];
return Promise.all(backdrops.map(backdrop =>
this.props.vm.addBackdrop(backdrop.md5, backdrop)
)).then(() =>
this.props.onActivateTab(COSTUMES_TAB_INDEX)
);
}
handleSurpriseBackdrop () {
// @todo should this not add a backdrop you already have?
@@ -74,9 +82,19 @@ class StageSelector extends React.Component {
}
handleBackdropUpload (e) {
const storage = this.props.vm.runtime.storage;
handleFileUpload(e.target, (buffer, fileType, fileName) => {
costumeUpload(buffer, fileType, fileName, storage, this.handleNewBackdrop);
});
this.props.onShowImporting();
handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => {
costumeUpload(buffer, fileType, storage, vmCostumes => {
vmCostumes.forEach((costume, i) => {
costume.name = `${fileName}${i ? i + 1 : ''}`;
});
this.handleNewBackdrop(vmCostumes).then(() => {
if (fileIndex === fileCount - 1) {
this.props.onCloseImporting();
}
});
}, this.props.onCloseImporting);
}, this.props.onCloseImporting);
}
handleFileUploadClick () {
this.fileInput.click();
@@ -114,9 +132,10 @@ class StageSelector extends React.Component {
}
render () {
const componentProps = omit(this.props, [
'asset', 'dispatchSetHoveredSprite', 'id', 'intl', 'onActivateTab', 'onSelect']);
'asset', 'dispatchSetHoveredSprite', 'id', 'intl',
'onActivateTab', 'onSelect', 'onShowImporting', 'onCloseImporting']);
return (
<DroppableStage
<DroppableThrottledStage
fileInputRef={this.setFileInput}
onBackdropFileUpload={this.handleBackdropUpload}
onBackdropFileUploadClick={this.handleFileUploadClick}
@@ -135,7 +154,9 @@ StageSelector.propTypes = {
...StageSelectorComponent.propTypes,
id: PropTypes.string,
intl: intlShape.isRequired,
onSelect: PropTypes.func
onCloseImporting: PropTypes.func,
onSelect: PropTypes.func,
onShowImporting: PropTypes.func
};
const mapStateToProps = (state, {asset, id}) => ({
@@ -156,7 +177,9 @@ const mapDispatchToProps = dispatch => ({
},
dispatchSetHoveredSprite: spriteId => {
dispatch(setHoveredSprite(spriteId));
}
},
onCloseImporting: () => dispatch(closeAlertWithId('importingAsset')),
onShowImporting: () => dispatch(showStandardAlert('importingAsset'))
});
export default injectIntl(connect(
+3 -1
View File
@@ -320,7 +320,6 @@ class Stage extends React.Component {
if (this.state.dragId) return;
const drawableId = this.renderer.pick(x, y);
if (drawableId === null) return;
const drawableData = this.renderer.extractDrawable(drawableId, x, y);
const targetId = this.props.vm.getTargetIdForDrawableId(drawableId);
if (targetId === null) return;
@@ -332,6 +331,9 @@ class Stage extends React.Component {
// Dragging always brings the target to the front
target.goToFront();
// Extract the drawable art
const drawableData = this.renderer.extractDrawable(drawableId, x, y);
this.props.vm.startDrag(targetId);
this.setState({
isDragging: true,
+24 -31
View File
@@ -1,6 +1,6 @@
import bindAll from 'lodash.bindall';
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {intlShape, injectIntl} from 'react-intl';
@@ -8,9 +8,9 @@ import {
openSpriteLibrary,
closeSpriteLibrary
} from '../reducers/modals';
import {activateTab, COSTUMES_TAB_INDEX, BLOCKS_TAB_INDEX} from '../reducers/editor-tab';
import {setReceivedBlocks} from '../reducers/hovered-target';
import {showStandardAlert, closeAlertWithId} from '../reducers/alerts';
import {setRestore} from '../reducers/restore-deletion';
import DragConstants from '../lib/drag-constants';
import TargetPaneComponent from '../components/target-pane/target-pane.jsx';
@@ -21,6 +21,7 @@ import {emptySprite} from '../lib/empty-assets';
import {highlightTarget} from '../reducers/targets';
import {fetchSprite, fetchCode} from '../lib/backpack-api';
import randomizeSpritePosition from '../lib/randomize-sprite-position';
import downloadBlob from '../lib/download-blob';
class TargetPane extends React.Component {
constructor (props) {
@@ -94,20 +95,7 @@ class TargetPane extends React.Component {
document.body.appendChild(saveLink);
this.props.vm.exportSprite(id).then(content => {
const filename = `${spriteName}.sprite3`;
// Use special ms version if available to get it working on Edge.
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(content, filename);
return;
}
const url = window.URL.createObjectURL(content);
saveLink.href = url;
saveLink.download = filename;
saveLink.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(saveLink);
downloadBlob(`${spriteName}.sprite3`, content);
});
}
handleSelectSprite (id) {
@@ -139,7 +127,7 @@ class TargetPane extends React.Component {
this.props.onActivateTab(BLOCKS_TAB_INDEX);
}
handleNewSprite (spriteJSONString) {
this.props.vm.addSprite(spriteJSONString)
return this.props.vm.addSprite(spriteJSONString)
.then(this.handleActivateBlocksTab);
}
handleFileUploadClick () {
@@ -147,10 +135,16 @@ class TargetPane extends React.Component {
}
handleSpriteUpload (e) {
const storage = this.props.vm.runtime.storage;
const costumeSuffix = this.props.intl.formatMessage(sharedMessages.costume, {index: 1});
handleFileUpload(e.target, (buffer, fileType, fileName) => {
spriteUpload(buffer, fileType, fileName, storage, this.handleNewSprite, costumeSuffix);
});
this.props.onShowImporting();
handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => {
spriteUpload(buffer, fileType, fileName, storage, newSprite => {
this.handleNewSprite(newSprite).then(() => {
if (fileIndex === fileCount - 1) {
this.props.onCloseImporting();
}
});
}, this.props.onCloseImporting);
}, this.props.onCloseImporting);
}
setFileInput (input) {
this.fileInput = input;
@@ -209,6 +203,8 @@ class TargetPane extends React.Component {
onReceivedBlocks, // eslint-disable-line no-unused-vars
onHighlightTarget, // eslint-disable-line no-unused-vars
dispatchUpdateRestore, // eslint-disable-line no-unused-vars
onShowImporting, // eslint-disable-line no-unused-vars
onCloseImporting, // eslint-disable-line no-unused-vars
...componentProps
} = this.props;
return (
@@ -245,25 +241,20 @@ const {
TargetPane.propTypes = {
intl: intlShape.isRequired,
onCloseImporting: PropTypes.func,
onShowImporting: PropTypes.func,
...targetPaneProps
};
const mapStateToProps = state => ({
editingTarget: state.scratchGui.targets.editingTarget,
hoveredTarget: state.scratchGui.hoveredTarget,
sprites: Object.keys(state.scratchGui.targets.sprites).reduce((sprites, k) => {
let {direction, size, x, y, ...sprite} = state.scratchGui.targets.sprites[k];
if (typeof direction !== 'undefined') direction = Math.round(direction);
if (typeof x !== 'undefined') x = Math.round(x);
if (typeof y !== 'undefined') y = Math.round(y);
if (typeof size !== 'undefined') size = Math.round(size);
sprites[k] = {...sprite, direction, size, x, y};
return sprites;
}, {}),
sprites: state.scratchGui.targets.sprites,
stage: state.scratchGui.targets.stage,
raiseSprites: state.scratchGui.blockDrag,
spriteLibraryVisible: state.scratchGui.modals.spriteLibrary
});
const mapDispatchToProps = dispatch => ({
onNewSpriteClick: e => {
e.preventDefault();
@@ -283,7 +274,9 @@ const mapDispatchToProps = dispatch => ({
},
onHighlightTarget: id => {
dispatch(highlightTarget(id));
}
},
onCloseImporting: () => dispatch(closeAlertWithId('importingAsset')),
onShowImporting: () => dispatch(showStandardAlert('importingAsset'))
});
export default injectIntl(connect(
+5 -3
View File
@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import ThrottledPropertyHOC from '../lib/throttled-property-hoc.jsx';
import VM from 'scratch-vm';
import storage from '../lib/storage';
import getCostumeUrl from '../lib/get-costume-url';
@@ -16,8 +18,6 @@ class Watermark extends React.Component {
bindAll(this, [
'getCostumeData'
]);
// Asset ID of the current sprite's current costume
this.decodedAssetId = null;
}
getCostumeData () {
@@ -64,6 +64,8 @@ const mapStateToProps = state => {
const ConnectedComponent = connect(
mapStateToProps
)(Watermark);
)(
ThrottledPropertyHOC('asset', 500)(Watermark)
);
export default ConnectedComponent;
+1 -1
View File
@@ -17,7 +17,7 @@ $text-primary-transparent: hsla(225, 15%, 40%, 0.75);
$motion-primary: hsla(215, 100%, 65%, 1); /* #4C97FF */
$motion-tertiary: hsla(215, 60%, 50%, 1); /* #3373CC */
$motion-transparent: hsla(215, 100%, 65%, 0.35); /* 35% transparent version of motion-primary */
$motion-light-transparent: hsla(215, 100%, 65%, 0.1); /* 10% transparent version of motion-primary */
$motion-light-transparent: hsla(215, 100%, 65%, 0.15); /* 15% transparent version of motion-primary */
$red-primary: hsla(20, 100%, 55%, 1); /* #FF661A */
$red-tertiary: hsla(20, 100%, 45%, 1); /* #E64D00 */
+3 -2
View File
@@ -5,20 +5,21 @@
*/
/* Toolbox z-index: 40; set in scratch-blocks */
$z-index-menu-bar: 41;
$z-index-extension-button: 42;
$z-index-stage-indicator: 45;
$z-index-add-button: 46;
$z-index-tooltip: 47; /* tooltips should go over add buttons if they overlap */
$z-index-monitor: 48; /* monitors go over add buttons */
/* Block drag z-index: 50; set in scratch-blocks */
$z-index-stage-question: 49; /* "ask" block text input goes above monitors */
$z-index-card: 480;
$z-index-alerts: 490;
$z-index-menu-bar: 491; /* menu-bar should go over monitors, alerts and tutorials */
$z-index-loader: 500;
$z-index-modal: 510;
$z-index-drag-layer: 1000;
/* Block drag z-index: 1000; default 50 is overriden in blocks.css */
$z-index-monitor-dragging: 1010;
$z-index-dragging-sprite: 1020; /* so it is draggable into other panes */
+14
View File
@@ -198,6 +198,20 @@ const alerts = [
closeButton: true,
level: AlertLevels.SUCCESS,
maxDisplaySecs: 15
},
{
alertId: 'importingAsset',
alertType: AlertTypes.STANDARD,
clearList: [],
content: (
<FormattedMessage
defaultMessage="Importing…"
description="Message indicating that project is in process of importing"
id="gui.alerts.importing"
/>
),
iconSpinner: true,
level: AlertLevels.SUCCESS
}
];
+1 -6
View File
@@ -51,7 +51,6 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
guiMiddleware,
initFullScreen,
initPlayer,
initPreviewInfo,
initTelemetryModal
} = guiRedux;
const {ScratchPaintReducer} = require('scratch-paint');
@@ -66,8 +65,6 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
}
} else if (props.showTelemetryModal) {
initializedGui = initTelemetryModal(initializedGui);
} else if (props.showPreviewInfo) {
initializedGui = initPreviewInfo(initializedGui);
}
reducers = {
locales: localesReducer,
@@ -100,7 +97,6 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
const {
isFullScreen, // eslint-disable-line no-unused-vars
isPlayerOnly, // eslint-disable-line no-unused-vars
showPreviewInfo, // eslint-disable-line no-unused-vars
showTelemetryModal, // eslint-disable-line no-unused-vars
...componentProps
} = this.props;
@@ -117,8 +113,7 @@ const AppStateHOC = function (WrappedComponent, localesOnly) {
}
AppStateWrapper.propTypes = {
isFullScreen: PropTypes.bool,
isPlayerOnly: PropTypes.bool,
showPreviewInfo: PropTypes.bool
isPlayerOnly: PropTypes.bool
};
return AppStateWrapper;
};
+8 -2
View File
@@ -96,8 +96,14 @@ class AudioRecorder {
}
}
const trimStart = Math.max(2, firstChunkAboveThreshold - 2) / this.buffers.length;
const trimEnd = Math.min(this.buffers.length - 2, lastChunkAboveThreshold + 2) / this.buffers.length;
let trimStart = Math.max(2, firstChunkAboveThreshold - 2) / this.buffers.length;
let trimEnd = Math.min(this.buffers.length - 2, lastChunkAboveThreshold + 2) / this.buffers.length;
// With very few samples, the automatic trimming can produce invalid values
if (trimStart >= trimEnd) {
trimStart = 0;
trimEnd = 1;
}
const buffer = new Float32Array(this.buffers.length * this.bufferLength);
+4 -1
View File
@@ -1,3 +1,5 @@
const SOUND_BYTE_LIMIT = 10 * 1000 * 1000; // 10mb
const computeRMS = function (samples, scaling = 0.55) {
if (samples.length === 0) return 0;
// Calculate RMS, adapted from https://github.com/Tonejs/Tone.js/blob/master/Tone/component/Meter.js#L88
@@ -23,5 +25,6 @@ const computeChunkedRMS = function (samples, chunkSize = 1024) {
export {
computeRMS,
computeChunkedRMS
computeChunkedRMS,
SOUND_BYTE_LIMIT
};
+7
View File
@@ -18,6 +18,13 @@ const soundPayload = sound => {
payload.mime = 'audio/x-wav';
payload.body = assetDataUrl.replace('data:audio/x-wav;base64,', '');
break;
case 'mp3':
payload.mime = 'audio/mp3';
// TODO scratch-storage should be fixed so that encodeDataURI does not
// always prepend the wave format header; Once that is fixed, the following
// line will have to change.
payload.body = assetDataUrl.replace('data:audio/x-wav;base64,', '');
break;
default:
alert(`Cannot serialize for format: ${assetDataFormat}`); // eslint-disable-line
}
+23 -17
View File
@@ -1,23 +1,29 @@
import jpegThumbnail from './jpeg-thumbnail';
const spritePayload = (sprite, vm) => vm.exportSprite(
sprite.id,
'base64'
).then(zippedSprite => {
const payload = {
type: 'sprite',
name: sprite.name,
mime: 'application/zip',
body: zippedSprite,
// Filled in below
thumbnail: ''
};
const spritePayload = (id, vm) => {
const target = vm.runtime.getTargetById(id);
if (!target) return null;
const costumeDataUrl = sprite.costume.asset.encodeDataURI();
return jpegThumbnail(costumeDataUrl).then(thumbnail => {
payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', '');
return payload;
return vm.exportSprite(
id,
'base64'
).then(zippedSprite => {
const payload = {
type: 'sprite',
name: target.sprite.name,
mime: 'application/zip',
body: zippedSprite,
// Filled in below
thumbnail: ''
};
const costumeDataUrl = target.sprite.costumes[target.currentCostume].asset.encodeDataURI();
return jpegThumbnail(costumeDataUrl).then(thumbnail => {
payload.thumbnail = thumbnail.replace('data:image/jpeg;base64,', '');
return payload;
});
});
});
};
export default spritePayload;
+8
View File
@@ -245,8 +245,15 @@ export default function (vm) {
// The block was in the flyout so look up future block info there.
lookupBlocks = vm.runtime.flyoutBlocks;
}
const sort = function (options) {
options.sort((str1, str2) => str1.localeCompare(str2, [], {
sensitivity: 'base',
numeric: true
}));
};
// Get all the stage variables (no lists) so we can add them to menu when the stage is selected.
const stageVariableOptions = vm.runtime.getTargetForStage().getAllVariableNamesInScopeByType('');
sort(stageVariableOptions);
const stageVariableMenuItems = stageVariableOptions.map(variable => [variable, variable]);
if (sensingOfBlock.inputs.OBJECT.shadow !== sensingOfBlock.inputs.OBJECT.block) {
// There's a block dropped on top of the menu. It'd be nice to evaluate it and
@@ -265,6 +272,7 @@ export default function (vm) {
// The target should exist, but there are ways for it not to (e.g. #4203).
if (target) {
spriteVariableOptions = target.getAllVariableNamesInScopeByType('', true);
sort(spriteVariableOptions);
}
const spriteVariableMenuItems = spriteVariableOptions.map(variable => [variable, variable]);
return spriteOptions.concat(spriteVariableMenuItems);
+1 -1
View File
@@ -15,7 +15,7 @@ import {
} from '../reducers/alerts';
/*
* Higher Order Component to manage the connection to the cloud dserver.
* Higher Order Component to manage the connection to the cloud server.
* @param {React.Component} WrappedComponent component to manage VM events for
* @returns {React.Component} connected component with vm events bound to redux
*/
+9 -2
View File
@@ -1,4 +1,3 @@
import {TextEncoder} from 'text-encoding';
import projectData from './project-data';
/* eslint-disable import/no-unresolved */
@@ -9,8 +8,16 @@ import costume1 from '!raw-loader!./09dc888b0b7df19f70d81588ae73420e.svg';
import costume2 from '!raw-loader!./3696356a03a8d938318876a593572843.svg';
/* eslint-enable import/no-unresolved */
const encoder = new TextEncoder();
const defaultProject = translator => {
let _TextEncoder;
if (typeof TextEncoder === 'undefined') {
_TextEncoder = require('text-encoding').TextEncoder;
} else {
/* global TextEncoder */
_TextEncoder = TextEncoder;
}
const encoder = new _TextEncoder();
const projectJson = projectData(translator);
return [{
id: 0,
+17
View File
@@ -0,0 +1,17 @@
export default (filename, blob) => {
const downloadLink = document.createElement('a');
document.body.appendChild(downloadLink);
// Use special ms version if available to get it working on Edge.
if (navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob, filename);
return;
}
const url = window.URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.download = filename;
downloadLink.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(downloadLink);
};
-13
View File
@@ -1,13 +0,0 @@
export default (filename, text) => {
const pom = document.createElement('a');
pom.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`);
pom.setAttribute('download', filename);
if (document.createEvent) {
const event = document.createEvent('MouseEvents');
event.initEvent('click', true, true);
pom.dispatchEvent(event);
} else {
pom.click();
}
};
+53 -38
View File
@@ -1,6 +1,7 @@
import {BitmapAdapter} from 'scratch-svg-renderer';
import log from './log.js';
import randomizeSpritePosition from './randomize-sprite-position.js';
import gifDecoder from './gif-decoder';
/**
* Extract the file name given a string of the form fileName + ext
@@ -19,24 +20,29 @@ const extractFileName = function (nameExt) {
* and a function to handle loading the file.
* @param {Input} fileInput The <input/> element that contains the file being loaded
* @param {Function} onload The function that handles loading the file
* @param {Function} onerror The function that handles any error loading the file
*/
const handleFileUpload = function (fileInput, onload) {
let thisFile = null;
const reader = new FileReader();
reader.onload = () => {
// Reset the file input value now that we have everything we need
// so that the user can upload the same sound multiple times if
// they choose
fileInput.value = null;
const fileType = thisFile.type;
const fileName = extractFileName(thisFile.name);
onload(reader.result, fileType, fileName);
const handleFileUpload = function (fileInput, onload, onerror) {
const readFile = (i, files) => {
if (i === files.length) {
// Reset the file input value now that we have everything we need
// so that the user can upload the same sound multiple times if
// they choose
fileInput.value = null;
return;
}
const file = files[i];
const reader = new FileReader();
reader.onload = () => {
const fileType = file.type;
const fileName = extractFileName(file.name);
onload(reader.result, fileType, fileName, i, files.length);
readFile(i + 1, files);
};
reader.onerror = onerror;
reader.readAsArrayBuffer(file);
};
if (fileInput.files) {
thisFile = fileInput.files[0];
reader.readAsArrayBuffer(thisFile);
}
readFile(0, fileInput.files);
};
/**
@@ -55,7 +61,6 @@ const handleFileUpload = function (fileInput, onload) {
* Create an asset (costume, sound) with storage and return an object representation
* of the asset to track in the VM.
* @param {ScratchStorage} storage The storage to cache the asset in
* @param {string} fileName The name of the asset
* @param {AssetType} assetType A ScratchStorage AssetType indicating what kind of
* asset this is.
* @param {string} dataFormat The format of this data (typically the file extension)
@@ -63,7 +68,7 @@ const handleFileUpload = function (fileInput, onload) {
* @return {VMAsset} An object representing this asset and relevant information
* which can be used to look up the data in storage
*/
const createVMAsset = function (storage, fileName, assetType, dataFormat, data) {
const createVMAsset = function (storage, assetType, dataFormat, data) {
const asset = storage.createAsset(
assetType,
dataFormat,
@@ -73,7 +78,7 @@ const createVMAsset = function (storage, fileName, assetType, dataFormat, data)
);
return {
name: fileName,
name: null, // Needs to be set by caller
dataFormat: dataFormat,
asset: asset,
md5: `${asset.assetId}.${dataFormat}`,
@@ -86,13 +91,13 @@ const createVMAsset = function (storage, fileName, assetType, dataFormat, data)
* @param {ArrayBuffer | string} fileData The costume data to load (this can be a base64 string
* iff the image is a bitmap)
* @param {string} fileType The MIME type of this file
* @param {string} costumeName The user-readable name to use for the costume.
* @param {ScratchStorage} storage The ScratchStorage instance to cache the costume data
* @param {Function} handleCostume The function to execute on the costume object returned after
* caching this costume in storage - This function should be responsible for
* adding the costume to the VM and handling other UI flow that should come after adding the costume
* @param {Function} handleError The function to execute if there is an error parsing the costume
*/
const costumeUpload = function (fileData, fileType, costumeName, storage, handleCostume) {
const costumeUpload = function (fileData, fileType, storage, handleCostume, handleError = () => {}) {
let costumeFormat = null;
let assetType = null;
switch (fileType) {
@@ -111,8 +116,20 @@ const costumeUpload = function (fileData, fileType, costumeName, storage, handle
assetType = storage.AssetType.ImageBitmap;
break;
}
case 'image/gif': {
let costumes = [];
gifDecoder(fileData, (frameNumber, dataUrl, numFrames) => {
costumeUpload(dataUrl, 'image/png', storage, costumes_ => {
costumes = costumes.concat(costumes_);
if (frameNumber === numFrames - 1) {
handleCostume(costumes);
}
}, handleError);
});
return; // Abandon this load, do not try to load gif itself
}
default:
log.warn(`Encountered unexpected file type: ${fileType}`);
handleError(`Encountered unexpected file type: ${fileType}`);
return;
}
@@ -120,12 +137,11 @@ const costumeUpload = function (fileData, fileType, costumeName, storage, handle
const addCostumeFromBuffer = function (dataBuffer) {
const vmCostume = createVMAsset(
storage,
costumeName,
assetType,
costumeFormat,
dataBuffer
);
handleCostume(vmCostume);
handleCostume([vmCostume]);
};
if (costumeFormat === storage.DataFormat.SVG) {
@@ -137,9 +153,7 @@ const costumeUpload = function (fileData, fileType, costumeName, storage, handle
} else {
// otherwise it's a bitmap
bitmapAdapter.importBitmap(fileData, fileType).then(addCostumeFromBuffer)
.catch(e => {
log.error(e);
});
.catch(handleError);
}
};
@@ -148,13 +162,12 @@ const costumeUpload = function (fileData, fileType, costumeName, storage, handle
* @param {ArrayBuffer} fileData The sound data to load
* @param {string} fileType The MIME type of this file; This function will exit
* early if the fileType is unexpected.
* @param {string} soundName The user-readable name to use for the sound.
* @param {ScratchStorage} storage The ScratchStorage instance to cache the sound data
* @param {Function} handleSound The function to execute on the sound object of type VMAsset
* This function should be responsible for adding the sound to the VM
* as well as handling other UI flow that should come after adding the sound
*/
const soundUpload = function (fileData, fileType, soundName, storage, handleSound) {
const soundUpload = function (fileData, fileType, storage, handleSound) {
let soundFormat;
switch (fileType) {
case 'audio/mp3':
@@ -176,7 +189,6 @@ const soundUpload = function (fileData, fileType, soundName, storage, handleSoun
const vmSound = createVMAsset(
storage,
soundName,
storage.AssetType.Sound,
soundFormat,
new Uint8Array(fileData));
@@ -184,8 +196,7 @@ const soundUpload = function (fileData, fileType, soundName, storage, handleSoun
handleSound(vmSound);
};
const spriteUpload = function (fileData, fileType, spriteName, storage, handleSprite, costumeSuffix) {
const costumeName = costumeSuffix || 'costume1';
const spriteUpload = function (fileData, fileType, spriteName, storage, handleSprite, handleError = () => {}) {
switch (fileType) {
case '':
case 'application/zip': { // We think this is a .sprite2 or .sprite3 file
@@ -194,9 +205,13 @@ const spriteUpload = function (fileData, fileType, spriteName, storage, handleSp
}
case 'image/svg+xml':
case 'image/png':
case 'image/jpeg': {
case 'image/jpeg':
case 'image/gif': {
// Make a sprite from an image by making it a costume first
costumeUpload(fileData, fileType, `${spriteName}-${costumeName}`, storage, (vmCostume => {
costumeUpload(fileData, fileType, storage, vmCostumes => {
vmCostumes.forEach((costume, i) => {
costume.name = `${spriteName}${i ? i + 1 : ''}`;
});
const newSprite = {
name: spriteName,
isStage: false,
@@ -206,21 +221,21 @@ const spriteUpload = function (fileData, fileType, spriteName, storage, handleSp
size: 100,
rotationStyle: 'all around',
direction: 90,
draggable: true,
draggable: false,
currentCostume: 0,
blocks: {},
variables: {},
costumes: [vmCostume],
costumes: vmCostumes,
sounds: [] // TODO are all of these necessary?
};
randomizeSpritePosition(newSprite);
// TODO probably just want sprite upload to handle this object directly
handleSprite(JSON.stringify(newSprite));
}));
}, handleError);
return;
}
default: {
log.warn(`Encountered unexpected file type: ${fileType}`);
handleError(`Encountered unexpected file type: ${fileType}`);
return;
}
}
+2 -4
View File
@@ -1,5 +1,5 @@
import storage from './storage';
import {SVGRenderer} from 'scratch-svg-renderer';
import {inlineSvgFonts} from 'scratch-svg-renderer';
// Contains 'font-family', but doesn't only contain 'font-family="none"'
const HAS_FONT_REGEXP = 'font-family(?!="none")';
@@ -21,9 +21,7 @@ const getCostumeUrl = (function () {
if (asset.assetType === storage.AssetType.ImageVector) {
const svgString = asset.decodeText();
if (svgString.match(HAS_FONT_REGEXP)) {
const svgRenderer = new SVGRenderer();
svgRenderer.loadString(svgString);
const svgText = svgRenderer.toString(true /* shouldInjectFonts */);
const svgText = inlineSvgFonts(svgString);
cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`;
} else {
cachedUrl = asset.encodeDataURI();
+60
View File
@@ -0,0 +1,60 @@
import {GifReader} from 'omggif';
export default (arrayBuffer, onFrame) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const gifReader = new GifReader(new Uint8Array(arrayBuffer));
const numFrames = gifReader.numFrames();
canvas.width = gifReader.width;
canvas.height = gifReader.height;
let imageData = ctx.createImageData(canvas.width, canvas.height);
let previousData = ctx.createImageData(canvas.width, canvas.height);
const loadFrame = i => {
const framePixels = [];
gifReader.decodeAndBlitFrameRGBA(i, framePixels);
const {x, y, width, height, disposal} = gifReader.frameInfo(i);
for (let row = 0; row < height; row++) {
for (let column = 0; column < width; column++) {
const indexOffset = 4 * (x + (y * canvas.width));
const j = indexOffset + (4 * (column + (row * canvas.width)));
if (framePixels[j + 3]) {
imageData.data[j + 0] = framePixels[j + 0];
imageData.data[j + 1] = framePixels[j + 1];
imageData.data[j + 2] = framePixels[j + 2];
imageData.data[j + 3] = framePixels[j + 3];
}
}
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.putImageData(imageData, 0, 0);
const dataUrl = canvas.toDataURL();
switch (disposal) {
case 2: // "Return to background", blank out the current frame
ctx.clearRect(x, y, width, height);
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
break;
case 3: // "Restore to previous", copy previous data to current
imageData = ctx.createImageData(canvas.width, canvas.height);
imageData.data.set(previousData.data);
break;
default: // 0 and 1, as well as 4+ modes = do-not-dispose, so cache frame
previousData = ctx.getImageData(0, 0, canvas.width, canvas.height);
break;
}
onFrame(i, dataUrl, numFrames);
if (i < numFrames - 1) {
setTimeout(() => {
loadFrame(i + 1);
});
}
};
loadFrame(0);
};
-5
View File
@@ -9,8 +9,6 @@ import {
setProjectId
} from '../reducers/project-state';
import {closePreviewInfo} from '../reducers/modals';
/* Higher Order Component to get the project id from location.hash
* @param {React.Component} WrappedComponent: component to render
* @returns {React.Component} component with hash parsing behavior
@@ -74,9 +72,6 @@ const HashParserHOC = function (WrappedComponent) {
const mapDispatchToProps = dispatch => ({
setProjectId: projectId => {
dispatch(setProjectId(projectId));
if (projectId !== defaultProjectId) {
dispatch(closePreviewInfo());
}
}
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

+188
View File
@@ -0,0 +1,188 @@
// Intro
import introMove from './steps/intro-move.en.gif';
import introSayHello from './steps/intro-say-hello.en.gif';
// Text to Speech
import speechAddExtension from './steps/speech-add-extension.en.gif';
import speechSaySomething from './steps/speech-say-something.en.gif';
import speechSetVoice from './steps/speech-set-voice.en.gif';
import speechMoveAround from './steps/speech-move-around.en.gif';
import speechAddBackdrop from './steps/speech-add-backdrop.en.gif';
import speechAddSprite from './steps/speech-add-sprite.en.gif';
import speechSong from './steps/speech-song.en.gif';
import speechChangeColor from './steps/speech-change-color.en.gif';
import speechSpin from './steps/speech-spin.en.gif';
import speechGrowShrink from './steps/speech-grow-shrink.en.gif';
// Cartoon Network
import cnShowCharacter from './steps/cn-show-character.en.gif';
import cnSay from './steps/cn-say.en.gif';
import cnGlide from './steps/cn-glide.en.gif';
import cnPickSprite from './steps/cn-pick-sprite.en.gif';
import cnCollect from './steps/cn-collect.en.gif';
import cnVariable from './steps/cn-variable.en.gif';
import cnScore from './steps/cn-score.en.gif';
import cnBackdrop from './steps/cn-backdrop.en.gif';
// Add sprite
import addSprite from './steps/add-sprite.en.gif';
// Animate a name
import namePickLetter from './steps/name-pick-letter.en.gif';
import namePlaySound from './steps/name-play-sound.en.gif';
import namePickLetter2 from './steps/name-pick-letter2.en.gif';
import nameChangeColor from './steps/name-change-color.en.gif';
import nameSpin from './steps/name-spin.en.gif';
import nameGrow from './steps/name-grow.en.gif';
// Make-Music
import musicPickInstrument from './steps/music-pick-instrument.en.gif';
import musicPlaySound from './steps/music-play-sound.en.gif';
import musicMakeSong from './steps/music-make-song.en.gif';
import musicMakeBeat from './steps/music-make-beat.en.gif';
import musicMakeBeatbox from './steps/music-make-beatbox.en.gif';
// Chase-Game
import chaseGameAddBackdrop from './steps/chase-game-add-backdrop.en.gif';
import chaseGameAddSprite1 from './steps/chase-game-add-sprite1.en.gif';
import chaseGameRightLeft from './steps/chase-game-right-left.en.gif';
import chaseGameUpDown from './steps/chase-game-up-down.en.gif';
import chaseGameAddSprite2 from './steps/chase-game-add-sprite2.en.gif';
import chaseGameMoveRandomly from './steps/chase-game-move-randomly.en.gif';
import chaseGamePlaySound from './steps/chase-game-play-sound.en.gif';
import chaseGameAddVariable from './steps/chase-game-add-variable.en.gif';
import chaseGameChangeScore from './steps/chase-game-change-score.en.gif';
// Clicker-Game (Pop Game)
import popGamePickSprite from './steps/pop-game-pick-sprite.en.gif';
import popGamePlaySound from './steps/pop-game-play-sound.en.gif';
import popGameAddScore from './steps/pop-game-add-score.en.gif';
import popGameChangeScore from './steps/pop-game-change-score.en.gif';
import popGameRandomPosition from './steps/pop-game-random-position.en.gif';
import popGameChangeColor from './steps/pop-game-change-color.en.gif';
import popGameResetScore from './steps/pop-game-reset-score.en.gif';
// Animate A Character
import animateCharPickBackdrop from './steps/animate-char-pick-backdrop.en.gif';
import animateCharPickSprite from './steps/animate-char-pick-sprite.en.gif';
import animateCharSaySomething from './steps/animate-char-say-something.en.gif';
import animateCharAddSound from './steps/animate-char-add-sound.en.gif';
import animateCharTalk from './steps/animate-char-talk.en.gif';
import animateCharMove from './steps/animate-char-move.en.gif';
import animateCharJump from './steps/animate-char-jump.en.gif';
import animateCharChangeColor from './steps/animate-char-change-color.en.gif';
// Tell A Story
import storyPickBackdrop from './steps/story-pick-backdrop.en.gif';
import storyPickSprite from './steps/story-pick-sprite.en.gif';
import storySaySomething from './steps/story-say-something.en.gif';
import storyPickSprite2 from './steps/story-pick-sprite2.en.gif';
import storyFlip from './steps/story-flip.en.gif';
import storyConversation from './steps/story-conversation.en.gif';
import storyPickBackdrop2 from './steps/story-pick-backdrop2.en.gif';
import storySwitchBackdrop from './steps/story-switch-backdrop.en.gif';
import storyHideCharacter from './steps/story-hide-character.en.gif';
import storyShowCharacter from './steps/story-show-character.en.gif';
// Video Sensing
import videoAddExtension from './steps/video-add-extension.en.gif';
import videoPet from './steps/video-pet.en.gif';
import videoAnimate from './steps/video-animate.en.gif';
import videoPop from './steps/video-pop.en.gif';
const enImages = {
// Intro
introMove: introMove,
introSayHello: introSayHello,
// Text to Speech
speechAddExtension: speechAddExtension,
speechSaySomething: speechSaySomething,
speechSetVoice: speechSetVoice,
speechMoveAround: speechMoveAround,
speechAddBackdrop: speechAddBackdrop,
speechAddSprite: speechAddSprite,
speechSong: speechSong,
speechChangeColor: speechChangeColor,
speechSpin: speechSpin,
speechGrowShrink: speechGrowShrink,
// Cartoon Network
cnShowCharacter: cnShowCharacter,
cnSay: cnSay,
cnGlide: cnGlide,
cnPickSprite: cnPickSprite,
cnCollect: cnCollect,
cnVariable: cnVariable,
cnScore: cnScore,
cnBackdrop: cnBackdrop,
// Add sprite
addSprite: addSprite,
// Animate a name
namePickLetter: namePickLetter,
namePlaySound: namePlaySound,
namePickLetter2: namePickLetter2,
nameChangeColor: nameChangeColor,
nameSpin: nameSpin,
nameGrow: nameGrow,
// Make-Music
musicPickInstrument: musicPickInstrument,
musicPlaySound: musicPlaySound,
musicMakeSong: musicMakeSong,
musicMakeBeat: musicMakeBeat,
musicMakeBeatbox: musicMakeBeatbox,
// Chase-Game
chaseGameAddBackdrop: chaseGameAddBackdrop,
chaseGameAddSprite1: chaseGameAddSprite1,
chaseGameRightLeft: chaseGameRightLeft,
chaseGameUpDown: chaseGameUpDown,
chaseGameAddSprite2: chaseGameAddSprite2,
chaseGameMoveRandomly: chaseGameMoveRandomly,
chaseGamePlaySound: chaseGamePlaySound,
chaseGameAddVariable: chaseGameAddVariable,
chaseGameChangeScore: chaseGameChangeScore,
// Make-A-Pop/Clicker Game
popGamePickSprite: popGamePickSprite,
popGamePlaySound: popGamePlaySound,
popGameAddScore: popGameAddScore,
popGameChangeScore: popGameChangeScore,
popGameRandomPosition: popGameRandomPosition,
popGameChangeColor: popGameChangeColor,
popGameResetScore: popGameResetScore,
// Animate A Character
animateCharPickBackdrop: animateCharPickBackdrop,
animateCharPickSprite: animateCharPickSprite,
animateCharSaySomething: animateCharSaySomething,
animateCharAddSound: animateCharAddSound,
animateCharTalk: animateCharTalk,
animateCharMove: animateCharMove,
animateCharJump: animateCharJump,
animateCharChangeColor: animateCharChangeColor,
// Tell A Story
storyPickBackdrop: storyPickBackdrop,
storyPickSprite: storyPickSprite,
storySaySomething: storySaySomething,
storyPickSprite2: storyPickSprite2,
storyFlip: storyFlip,
storyConversation: storyConversation,
storyPickBackdrop2: storyPickBackdrop2,
storySwitchBackdrop: storySwitchBackdrop,
storyHideCharacter: storyHideCharacter,
storyShowCharacter: storyShowCharacter,
// Video Sensing
videoAddExtension: videoAddExtension,
videoPet: videoPet,
videoAnimate: videoAnimate,
videoPop: videoPop
};
export {enImages};
+188
View File
@@ -0,0 +1,188 @@
// Intro
import introMove from './steps/intro-move.es.gif';
import introSayHello from './steps/intro-say-hello.es.gif';
// Text to Speech
// import speechAddExtension from './steps/speech-add-extension.es.gif';
// import speechSaySomething from './steps/speech-say-something.es.gif';
// import speechSetVoice from './steps/speech-set-voice.es.gif';
// import speechMoveAround from './steps/speech-move-around.es.gif';
// import speechAddBackdrop from './steps/speech-add-backdrop.es.gif';
// import speechAddSprite from './steps/speech-add-sprite.es.gif';
// import speechSong from './steps/speech-song.es.gif';
// import speechChangeColor from './steps/speech-change-color.es.gif';
// import speechSpin from './steps/speech-spin.es.gif';
// import speechGrowShrink from './steps/speech-grow-shrink.es.gif';
//
// // Cartoon Network
// import cnShowCharacter from './steps/cn-show-character.es.gif';
// import cnSay from './steps/cn-say.es.gif';
// import cnGlide from './steps/cn-glide.es.gif';
// import cnPickSprite from './steps/cn-pick-sprite.es.gif';
// import cnCollect from './steps/cn-collect.es.gif';
// import cnVariable from './steps/cn-variable.es.gif';
// import cnScore from './steps/cn-score.es.gif';
// import cnBackdrop from './steps/cn-backdrop.es.gif';
// Add sprite
import addSprite from './steps/add-sprite.es.gif';
// Animate a name
import namePickLetter from './steps/name-pick-letter.es.gif';
import namePlaySound from './steps/name-play-sound.es.gif';
import namePickLetter2 from './steps/name-pick-letter2.es.gif';
import nameChangeColor from './steps/name-change-color.es.gif';
import nameSpin from './steps/name-spin.es.gif';
import nameGrow from './steps/name-grow.es.gif';
// Make-Music
import musicPickInstrument from './steps/music-pick-instrument.es.gif';
import musicPlaySound from './steps/music-play-sound.es.gif';
import musicMakeSong from './steps/music-make-song.es.gif';
import musicMakeBeat from './steps/music-make-beat.es.gif';
import musicMakeBeatbox from './steps/music-make-beatbox.es.gif';
// Chase-Game
import chaseGameAddBackdrop from './steps/chase-game-add-backdrop.es.gif';
import chaseGameAddSprite1 from './steps/chase-game-add-sprite1.es.gif';
import chaseGameRightLeft from './steps/chase-game-right-left.es.gif';
import chaseGameUpDown from './steps/chase-game-up-down.es.gif';
import chaseGameAddSprite2 from './steps/chase-game-add-sprite2.es.gif';
import chaseGameMoveRandomly from './steps/chase-game-move-randomly.es.gif';
import chaseGamePlaySound from './steps/chase-game-play-sound.es.gif';
import chaseGameAddVariable from './steps/chase-game-add-variable.es.gif';
import chaseGameChangeScore from './steps/chase-game-change-score.es.gif';
// Clicker-Game (Pop Game)
import popGamePickSprite from './steps/pop-game-pick-sprite.es.gif';
import popGamePlaySound from './steps/pop-game-play-sound.es.gif';
import popGameAddScore from './steps/pop-game-add-score.es.gif';
import popGameChangeScore from './steps/pop-game-change-score.es.gif';
import popGameRandomPosition from './steps/pop-game-random-position.es.gif';
import popGameChangeColor from './steps/pop-game-change-color.es.gif';
import popGameResetScore from './steps/pop-game-reset-score.es.gif';
// Animate A Character
import animateCharPickBackdrop from './steps/animate-char-pick-backdrop.es.gif';
import animateCharPickSprite from './steps/animate-char-pick-sprite.es.gif';
import animateCharSaySomething from './steps/animate-char-say-something.es.gif';
import animateCharAddSound from './steps/animate-char-add-sound.es.gif';
import animateCharTalk from './steps/animate-char-talk.es.gif';
import animateCharMove from './steps/animate-char-move.es.gif';
import animateCharJump from './steps/animate-char-jump.es.gif';
import animateCharChangeColor from './steps/animate-char-change-color.es.gif';
// Tell A Story
import storyPickBackdrop from './steps/story-pick-backdrop.es.gif';
import storyPickSprite from './steps/story-pick-sprite.es.gif';
import storySaySomething from './steps/story-say-something.es.gif';
import storyPickSprite2 from './steps/story-pick-sprite2.es.gif';
import storyFlip from './steps/story-flip.es.gif';
import storyConversation from './steps/story-conversation.es.gif';
import storyPickBackdrop2 from './steps/story-pick-backdrop2.es.gif';
import storySwitchBackdrop from './steps/story-switch-backdrop.es.gif';
import storyHideCharacter from './steps/story-hide-character.es.gif';
import storyShowCharacter from './steps/story-show-character.es.gif';
// Video Sensing
import videoAddExtension from './steps/video-add-extension.es.gif';
import videoPet from './steps/video-pet.es.gif';
import videoAnimate from './steps/video-animate.es.gif';
import videoPop from './steps/video-pop.es.gif';
const esImages = {
// Intro
introMove: introMove,
introSayHello: introSayHello,
// // Text to Speech
// speechAddExtension: speechAddExtension,
// speechSaySomething: speechSaySomething,
// speechSetVoice: speechSetVoice,
// speechMoveAround: speechMoveAround,
// speechAddBackdrop: speechAddBackdrop,
// speechAddSprite: speechAddSprite,
// speechSong: speechSong,
// speechChangeColor: speechChangeColor,
// speechSpin: speechSpin,
// speechGrowShrink: speechGrowShrink,
//
// // Cartoon Network
// cnShowCharacter: cnShowCharacter,
// cnSay: cnSay,
// cnGlide: cnGlide,
// cnPickSprite: cnPickSprite,
// cnCollect: cnCollect,
// cnVariable: cnVariable,
// cnScore: cnScore,
// cnBackdrop: cnBackdrop,
// Add sprite
addSprite: addSprite,
// Animate a name
namePickLetter: namePickLetter,
namePlaySound: namePlaySound,
namePickLetter2: namePickLetter2,
nameChangeColor: nameChangeColor,
nameSpin: nameSpin,
nameGrow: nameGrow,
// Make-Music
musicPickInstrument: musicPickInstrument,
musicPlaySound: musicPlaySound,
musicMakeSong: musicMakeSong,
musicMakeBeat: musicMakeBeat,
musicMakeBeatbox: musicMakeBeatbox,
// Chase-Game
chaseGameAddBackdrop: chaseGameAddBackdrop,
chaseGameAddSprite1: chaseGameAddSprite1,
chaseGameRightLeft: chaseGameRightLeft,
chaseGameUpDown: chaseGameUpDown,
chaseGameAddSprite2: chaseGameAddSprite2,
chaseGameMoveRandomly: chaseGameMoveRandomly,
chaseGamePlaySound: chaseGamePlaySound,
chaseGameAddVariable: chaseGameAddVariable,
chaseGameChangeScore: chaseGameChangeScore,
// Make-A-Pop/Clicker Game
popGamePickSprite: popGamePickSprite,
popGamePlaySound: popGamePlaySound,
popGameAddScore: popGameAddScore,
popGameChangeScore: popGameChangeScore,
popGameRandomPosition: popGameRandomPosition,
popGameChangeColor: popGameChangeColor,
popGameResetScore: popGameResetScore,
// Animate A Character
animateCharPickBackdrop: animateCharPickBackdrop,
animateCharPickSprite: animateCharPickSprite,
animateCharSaySomething: animateCharSaySomething,
animateCharAddSound: animateCharAddSound,
animateCharTalk: animateCharTalk,
animateCharMove: animateCharMove,
animateCharJump: animateCharJump,
animateCharChangeColor: animateCharChangeColor,
// Tell A Story
storyPickBackdrop: storyPickBackdrop,
storyPickSprite: storyPickSprite,
storySaySomething: storySaySomething,
storyPickSprite2: storyPickSprite2,
storyFlip: storyFlip,
storyConversation: storyConversation,
storyPickBackdrop2: storyPickBackdrop2,
storySwitchBackdrop: storySwitchBackdrop,
storyHideCharacter: storyHideCharacter,
storyShowCharacter: storyShowCharacter,
// Video Sensing
videoAddExtension: videoAddExtension,
videoPet: videoPet,
videoAnimate: videoAnimate,
videoPop: videoPop
};
export {esImages};
+93 -162
View File
@@ -1,120 +1,51 @@
import React from 'react';
import {FormattedMessage} from 'react-intl';
// Tutorial thumbnails: Avoid using any text that would need to be
// translated in thumbnails.
// Intro
import libraryIntro from './intro/lib-getting-started.jpg';
import stepMove from './intro/intro1.gif';
import stepMoveSayHello from './intro/intro2.gif';
import libraryIntro from './thumbnails/getting-started.jpg';
// Text to Speech
import libraryTXTSpeech from './txt/lib_txt-to-speech.jpg';
import stepAddTXTextension from './txt/01_hoc-add-extensions.gif';
import stepTXTSpeech from './txt/02_hoc-say-something.gif';
import stepTXTSetVoice from './txt/03_hoc-set-voice.gif';
import stepTXTMove from './txt/04_hoc-move-around.gif';
import stepTXTBackdrop from './txt/05_hoc-add-backdrop.gif';
import stepTXTAddSprite from './txt/06_hoc-add-character.gif';
import stepTXTSong from './txt/07_hoc-perform-song.gif';
import stepTXTColor from './txt/08_hoc-color-click.gif';
import stepTXTSpin from './txt/09_hoc-spin.gif';
import stepTXTGrow from './txt/10_hoc-grow-shrink.gif';
import libraryTXTSpeech from './thumbnails/text-to-speech.jpg';
// Cartoon Network
import libraryCartoonNetwork from './cartoonnetwork/lib_CartoonNetwork.jpg';
import stepCNcharacter from './cartoonnetwork/1-cn-hideshow.gif';
import stepCNsay from './cartoonnetwork/2-cn-say.gif';
import stepCNglide from './cartoonnetwork/3-cn-glide.gif';
import stepCNpicksprite from './cartoonnetwork/4-cn-pick-gem.gif';
import stepCNcollect from './cartoonnetwork/5-cn-collect.gif';
import stepCNvariable from './cartoonnetwork/6-cn-variable.gif';
import stepCNscore from './cartoonnetwork/7-cn-score.gif';
import stepCNbackdrop from './cartoonnetwork/8-cn-change-backdrop.gif';
import libraryCartoonNetwork from './thumbnails/cartoon-network.jpg';
// Add sprite
import libraryAddSprite from './sprite/cover-add-sprite.jpg';
import stepAddSprite from './sprite/intro-choose-sprite.gif';
import libraryAddSprite from './thumbnails/add-sprite.jpg';
// Animate a name
import libraryAnimate from './animate/lib_animate-a-name.jpg';
import stepAnimatePickLetter from './animate/animate-name-pick-a-letter.gif';
import stepAnimatePlaySound from './animate/animate-name-play-sound.gif';
import stepAnimatePickLetter2 from './animate/animate-name-pick-a-letter2.gif';
import stepAnimateChangeColor from './animate/animate-name-change-color.gif';
import stepAnimateSpin from './animate/animate-name-spin.gif';
import stepAnimateGrow from './animate/animate-name-grow.gif';
import libraryAnimate from './thumbnails/animate-a-name.jpg';
// Make-Music
import libraryMakeMusic from './make-music/lib-make-music.jpg';
import stepPickInstrument from './make-music/make-music-pick-instrument.gif';
import stepPlaySoundClick from './make-music/make-music-play-sound.gif';
import stepMakeSong from './make-music/make-music-make-song.gif';
import stepMakeBeat from './make-music/make-music-make-beat.gif';
import stepMakeBeatbox from './make-music/make-music-beatbox.gif';
import libraryMakeMusic from './thumbnails/make-music.jpg';
// Chase-Game
import libraryChaseGame from './chase-game/lib-chasegame.jpg';
import stepBG from './chase-game/chase-game-add-backdrop.gif';
import stepAddOcto from './chase-game/chase-game-add-sprite1.gif';
import stepLeftRight from './chase-game/chase-game-move-rightleft.gif';
import stepUpDown from './chase-game/chase-game-move-updown.gif';
import stepAddStar from './chase-game/chase-game-add-sprite2.gif';
import stepMoveRandom from './chase-game/chase-game-move-randomly.gif';
import stepWhenTouch from './chase-game/chase-game-touching.gif';
import stepScoreVariable from './chase-game/chase-game-add-variable.gif';
import stepScoreWhenTouch from './chase-game/chase-game-change-score.gif';
import libraryChaseGame from './thumbnails/chase-game.jpg';
// Make-A-Game
import libraryMakeAGame from './game/lib-pop.jpg';
import stepGamePickSprite from './game/game-pick-sprite.gif';
import stepGamePlaySound from './game/game-play-sound.gif';
import stepGameAddScore from './game/game-add-score.gif';
import stepGameChangeScore from './game/game-change-score.gif';
import stepRandom from './game/game-random-position.gif';
import stepGameChangeColor from './game/game-change-color.gif';
import stepResetScore from './game/game-reset-score.gif';
import libraryMakeAGame from './thumbnails/pop-game.jpg';
// 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';
import libraryAnimateChar from './thumbnails/animate-a-character.jpg';
// 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';
import libraryStory from './thumbnails/tell-a-story.jpg';
// 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';
import libraryVideoSens from './thumbnails/video-sensing.jpg';
// Videos
import recordASound from './videos/record-a-sound.jpg';
import glideAroundThumb from './videos/glide-around.jpg';
import changeSizeThumb from './videos/change-size.jpg';
import switchCostumeThumb from './videos/animate-sprite.jpg';
import hideAndShowThumb from './videos/hide-and-show.jpg';
import addBackdropThumb from './videos/add-backdrop.jpg';
import addEffectsThumb from './videos/add-effects.jpg';
import moveArrowKeysThumb from './videos/move-arrow-keys.jpg';
import spinThumb from './videos/spin.jpg';
import recordASound from './thumbnails/record-a-sound.jpg';
import glideAroundThumb from './thumbnails/glide-around.jpg';
import changeSizeThumb from './thumbnails/change-size.jpg';
import switchCostumeThumb from './thumbnails/animate-sprite.jpg';
import hideAndShowThumb from './thumbnails/hide-and-show.jpg';
import addBackdropThumb from './thumbnails/add-backdrop.jpg';
import addEffectsThumb from './thumbnails/add-effects.jpg';
import moveArrowKeysThumb from './thumbnails/move-arrow-keys.jpg';
import spinThumb from './thumbnails/spin.jpg';
export default {
@@ -138,7 +69,7 @@ export default {
id="gui.howtos.intro-move.step_stepMove"
/>
),
image: stepMove
image: 'introMove'
}, {
title: (
<FormattedMessage
@@ -147,7 +78,7 @@ export default {
id="gui.howtos.add-a-move-block.step_stepMoveSayHello"
/>
),
image: stepMoveSayHello
image: 'introSayHello'
}, {
deckIds: [
'add-a-backdrop',
@@ -180,16 +111,16 @@ export default {
id="gui.howtos.say-it-out-loud.step_AddTXTextension"
/>
),
image: stepAddTXTextension
image: 'speechAddExtension'
}, {
title: (
<FormattedMessage
defaultMessage="Say Something"
description="Step name for 'Say Something' step"
id="gui.howtos. say-it-out-loud.step_TXTSpeech"
id="gui.howtos.say-it-out-loud.step_TXTSpeech"
/>
),
image: stepTXTSpeech
image: 'speechSaySomething'
}, {
title: (
<FormattedMessage
@@ -198,7 +129,7 @@ export default {
id="gui.howtos.say-it-out-loud_TXTSetVoice"
/>
),
image: stepTXTSetVoice
image: 'speechSetVoice'
}, {
title: (
<FormattedMessage
@@ -207,7 +138,7 @@ export default {
id="gui.howtos.say-it-out-loud.step_TXTMove"
/>
),
image: stepTXTMove
image: 'speechMoveAround'
}, {
title: (
<FormattedMessage
@@ -216,7 +147,7 @@ export default {
id="gui.howtos.say-it-out-loud.step_TXTBackdrop"
/>
),
image: stepTXTBackdrop
image: 'speechAddBackdrop'
}, {
title: (
<FormattedMessage
@@ -225,7 +156,7 @@ export default {
id="gui.howtos.say-it-out-loud.step_TXTAddSprite"
/>
),
image: stepTXTAddSprite
image: 'speechAddSprite'
}, {
title: (
<FormattedMessage
@@ -234,7 +165,7 @@ export default {
id="gui.howtos.say-it-out-loud.step_TXTSong"
/>
),
image: stepTXTSong
image: 'speechSong'
}, {
title: (
<FormattedMessage
@@ -243,7 +174,7 @@ export default {
id="gui.howtos.say-it-out-loud.step_TXTColor"
/>
),
image: stepTXTColor
image: 'speechChangeColor'
}, {
title: (
<FormattedMessage
@@ -252,7 +183,7 @@ export default {
id="gui.howtos.say-it-out-loud.step_TXTSpin"
/>
),
image: stepTXTSpin
image: 'speechSpin'
}, {
title: (
<FormattedMessage
@@ -261,7 +192,7 @@ export default {
id="gui.howtos.say-it-out-loud.step_TXTGrow"
/>
),
image: stepTXTGrow
image: 'speechGrowShrink'
}, {
deckIds: [
'animate-a-name',
@@ -295,7 +226,7 @@ export default {
id="gui.howtos.cartoon-network.step_CNcharacter"
/>
),
image: stepCNcharacter
image: 'cnShowCharacter'
}, {
title: (
<FormattedMessage
@@ -304,7 +235,7 @@ export default {
id="gui.howtos.cartoon-network.step_CNsay"
/>
),
image: stepCNsay
image: 'cnSay'
}, {
title: (
<FormattedMessage
@@ -313,7 +244,7 @@ export default {
id="gui.howtos.cartoon-network.step_CNglide"
/>
),
image: stepCNglide
image: 'cnGlide'
}, {
title: (
<FormattedMessage
@@ -322,7 +253,7 @@ export default {
id="gui.howtos.cartoon-network.step_CNpicksprite"
/>
),
image: stepCNpicksprite
image: 'cnPickSprite'
}, {
title: (
<FormattedMessage
@@ -331,7 +262,7 @@ export default {
id="gui.howtos.cartoon-network.step_CNcollect"
/>
),
image: stepCNcollect
image: 'cnCollect'
}, {
title: (
<FormattedMessage
@@ -340,7 +271,7 @@ export default {
id="gui.howtos.cartoon-network.step_CNvariable"
/>
),
image: stepCNvariable
image: 'cnVariable'
}, {
title: (
<FormattedMessage
@@ -349,7 +280,7 @@ export default {
id="gui.howtos.cartoon-network.step_CNscore"
/>
),
image: stepCNscore
image: 'cnScore'
}, {
title: (
<FormattedMessage
@@ -358,7 +289,7 @@ export default {
id="gui.howtos.cartoon-network.step_CNbackdrop"
/>
),
image: stepCNbackdrop
image: 'cnBackdrop'
},
{
video: '6o76f5ivo1'
@@ -392,7 +323,7 @@ export default {
id="gui.howtos.animate-a-name.step_AnimatePickLetter"
/>
),
image: stepAnimatePickLetter
image: 'namePickLetter'
}, {
title: (
<FormattedMessage
@@ -401,7 +332,7 @@ export default {
id="gui.howtos.animate-a-name.step_AnimatePlaySound"
/>
),
image: stepAnimatePlaySound
image: 'namePlaySound'
}, {
title: (
<FormattedMessage
@@ -410,7 +341,7 @@ export default {
id="gui.howtos.animate-a-name.step_AnimatePickLetter2"
/>
),
image: stepAnimatePickLetter2
image: 'namePickLetter2'
}, {
title: (
<FormattedMessage
@@ -419,7 +350,7 @@ export default {
id="gui.howtos.animate-a-name.step_AnimateChangeColor"
/>
),
image: stepAnimateChangeColor
image: 'nameChangeColor'
}, {
title: (
<FormattedMessage
@@ -428,7 +359,7 @@ export default {
id="gui.howtos.animate-a-name.step_AnimateSpin"
/>
),
image: stepAnimateSpin
image: 'nameSpin'
}, {
title: (
<FormattedMessage
@@ -437,7 +368,7 @@ export default {
id="gui.howtos.animate-a-name.step_AnimateGrow"
/>
),
image: stepAnimateGrow
image: 'nameGrow'
}, {
deckIds: [
'add-a-backdrop',
@@ -468,7 +399,7 @@ export default {
id="gui.howtos.Make-Music.step_PickInstrument"
/>
),
image: stepPickInstrument
image: 'musicPickInstrument'
},
{
title: (
@@ -478,7 +409,7 @@ export default {
id="gui.howtos.Make-Music.step_PlaySoundClick"
/>
),
image: stepPlaySoundClick
image: 'musicPlaySound'
},
{
title: (
@@ -488,7 +419,7 @@ export default {
id="gui.howtos.Make-Music.step_MakeSong"
/>
),
image: stepMakeSong
image: 'musicMakeSong'
}, {
title: (
<FormattedMessage
@@ -497,7 +428,7 @@ export default {
id="gui.howtos.make-music.step_MakeBeat"
/>
),
image: stepMakeBeat
image: 'musicMakeBeat'
}, {
title: (
<FormattedMessage
@@ -506,7 +437,7 @@ export default {
id="gui.howtos.make-music.step_MakeBeatBox"
/>
),
image: stepMakeBeatbox
image: 'musicMakeBeatbox'
}, {
deckIds: [
'add-a-backdrop',
@@ -537,7 +468,7 @@ export default {
id="gui.howtos.Make-A-Game.step_GamePickSprite"
/>
),
image: stepGamePickSprite
image: 'popGamePickSprite'
}, {
title: (
<FormattedMessage
@@ -546,7 +477,7 @@ export default {
id="gui.howtos.make-a-game.step_GamePlaySound"
/>
),
image: stepGamePlaySound
image: 'popGamePlaySound'
}, {
title: (
<FormattedMessage
@@ -555,7 +486,7 @@ export default {
id="gui.howtos.make-a-game.step_GameAddScore"
/>
),
image: stepGameAddScore
image: 'popGameAddScore'
}, {
title: (
<FormattedMessage
@@ -565,7 +496,7 @@ export default {
/>
),
image: stepGameChangeScore
image: 'popGameChangeScore'
}, {
title: (
<FormattedMessage
@@ -574,7 +505,7 @@ export default {
id="gui.howtos.make-a-game.step_Random"
/>
),
image: stepRandom
image: 'popGameRandomPosition'
}, {
title: (
<FormattedMessage
@@ -583,7 +514,7 @@ export default {
id="gui.howtos.make-music.step_GameChangeColor"
/>
),
image: stepGameChangeColor
image: 'popGameChangeColor'
}, {
title: (
<FormattedMessage
@@ -592,7 +523,7 @@ export default {
id="gui.howtos.make-music.step_ResetScore"
/>
),
image: stepResetScore
image: 'popGameResetScore'
}, {
deckIds: [
'add-a-backdrop',
@@ -624,7 +555,7 @@ export default {
id="gui.howtos.Chase-Game.step_BG"
/>
),
image: stepBG
image: 'chaseGameAddBackdrop'
}, {
title: (
<FormattedMessage
@@ -633,7 +564,7 @@ export default {
id="gui.howtos.chase-game.step_AddOcto"
/>
),
image: stepAddOcto
image: 'chaseGameAddSprite1'
}, {
title: (
<FormattedMessage
@@ -642,7 +573,7 @@ export default {
id="gui.howtos.make-music.step_LeftRight"
/>
),
image: stepLeftRight
image: 'chaseGameRightLeft'
}, {
title: (
<FormattedMessage
@@ -651,7 +582,7 @@ export default {
id="gui.howtos.Chase-Game.step_UpDown"
/>
),
image: stepUpDown
image: 'chaseGameUpDown'
}, {
title: (
<FormattedMessage
@@ -660,7 +591,7 @@ export default {
id="gui.howtos.Chase-Game.step_AddStar"
/>
),
image: stepAddStar
image: 'chaseGameAddSprite2'
}, {
title: (
<FormattedMessage
@@ -669,7 +600,7 @@ export default {
id="gui.howtos.Chase-Game.step_MoveRandom"
/>
),
image: stepMoveRandom
image: 'chaseGameMoveRandomly'
}, {
title: (
<FormattedMessage
@@ -678,7 +609,7 @@ export default {
id="gui.howtos.Chase-Game.step_WhenTouch"
/>
),
image: stepWhenTouch
image: 'chaseGamePlaySound'
}, {
title: (
<FormattedMessage
@@ -687,7 +618,7 @@ export default {
id="gui.howtos.Chase-Game.step_ScoreVariable"
/>
),
image: stepScoreVariable
image: 'chaseGameAddVariable'
}, {
title: (
<FormattedMessage
@@ -696,7 +627,7 @@ export default {
id="gui.howtos.Chase-Game.ScoreWhenTouch"
/>
),
image: stepScoreWhenTouch
image: 'chaseGameChangeScore'
}, {
deckIds: [
'add-effects',
@@ -726,7 +657,7 @@ export default {
id="gui.howtos.animate-char.step_addbg"
/>
),
image: stepAnimateCharPickBackdrop
image: 'animateCharPickBackdrop'
}, {
title: (
<FormattedMessage
@@ -735,7 +666,7 @@ export default {
id="gui.howtos.animate-char.step_addsprite"
/>
),
image: stepAnimateCharPickSprite
image: 'animateCharPickSprite'
}, {
title: (
<FormattedMessage
@@ -744,7 +675,7 @@ export default {
id="gui.howtos.animate-char.step_saysomething"
/>
),
image: stepAnimateCharSaySomething
image: 'animateCharSaySomething'
}, {
title: (
<FormattedMessage
@@ -753,7 +684,7 @@ export default {
id="gui.howtos.animate-char.step_addsound"
/>
),
image: stepAnimateCharAddSound
image: 'animateCharAddSound'
}, {
title: (
<FormattedMessage
@@ -762,7 +693,7 @@ export default {
id="gui.howtos.animate-char.step_animatetalking"
/>
),
image: stepAnimateCharTalk
image: 'animateCharTalk'
}, {
title: (
<FormattedMessage
@@ -771,7 +702,7 @@ export default {
id="gui.howtos.animate-char.step_arrowkeys"
/>
),
image: stepAnimateCharMove
image: 'animateCharMove'
}, {
title: (
<FormattedMessage
@@ -780,7 +711,7 @@ export default {
id="gui.howtos.animate-char.step_jump"
/>
),
image: stepAnimateCharJump
image: 'animateCharJump'
}, {
title: (
<FormattedMessage
@@ -789,7 +720,7 @@ export default {
id="gui.howtos.animate-char.step_changecolor"
/>
),
image: stepAnimateCharChangeColor
image: 'animateCharChangeColor'
}, {
deckIds: [
'Chase-Game',
@@ -819,7 +750,7 @@ export default {
id="gui.howtos.story.step_addbg"
/>
),
image: stepStoryPickBackdrop
image: 'storyPickBackdrop'
}, {
title: (
<FormattedMessage
@@ -828,7 +759,7 @@ export default {
id="gui.howtos.story.step_addsprite"
/>
),
image: stepStoryPickSprite
image: 'storyPickSprite'
}, {
title: (
<FormattedMessage
@@ -837,7 +768,7 @@ export default {
id="gui.howtos.story.step_saysomething"
/>
),
image: stepStoryCharSaySomething
image: 'storySaySomething'
}, {
title: (
<FormattedMessage
@@ -846,7 +777,7 @@ export default {
id="gui.howtos.story.step_addanothersprite"
/>
),
image: stepStoryCharAddAnotherSprite
image: 'storyPickSprite2'
}, {
title: (
<FormattedMessage
@@ -855,7 +786,7 @@ export default {
id="gui.howtos.story.step_flip"
/>
),
image: stepStoryFlip
image: 'storyFlip'
}, {
title: (
<FormattedMessage
@@ -864,7 +795,7 @@ export default {
id="gui.howtos.story.step_conversation"
/>
),
image: stepStoryConversation
image: 'storyConversation'
}, {
title: (
<FormattedMessage
@@ -873,7 +804,7 @@ export default {
id="gui.howtos.story.addanotherbg"
/>
),
image: stepStoryAddAnotherBackdrop
image: 'storyPickBackdrop2'
}, {
title: (
<FormattedMessage
@@ -882,7 +813,7 @@ export default {
id="gui.howtos.story.step_swithbg"
/>
),
image: stepStorySwitchBackdrop
image: 'storySwitchBackdrop'
}, {
title: (
<FormattedMessage
@@ -891,7 +822,7 @@ export default {
id="gui.howtos.story.step_hidewizard"
/>
),
image: stepStoryHide
image: 'storyHideCharacter'
}, {
title: (
<FormattedMessage
@@ -900,7 +831,7 @@ export default {
id="gui.howtos.story.step_showwizard"
/>
),
image: stepStoryShow
image: 'storyShowCharacter'
},
{
deckIds: [
@@ -931,7 +862,7 @@ export default {
id="gui.howtos.videosens.step_addextension"
/>
),
image: stepVideoSensAddExtension
image: 'videoAddExtension'
}, {
title: (
<FormattedMessage
@@ -940,7 +871,7 @@ export default {
id="gui.howtos.videosens.step_pet"
/>
),
image: stepVideoSensPet
image: 'videoPet'
}, {
title: (
<FormattedMessage
@@ -949,7 +880,7 @@ export default {
id="gui.howtos.videosens.step_animate"
/>
),
image: stepVideoSensAnimate
image: 'videoAnimate'
}, {
title: (
<FormattedMessage
@@ -958,7 +889,7 @@ export default {
id="gui.howtos.videosens.step_pop"
/>
),
image: stepVideoSensPop
image: 'videoPop'
}, {
deckIds: [
'Make-Music',
@@ -987,7 +918,7 @@ export default {
id="gui.howtos.add-sprite.step_addSprite"
/>
),
image: stepAddSprite
image: 'addSprite'
},
{
deckIds: [
Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Loaded 100 of 276 files, more files were not shown because too many files have changed in this diff. Show more