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
No files matched your search
@@ -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
|
||||
|
||||
@@ -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",
|
||||
@@ -104,14 +105,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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
@@ -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 : (
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
@@ -107,7 +105,6 @@ const GUIComponent = props => {
|
||||
onTelemetryModalCancel,
|
||||
onTelemetryModalOptIn,
|
||||
onTelemetryModalOptOut,
|
||||
previewInfoVisible,
|
||||
showComingSoon,
|
||||
soundsTabVisible,
|
||||
stageSizeMode,
|
||||
@@ -139,6 +136,7 @@ const GUIComponent = props => {
|
||||
|
||||
return isPlayerOnly ? (
|
||||
<StageWrapper
|
||||
isFullScreen={isFullScreen}
|
||||
isRendererSupported={isRendererSupported}
|
||||
isRtl={isRtl}
|
||||
loading={loading}
|
||||
@@ -155,9 +153,6 @@ const GUIComponent = props => {
|
||||
dir={isRtl ? 'rtl' : 'ltr'}
|
||||
{...componentProps}
|
||||
>
|
||||
{previewInfoVisible ? (
|
||||
<PreviewModal />
|
||||
) : null}
|
||||
{telemetryModalVisible ? (
|
||||
<TelemetryModal
|
||||
onCancel={onTelemetryModalCancel}
|
||||
@@ -172,9 +167,6 @@ const GUIComponent = props => {
|
||||
{isCreating ? (
|
||||
<Loader messageId="gui.loader.creating" />
|
||||
) : null}
|
||||
{importInfoVisible ? (
|
||||
<ImportModal />
|
||||
) : null}
|
||||
{isRendererSupported ? null : (
|
||||
<WebGlModal isRtl={isRtl} />
|
||||
)}
|
||||
@@ -335,6 +327,7 @@ const GUIComponent = props => {
|
||||
<Box className={classNames(styles.stageAndTargetWrapper, styles[stageSize])}>
|
||||
<StageWrapper
|
||||
isRendererSupported={isRendererSupported}
|
||||
isRtl={isRtl}
|
||||
stageSize={stageSize}
|
||||
vm={vm}
|
||||
/>
|
||||
@@ -376,9 +369,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,
|
||||
@@ -403,7 +396,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;
|
||||
|
||||
@@ -10,6 +10,7 @@ import classNames from 'classnames';
|
||||
import bluetoothIconURL from './bluetooth.svg';
|
||||
import internetConnectionIconURL from './internet-connection.svg';
|
||||
|
||||
/* eslint-disable react/prefer-stateless-function */
|
||||
class LibraryItemComponent extends React.PureComponent {
|
||||
render () {
|
||||
return this.props.featured ? (
|
||||
@@ -132,6 +133,8 @@ class LibraryItemComponent extends React.PureComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
/* eslint-enable react/prefer-stateless-function */
|
||||
|
||||
|
||||
LibraryItemComponent.propTypes = {
|
||||
bluetoothRequired: PropTypes.bool,
|
||||
@@ -151,12 +154,12 @@ LibraryItemComponent.propTypes = {
|
||||
PropTypes.string,
|
||||
PropTypes.node
|
||||
]),
|
||||
onBlur: PropTypes.func,
|
||||
onClick: PropTypes.func,
|
||||
onFocus: PropTypes.func,
|
||||
onKeyPress: PropTypes.func,
|
||||
onMouseEnter: PropTypes.func,
|
||||
onMouseLeave: 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
|
||||
};
|
||||
|
||||
LibraryItemComponent.defaultProps = {
|
||||
|
||||
@@ -52,11 +52,9 @@ class LibraryComponent extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleBlur',
|
||||
'handleClose',
|
||||
'handleFilterChange',
|
||||
'handleFilterClear',
|
||||
'handleFocus',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave',
|
||||
'handleSelect',
|
||||
@@ -75,12 +73,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]);
|
||||
@@ -210,13 +202,12 @@ class LibraryComponent extends React.Component {
|
||||
featured={dataItem.featured}
|
||||
hidden={dataItem.hidden}
|
||||
iconSource={iconSource}
|
||||
icons={dataItem.json && dataItem.json.costumes}
|
||||
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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
|
||||
@@ -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 Box from '../box/box.jsx';
|
||||
@@ -66,13 +67,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',
|
||||
@@ -151,11 +147,18 @@ class MenuBar extends React.Component {
|
||||
'handleClickSeeCommunity',
|
||||
'handleClickShare',
|
||||
'handleCloseFileMenuAndThen',
|
||||
'handleKeyPress',
|
||||
'handleLanguageMouseUp',
|
||||
'handleRestoreOption',
|
||||
'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.
|
||||
@@ -165,7 +168,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();
|
||||
@@ -219,6 +222,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();
|
||||
}
|
||||
}
|
||||
handleLanguageMouseUp (e) {
|
||||
if (!this.props.languageMenuOpen) {
|
||||
this.props.onClickLanguage(e);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,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
|
||||
};
|
||||
|
||||
|
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);
|
||||
|
Before Width: | Height: | Size: 102 KiB |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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')(
|
||||
|
||||
@@ -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 => ({
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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);
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
|
||||
@@ -13,14 +14,21 @@ class LibraryItem extends React.PureComponent {
|
||||
'handleFocus',
|
||||
'handleKeyPress',
|
||||
'handleMouseEnter',
|
||||
'handleMouseLeave'
|
||||
'handleMouseLeave',
|
||||
'rotateIcon',
|
||||
'startRotatingIcons',
|
||||
'stopRotatingIcons'
|
||||
]);
|
||||
this.state = {
|
||||
iconIndex: 0,
|
||||
isRotatingIcon: false
|
||||
};
|
||||
}
|
||||
handleBlur () {
|
||||
this.props.onBlur(this.props.id);
|
||||
componentWillUnmount () {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
handleFocus () {
|
||||
this.props.onFocus(this.props.id);
|
||||
handleBlur (id) {
|
||||
this.handleMouseLeave(id);
|
||||
}
|
||||
handleClick (e) {
|
||||
if (!this.props.disabled) {
|
||||
@@ -28,6 +36,9 @@ class LibraryItem extends React.PureComponent {
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
handleFocus (id) {
|
||||
this.handleMouseEnter(id);
|
||||
}
|
||||
handleKeyPress (e) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
@@ -36,47 +47,93 @@ class LibraryItem extends React.PureComponent {
|
||||
}
|
||||
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});
|
||||
}
|
||||
curIconSource () {
|
||||
if (this.props.icons &&
|
||||
this.state.isRotatingIcon &&
|
||||
this.state.iconIndex < this.props.icons.length &&
|
||||
this.props.icons[this.state.iconIndex]) {
|
||||
return this.props.icons[this.state.iconIndex];
|
||||
}
|
||||
return this.props.iconSource;
|
||||
}
|
||||
render () {
|
||||
const {
|
||||
id: _id,
|
||||
onBlur: _onBlur,
|
||||
onFocus: _onFocus,
|
||||
onMouseEnter: _onMouseEnter,
|
||||
onMouseLeave: _onMouseLeave,
|
||||
onSelect: _onSelect,
|
||||
...childProps
|
||||
} = this.props;
|
||||
return (<LibraryItemComponent
|
||||
imageSource={this.props.imageSource}
|
||||
onBlur={this.props.onBlur && this.handleBlur}
|
||||
onClick={this.handleClick}
|
||||
onFocus={this.props.onFocus && this.handleFocus}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
onMouseEnter={this.props.onMouseEnter && this.handleMouseEnter}
|
||||
onMouseLeave={this.props.onMouseLeave && this.handleMouseLeave}
|
||||
{...childProps}
|
||||
/>);
|
||||
const iconSource = this.curIconSource();
|
||||
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}
|
||||
iconSource={iconSource}
|
||||
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 = Object.assign({},
|
||||
(() => {
|
||||
// copy all prop types EXCEPT these from LibraryItemComponent
|
||||
const {
|
||||
onClick: _onClick,
|
||||
onKeyPress: _onKeyPress,
|
||||
...otherPropTypes
|
||||
} = LibraryItemComponent.propTypes;
|
||||
return otherPropTypes;
|
||||
})(),
|
||||
{
|
||||
id: PropTypes.number.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
}
|
||||
);
|
||||
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,
|
||||
iconSource: LibraryItemComponent.propTypes.iconSource, // single icon
|
||||
icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons
|
||||
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 LibraryItem;
|
||||
export default injectIntl(LibraryItem);
|
||||
@@ -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']),
|
||||
|
||||
@@ -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);
|
||||
@@ -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});
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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')(
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
|
Before Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 308 KiB |
@@ -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};
|
||||
@@ -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};
|
||||
@@ -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: [
|
||||
|
||||
|
Before Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 652 KiB |
|
After Width: | Height: | Size: 802 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 718 KiB |
|
After Width: | Height: | Size: 487 KiB |