Compare commits

..
Author SHA1 Message Date
Christopher Willis-Ford eeb5cfd930 Merge tag '0.1.0-prerelease.20190828224521' into scratch-desktop 2019-08-29 12:03:49 -07:00
Eric Rosenbaum 99bdad90d6 Merge pull request #5129 from LLK/hotfix/add-marian-sprite
[hotfix] Add Marian sprite and costumes
2019-08-28 12:14:35 -07:00
Christopher Willis-Ford 1f5ac6e383 add react-visibility-sensor to package-lock.json 2019-07-27 15:42:34 -07:00
Christopher Willis-Ford 9cb5f3f96d Merge tag '0.1.0-prerelease.20190725155313' into scratch-desktop 2019-07-27 15:31:18 -07:00
Christopher Willis-Ford 09b14ed4d4 expose TitledHOC for use with Scratch Desktop 2019-07-18 14:37:55 -07:00
Christopher Willis-Ford 5859af7efa Merge tag '0.1.0-prerelease.20190624143137' into scratch-desktop 2019-06-24 11:10:17 -07:00
Christopher Willis-Ford d4e71024fb Merge tag '0.1.0-prerelease.20190605201216' into scratch-desktop 2019-06-09 22:14:49 -07:00
Christopher Willis-Ford a83d9369dc Revert "Merge pull request #4735 from paulkaplan/revert-storage-update"
Go back to `scratch-storage` version 1.3.1 in the `scratch-desktop`
branch so that we have the performance improvements from
LLK/scratch-storage#83

If I understand correctly we reverted from 1.3.1 to 1.2.2 because of
problems affecting HTTP saves, which are not available in Scratch
Desktop.

This reverts commit b435ac3101, reversing
changes made to 0d7a05d37a.
2019-05-16 19:59:58 -07:00
Christopher Willis-Ford 012bb612ad Revert "Merge pull request #4736 from LLK/revert-4644-load-library-images-through-storage"
This brings "Load library images through storage" back into the
`scratch-desktop` branch since it's necessary here. We need to figure
out some performance details before trying to merge it into `develop`
though.
2019-05-16 19:50:38 -07:00
Christopher Willis-Ford b9e7bde676 Merge tag '0.1.0-prerelease.20190516172446' into scratch-desktop 2019-05-16 12:38:02 -07:00
Christopher Willis-Ford 992f012ff3 TO REVERT: Scratch Desktop GUI hacks
At some point these things will be changed / disabled / removed in
master or develop; at that point this change should be replaced with the
more official approach.
2019-03-14 11:51:38 -07:00
Christopher Willis-Ford c53b88dbad use relative publicPath for file-loader 2019-03-14 11:51:38 -07:00
66 changed files with 1114 additions and 1623 deletions

No files matched your search

-207
View File
@@ -1,207 +0,0 @@
version: 2.1
aliases:
- &save_git_cache
save_cache:
paths:
- .git
key: v1-git-{{ .Revision }}
- &restore_git_cache
restore_cache:
keys:
- v1-git-{{ .Revision }}
- v1-git-
- &save_build_cache
save_cache:
paths:
- build
key: v1-build-{{ .Revision }}
- &restore_build_cache
restore_cache:
keys:
- v1-build-{{ .Revision }}
- &save_dist_cache
save_cache:
paths:
- dist
key: v1-dist-{{ .Revision }}
- &restore_dist_cache
restore_cache:
keys:
- v1-dist-{{ .Revision }}
- &save_npm_cache
save_cache:
paths:
- node_modules
key: v1-npm-{{ checksum "package-lock.json" }}
- &restore_npm_cache
restore_cache:
keys:
- v1-npm-{{ checksum "package-lock.json" }}
- v1-npm-
- &defaults
docker:
- image: circleci/node:8.16-browsers
working_directory: ~/repo
jobs:
setup:
<<: *defaults
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run: npm install
- *save_git_cache
- *save_npm_cache
lint:
<<: *defaults
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: Lint
command: npm run test:lint -- --quiet --output-file test-results/eslint/results.xml --format junit
- store_test_results:
path: test-results
unit:
<<: *defaults
environment:
JEST_JUNIT_OUTPUT_NAME: results.xml
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: Unit
environment:
JEST_JUNIT_OUTPUT_DIR: test-results/unit
command: npm run test:unit -- --reporters="default" --reporters="jest-junit" --coverage --coverageReporters=text --coverageReporters=lcov
- store_artifacts:
path: coverage
- store_test_results:
path: test-results
build:
<<: *defaults
environment:
NODE_ENV: production
NODE_OPTIONS: --max-old-space-size=4000
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: Build
command: npm run build
- *save_build_cache
- *save_dist_cache
store_build:
<<: *defaults
steps:
- *restore_build_cache
- store_artifacts:
path: build
store_dist:
<<: *defaults
steps:
- *restore_dist_cache
- store_artifacts:
path: dist
integration:
<<: *defaults
parallelism: 2
environment:
JEST_JUNIT_OUTPUT_NAME: results.txt
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- *restore_build_cache
- run:
name: Integration
environment:
JEST_JUNIT_OUTPUT_DIR: test-results/integration
command: |
export TESTFILES=$(circleci tests glob "test/integration/*.test.js" | circleci tests split --split-by=timings)
$(npm bin)/jest ${TESTFILES} --reporters="default" --reporters="jest-junit"
- store_test_results:
path: test-results
deploy-npm:
<<: *defaults
environment:
NODE_OPTIONS: --max-old-space-size=4000
steps:
- *restore_git_cache
- *restore_dist_cache
- checkout
- run: |
echo export RELEASE_VERSION="0.1.0-prerelease.$(date +'%Y%m%d%H%M%S')" >> $BASH_ENV
echo export NPM_TAG=circlelatest >> $BASH_ENV
if [ "$CIRCLE_BRANCH" == "master" ]
then echo export NPM_TAG=circlestable >> $BASH_ENV
fi
if [[ "$CIRCLE_BRANCH" == hotfix/* ]] # double brackets are important for matching the wildcard
then echo export NPM_TAG=circlehotfix >> $BASH_ENV
fi
- run: npm version --no-git-tag-version $RELEASE_VERSION
- run: |
npm set //registry.npmjs.org/:_authToken=$NPM_TOKEN
npm publish --tag $NPM_TAG
- run: git tag $RELEASE_VERSION
- run: git push $CIRCLE_REPOSITORY_URL $RELEASE_VERSION
deploy-gh-pages:
<<: *defaults
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- *restore_build_cache
- run: |
git config --global user.email $(git log --pretty=format:"%ae" -n1)
git config --global user.name $(git log --pretty=format:"%an" -n1)
- run: npm run deploy -- -e $CIRCLE_BRANCH
workflows:
version: 2
build-test-deploy:
jobs:
- setup
- lint:
requires:
- setup
- unit:
requires:
- setup
- build:
requires:
- setup
- integration:
requires:
- build
- store_build:
requires:
- build
- store_dist:
requires:
- build
# Disable deployment while running in parallel with Travis
# - deploy-npm:
# requires:
# - lint
# - unit
# - integration
# - build
# filters:
# branches:
# only:
# - master
# - develop
# - /^hotfix\/.*/
# - deploy-gh-pages:
# requires:
# - lint
# - unit
# - integration
# - build
-3
View File
@@ -5,9 +5,6 @@
/node_modules
npm-*
# Double copies of all the static assets and tutorial gifs
/src
# Testing
/.nyc_output
/coverage
+2 -5
View File
@@ -1,18 +1,16 @@
language: node_js
sudo: required
dist: xenial
dist: trusty
addons:
chrome: stable
node_js:
- 8
env:
global:
- CHROMEDRIVER_VERSION=LATEST
- NODE_ENV=production
- NODE_OPTIONS=--max-old-space-size=7250
- 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')"
- SMOKE_URL=https://llk.github.io/scratch-gui/$TRAVIS_PULL_REQUEST_BRANCH
cache:
directories:
- node_modules
@@ -76,7 +74,6 @@ deploy:
on:
all_branches: true
condition: $TRAVIS_EVENT_TYPE != cron && ! $TRAVIS_BRANCH =~ ^greenkeeper/
tags: false # Don't push tags to gh-pages
skip_cleanup: true
script: npm run deploy -- -x -e $TRAVIS_BRANCH -r https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git
- provider: script
+534 -591
View File
File diff suppressed because it is too large. Load diff
+8 -8
View File
@@ -44,7 +44,7 @@
"babel-loader": "^8.0.4",
"base64-loader": "1.0.0",
"bowser": "1.9.4",
"chromedriver": "77.0.0",
"chromedriver": "76.0.0",
"classnames": "2.2.6",
"computed-style-to-inline-style": "3.0.0",
"copy-webpack-plugin": "^4.5.1",
@@ -67,7 +67,6 @@
"immutable": "3.8.2",
"intl": "1.2.5",
"jest": "^21.0.0",
"jest-junit": "^7.0.0",
"js-base64": "2.4.9",
"keymirror": "0.1.1",
"lodash.bindall": "4.4.0",
@@ -103,18 +102,19 @@
"react-test-renderer": "16.2.0",
"react-tooltip": "3.8.0",
"react-virtualized": "9.20.1",
"react-visibility-sensor": "5.0.2",
"redux": "3.7.2",
"redux-mock-store": "^1.2.3",
"redux-throttle": "0.1.1",
"rimraf": "^2.6.1",
"scratch-audio": "0.1.0-prerelease.20190925183642",
"scratch-l10n": "3.6.20191008224547",
"scratch-blocks": "0.1.0-prerelease.1570730963",
"scratch-paint": "0.2.0-prerelease.20191010161144",
"scratch-render": "0.1.0-prerelease.20191021180653",
"scratch-audio": "0.1.0-prerelease.20190114210212",
"scratch-l10n": "3.5.20190827223456",
"scratch-blocks": "0.1.0-prerelease.1566984014",
"scratch-paint": "0.2.0-prerelease.20190822205009",
"scratch-render": "0.1.0-prerelease.20190822204747",
"scratch-storage": "1.3.2",
"scratch-svg-renderer": "0.2.0-prerelease.20190822202608",
"scratch-vm": "0.2.0-prerelease.20191017151616",
"scratch-vm": "0.2.0-prerelease.20190822194548",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
+1 -9
View File
@@ -50,16 +50,8 @@ $fade-out-distance: 100px;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
.list-area:after {
/* Make sure there is room to scroll beyond the last tile */
content: '';
display: block;
height: 70px;
width: 100%;
flex-shrink: 0;
order: 99999999;
padding-bottom: 70px;
}
.list-item {
+4
View File
@@ -94,6 +94,7 @@ const GUIComponent = props => {
onLogOut,
onOpenRegistration,
onToggleLoginOpen,
onUpdateProjectTitle,
onActivateCostumesTab,
onActivateSoundsTab,
onActivateTab,
@@ -227,6 +228,7 @@ const GUIComponent = props => {
onSeeCommunity={onSeeCommunity}
onShare={onShare}
onToggleLoginOpen={onToggleLoginOpen}
onUpdateProjectTitle={onUpdateProjectTitle}
/>
<Box className={styles.bodyWrapper}>
<Box className={styles.flexWrapper}>
@@ -404,6 +406,7 @@ GUIComponent.propTypes = {
onTelemetryModalOptIn: PropTypes.func,
onTelemetryModalOptOut: PropTypes.func,
onToggleLoginOpen: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
renderLogin: PropTypes.func,
showComingSoon: PropTypes.bool,
soundsTabVisible: PropTypes.bool,
@@ -430,6 +433,7 @@ GUIComponent.defaultProps = {
isCreating: false,
isShared: false,
loading: false,
onUpdateProjectTitle: () => {},
showComingSoon: false,
stageSizeMode: STAGE_SIZE_MODES.large
};
+1 -3
View File
@@ -139,9 +139,7 @@
.featured-description {
display: block;
font-weight: normal;
line-height: 1.375rem;
padding-top: .3125rem;
padding-bottom: .25rem;
line-height: 2;
}
.featured-extension-metadata {
+10 -7
View File
@@ -4,6 +4,7 @@ import React from 'react';
import Box from '../box/box.jsx';
import PlayButton from '../../containers/play-button.jsx';
import ScratchImage from '../scratch-image/scratch-image.jsx';
import styles from './library-item.css';
import classNames from 'classnames';
@@ -36,10 +37,12 @@ class LibraryItemComponent extends React.PureComponent {
/>
</div>
) : null}
<img
className={styles.featuredImage}
src={this.props.iconURL}
/>
{this.props.iconSource ? (
<ScratchImage
className={styles.featuredImage}
imageSource={this.props.iconSource}
/>
) : null}
</div>
{this.props.insetIconURL ? (
<div className={styles.libraryItemInsetImageContainer}>
@@ -127,9 +130,9 @@ class LibraryItemComponent extends React.PureComponent {
onMouseEnter={this.props.showPlayButton ? this.props.onMouseEnter : null}
onMouseLeave={this.props.showPlayButton ? this.props.onMouseLeave : null}
>
<img
<ScratchImage
className={styles.libraryItemImage}
src={this.props.iconURL}
imageSource={this.props.iconSource}
/>
</Box>
</Box>
@@ -159,7 +162,7 @@ LibraryItemComponent.propTypes = {
extensionId: PropTypes.string,
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconURL: PropTypes.string,
iconSource: ScratchImage.ImageSourcePropType,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
isPlaying: PropTypes.bool,
+50 -7
View File
@@ -10,6 +10,7 @@ import Divider from '../divider/divider.jsx';
import Filter from '../filter/filter.jsx';
import TagButton from '../../containers/tag-button.jsx';
import Spinner from '../spinner/spinner.jsx';
import storage from '../../lib/storage';
import styles from './library.css';
@@ -29,6 +30,47 @@ const messages = defineMessages({
const ALL_TAG = {tag: 'all', intlLabel: messages.allTag};
const tagListPrefix = [ALL_TAG];
/**
* Find the AssetType which corresponds to a particular file extension. For example, 'png' => AssetType.ImageBitmap.
* @param {string} fileExtension - the file extension to look up.
* @returns {AssetType} - the AssetType corresponding to the extension, if any.
*/
const getAssetTypeForFileExtension = function (fileExtension) {
const compareOptions = {
sensitivity: 'accent',
usage: 'search'
};
for (const assetTypeId in storage.AssetType) {
const assetType = storage.AssetType[assetTypeId];
if (fileExtension.localeCompare(assetType.runtimeFormat, compareOptions) === 0) {
return assetType;
}
}
};
/**
* Figure out an `imageSource` (URI or asset ID & type) for a library item's icon.
* @param {object} item - either a library item or one of a library item's costumes.
* @returns {object} - an `imageSource` ready to be passed to a `ScratchImage`.
*/
const getItemImageSource = function (item) {
if (item.rawURL) {
return {
uri: item.rawURL
};
}
// TODO: adjust libraries to be more storage-friendly; don't use split() here.
const md5 = item.md5 || item.baseLayerMD5;
if (md5) {
const [assetId, fileExtension] = md5.split('.');
return {
assetId: assetId,
assetType: getAssetTypeForFileExtension(fileExtension)
};
}
};
class LibraryComponent extends React.Component {
constructor (props) {
super(props);
@@ -208,8 +250,10 @@ class LibraryComponent extends React.Component {
})}
ref={this.setFilteredDataRef}
>
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => (
<LibraryItem
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => {
const iconSource = getItemImageSource(dataItem);
const icons = dataItem.json && dataItem.json.costumes.map(getItemImageSource);
return (<LibraryItem
bluetoothRequired={dataItem.bluetoothRequired}
collaborator={dataItem.collaborator}
description={dataItem.description}
@@ -217,9 +261,8 @@ class LibraryComponent extends React.Component {
extensionId={dataItem.extensionId}
featured={dataItem.featured}
hidden={dataItem.hidden}
iconMd5={dataItem.md5}
iconRawURL={dataItem.rawURL}
icons={dataItem.json && dataItem.json.costumes}
iconSource={iconSource}
icons={icons}
id={index}
insetIconURL={dataItem.insetIconURL}
internetConnectionRequired={dataItem.internetConnectionRequired}
@@ -230,8 +273,8 @@ class LibraryComponent extends React.Component {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
)) : (
/>);
}) : (
<div className={styles.spinnerWrapper}>
<Spinner
large
+3
View File
@@ -395,6 +395,7 @@ class MenuBar extends React.Component {
<SBFileUploader
canSave={this.props.canSave}
userOwnsProject={this.props.userOwnsProject}
onUpdateProjectTitle={this.props.onUpdateProjectTitle}
>
{(className, renderFileInput, handleLoadProject) => (
<MenuItem
@@ -494,6 +495,7 @@ class MenuBar extends React.Component {
>
<ProjectTitleInput
className={classNames(styles.titleFieldGrowable)}
onUpdateProjectTitle={this.props.onUpdateProjectTitle}
/>
</MenuBarItemTooltip>
</div>
@@ -744,6 +746,7 @@ MenuBar.propTypes = {
onSeeCommunity: PropTypes.func,
onShare: PropTypes.func,
onToggleLoginOpen: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectTitle: PropTypes.string,
renderLogin: PropTypes.func,
sessionExists: PropTypes.bool,
+31 -21
View File
@@ -1,9 +1,9 @@
import classNames from 'classnames';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
import React from 'react';
import {defineMessages, intlShape, injectIntl} from 'react-intl';
import {setProjectTitle} from '../../reducers/project-title';
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
import Input from '../forms/input.jsx';
@@ -19,27 +19,39 @@ const messages = defineMessages({
}
});
const ProjectTitleInput = ({
className,
intl,
onSubmit,
projectTitle
}) => (
<BufferedInput
className={classNames(styles.titleField, className)}
maxLength="100"
placeholder={intl.formatMessage(messages.projectTitlePlaceholder)}
tabIndex="0"
type="text"
value={projectTitle}
onSubmit={onSubmit}
/>
);
class ProjectTitleInput extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleUpdateProjectTitle'
]);
}
// call onUpdateProjectTitle if it is defined (only defined when gui
// is used within scratch-www)
handleUpdateProjectTitle (newTitle) {
if (this.props.onUpdateProjectTitle) {
this.props.onUpdateProjectTitle(newTitle);
}
}
render () {
return (
<BufferedInput
className={classNames(styles.titleField, this.props.className)}
maxLength="100"
placeholder={this.props.intl.formatMessage(messages.projectTitlePlaceholder)}
tabIndex="0"
type="text"
value={this.props.projectTitle}
onSubmit={this.handleUpdateProjectTitle}
/>
);
}
}
ProjectTitleInput.propTypes = {
className: PropTypes.string,
intl: intlShape.isRequired,
onSubmit: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectTitle: PropTypes.string
};
@@ -47,9 +59,7 @@ const mapStateToProps = state => ({
projectTitle: state.scratchGui.projectTitle
});
const mapDispatchToProps = dispatch => ({
onSubmit: title => dispatch(setProjectTitle(title))
});
const mapDispatchToProps = () => ({});
export default injectIntl(connect(
mapStateToProps,
+1 -3
View File
@@ -43,7 +43,6 @@
margin: 0 5px;
border-radius: calc($space / 2);
padding: 0 2px;
white-space: pre-wrap;
transform: translateZ(0); /* Fixes flickering in Safari */
}
@@ -54,7 +53,6 @@
text-align: center;
color: white;
font-size: 1rem;
white-space: pre-wrap;
transform: translateZ(0); /* Fixes flickering in Safari */
}
@@ -151,7 +149,7 @@
overflow: hidden; /* Don't let long values escape container */
text-overflow: ellipsis;
user-select: text; /* Allow selecting list values for 2.0 compatibility, only relevant in player */
white-space: pre;
white-space: nowrap;
}
.list-input {
+1 -3
View File
@@ -20,10 +20,8 @@
border: 1px solid $ui-black-transparent;
border-radius: 5px;
padding: 3px;
/* Force these to be on their own render layer because they update often */
transform: translateZ(0);
}
}
.meter-container {
margin-right: 5px;
height: 180px;
@@ -0,0 +1,139 @@
import PropTypes from 'prop-types';
import React from 'react';
import VisibilitySensor from 'react-visibility-sensor';
import storage from '../../lib/storage';
class ScratchImage extends React.PureComponent {
static init () {
this._maxParallelism = 6;
this._currentJobs = 0;
this._pendingImages = new Set();
}
static loadPendingImages () {
if (this._currentJobs >= this._maxParallelism) {
// already busy
return;
}
// Find the first visible image. If there aren't any, find the first non-visible image.
let nextImage;
for (const image of this._pendingImages) {
if (image.isVisible) {
nextImage = image;
break;
} else {
nextImage = nextImage || image;
}
}
// If we found an image to load:
// 1) Remove it from the queue
// 2) Load the image
// 3) Pump the queue again
if (nextImage) {
this._pendingImages.delete(nextImage);
const imageSource = nextImage.props.imageSource;
++this._currentJobs;
storage
.load(imageSource.assetType, imageSource.assetId)
.then(asset => {
if (!nextImage.wasUnmounted) {
const dataURI = asset.encodeDataURI();
nextImage.setState({
imageURI: dataURI
});
}
--this._currentJobs;
this.loadPendingImages();
});
}
}
constructor (props) {
super(props);
this.state = {};
Object.assign(this.state, this._loadImageSource(props.imageSource));
}
componentWillReceiveProps (nextProps) {
const newState = this._loadImageSource(nextProps.imageSource);
this.setState(newState);
}
componentWillUnmount () {
this.wasUnmounted = true;
ScratchImage._pendingImages.delete(this);
}
/**
* Calculate the state changes necessary to load the image specified in the provided source info. If the component
* is mounted, call setState() with the return value of this function. If the component has not yet mounted, use
* the return value of this function as initial state for the component.
*
* @param {object} imageSource - the new source for the image, including either assetId or URI
* @returns {object} - the new state values, if any.
*/
_loadImageSource (imageSource) {
if (imageSource) {
if (imageSource.uri) {
ScratchImage._pendingImages.delete(this);
return {
imageURI: imageSource.uri,
lastRequestedAsset: null
};
}
if (this.state.lastRequestedAsset !== imageSource.assetId) {
ScratchImage._pendingImages.add(this);
return {
lastRequestedAsset: imageSource.assetId
};
}
}
// Nothing to do - don't change any state.
return {};
}
render () {
const {
src: _src,
imageSource: _imageSource,
...imgProps
} = this.props;
return (
<VisibilitySensor
intervalCheck
scrollCheck
>
{
({isVisible}) => {
this.isVisible = isVisible;
ScratchImage.loadPendingImages();
return (
<img
src={this.state.imageURI}
{...imgProps}
/>
);
}
}
</VisibilitySensor>
);
}
}
ScratchImage.ImageSourcePropType = PropTypes.oneOfType([
PropTypes.shape({
assetId: PropTypes.string.isRequired,
assetType: PropTypes.oneOf(Object.values(storage.AssetType)).isRequired
}),
PropTypes.shape({
uri: PropTypes.string.isRequired
})
]);
ScratchImage.propTypes = {
imageSource: ScratchImage.ImageSourcePropType.isRequired
};
ScratchImage.init();
export default ScratchImage;
@@ -265,7 +265,6 @@ const SoundEditor = props => (
onClick={props.onSlower}
/>
<IconButton
disabled={props.tooLoud}
className={classNames(styles.effectButton, styles.flipInRtl)}
img={louderIcon}
title={<FormattedMessage {...messages.louder} />}
@@ -341,7 +340,6 @@ SoundEditor.propTypes = {
onUndo: PropTypes.func.isRequired,
playhead: PropTypes.number,
setRef: PropTypes.func,
tooLoud: PropTypes.bool.isRequired,
trimEnd: PropTypes.number,
trimStart: PropTypes.number
};
+3 -3
View File
@@ -340,9 +340,9 @@ class Blocks extends React.Component {
const targetSounds = target.getSounds();
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML,
targetCostumes[targetCostumes.length - 1].name,
stageCostumes[stageCostumes.length - 1].name,
targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : ''
targetCostumes[0].name,
stageCostumes[0].name,
targetSounds.length > 0 ? targetSounds[0].name : ''
);
} catch {
return null;
+30 -4
View File
@@ -4,13 +4,14 @@ import {compose} from 'redux';
import {connect} from 'react-redux';
import ReactModal from 'react-modal';
import VM from 'scratch-vm';
import {injectIntl, intlShape} from 'react-intl';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import {
getIsError,
getIsShowingProject
} from '../reducers/project-state';
import {setProjectTitle} from '../reducers/project-title';
import {
activateTab,
BLOCKS_TAB_INDEX,
@@ -28,7 +29,6 @@ import {
import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
import LocalizationHOC from '../lib/localization-hoc.jsx';
import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
import TitledHOC from '../lib/titled-hoc.jsx';
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
import QueryParserHOC from '../lib/query-parser-hoc.jsx';
import storage from '../lib/storage';
@@ -39,9 +39,18 @@ import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx';
import GUIComponent from '../components/gui/gui.jsx';
import {setIsScratchDesktop} from '../lib/isScratchDesktop.js';
const messages = defineMessages({
defaultProjectTitle: {
id: 'gui.gui.defaultProjectTitle',
description: 'Default title for project',
defaultMessage: 'Scratch Project'
}
});
class GUI extends React.Component {
componentDidMount () {
setIsScratchDesktop(this.props.isScratchDesktop);
this.setReduxTitle(this.props.projectTitle);
this.props.onStorageInit(storage);
this.props.onVmInit(this.props.vm);
}
@@ -49,12 +58,24 @@ class GUI extends React.Component {
if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
this.props.onUpdateProjectId(this.props.projectId);
}
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') {
this.props.onUpdateReduxProjectTitle(
this.props.intl.formatMessage(messages.defaultProjectTitle)
);
} else {
this.props.onUpdateReduxProjectTitle(newTitle);
}
}
render () {
if (this.props.isError) {
throw new Error(
@@ -71,9 +92,11 @@ class GUI extends React.Component {
onProjectLoaded,
onStorageInit,
onUpdateProjectId,
onUpdateReduxProjectTitle,
onVmInit,
projectHost,
projectId,
projectTitle,
/* eslint-enable no-unused-vars */
children,
fetchingProject,
@@ -108,9 +131,12 @@ GUI.propTypes = {
onSeeCommunity: PropTypes.func,
onStorageInit: PropTypes.func,
onUpdateProjectId: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
onUpdateReduxProjectTitle: PropTypes.func,
onVmInit: PropTypes.func,
projectHost: PropTypes.string,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
projectTitle: PropTypes.string,
telemetryModalVisible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
@@ -160,7 +186,8 @@ const mapDispatchToProps = dispatch => ({
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal())
onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()),
onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title))
});
const ConnectedGUI = injectIntl(connect(
@@ -177,7 +204,6 @@ const WrappedGui = compose(
FontLoaderHOC,
QueryParserHOC,
ProjectFetcherHOC,
TitledHOC,
ProjectSaverHOC,
vmListenerHOC,
vmManagerHOC,
+8 -18
View File
@@ -91,21 +91,17 @@ class LibraryItem extends React.PureComponent {
const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length;
this.setState({iconIndex: nextIconIndex});
}
curIconMd5 () {
curIconSource () {
if (this.props.icons &&
this.state.isRotatingIcon &&
this.state.iconIndex < this.props.icons.length &&
this.props.icons[this.state.iconIndex] &&
this.props.icons[this.state.iconIndex].baseLayerMD5) {
return this.props.icons[this.state.iconIndex].baseLayerMD5;
this.props.icons[this.state.iconIndex]) {
return this.props.icons[this.state.iconIndex];
}
return this.props.iconMd5;
return this.props.iconSource;
}
render () {
const iconMd5 = this.curIconMd5();
const iconURL = iconMd5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` :
this.props.iconRawURL;
const iconSource = this.curIconSource();
return (
<LibraryItemComponent
bluetoothRequired={this.props.bluetoothRequired}
@@ -115,8 +111,7 @@ class LibraryItem extends React.PureComponent {
extensionId={this.props.extensionId}
featured={this.props.featured}
hidden={this.props.hidden}
iconURL={iconURL}
icons={this.props.icons}
iconSource={iconSource}
id={this.props.id}
insetIconURL={this.props.insetIconURL}
internetConnectionRequired={this.props.internetConnectionRequired}
@@ -147,13 +142,8 @@ LibraryItem.propTypes = {
extensionId: PropTypes.string,
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconMd5: PropTypes.string,
iconRawURL: PropTypes.string,
icons: PropTypes.arrayOf(
PropTypes.shape({
baseLayerMD5: PropTypes.string
})
),
iconSource: LibraryItemComponent.propTypes.iconSource, // single icon
icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons
id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
+4 -4
View File
@@ -44,10 +44,10 @@ class RecordingStep extends React.Component {
alert(this.props.intl.formatMessage(messages.alertMsg)); // eslint-disable-line no-alert
}
handleLevelUpdate (level) {
this.setState({
level: level,
levels: this.props.recording ? (this.state.levels || []).concat([level]) : this.state.levels
});
this.setState({level});
if (this.props.recording) {
this.setState({levels: (this.state.levels || []).concat([level])});
}
}
handleRecord () {
this.audioRecorder.startRecording();
+3 -5
View File
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import {setProjectTitle} from '../reducers/project-title';
import log from '../lib/log';
import sharedMessages from '../lib/shared-messages';
@@ -132,7 +131,7 @@ class SBFileUploader extends React.Component {
// This is necessary in case the user wants to reload a project
if (filename) {
const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
this.props.onReceivedProjectTitle(uploadedProjectTitle);
this.props.onUpdateProjectTitle(uploadedProjectTitle);
}
this.resetFileInput();
})
@@ -180,9 +179,9 @@ SBFileUploader.propTypes = {
loadingState: PropTypes.oneOf(LoadingStates),
onLoadingFinished: PropTypes.func,
onLoadingStarted: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectChanged: PropTypes.bool,
requestProjectUpload: PropTypes.func,
onReceivedProjectTitle: PropTypes.func,
userOwnsProject: PropTypes.bool,
vm: PropTypes.shape({
loadProject: PropTypes.func
@@ -210,8 +209,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
dispatch(closeFileMenu());
},
requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)),
onLoadingStarted: () => dispatch(openLoadingProject()),
onReceivedProjectTitle: title => dispatch(setProjectTitle(title))
onLoadingStarted: () => dispatch(openLoadingProject())
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
-12
View File
@@ -14,8 +14,6 @@ import log from '../lib/log.js';
const UNDO_STACK_SIZE = 99;
const MAX_RMS = 1.2;
class SoundEditor extends React.Component {
constructor (props) {
super(props);
@@ -267,15 +265,6 @@ class SoundEditor extends React.Component {
}
});
}
tooLoud () {
const numChunks = this.state.chunkLevels.length;
const startIndex = this.state.trimStart === null ?
0 : Math.floor(this.state.trimStart * numChunks);
const endIndex = this.state.trimEnd === null ?
numChunks - 1 : Math.ceil(this.state.trimEnd * numChunks);
const trimChunks = this.state.chunkLevels.slice(startIndex, endIndex);
return Math.max(...trimChunks) > MAX_RMS;
}
getUndoItem () {
return {
...this.copyCurrentBuffer(),
@@ -410,7 +399,6 @@ class SoundEditor extends React.Component {
name={this.props.name}
playhead={this.state.playhead}
setRef={this.setRef}
tooLoud={this.tooLoud()}
trimEnd={this.state.trimEnd}
trimStart={this.state.trimStart}
onChangeName={this.handleChangeName}
+2
View File
@@ -1,5 +1,6 @@
import GUI from './containers/gui.jsx';
import AppStateHOC from './lib/app-state-hoc.jsx';
import TitledHOC from './lib/titled-hoc.jsx';
import GuiReducer, {guiInitialState, guiMiddleware, initEmbedded, initFullScreen, initPlayer} from './reducers/gui';
import LocalesReducer, {localesInitialState, initLocale} from './reducers/locales';
import {ScratchPaintReducer} from 'scratch-paint';
@@ -16,6 +17,7 @@ const guiReducers = {
export {
GUI as default,
AppStateHOC,
TitledHOC,
setAppElement,
guiReducers,
guiInitialState,
+14 -14
View File
@@ -1,12 +1,12 @@
import 'get-float-time-domain-data';
import getUserMedia from 'get-user-media-promise';
import SharedAudioContext from './shared-audio-context.js';
import {computeRMS, computeChunkedRMS} from './audio-util.js';
import {computeRMS} from './audio-util.js';
class AudioRecorder {
constructor () {
this.audioContext = new SharedAudioContext();
this.bufferLength = 8192;
this.bufferLength = 1024;
this.userMediaStream = null;
this.mediaStreamSource = null;
@@ -51,7 +51,7 @@ class AudioRecorder {
this.userMediaStream = userMediaStream;
this.mediaStreamSource = this.audioContext.createMediaStreamSource(userMediaStream);
this.sourceNode = this.audioContext.createGain();
this.scriptProcessorNode = this.audioContext.createScriptProcessor(this.bufferLength, 1, 1);
this.scriptProcessorNode = this.audioContext.createScriptProcessor(this.bufferLength, 2, 2);
this.scriptProcessorNode.onaudioprocess = processEvent => {
if (this.recording && !this.disposed) {
@@ -68,9 +68,9 @@ class AudioRecorder {
const update = () => {
if (this.disposed) return;
requestAnimationFrame(update);
this.analyserNode.getFloatTimeDomainData(dataArray);
onUpdate(computeRMS(dataArray));
requestAnimationFrame(update);
};
requestAnimationFrame(update);
@@ -83,16 +83,7 @@ class AudioRecorder {
}
stop () {
const buffer = new Float32Array(this.buffers.length * this.bufferLength);
let offset = 0;
for (let i = 0; i < this.buffers.length; i++) {
const bufferChunk = this.buffers[i];
buffer.set(bufferChunk, offset);
offset += bufferChunk.length;
}
const chunkLevels = computeChunkedRMS(buffer);
const chunkLevels = this.buffers.map(buffer => computeRMS(buffer));
const maxRMS = Math.max.apply(null, chunkLevels);
const threshold = maxRMS / 8;
@@ -114,6 +105,15 @@ class AudioRecorder {
trimEnd = 1;
}
const buffer = new Float32Array(this.buffers.length * this.bufferLength);
let offset = 0;
for (let i = 0; i < this.buffers.length; i++) {
const bufferChunk = this.buffers[i];
buffer.set(bufferChunk, offset);
offset += bufferChunk.length;
}
return {
levels: chunkLevels,
samples: buffer,
+6 -11
View File
@@ -3,20 +3,15 @@ const jpegThumbnail = dataUrl => new Promise((resolve, reject) => {
image.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const maxDimension = 96; // 3x the maximum displayed size of 32px
if (image.height > image.width) {
canvas.height = maxDimension;
canvas.width = (maxDimension / image.height) * image.width;
} else {
canvas.width = maxDimension;
canvas.height = (maxDimension / image.width) * image.height;
}
// TODO we may want to draw to a smaller, fixed size to optimize file size
canvas.width = image.width;
canvas.height = image.height;
ctx.fillStyle = 'white'; // Create white background, since jpeg doesn't have transparency
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
// TODO we can play with the `quality` option here to optimize file size
resolve(canvas.toDataURL('image/jpeg', 0.92 /* quality */)); // Default quality is 0.92
};
image.onerror = err => {
+1 -1
View File
@@ -52,7 +52,7 @@
},
{
"name": "Basketball 1",
"md5": "ae21eac3d1814aee1d37ae82ea287816.svg",
"md5": "e494c4f44897d94e0541f7036a302449.svg",
"type": "backdrop",
"tags": [
"sports",
+4 -4
View File
@@ -2563,7 +2563,7 @@
},
{
"name": "Dinosaur1-c",
"md5": "78f7e20f0e187fdca81195e4cc134175.svg",
"md5": "a2bb28d5ba55ba997bec63772437cecf.svg",
"type": "costume",
"tags": [
"animals",
@@ -10493,7 +10493,7 @@
},
{
"name": "Watermelon-a",
"md5": "8736ecc2524895733534c888cd91fa1f.svg",
"md5": "26fecef75cf3b6e0d98bff5c06475036.svg",
"type": "costume",
"tags": [
"food",
@@ -10511,7 +10511,7 @@
},
{
"name": "Watermelon-b",
"md5": "1ed1c8b78eae2ee7422074d7f883031d.svg",
"md5": "fbdaf4d1d349edd3ddf3a1c4528aa9ec.svg",
"type": "costume",
"tags": [
"food",
@@ -10528,7 +10528,7 @@
},
{
"name": "Watermelon-c",
"md5": "677738282686d2dcce35d731c3ddc043.svg",
"md5": "5976c10412306fc093c1d1930fa05119.svg",
"type": "costume",
"tags": [
"food",
+14 -68
View File
@@ -119,7 +119,7 @@ import pongResetScore from './steps/pong-reset-score.en.png';
import pongAddLineSprite from './steps/pong-add-line.en.gif';
import pongGameOver from './steps/pong-game-over.en.png';
// Imagine a World
// Imagine If
import imagineTypeWhatYouWant from './steps/imagine-type-what-you-want.en.png';
import imagineClickGreenFlag from './steps/imagine-click-green-flag.en.png';
import imagineChooseBackdrop from './steps/imagine-choose-backdrop.en.png';
@@ -170,34 +170,6 @@ import moveArrowKeysUpDown from './steps/move-arrow-keys-up-down.en.png';
import glideAroundBackAndForth from './steps/glide-around-back-and-forth.en.png';
import glideAroundPoint from './steps/glide-around-point.en.png';
// Code a Cartoon
import codeCartoonSaySomething from './steps/code-cartoon-01-say-something.en.png';
import codeCartoonAnimate from './steps/code-cartoon-02-animate.en.gif';
import codeCartoonSelectDifferentCharacter from './steps/code-cartoon-03-select-different-character.en.gif';
import codeCartoonUseMinusSign from './steps/code-cartoon-04-use-minus-sign.en.gif';
import codeCartoonGrowShrink from './steps/code-cartoon-05-grow-shrink.en.gif';
import codeCartoonSelectDifferentCharacter2 from './steps/code-cartoon-06-select-another-different-character.en.gif';
import codeCartoonJump from './steps/code-cartoon-07-jump.en.gif';
import codeCartoonChangeScenes from './steps/code-cartoon-08-change-scenes.en.gif';
import codeCartoonGlideAround from './steps/code-cartoon-09-glide-around.en.gif';
import codeCartoonChangeCostumes from './steps/code-cartoon-10-change-costumes.en.gif';
import codeCartoonChooseMoreCharacters from './steps/code-cartoon-11-choose-more-characters.en.jpg';
// Talking Tales
import talesAddExtension from './steps/talking-1-add-extension.en.gif';
import talesChooseSprite from './steps/talking-2-choose-sprite.en.png';
import talesSaySomething from './steps/talking-3-say-something.en.gif';
import talesChooseBackdrop from './steps/talking-4-choose-backdrop.en.png';
import talesSwitchBackdrop from './steps/talking-5-switch-backdrop.en.gif';
import talesChooseAnotherSprite from './steps/talking-6-choose-another-sprite.en.png';
import talesMoveAround from './steps/talking-7-move-around.en.gif';
import talesChooseAnotherBackdrop from './steps/talking-8-choose-another-backdrop.en.png';
import talesAnimateTalking from './steps/talking-9-animate.en.gif';
import talesChooseThirdBackdrop from './steps/talking-10-choose-third-backdrop.en.png';
import talesChooseSound from './steps/talking-11-choose-sound.en.gif';
import talesDanceMoves from './steps/talking-12-dance-moves.en.gif';
import talesAskAnswer from './steps/talking-13-ask-and-answer.en.gif';
const enImages = {
// Intro
introMove: introMove,
@@ -304,7 +276,7 @@ const enImages = {
flyAddScenery: flyAddScenery,
flyMoveScenery: flyMoveScenery,
flySwitchLooks: flySwitchLooks,
// Pong
pongAddBackdrop: pongAddBackdrop,
pongAddBallSprite: pongAddBallSprite,
@@ -319,8 +291,8 @@ const enImages = {
pongResetScore: pongResetScore,
pongAddLineSprite: pongAddLineSprite,
pongGameOver: pongGameOver,
// Imagine a World
// Imagine If
imagineTypeWhatYouWant: imagineTypeWhatYouWant,
imagineClickGreenFlag: imagineClickGreenFlag,
imagineChooseBackdrop: imagineChooseBackdrop,
@@ -336,26 +308,26 @@ const enImages = {
imagineSwitchBackdrops: imagineSwitchBackdrops,
imagineRecordASound: imagineRecordASound,
imagineChooseSound: imagineChooseSound,
// Add a Backdrop
addBackdrop: addBackdrop,
// Add Effects
addEffects: addEffects,
// Hide and Show
hideAndShow: hideAndShow,
// Switch Costumes
switchCostumes: switchCostumes,
// Change Size
changeSize: changeSize,
// Spin
spinTurn: spinTurn,
spinPointInDirection: spinPointInDirection,
// Record a Sound
recordASoundSoundsTab: recordASoundSoundsTab,
recordASoundClickRecord: recordASoundClickRecord,
@@ -369,35 +341,9 @@ const enImages = {
// Glide Around
glideAroundBackAndForth: glideAroundBackAndForth,
glideAroundPoint: glideAroundPoint,
// Code a Cartoon
codeCartoonSaySomething: codeCartoonSaySomething,
codeCartoonAnimate: codeCartoonAnimate,
codeCartoonSelectDifferentCharacter: codeCartoonSelectDifferentCharacter,
codeCartoonUseMinusSign: codeCartoonUseMinusSign,
codeCartoonGrowShrink: codeCartoonGrowShrink,
codeCartoonSelectDifferentCharacter2: codeCartoonSelectDifferentCharacter2,
codeCartoonJump: codeCartoonJump,
codeCartoonChangeScenes: codeCartoonChangeScenes,
codeCartoonGlideAround: codeCartoonGlideAround,
codeCartoonChangeCostumes: codeCartoonChangeCostumes,
codeCartoonChooseMoreCharacters: codeCartoonChooseMoreCharacters,
// Talking Tales
talesAddExtension: talesAddExtension,
talesChooseSprite: talesChooseSprite,
talesSaySomething: talesSaySomething,
talesAskAnswer: talesAskAnswer,
talesChooseBackdrop: talesChooseBackdrop,
talesSwitchBackdrop: talesSwitchBackdrop,
talesChooseAnotherSprite: talesChooseAnotherSprite,
talesMoveAround: talesMoveAround,
talesChooseAnotherBackdrop: talesChooseAnotherBackdrop,
talesAnimateTalking: talesAnimateTalking,
talesChooseThirdBackdrop: talesChooseThirdBackdrop,
talesChooseSound: talesChooseSound,
talesDanceMoves: talesDanceMoves
glideAroundPoint: glideAroundPoint
};
export {enImages};
+173 -440
View File
@@ -42,14 +42,8 @@ import libraryMakeFly from './thumbnails/make-it-fly.jpg';
// Pong
import libraryPong from './thumbnails/pong.jpg';
// Imagine a World
import libraryImagine from './thumbnails/imagine.jpg';
// Code a Cartoon
import libraryCodeCartoon from './thumbnails/code-a-cartoon.jpg';
// Talking Tales
import libraryTalking from './thumbnails/talking.png';
// Imagine If
import libraryImagineIf from './thumbnails/imagine-if.jpg';
// Videos
import recordASound from './thumbnails/record-a-sound.jpg';
@@ -179,25 +173,25 @@ export default {
],
urlId: 'name'
},
'imagine': {
'imagine-if': {
name: (
<FormattedMessage
defaultMessage="Imagine a World"
description="Name for the 'Imagine a World' how-to"
id="gui.howtos.imagine"
defaultMessage="Imagine If"
description="Name for the 'Imagine If' how-to"
id="gui.howtos.imagine-if"
/>
),
tags: ['imagine'],
img: libraryImagine,
tags: ['imagine-if'],
img: libraryImagineIf,
steps: [{
video: 'imagine'
video: 'imagine-if'
}, {
title: (
<FormattedMessage
defaultMessage="Type What You Want to Say"
description="Step name for 'Type What You Want to Say' step"
id="gui.howtos.imagine.step_imagineTypeWhatYouWant"
id="gui.howtos.imagine-if.step_imagineTypeWhatYouWant"
/>
),
image: 'imagineTypeWhatYouWant'
@@ -206,7 +200,7 @@ export default {
<FormattedMessage
defaultMessage="Click the Green Flag to Start"
description="Step name for 'Click the Green Flag to Start' step"
id="gui.howtos.imagine.step_imagineClickGreenFlag"
id="gui.howtos.imagine-if.step_imagineClickGreenFlag"
/>
),
image: 'imagineClickGreenFlag'
@@ -215,7 +209,7 @@ export default {
<FormattedMessage
defaultMessage="Choose Any Backdrop"
description="Step name for 'Choose Any Backdrop' step"
id="gui.howtos.imagine.step_imagineChooseBackdrop"
id="gui.howtos.imagine-if.step_imagineChooseBackdrop"
/>
),
image: 'imagineChooseBackdrop'
@@ -224,7 +218,7 @@ export default {
<FormattedMessage
defaultMessage="Choose Any Sprite"
description="Step name for 'Choose Any Sprite' step"
id="gui.howtos.imagine.step_imagineChooseSprite"
id="gui.howtos.imagine-if.step_imagineChooseSprite"
/>
),
image: 'imagineChooseSprite'
@@ -233,7 +227,7 @@ export default {
<FormattedMessage
defaultMessage="Press the Space Key to Glide"
description="Step name for 'Press the Space Key to Glide' step"
id="gui.howtos.imagine.step_imagineFlyAround"
id="gui.howtos.imagine-if.step_imagineFlyAround"
/>
),
image: 'imagineFlyAround'
@@ -242,7 +236,7 @@ export default {
<FormattedMessage
defaultMessage="Choose Another Sprite"
description="Step name for 'Choose Another Sprite' step"
id="gui.howtos.imagine.step_imagineChooseAnotherSprite"
id="gui.howtos.imagine-if.step_imagineChooseAnotherSprite"
/>
),
image: 'imagineChooseAnotherSprite'
@@ -251,7 +245,7 @@ export default {
<FormattedMessage
defaultMessage="Move Left-Right"
description="Step name for 'Move Left-Right' step"
id="gui.howtos.imagine.step_imagineLeftRight"
id="gui.howtos.imagine-if.step_imagineLeftRight"
/>
),
image: 'imagineLeftRight'
@@ -260,7 +254,7 @@ export default {
<FormattedMessage
defaultMessage="Move Up-Down"
description="Step name for 'Move Up-Down' step"
id="gui.howtos.imagine.step_imagineUpDown"
id="gui.howtos.imagine-if.step_imagineUpDown"
/>
),
image: 'imagineUpDown'
@@ -269,7 +263,7 @@ export default {
<FormattedMessage
defaultMessage="Change Costumes"
description="Step name for 'Change Costumes' step"
id="gui.howtos.imagine.step_imagineChangeCostumes"
id="gui.howtos.imagine-if.step_imagineChangeCostumes"
/>
),
image: 'imagineChangeCostumes'
@@ -278,7 +272,7 @@ export default {
<FormattedMessage
defaultMessage="Glide to a Point"
description="Step name for 'Glide to a Point' step"
id="gui.howtos.imagine.step_imagineGlideToPoint"
id="gui.howtos.imagine-if.step_imagineGlideToPoint"
/>
),
image: 'imagineGlideToPoint'
@@ -287,7 +281,7 @@ export default {
<FormattedMessage
defaultMessage="Grow and Shrink"
description="Step name for 'Grow and Shrink' step"
id="gui.howtos.imagine.step_imagineGrowShrink"
id="gui.howtos.imagine-if.step_imagineGrowShrink"
/>
),
image: 'imagineGrowShrink'
@@ -296,7 +290,7 @@ export default {
<FormattedMessage
defaultMessage="Choose Another Backdrop"
description="Step name for 'Choose Another Backdrop' step"
id="gui.howtos.imagine.step_imagineChooseAnotherBackdrop"
id="gui.howtos.imagine-if.step_imagineChooseAnotherBackdrop"
/>
),
image: 'imagineChooseAnotherBackdrop'
@@ -305,7 +299,7 @@ export default {
<FormattedMessage
defaultMessage="Switch Backdrops"
description="Step name for 'Switch Backdrops' step"
id="gui.howtos.imagine.step_imagineSwitchBackdrops"
id="gui.howtos.imagine-if.step_imagineSwitchBackdrops"
/>
),
image: 'imagineSwitchBackdrops'
@@ -314,7 +308,7 @@ export default {
<FormattedMessage
defaultMessage="Add a Sound"
description="Step name for 'Add a Sound' step"
id="gui.howtos.imagine.step_imagineRecordASound"
id="gui.howtos.imagine-if.step_imagineRecordASound"
/>
),
image: 'imagineRecordASound'
@@ -323,7 +317,7 @@ export default {
<FormattedMessage
defaultMessage="Choose Your Sound"
description="Step name for 'Choose Your Sound' step"
id="gui.howtos.imagine.step_imagineChooseSound"
id="gui.howtos.imagine-if.step_imagineChooseSound"
/>
),
image: 'imagineChooseSound'
@@ -334,9 +328,9 @@ export default {
]
}
],
urlId: 'imagine'
urlId: 'imagine-if'
},
'Make-Music': {
name: (
<FormattedMessage
@@ -406,7 +400,7 @@ export default {
],
urlId: 'music'
},
'Tell-A-Story': {
name: (
<FormattedMessage
@@ -519,7 +513,7 @@ export default {
],
urlId: 'tell-a-story'
},
'Chase-Game': {
name: (
<FormattedMessage
@@ -717,7 +711,7 @@ export default {
],
urlId: 'animate-a-character'
},
'Make-A-Game': {
name: (
<FormattedMessage
@@ -804,7 +798,7 @@ export default {
],
urlId: 'clicker-game'
},
'make-it-fly': {
name: (
<FormattedMessage
@@ -934,7 +928,7 @@ export default {
],
urlId: 'make-it-fly'
},
'pong': {
name: (
<FormattedMessage
@@ -1075,287 +1069,6 @@ export default {
urlId: 'pong'
},
'code-cartoon': {
name: (
<FormattedMessage
defaultMessage="Code a Cartoon"
description="Name for the 'Code a Cartoon' how-to"
id="gui.howtos.code-cartoon"
/>
),
tags: ['code-cartoon'],
requiredProjectId: '331474033',
img: libraryCodeCartoon,
steps: [{
video: 'code-cartoon'
}, {
title: (
<FormattedMessage
defaultMessage="Say Something When You Click the Green Flag"
description="Step name for 'Say Something When You Click the Green Flag' step"
id="gui.howtos.code-cartoon.step_codeCartoonSaySomething"
/>
),
image: 'codeCartoonSaySomething'
}, {
title: (
<FormattedMessage
defaultMessage="Animate a Character When You Click It"
description="Step name for 'Animate a Character When You Click It' step"
id="gui.howtos.code-cartoon.step_codeCartoonAnimate"
/>
),
image: 'codeCartoonAnimate'
}, {
title: (
<FormattedMessage
defaultMessage="Select a Different Character"
description="Step name for 'Select a Different Character' step"
id="gui.howtos.code-cartoon.step_codeCartoonSelectDifferentCharacter"
/>
),
image: 'codeCartoonSelectDifferentCharacter'
}, {
title: (
<FormattedMessage
defaultMessage="Use a Minus Sign to Get Smaller"
description="Step name for 'Use a Minus Sign to Get Smaller' step"
id="gui.howtos.code-cartoon.step_codeCartoonUseMinusSign"
/>
),
image: 'codeCartoonUseMinusSign'
}, {
title: (
<FormattedMessage
defaultMessage="Make a Character Grow and Shrink"
description="Step name for 'Make a Character Grow and Shrink' step"
id="gui.howtos.code-cartoon.step_codeCartoonGrowShrink"
/>
),
image: 'codeCartoonGrowShrink'
}, {
title: (
<FormattedMessage
defaultMessage="Select a Different Character"
description="Step name for 'Select a Different Character' step"
id="gui.howtos.code-cartoon.step_codeCartoonSelectDifferentCharacter2"
/>
),
image: 'codeCartoonSelectDifferentCharacter2'
}, {
title: (
<FormattedMessage
defaultMessage="Jump Up and Down"
description="Step name for 'Jump Up and Down' step"
id="gui.howtos.code-cartoon.step_codeCartoonJump"
/>
),
image: 'codeCartoonJump'
}, {
title: (
<FormattedMessage
defaultMessage="Click a Character to Change Scenes"
description="Step name for 'Click a Character to Change Scenes' step"
id="gui.howtos.code-cartoon.step_codeCartoonChangeScenes"
/>
),
image: 'codeCartoonChangeScenes'
}, {
title: (
<FormattedMessage
defaultMessage="Glide Around"
description="Step name for 'Glide Around' step"
id="gui.howtos.code-cartoon.step_codeCartoonGlideAround"
/>
),
image: 'codeCartoonGlideAround'
}, {
title: (
<FormattedMessage
defaultMessage="Change Costumes"
description="Step name for 'Change Costumes' step"
id="gui.howtos.code-cartoon.step_codeCartoonChangeCostumes"
/>
),
image: 'codeCartoonChangeCostumes'
}, {
title: (
<FormattedMessage
defaultMessage="Choose More Characters to Add to Your Cartoon"
description="Step name for 'Choose More Characters to Add to Your Cartoon' step"
id="gui.howtos.code-cartoon.step_codeCartoonChooseMoreCharacters"
/>
),
image: 'codeCartoonChooseMoreCharacters'
}, {
deckIds: [
'Chase-Game',
'Tell-A-Story'
]
}
],
urlId: 'code-cartoon'
},
'cartoon-network': {
name: (
<FormattedMessage
defaultMessage="Animate an Adventure Game"
description="Animate an Adventure Game' how-to"
id="gui.howtos.cartoon-network"
/>
),
requiredProjectId: '249143200',
img: libraryCartoonNetwork,
steps: [{
video: 'uz5oz5h9yg',
trackingPixel: (
<img src="https://code.org/api/hour/begin_scratch_adventure.png" />
)
}, {
title: (
<FormattedMessage
defaultMessage="Choose a Character to Show"
description="Step name for 'Choose a Character to Show' step"
id="gui.howtos.cartoon-network.step_CNcharacter"
/>
),
image: 'cnShowCharacter'
}, {
title: (
<FormattedMessage
defaultMessage="Say Something"
description="Step name for 'Say Something' step"
id="gui.howtos.cartoon-network.step_CNsay"
/>
),
image: 'cnSay'
}, {
title: (
<FormattedMessage
defaultMessage="Glide Around"
description="Step name for 'Glide Around' step"
id="gui.howtos.cartoon-network.step_CNglide"
/>
),
image: 'cnGlide'
}, {
title: (
<FormattedMessage
defaultMessage=" Choose an Object to Chase "
description="Step name for 'Choose an Object to Chase' step"
id="gui.howtos.cartoon-network.step_CNpicksprite"
/>
),
image: 'cnPickSprite'
}, {
title: (
<FormattedMessage
defaultMessage="Collect Objects"
description="Step name for 'Collect Objects' step"
id="gui.howtos.cartoon-network.step_CNcollect"
/>
),
image: 'cnCollect'
}, {
title: (
<FormattedMessage
defaultMessage="Make a Score Variable"
description="Step name for 'Make a Score Variable' step"
id="gui.howtos.cartoon-network.step_CNvariable"
/>
),
image: 'cnVariable'
}, {
title: (
<FormattedMessage
defaultMessage="Keep Score"
description="Step name for 'Keep Score' step"
id="gui.howtos.cartoon-network.step_CNscore"
/>
),
image: 'cnScore'
}, {
title: (
<FormattedMessage
defaultMessage="Level Up: Change Backdrop"
description="Step name for 'Level Up: Change Backdrop' step"
id="gui.howtos.cartoon-network.step_CNbackdrop"
/>
),
image: 'cnBackdrop'
},
{
video: '6o76f5ivo1'
},
{
deckIds: [
'switch-costume',
'add-effects'
]
}
],
urlId: 'animate-an-adventure-game'
},
'Video-Sensing': {
name: (
<FormattedMessage
defaultMessage="Video Sensing"
description="Name for the 'Video Sensing' how-to"
id="gui.howtos.videosens.name"
/>
),
img: libraryVideoSens,
steps: [{
video: '3pd1z110d6'
},
{
title: (
<FormattedMessage
defaultMessage="Add Extension"
description="Step name for 'Add Extension' step"
id="gui.howtos.videosens.step_addextension"
/>
),
image: 'videoAddExtension'
}, {
title: (
<FormattedMessage
defaultMessage="Pet the Cat"
description="Step name for 'Pet the Cat' step"
id="gui.howtos.videosens.step_pet"
/>
),
image: 'videoPet'
}, {
title: (
<FormattedMessage
defaultMessage="Animate"
description="Step name for 'Animate' step"
id="gui.howtos.videosens.step_animate"
/>
),
image: 'videoAnimate'
}, {
title: (
<FormattedMessage
defaultMessage="Pop a Balloon"
description="Step name for 'Pop a Balloon' step"
id="gui.howtos.videosens.step_pop"
/>
),
image: 'videoPop'
}, {
deckIds: [
'Make-Music',
'add-effects'
]
}
],
urlId: 'video-sensing'
},
'say-it-out-loud': {
name: (
<FormattedMessage
@@ -1470,143 +1183,163 @@ export default {
urlId: 'animations-that-talk'
},
'talking': {
'Video-Sensing': {
name: (
<FormattedMessage
defaultMessage="Talking Tales"
description="Name for the 'Talking Tales' how-to"
id="gui.howtos.talking"
defaultMessage="Video Sensing"
description="Name for the 'Video Sensing' how-to"
id="gui.howtos.videosens.name"
/>
),
tags: ['talking'],
img: libraryTalking,
img: libraryVideoSens,
steps: [{
video: 'talking'
video: '3pd1z110d6'
},
{
title: (
<FormattedMessage
defaultMessage="Add Extension"
description="Step name for 'Add Extension' step"
id="gui.howtos.videosens.step_addextension"
/>
),
image: 'videoAddExtension'
}, {
title: (
<FormattedMessage
defaultMessage="Click to Add the Text-to-Speech Blocks"
description="Step name for 'Click to Add the Text-to-Speech Blocks' step"
id="gui.howtos.talking.step_talesAddExtension"
defaultMessage="Pet the Cat"
description="Step name for 'Pet the Cat' step"
id="gui.howtos.videosens.step_pet"
/>
),
image: 'talesAddExtension'
image: 'videoPet'
}, {
title: (
<FormattedMessage
defaultMessage="Choose a Sprite"
description="Step name for 'Choose a Sprite' step"
id="gui.howtos.talking.step_talesChooseSprite"
defaultMessage="Animate"
description="Step name for 'Animate' step"
id="gui.howtos.videosens.step_animate"
/>
),
image: 'talesChooseSprite'
image: 'videoAnimate'
}, {
title: (
<FormattedMessage
defaultMessage="Make a Character Speak"
description="Step name for 'Make a Character Speak' step"
id="gui.howtos.talking.step_talesSaySomething"
defaultMessage="Pop a Balloon"
description="Step name for 'Pop a Balloon' step"
id="gui.howtos.videosens.step_pop"
/>
),
image: 'talesSaySomething'
}, {
title: (
<FormattedMessage
defaultMessage="Choose a Backdrop"
description="Step name for 'Choose a Backdrop' step"
id="gui.howtos.talking.step_talesChooseBackdrop"
/>
),
image: 'talesChooseBackdrop'
}, {
title: (
<FormattedMessage
defaultMessage="Click a Character to Go to the Next Backdrop"
description="Step name for 'Click a Character to Go to the Next Backdrop' step"
id="gui.howtos.talking.step_talesSwitchBackdrop"
/>
),
image: 'talesSwitchBackdrop'
}, {
title: (
<FormattedMessage
defaultMessage="Choose Another Sprite"
description="Step name for 'Choose Another Sprite' step"
id="gui.howtos.talking.step_talesChooseAnotherSprite"
/>
),
image: 'talesChooseAnotherSprite'
}, {
title: (
<FormattedMessage
defaultMessage="Move Around"
description="Step name for 'Move Around' step"
id="gui.howtos.talking.step_talesMoveAround"
/>
),
image: 'talesMoveAround'
}, {
title: (
<FormattedMessage
defaultMessage="Choose Another Backdrop"
description="Step name for 'Choose Another Backdrop' step"
id="gui.howtos.talking.step_talesChooseAnotherBackdrop"
/>
),
image: 'talesChooseAnotherBackdrop'
}, {
title: (
<FormattedMessage
defaultMessage="Animate Talking"
description="Step name for 'Animate Talking' step"
id="gui.howtos.talking.step_talesAnimateTalking"
/>
),
image: 'talesAnimateTalking'
}, {
title: (
<FormattedMessage
defaultMessage="Choose Another Backdrop"
description="Step name for 'Choose Another Backdrop' step"
id="gui.howtos.talking.step_talesChooseThirdBackdrop"
/>
),
image: 'talesChooseThirdBackdrop'
}, {
title: (
<FormattedMessage
defaultMessage="Choose a Song to Dance To"
description="Step name for 'Choose a Song to Dance To' step"
id="gui.howtos.talking.step_talesChooseSound"
/>
),
image: 'talesChooseSound'
}, {
title: (
<FormattedMessage
defaultMessage="Dance Moves"
description="Step name for 'Dance Moves' step"
id="gui.howtos.talking.step_talesDanceMoves"
/>
),
image: 'talesDanceMoves'
}, {
title: (
<FormattedMessage
defaultMessage="Get the Ask and Answer Blocks from the Sensing Category"
description="Step name for 'Get the Ask and Answer Blocks from the Sensing Category' step"
id="gui.howtos.talking.step_talesAskAnswer"
/>
),
image: 'talesAskAnswer'
image: 'videoPop'
}, {
deckIds: [
'Tell-A-Story',
'Animate-A-Character'
'Make-Music',
'add-effects'
]
}
],
urlId: 'talking'
urlId: 'video-sensing'
},
'cartoon-network': {
name: (
<FormattedMessage
defaultMessage="Animate an Adventure Game"
description="Animate an Adventure Game' how-to"
id="gui.howtos.cartoon-network"
/>
),
requiredProjectId: '249143200',
img: libraryCartoonNetwork,
steps: [{
video: 'uz5oz5h9yg',
trackingPixel: (
<img src="https://code.org/api/hour/begin_scratch_adventure.png" />
)
}, {
title: (
<FormattedMessage
defaultMessage="Choose a Character to Show"
description="Step name for 'Choose a Character to Show' step"
id="gui.howtos.cartoon-network.step_CNcharacter"
/>
),
image: 'cnShowCharacter'
}, {
title: (
<FormattedMessage
defaultMessage="Say Something"
description="Step name for 'Say Something' step"
id="gui.howtos.cartoon-network.step_CNsay"
/>
),
image: 'cnSay'
}, {
title: (
<FormattedMessage
defaultMessage="Glide Around"
description="Step name for 'Glide Around' step"
id="gui.howtos.cartoon-network.step_CNglide"
/>
),
image: 'cnGlide'
}, {
title: (
<FormattedMessage
defaultMessage=" Choose an Object to Chase "
description="Step name for 'Choose an Object to Chase' step"
id="gui.howtos.cartoon-network.step_CNpicksprite"
/>
),
image: 'cnPickSprite'
}, {
title: (
<FormattedMessage
defaultMessage="Collect Objects"
description="Step name for 'Collect Objects' step"
id="gui.howtos.cartoon-network.step_CNcollect"
/>
),
image: 'cnCollect'
}, {
title: (
<FormattedMessage
defaultMessage="Make a Score Variable"
description="Step name for 'Make a Score Variable' step"
id="gui.howtos.cartoon-network.step_CNvariable"
/>
),
image: 'cnVariable'
}, {
title: (
<FormattedMessage
defaultMessage="Keep Score"
description="Step name for 'Keep Score' step"
id="gui.howtos.cartoon-network.step_CNscore"
/>
),
image: 'cnScore'
}, {
title: (
<FormattedMessage
defaultMessage="Level Up: Change Backdrop"
description="Step name for 'Level Up: Change Backdrop' step"
id="gui.howtos.cartoon-network.step_CNbackdrop"
/>
),
image: 'cnBackdrop'
},
{
video: '6o76f5ivo1'
},
{
deckIds: [
'switch-costume',
'add-effects'
]
}
],
urlId: 'animate-an-adventure-game'
},
'add-sprite': {
@@ -1639,7 +1372,7 @@ export default {
],
urlId: 'add-a-sprite'
},
'add-a-backdrop': {
name: (
<FormattedMessage
@@ -1671,7 +1404,7 @@ export default {
}],
urlId: 'add-a-backdrop'
},
'change-size': {
name: (
<FormattedMessage
@@ -1701,7 +1434,7 @@ export default {
}],
urlId: 'change-size'
},
'glide-around': {
name: (
<FormattedMessage
@@ -1806,7 +1539,7 @@ export default {
}],
urlId: 'record-a-sound'
},
'spin-video': {
name: (
<FormattedMessage
@@ -1845,7 +1578,7 @@ export default {
}],
urlId: 'make-it-spin'
},
'hide-and-show': {
name: (
<FormattedMessage
@@ -1944,7 +1677,7 @@ export default {
}],
urlId: 'arrow-keys'
},
'add-effects': {
name: (
<FormattedMessage
@@ -1975,7 +1708,7 @@ export default {
}],
urlId: 'add-effects'
},
'wedo2-getting-started': {
steps: [{
video: '4im7iizv47'
@@ -1983,7 +1716,7 @@ export default {
urlId: 'wedo',
hidden: true
},
'ev3-getting-started': {
steps: [{
video: 'qgu78c5y7d'
@@ -1991,7 +1724,7 @@ export default {
urlId: 'ev3',
hidden: true
},
'whats-new': {
steps: [{
video: 'mtqymxg0qq'
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 913 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

+14 -20
View File
@@ -9,7 +9,7 @@ const videos = {
'en': 'rpjvs3v9gj',
'it': '2im6c1f9kn',
'pt': 'ngdfp8xg4x',
'pt-br': 'ngdfp8xg4x',
'pt-BR': 'ngdfp8xg4x',
'ja': 'v2c2f3y2sc',
'ja-Hira': 'v2c2f3y2sc'
},
@@ -17,7 +17,7 @@ const videos = {
'en': 'pyur30ho05',
'it': 'c8zcrk6w09',
'pt': 't49tfrukyf',
'pt-br': 't49tfrukyf',
'pt-BR': 't49tfrukyf',
'ja': 'xrmgno02th',
'ja-Hira': 'xrmgno02th'
},
@@ -25,7 +25,7 @@ const videos = {
'en': 'ir0j8ljsgm',
'it': '8nm1v1m9zx',
'pt': '9v4zl8504z',
'pt-br': '9v4zl8504z',
'pt-BR': '9v4zl8504z',
'ja': 'iybhyobckr',
'ja-Hira': 'iybhyobckr'
},
@@ -33,7 +33,7 @@ const videos = {
'en': '5rp47ys13g',
'it': 'vrs1e38944',
'pt': 'rjst5ng61s',
'pt-br': 'rjst5ng61s',
'pt-BR': 'rjst5ng61s',
'ja': 'h4q2ea3ojw',
'ja-Hira': 'h4q2ea3ojw'
},
@@ -41,7 +41,7 @@ const videos = {
'en': 'kusyx9thl5',
'it': '899b2jmjlu',
'pt': 'rw6kr9e37n',
'pt-br': 'rw6kr9e37n',
'pt-BR': 'rw6kr9e37n',
'ja': 'wr6ysk8s6d',
'ja-Hira': 'wr6ysk8s6d'
},
@@ -49,7 +49,7 @@ const videos = {
'en': 'nict6zdzlx',
'it': 'vrrfpm5grh',
'pt': 'wq7sm038pq',
'pt-br': 'wq7sm038pq',
'pt-BR': 'wq7sm038pq',
'ja': 'j382ohngg7',
'ja-Hira': 'j382ohngg7'
},
@@ -57,7 +57,7 @@ const videos = {
'en': 'p8va85hh61',
'it': '1xb1jztsvr',
'pt': '1ad1ip3ly7',
'pt-br': '1ad1ip3ly7',
'pt-BR': '1ad1ip3ly7',
'ja': 'uzkfi9uai0',
'ja-Hira': 'uzkfi9uai0'
},
@@ -71,7 +71,7 @@ const videos = {
'en': 'ulzl1fbzny',
'it': 'ncr9lqk7bt',
'pt': 'aavagpvh5w',
'pt-br': 'aavagpvh5w',
'pt-BR': 'aavagpvh5w',
'ja': '5zmy6u614k',
'ja-Hira': '5zmy6u614k'
},
@@ -79,7 +79,7 @@ const videos = {
'en': '07fed5hhpv',
'it': 'obmrphhobt',
'pt': 'jxqksu6zcw',
'pt-br': 'jxqksu6zcw',
'pt-BR': 'jxqksu6zcw',
'ja': 'hjhc0a8r7j',
'ja-Hira': 'hjhc0a8r7j'
},
@@ -87,7 +87,7 @@ const videos = {
'en': 'g479ahobo9',
'it': 'v0vbx3l5uk',
'pt': 'ibtmwb58y8',
'pt-br': 'ibtmwb58y8',
'pt-BR': 'ibtmwb58y8',
'ja': 'ruynguvag5',
'ja-Hira': 'ruynguvag5'
},
@@ -95,7 +95,7 @@ const videos = {
'en': '1ocp6a1ejn',
'it': 'nty4xjtqcj',
'pt': 'pnlmmv2hs7',
'pt-br': 'pnlmmv2hs7',
'pt-BR': 'pnlmmv2hs7',
'ja': 'g3vluv0ms8',
'ja-Hira': 'g3vluv0ms8'
},
@@ -103,7 +103,7 @@ const videos = {
'en': 'yetrmk4iuu',
'it': 'jse0g0uddw',
'pt': 'erv6eff78p',
'pt-br': 'erv6eff78p',
'pt-BR': 'erv6eff78p',
'ja': 'fk34dcg5oz',
'ja-Hira': 'fk34dcg5oz'
},
@@ -111,7 +111,7 @@ const videos = {
'en': '3jvl8zgjo2',
'it': 'q3cjjyfju9',
'pt': 'dz6jzpf7hm',
'pt-br': 'dz6jzpf7hm',
'pt-BR': 'dz6jzpf7hm',
'ja': 'q7zq9vu0wb',
'ja-Hira': 'q7zq9vu0wb'
},
@@ -121,14 +121,8 @@ const videos = {
'pong-game': {
en: '8m48dv0ens'
},
'imagine': {
'imagine-if': {
en: '1ndh08yiso'
},
'code-cartoon': {
en: 'fpfuky3x6g'
},
'talking': {
en: 'j0208mq4qi'
}
};
+5 -5
View File
@@ -3845,7 +3845,7 @@
{
"costumeName": "dinosaur1-c",
"baseLayerID": -1,
"baseLayerMD5": "78f7e20f0e187fdca81195e4cc134175.svg",
"baseLayerMD5": "a2bb28d5ba55ba997bec63772437cecf.svg",
"bitmapResolution": 1,
"rotationCenterX": 81,
"rotationCenterY": 91
@@ -15612,7 +15612,7 @@
},
{
"name": "Watermelon",
"md5": "8736ecc2524895733534c888cd91fa1f.svg",
"md5": "26fecef75cf3b6e0d98bff5c06475036.svg",
"type": "sprite",
"tags": [
"food",
@@ -15643,7 +15643,7 @@
{
"costumeName": "watermelon-a",
"baseLayerID": -1,
"baseLayerMD5": "8736ecc2524895733534c888cd91fa1f.svg",
"baseLayerMD5": "26fecef75cf3b6e0d98bff5c06475036.svg",
"bitmapResolution": 1,
"rotationCenterX": 40,
"rotationCenterY": 27
@@ -15651,7 +15651,7 @@
{
"costumeName": "watermelon-b",
"baseLayerID": -1,
"baseLayerMD5": "1ed1c8b78eae2ee7422074d7f883031d.svg",
"baseLayerMD5": "fbdaf4d1d349edd3ddf3a1c4528aa9ec.svg",
"bitmapResolution": 1,
"rotationCenterX": 22,
"rotationCenterY": 27
@@ -15659,7 +15659,7 @@
{
"costumeName": "watermelon-c",
"baseLayerID": -1,
"baseLayerMD5": "677738282686d2dcce35d731c3ddc043.svg",
"baseLayerMD5": "5976c10412306fc093c1d1930fa05119.svg",
"bitmapResolution": 1,
"rotationCenterX": 21,
"rotationCenterY": 15
+16 -77
View File
@@ -1,18 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import {getIsShowingWithoutId} from '../reducers/project-state';
import {setProjectTitle} from '../reducers/project-title';
const messages = defineMessages({
defaultProjectTitle: {
id: 'gui.gui.defaultProjectTitle',
description: 'Default title for project',
defaultMessage: 'Scratch Project'
}
});
import bindAll from 'lodash.bindall';
/* Higher Order Component to get and set the project title
* @param {React.Component} WrappedComponent component to receive project title related props
@@ -20,79 +7,31 @@ const messages = defineMessages({
*/
const TitledHOC = function (WrappedComponent) {
class TitledComponent extends React.Component {
componentDidMount () {
this.handleReceivedProjectTitle(this.props.projectTitle);
constructor (props) {
super(props);
bindAll(this, [
'handleUpdateProjectTitle'
]);
this.state = {
projectTitle: null
};
}
componentDidUpdate (prevProps) {
if (this.props.projectTitle !== prevProps.projectTitle) {
this.handleReceivedProjectTitle(this.props.projectTitle);
}
// if the projectTitle hasn't changed, but the reduxProjectTitle
// HAS changed, we need to report that change to the projectTitle's owner
if (this.props.reduxProjectTitle !== prevProps.reduxProjectTitle &&
this.props.reduxProjectTitle !== this.props.projectTitle) {
this.props.onUpdateProjectTitle(this.props.reduxProjectTitle);
}
}
handleReceivedProjectTitle (requestedTitle) {
let newTitle = requestedTitle;
if (newTitle === null || typeof newTitle === 'undefined') {
newTitle = this.props.intl.formatMessage(messages.defaultProjectTitle);
}
this.props.onChangedProjectTitle(newTitle);
handleUpdateProjectTitle (newTitle) {
this.setState({projectTitle: newTitle});
}
render () {
const {
/* eslint-disable no-unused-vars */
intl,
isShowingWithoutId,
onChangedProjectTitle,
// for children, we replace onUpdateProjectTitle with our own
onUpdateProjectTitle,
// we don't pass projectTitle prop to children -- they must use
// redux value
projectTitle,
reduxProjectTitle,
/* eslint-enable no-unused-vars */
...componentProps
} = this.props;
return (
<WrappedComponent
{...componentProps}
canEditTitle
projectTitle={this.state.projectTitle}
onUpdateProjectTitle={this.handleUpdateProjectTitle}
{...this.props}
/>
);
}
}
TitledComponent.propTypes = {
intl: intlShape,
isShowingWithoutId: PropTypes.bool,
onChangedProjectTitle: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectTitle: PropTypes.string,
reduxProjectTitle: PropTypes.string
};
TitledComponent.defaultProps = {
onUpdateProjectTitle: () => {}
};
const mapStateToProps = state => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
isShowingWithoutId: getIsShowingWithoutId(loadingState),
reduxProjectTitle: state.scratchGui.projectTitle
};
};
const mapDispatchToProps = dispatch => ({
onChangedProjectTitle: title => dispatch(setProjectTitle(title))
});
return injectIntl(connect(
mapStateToProps,
mapDispatchToProps,
)(TitledComponent));
return TitledComponent;
};
export {
+4 -4
View File
@@ -87,9 +87,9 @@ const vmListenerHOC = function (WrappedComponent) {
// Don't capture keys intended for Blockly inputs.
if (e.target !== document && e.target !== document.body) return;
const key = (!e.key || e.key === 'Dead') ? e.keyCode : e.key;
this.props.vm.postIOData('keyboard', {
key: key,
keyCode: e.keyCode,
key: e.key,
isDown: true
});
@@ -102,9 +102,9 @@ const vmListenerHOC = function (WrappedComponent) {
handleKeyUp (e) {
// Always capture up events,
// even those that have switched to other targets.
const key = (!e.key || e.key === 'Dead') ? e.keyCode : e.key;
this.props.vm.postIOData('keyboard', {
key: key,
keyCode: e.keyCode,
key: e.key,
isDown: false
});
+3 -2
View File
@@ -9,6 +9,7 @@ import Box from '../components/box/box.jsx';
import GUI from '../containers/gui.jsx';
import HashParserHOC from '../lib/hash-parser-hoc.jsx';
import AppStateHOC from '../lib/app-state-hoc.jsx';
import TitledHOC from '../lib/titled-hoc.jsx';
import {setPlayer} from '../reducers/mode';
@@ -23,7 +24,6 @@ const Player = ({isPlayerOnly, onSeeInside, projectId}) => (
<Box className={classNames(isPlayerOnly ? styles.stageOnly : styles.editor)}>
{isPlayerOnly && <button onClick={onSeeInside}>{'See inside'}</button>}
<GUI
canEditTitle
enableCommunity
isPlayerOnly={isPlayerOnly}
projectId={projectId}
@@ -55,7 +55,8 @@ const ConnectedPlayer = connect(
// ability to compose reducers.
const WrappedPlayer = compose(
AppStateHOC,
HashParserHOC
HashParserHOC,
TitledHOC
)(ConnectedPlayer);
const appTarget = document.createElement('div');
+3 -3
View File
@@ -5,6 +5,7 @@ import {compose} from 'redux';
import AppStateHOC from '../lib/app-state-hoc.jsx';
import GUI from '../containers/gui.jsx';
import HashParserHOC from '../lib/hash-parser-hoc.jsx';
import TitledHOC from '../lib/titled-hoc.jsx';
import log from '../lib/log.js';
const onClickLogo = () => {
@@ -36,7 +37,8 @@ export default appTarget => {
// ability to compose reducers.
const WrappedGui = compose(
AppStateHOC,
HashParserHOC
HashParserHOC,
TitledHOC
)(GUI);
// TODO a hack for testing the backpack, allow backpack host to be set by url param
@@ -65,7 +67,6 @@ export default appTarget => {
// important: this is checking whether `simulateScratchDesktop` is truthy, not just defined!
simulateScratchDesktop ?
<WrappedGui
canEditTitle
isScratchDesktop
showTelemetryModal
canSave={false}
@@ -74,7 +75,6 @@ export default appTarget => {
onTelemetryModalOptOut={handleTelemetryModalOptOut}
/> :
<WrappedGui
canEditTitle
backpackVisible
showComingSoon
backpackHost={backpackHost}
+14 -13
View File
@@ -182,8 +182,7 @@ describe('Working with the blocks', () => {
// Rename the costume
await clickText('Costumes');
await clickText('costume2', scope.costumesTab);
const el = await findByXpath("//input[@value='costume2']");
const el = await findByXpath("//input[@value='costume1']");
await el.sendKeys('newname');
// Make sure it is updated in the block menu
@@ -198,8 +197,7 @@ describe('Working with the blocks', () => {
// Rename the costume
await clickText('Costumes');
await clickText('costume2', scope.costumesTab);
const el = await findByXpath("//input[@value='costume2']");
const el = await findByXpath("//input[@value='costume1']");
await el.sendKeys('<NewCostume>');
// Make sure it is updated in the block menu
@@ -211,15 +209,17 @@ describe('Working with the blocks', () => {
await clickText('Sound', scope.blocksTab);
});
test('Adding costumes DOES update the default costume name in the toolbox', async () => {
// NOTE: This test describes the current behavior so that changes are not
// introduced inadvertly, but I know this is not the desired behavior
test('Adding costumes DOES NOT update the default costume name in the toolbox', async () => {
await loadUri(uri);
// By default, costume2 is in the costume tab
// By default, costume1 is in the costume tab
await clickText('Looks', scope.blocksTab);
await driver.sleep(500); // Wait for scroll to finish
await clickText('costume2', scope.blocksTab);
await clickText('costume1', scope.blocksTab);
// Also check that adding a new costume does update the list
// Also check that adding a new costume does not update the list
await clickText('Costumes');
const el = await findByXpath('//button[@aria-label="Choose a Costume"]');
await driver.actions().mouseMove(el)
@@ -227,13 +227,14 @@ describe('Working with the blocks', () => {
await driver.sleep(500); // Wait for thermometer menu to come up
await clickXpath('//button[@aria-label="Paint"]');
await clickText('costume3', scope.costumesTab);
// Check that the menu has been updated
// Check that the menu has not been updated
await clickText('Code');
await clickText('costume3', scope.blocksTab);
await clickText('costume1', scope.blocksTab);
});
// Skipped because it was flakey on travis, but seems to run locally ok
test.skip('Adding a sound DOES update the default sound name in the toolbox', async () => {
// NOTE: This test describes the current behavior so that changes are not
// introduced inadvertly, but I know this is not the desired behavior
test('Adding a sound DOES NOT update the default sound name in the toolbox', async () => {
await loadUri(uri);
await clickText('Sounds');
await clickXpath('//button[@aria-label="Choose a Sound"]');
@@ -241,7 +242,7 @@ describe('Working with the blocks', () => {
await clickText('Code');
await clickText('Sound', scope.blocksTab);
await driver.sleep(500); // Wait for scroll to finish
await clickText('A\u00A0Bass', scope.blocksTab); // Need &nbsp; for block text
await clickText('Meow', scope.blocksTab); // Meow, not A Bass
});
// Regression test for switching between editor/player causing toolbox to stop updating
+5 -1
View File
@@ -193,7 +193,11 @@ describe('Working with costumes', () => {
.mouseMove(abbyElement)
.perform();
// wait for one of Abby's alternate costumes to appear
await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/b6e23922f23b49ddc6f62f675e77417c.svg/get/"]');
const src1 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
await driver.sleep(300);
const src2 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
const sourcesMatch = (src1 === src2);
await expect(sourcesMatch).toBeFalsy(); // 'src' attribute should have changed by now
const logs = await getLogs();
await expect(logs).toEqual([]);
});
@@ -11,6 +11,7 @@ describe('SBFileUploader Container', () => {
const mockStore = configureStore();
let onLoadingFinished;
let onLoadingStarted;
let onUpdateProjectTitle;
let store;
// Wrap this in a function so it gets test specific states and can be reused.
@@ -19,6 +20,7 @@ describe('SBFileUploader Container', () => {
<SBFileUploader
onLoadingFinished={onLoadingFinished}
onLoadingStarted={onLoadingStarted}
onUpdateProjectTitle={onUpdateProjectTitle}
>
{(renderFileInput, loadProject) => (
<div
@@ -38,6 +40,7 @@ describe('SBFileUploader Container', () => {
vm: {}
}
});
onUpdateProjectTitle = jest.fn();
onLoadingFinished = jest.fn();
onLoadingStarted = jest.fn();
});
-45
View File
@@ -133,49 +133,4 @@ describe('VMListenerHOC', () => {
const actions = store.getActions();
expect(actions.length).toEqual(0);
});
test('keypresses go to the vm', () => {
const Component = () => (<div />);
const WrappedComponent = vmListenerHOC(Component);
// Mock document.addEventListener so we can trigger keypresses manually
// Cannot use the enzyme simulate method because that only works on synthetic events
const eventTriggers = {};
document.addEventListener = jest.fn((event, cb) => {
eventTriggers[event] = cb;
});
vm.postIOData = jest.fn();
store = mockStore({
scratchGui: {
mode: {isFullScreen: true},
modals: {soundRecorder: true},
vm: vm
}
});
mount(
<WrappedComponent
attachKeyboardEvents
store={store}
vm={vm}
/>
);
// keyboard events that do not target the document or body are ignored
eventTriggers.keydown({key: 'A', target: null});
expect(vm.postIOData).not.toHaveBeenLastCalledWith('keyboard', {key: 'A', isDown: true});
// keydown/up with target as the document are sent to the vm via postIOData
eventTriggers.keydown({key: 'A', target: document});
expect(vm.postIOData).toHaveBeenLastCalledWith('keyboard', {key: 'A', isDown: true});
eventTriggers.keyup({key: 'A', target: document});
expect(vm.postIOData).toHaveBeenLastCalledWith('keyboard', {key: 'A', isDown: false});
// When key is 'Dead' e.g. bluetooth keyboards on iOS, it sends keyCode instead
// because the VM can process both named keys or keyCodes as the `key` property
eventTriggers.keyup({key: 'Dead', keyCode: 10, target: document});
expect(vm.postIOData).toHaveBeenLastCalledWith('keyboard', {key: 10, isDown: false});
});
});