Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeb5cfd930 | ||
|
|
99bdad90d6 | ||
|
|
1f5ac6e383 | ||
|
|
9cb5f3f96d | ||
|
|
09b14ed4d4 | ||
|
|
5859af7efa | ||
|
|
d4e71024fb | ||
|
|
a83d9369dc | ||
|
|
012bb612ad | ||
|
|
b9e7bde676 | ||
|
|
992f012ff3 | ||
|
|
c53b88dbad |
No files matched your search
@@ -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
|
||||
@@ -5,9 +5,6 @@
|
||||
/node_modules
|
||||
npm-*
|
||||
|
||||
# Double copies of all the static assets and tutorial gifs
|
||||
/src
|
||||
|
||||
# Testing
|
||||
/.nyc_output
|
||||
/coverage
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
},
|
||||
{
|
||||
"name": "Basketball 1",
|
||||
"md5": "ae21eac3d1814aee1d37ae82ea287816.svg",
|
||||
"md5": "e494c4f44897d94e0541f7036a302449.svg",
|
||||
"type": "backdrop",
|
||||
"tags": [
|
||||
"sports",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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};
|
||||
@@ -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'
|
||||
|
||||
|
Before Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 240 KiB |
|
Before Width: | Height: | Size: 409 KiB |
|
Before Width: | Height: | Size: 913 KiB |
|
Before Width: | Height: | Size: 247 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 291 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 92 KiB |
@@ -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'
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 for block text
|
||||
await clickText('Meow', scope.blocksTab); // Meow, not A Bass
|
||||
});
|
||||
|
||||
// Regression test for switching between editor/player causing toolbox to stop updating
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
});
|
||||