Compare commits

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

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

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

No files matched your search

-207
View File
@@ -1,207 +0,0 @@
version: 2.1
aliases:
- &save_git_cache
save_cache:
paths:
- .git
key: v1-git-{{ .Revision }}
- &restore_git_cache
restore_cache:
keys:
- v1-git-{{ .Revision }}
- v1-git-
- &save_build_cache
save_cache:
paths:
- build
key: v1-build-{{ .Revision }}
- &restore_build_cache
restore_cache:
keys:
- v1-build-{{ .Revision }}
- &save_dist_cache
save_cache:
paths:
- dist
key: v1-dist-{{ .Revision }}
- &restore_dist_cache
restore_cache:
keys:
- v1-dist-{{ .Revision }}
- &save_npm_cache
save_cache:
paths:
- node_modules
key: v1-npm-{{ checksum "package-lock.json" }}
- &restore_npm_cache
restore_cache:
keys:
- v1-npm-{{ checksum "package-lock.json" }}
- v1-npm-
- &defaults
docker:
- image: circleci/node:8.16-browsers
working_directory: ~/repo
jobs:
setup:
<<: *defaults
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run: npm install
- *save_git_cache
- *save_npm_cache
lint:
<<: *defaults
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: Lint
command: npm run test:lint -- --quiet --output-file test-results/eslint/results.xml --format junit
- store_test_results:
path: test-results
unit:
<<: *defaults
environment:
JEST_JUNIT_OUTPUT_NAME: results.xml
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: Unit
environment:
JEST_JUNIT_OUTPUT_DIR: test-results/unit
command: npm run test:unit -- --reporters="default" --reporters="jest-junit" --coverage --coverageReporters=text --coverageReporters=lcov
- store_artifacts:
path: coverage
- store_test_results:
path: test-results
build:
<<: *defaults
environment:
NODE_ENV: production
NODE_OPTIONS: --max-old-space-size=4000
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- run:
name: Build
command: npm run build
- *save_build_cache
- *save_dist_cache
store_build:
<<: *defaults
steps:
- *restore_build_cache
- store_artifacts:
path: build
store_dist:
<<: *defaults
steps:
- *restore_dist_cache
- store_artifacts:
path: dist
integration:
<<: *defaults
parallelism: 2
environment:
JEST_JUNIT_OUTPUT_NAME: results.txt
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- *restore_build_cache
- run:
name: Integration
environment:
JEST_JUNIT_OUTPUT_DIR: test-results/integration
command: |
export TESTFILES=$(circleci tests glob "test/integration/*.test.js" | circleci tests split --split-by=timings)
$(npm bin)/jest ${TESTFILES} --reporters="default" --reporters="jest-junit"
- store_test_results:
path: test-results
deploy-npm:
<<: *defaults
environment:
NODE_OPTIONS: --max-old-space-size=4000
steps:
- *restore_git_cache
- *restore_dist_cache
- checkout
- run: |
echo export RELEASE_VERSION="0.1.0-prerelease.$(date +'%Y%m%d%H%M%S')" >> $BASH_ENV
echo export NPM_TAG=circlelatest >> $BASH_ENV
if [ "$CIRCLE_BRANCH" == "master" ]
then echo export NPM_TAG=circlestable >> $BASH_ENV
fi
if [[ "$CIRCLE_BRANCH" == hotfix/* ]] # double brackets are important for matching the wildcard
then echo export NPM_TAG=circlehotfix >> $BASH_ENV
fi
- run: npm version --no-git-tag-version $RELEASE_VERSION
- run: |
npm set //registry.npmjs.org/:_authToken=$NPM_TOKEN
npm publish --tag $NPM_TAG
- run: git tag $RELEASE_VERSION
- run: git push $CIRCLE_REPOSITORY_URL $RELEASE_VERSION
deploy-gh-pages:
<<: *defaults
steps:
- *restore_git_cache
- checkout
- *restore_npm_cache
- *restore_build_cache
- run: |
git config --global user.email $(git log --pretty=format:"%ae" -n1)
git config --global user.name $(git log --pretty=format:"%an" -n1)
- run: npm run deploy -- -e $CIRCLE_BRANCH
workflows:
version: 2
build-test-deploy:
jobs:
- setup
- lint:
requires:
- setup
- unit:
requires:
- setup
- build:
requires:
- setup
- integration:
requires:
- build
- store_build:
requires:
- build
- store_dist:
requires:
- build
# Disable deployment while running in parallel with Travis
# - deploy-npm:
# requires:
# - lint
# - unit
# - integration
# - build
# filters:
# branches:
# only:
# - master
# - develop
# - /^hotfix\/.*/
# - deploy-gh-pages:
# requires:
# - lint
# - unit
# - integration
# - build
-1
View File
@@ -12,7 +12,6 @@
*.svg binary
# Prefer LF for most file types
*.css text eol=lf
*.frag text eol=lf
*.htm text eol=lf
*.html text eol=lf
+1 -1
View File
@@ -14,6 +14,6 @@ npm-*
/dist
/.opt-in
# Generated translation files
# generated translation files
/translations
/locale
-3
View File
@@ -5,9 +5,6 @@
/node_modules
npm-*
# Double copies of all the static assets and tutorial gifs
/src
# Testing
/.nyc_output
/coverage
+26 -6
View File
@@ -1,24 +1,31 @@
language: node_js
sudo: required
dist: xenial
dist: trusty
addons:
chrome: stable
node_js:
- 10
- 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')"
cache:
directories:
- node_modules
before_install:
# package-lock.json was introduced in npm@5
- '[[ $(node -v) =~ ^v9.*$ ]] || npm install -g npm@latest' # skipped when using node 9
- npm install -g greenkeeper-lockfile
install:
- npm --production=false install
- npm --production=false update
before_script: greenkeeper-lockfile-update
script:
- npm test
- if [ "$TRAVIS_EVENT_TYPE" == "pull_request" ] && [ "$TRAVIS_BRANCH" == "master" ]; then npm run test:smoke; fi
after_script: greenkeeper-lockfile-upload
before_deploy:
- >
if [ -z "$BEFORE_DEPLOY_RAN" ]; then
@@ -35,6 +42,7 @@ deploy:
branch:
- master
- develop
- smoke
- hotfix/*
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
@@ -47,15 +55,25 @@ deploy:
- master
- develop
- smoke
- hotfix/*
condition: $TRAVIS_EVENT_TYPE != cron
skip_cleanup: true
script: if npm info scratch-gui | grep -q $RELEASE_VERSION; then git tag $RELEASE_VERSION && git push https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git $RELEASE_VERSION; fi
- provider: s3
on:
branch:
- master
- $PREVIEW_BRANCH
condition: $TRAVIS_EVENT_TYPE != cron
access_key_id: $AWS_ACCESS_KEY_ID
secret_access_key: $AWS_SECRET_ACCESS_KEY
bucket: $AWS_BUCKET_NAME
acl: public_read
skip_cleanup: true
local_dir: build
- provider: script
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
@@ -64,3 +82,5 @@ deploy:
condition: $TRAVIS_EVENT_TYPE == cron
skip_cleanup: true
script: npm run i18n:src && npm run i18n:push
after_deploy:
- 'curl -X POST -H "Fastly-Key: $FASTLY_TOKEN" -H "Accept: application/json" https://api.fastly.com/service/$FASTLY_SERVICE_ID/purge_all'
+1 -58
View File
@@ -18,8 +18,6 @@ cd scratch-gui
npm install
```
**You may want to add `--depth=1` to the `git clone` command because there are some [large files in the git repository history](https://github.com/LLK/scratch-gui/issues/5140).**
## Getting started
Running the project requires Node.js to be installed.
@@ -76,7 +74,7 @@ See [jest cli docs](https://facebook.github.io/jest/docs/en/cli.html#content) fo
*NOTE: If you're a windows user, please run these scripts in Windows `cmd.exe` instead of Git Bash/MINGW64.*
Before running any tests, make sure you have run `npm install` from this (scratch-gui) repository's top level.
Before running any test, make sure you have run `npm install` from this (scratch-gui) repository's top level.
#### Main testing command
@@ -196,60 +194,5 @@ Further reading: [Stack Overflow](https://stackoverflow.com/questions/46602286/n
You can publish the GUI to github.io so that others on the Internet can view it.
[Read the wiki for a step-by-step guide.](https://github.com/LLK/scratch-gui/wiki/Publishing-to-GitHub-Pages)
## Understanding the project state machine
Since so much code throughout scratch-gui depends on the state of the project, which goes through many different phases of loading, displaying and saving, we created a "finite state machine" to make it clear which state it is in at any moment. This is contained in the file src/reducers/project-state.js .
It can be hard to understand the code in src/reducers/project-state.js . There are several types of data and functions used, which relate to each other:
### Loading states
These include state constant strings like:
* `NOT_LOADED` (the default state),
* `ERROR`,
* `FETCHING_WITH_ID`,
* `LOADING_VM_WITH_ID`,
* `REMIXING`,
* `SHOWING_WITH_ID`,
* `SHOWING_WITHOUT_ID`,
* etc.
### Transitions
These are names for the action which causes a state change. Some examples are:
* `START_FETCHING_NEW`,
* `DONE_FETCHING_WITH_ID`,
* `DONE_LOADING_VM_WITH_ID`,
* `SET_PROJECT_ID`,
* `START_AUTO_UPDATING`,
### How transitions relate to loading states
As this diagram of the project state machine shows, various transition actions can move us from one loading state to another:
![Project state diagram](docs/project_state_diagram.svg)
_Note: for clarity, the diagram above excludes states and transitions relating to error handling._
#### Example
Here's an example of how states transition.
Suppose a user clicks on a project, and the page starts to load with url https://scratch.mit.edu/projects/123456 .
Here's what will happen in the project state machine:
![Project state example](docs/project_state_example.png)
1. When the app first mounts, the project state is `NOT_LOADED`.
2. The `SET_PROJECT_ID` redux action is dispatched (from src/lib/project-fetcher-hoc.jsx), with `projectId` set to `123456`. This transitions the state from `NOT_LOADED` to `FETCHING_WITH_ID`.
3. The `FETCHING_WITH_ID` state. In src/lib/project-fetcher-hoc.jsx, the `projectId` value `123456` is used to request the data for that project from the server.
4. When the server responds with the data, src/lib/project-fetcher-hoc.jsx dispatches the `DONE_FETCHING_WITH_ID` action, with `projectData` set. This transitions the state from `FETCHING_WITH_ID` to `LOADING_VM_WITH_ID`.
5. The `LOADING_VM_WITH_ID` state. In src/lib/vm-manager-hoc.jsx, we load the `projectData` into Scratch's virtual machine ("the vm").
6. When loading is done, src/lib/vm-manager-hoc.jsx dispatches the `DONE_LOADING_VM_WITH_ID` action. This transitions the state from `LOADING_VM_WITH_ID` to `SHOWING_WITH_ID`
7. The `SHOWING_WITH_ID` state. Now the project appears normally and is playable and editable.
## Donate
We provide [Scratch](https://scratch.mit.edu) free of charge, and want to keep it that way! Please consider making a [donation](https://secure.donationpay.org/scratchfoundation/) to support our continued engineering, design, community, and resource development efforts. Donations of any size are appreciated. Thank you!
-3
View File
@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1381px" height="1012px" viewBox="-0.5 -0.5 1381 1012" content="&lt;mxfile host=&quot;www.draw.io&quot; modified=&quot;2020-02-04T16:31:44.889Z&quot; agent=&quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36&quot; etag=&quot;8a2X5Jj65jOGG04xBBh1&quot; version=&quot;12.6.4&quot; type=&quot;device&quot;&gt;&lt;diagram name=&quot;Page-1&quot; id=&quot;58cdce13-f638-feb5-8d6f-7d28b1aa9fa0&quot;&gt;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 truncated

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

+1732 -4590
View File
File diff suppressed because it is too large. Load diff
+14 -15
View File
@@ -6,7 +6,7 @@
"scripts": {
"build": "npm run clean && webpack --progress --colors --bail",
"clean": "rimraf ./build && mkdirp build && rimraf ./dist && mkdirp dist",
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1) [skip ci]\"",
"deploy": "touch build/.nojekyll && gh-pages -t -d build -m \"Build for $(git log --pretty=format:%H -n1)\"",
"prune": "./prune-gh-pages.sh",
"i18n:push": "tx-push-src scratch-editor interface translations/en.json",
"i18n:src": "rimraf ./translations/messages/src && babel src > tmp.js && rimraf tmp.js && build-i18n-src ./translations/messages/src ./translations/ && npm run i18n:push",
@@ -44,7 +44,7 @@
"babel-loader": "^8.0.4",
"base64-loader": "1.0.0",
"bowser": "1.9.4",
"chromedriver": "80.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",
@@ -78,9 +77,9 @@
"lodash.pick": "4.4.0",
"lodash.throttle": "4.0.1",
"minilog": "3.1.0",
"mkdirp": "^1.0.3",
"mkdirp": "^0.5.1",
"omggif": "1.0.9",
"papaparse": "5.1.1",
"papaparse": "4.6.2",
"postcss-import": "^12.0.0",
"postcss-loader": "^3.0.0",
"postcss-simple-vars": "^5.0.1",
@@ -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.20200528195344",
"scratch-blocks": "0.1.0-prerelease.20200528193139",
"scratch-l10n": "3.9.20200528181319",
"scratch-paint": "0.2.0-prerelease.20200529184518",
"scratch-render": "0.1.0-prerelease.20200512203149",
"scratch-storage": "1.3.3",
"scratch-svg-renderer": "0.2.0-prerelease.20200507183648",
"scratch-vm": "0.2.0-prerelease.20200527195112",
"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.20190822194548",
"selenium-webdriver": "3.6.0",
"startaudiocontext": "1.2.1",
"style-loader": "^0.23.0",
@@ -139,8 +139,7 @@
],
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/test/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
"editor-msgs(\\.js)?$": "<rootDir>/test/__mocks__/editor-msgs-mock.js"
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js"
}
}
}
-1
View File
@@ -15,7 +15,6 @@
padding-right: 1rem;
margin-bottom: 7px;
min-height: 1.5rem;
pointer-events: all;
}
.alert.warn {
+1 -9
View File
@@ -50,16 +50,8 @@ $fade-out-distance: 100px;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
.list-area:after {
/* Make sure there is room to scroll beyond the last tile */
content: '';
display: block;
height: 70px;
width: 100%;
flex-shrink: 0;
order: 99999999;
padding-bottom: 70px;
}
.list-item {
-1
View File
@@ -309,7 +309,6 @@ $fade-out-distance: 15px;
z-index: $z-index-alerts;
position: absolute;
margin-top: 53px;
pointer-events: none;
}
/*
+4 -6
View File
@@ -89,12 +89,12 @@ const GUIComponent = props => {
loading,
logo,
renderLogin,
onClickAbout,
onClickAccountNav,
onCloseAccountNav,
onLogOut,
onOpenRegistration,
onToggleLoginOpen,
onUpdateProjectTitle,
onActivateCostumesTab,
onActivateSoundsTab,
onActivateTab,
@@ -106,7 +106,6 @@ const GUIComponent = props => {
onRequestCloseTelemetryModal,
onSeeCommunity,
onShare,
onShowPrivacyPolicy,
onTelemetryModalCancel,
onTelemetryModalOptIn,
onTelemetryModalOptOut,
@@ -164,7 +163,6 @@ const GUIComponent = props => {
onOptIn={onTelemetryModalOptIn}
onOptOut={onTelemetryModalOptOut}
onRequestClose={onRequestCloseTelemetryModal}
onShowPrivacyPolicy={onShowPrivacyPolicy}
/>
) : null}
{loading ? (
@@ -221,7 +219,6 @@ const GUIComponent = props => {
logo={logo}
renderLogin={renderLogin}
showComingSoon={showComingSoon}
onClickAbout={onClickAbout}
onClickAccountNav={onClickAccountNav}
onClickLogo={onClickLogo}
onCloseAccountNav={onCloseAccountNav}
@@ -231,6 +228,7 @@ const GUIComponent = props => {
onSeeCommunity={onSeeCommunity}
onShare={onShare}
onToggleLoginOpen={onToggleLoginOpen}
onUpdateProjectTitle={onUpdateProjectTitle}
/>
<Box className={styles.bodyWrapper}>
<Box className={styles.flexWrapper}>
@@ -392,7 +390,6 @@ GUIComponent.propTypes = {
onActivateCostumesTab: PropTypes.func,
onActivateSoundsTab: PropTypes.func,
onActivateTab: PropTypes.func,
onClickAbout: PropTypes.func,
onClickAccountNav: PropTypes.func,
onClickLogo: PropTypes.func,
onCloseAccountNav: PropTypes.func,
@@ -404,12 +401,12 @@ GUIComponent.propTypes = {
onRequestCloseTelemetryModal: PropTypes.func,
onSeeCommunity: PropTypes.func,
onShare: PropTypes.func,
onShowPrivacyPolicy: PropTypes.func,
onTabSelect: PropTypes.func,
onTelemetryModalCancel: PropTypes.func,
onTelemetryModalOptIn: PropTypes.func,
onTelemetryModalOptOut: PropTypes.func,
onToggleLoginOpen: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
renderLogin: PropTypes.func,
showComingSoon: PropTypes.bool,
soundsTabVisible: PropTypes.bool,
@@ -436,6 +433,7 @@ GUIComponent.defaultProps = {
isCreating: false,
isShared: false,
loading: false,
onUpdateProjectTitle: () => {},
showComingSoon: false,
stageSizeMode: STAGE_SIZE_MODES.large
};
+1 -3
View File
@@ -139,9 +139,7 @@
.featured-description {
display: block;
font-weight: normal;
line-height: 1.375rem;
padding-top: .3125rem;
padding-bottom: .25rem;
line-height: 2;
}
.featured-extension-metadata {
+10 -7
View File
@@ -4,6 +4,7 @@ import React from 'react';
import Box from '../box/box.jsx';
import PlayButton from '../../containers/play-button.jsx';
import ScratchImage from '../scratch-image/scratch-image.jsx';
import styles from './library-item.css';
import classNames from 'classnames';
@@ -36,10 +37,12 @@ class LibraryItemComponent extends React.PureComponent {
/>
</div>
) : null}
<img
className={styles.featuredImage}
src={this.props.iconURL}
/>
{this.props.iconSource ? (
<ScratchImage
className={styles.featuredImage}
imageSource={this.props.iconSource}
/>
) : null}
</div>
{this.props.insetIconURL ? (
<div className={styles.libraryItemInsetImageContainer}>
@@ -127,9 +130,9 @@ class LibraryItemComponent extends React.PureComponent {
onMouseEnter={this.props.showPlayButton ? this.props.onMouseEnter : null}
onMouseLeave={this.props.showPlayButton ? this.props.onMouseLeave : null}
>
<img
<ScratchImage
className={styles.libraryItemImage}
src={this.props.iconURL}
imageSource={this.props.iconSource}
/>
</Box>
</Box>
@@ -159,7 +162,7 @@ LibraryItemComponent.propTypes = {
extensionId: PropTypes.string,
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconURL: PropTypes.string,
iconSource: ScratchImage.ImageSourcePropType,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
isPlaying: PropTypes.bool,
+50 -7
View File
@@ -10,6 +10,7 @@ import Divider from '../divider/divider.jsx';
import Filter from '../filter/filter.jsx';
import TagButton from '../../containers/tag-button.jsx';
import Spinner from '../spinner/spinner.jsx';
import storage from '../../lib/storage';
import styles from './library.css';
@@ -29,6 +30,47 @@ const messages = defineMessages({
const ALL_TAG = {tag: 'all', intlLabel: messages.allTag};
const tagListPrefix = [ALL_TAG];
/**
* Find the AssetType which corresponds to a particular file extension. For example, 'png' => AssetType.ImageBitmap.
* @param {string} fileExtension - the file extension to look up.
* @returns {AssetType} - the AssetType corresponding to the extension, if any.
*/
const getAssetTypeForFileExtension = function (fileExtension) {
const compareOptions = {
sensitivity: 'accent',
usage: 'search'
};
for (const assetTypeId in storage.AssetType) {
const assetType = storage.AssetType[assetTypeId];
if (fileExtension.localeCompare(assetType.runtimeFormat, compareOptions) === 0) {
return assetType;
}
}
};
/**
* Figure out an `imageSource` (URI or asset ID & type) for a library item's icon.
* @param {object} item - either a library item or one of a library item's costumes.
* @returns {object} - an `imageSource` ready to be passed to a `ScratchImage`.
*/
const getItemImageSource = function (item) {
if (item.rawURL) {
return {
uri: item.rawURL
};
}
// TODO: adjust libraries to be more storage-friendly; don't use split() here.
const md5 = item.md5 || item.baseLayerMD5;
if (md5) {
const [assetId, fileExtension] = md5.split('.');
return {
assetId: assetId,
assetType: getAssetTypeForFileExtension(fileExtension)
};
}
};
class LibraryComponent extends React.Component {
constructor (props) {
super(props);
@@ -208,8 +250,10 @@ class LibraryComponent extends React.Component {
})}
ref={this.setFilteredDataRef}
>
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => (
<LibraryItem
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => {
const iconSource = getItemImageSource(dataItem);
const icons = dataItem.json && dataItem.json.costumes.map(getItemImageSource);
return (<LibraryItem
bluetoothRequired={dataItem.bluetoothRequired}
collaborator={dataItem.collaborator}
description={dataItem.description}
@@ -217,9 +261,8 @@ class LibraryComponent extends React.Component {
extensionId={dataItem.extensionId}
featured={dataItem.featured}
hidden={dataItem.hidden}
iconMd5={dataItem.md5}
iconRawURL={dataItem.rawURL}
icons={dataItem.json && dataItem.json.costumes}
iconSource={iconSource}
icons={icons}
id={index}
insetIconURL={dataItem.insetIconURL}
internetConnectionRequired={dataItem.internetConnectionRequired}
@@ -230,8 +273,8 @@ class LibraryComponent extends React.Component {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
)) : (
/>);
}) : (
<div className={styles.spinnerWrapper}>
<Spinner
large
-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M10 2c4.418 0 8 3.582 8 8s-3.582 8-8 8-8-3.582-8-8 3.582-8 8-8zm-.072 10.42c-.256 0-.472.076-.648.227-.176.151-.264.338-.264.56 0 .25.091.445.273.584.182.139.395.209.639.209.236 0 .444-.07.624-.211s.27-.335.27-.581c0-.222-.086-.409-.258-.56-.172-.151-.384-.227-.636-.227zm.144-6.42c-.624 0-1.17.113-1.638.338-.468.225-.824.51-1.068.853-.244.343-.366.683-.366 1.017 0 .162.077.313.231.452.154.139.343.209.567.209.38 0 .638-.199.774-.597.144-.38.32-.668.528-.864.208-.196.532-.293.972-.293.376 0 .683.097.921.291.238.194.357.432.357.713 0 .144-.039.278-.117.402-.078.123-.174.235-.288.336-.114.1-.299.249-.555.446-.292.225-.524.42-.696.584-.172.164-.31.354-.414.571-.104.217-.156.473-.156.769 0 .236.071.414.213.534.142.12.317.18.525.18.4 0 .638-.183.714-.55l.099-.362.093-.206.183-.227.327-.288c.492-.387.833-.663 1.023-.827.19-.164.354-.358.492-.584.138-.225.207-.488.207-.787 0-.38-.121-.733-.363-1.057-.242-.324-.585-.58-1.029-.769-.444-.188-.956-.283-1.536-.283z" id="a"/></defs><g transform="translate(-2 -2)" fill="none"><mask><use xlink:href="#a"/></mask><use fill="#fff" xlink:href="#a"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-6
View File
@@ -218,9 +218,3 @@
.mystuff > a {
background-image: url("/images/mystuff.png");
}
.about-icon {
height: 1.25rem;
margin: 0.5rem;
vertical-align: middle;
}
+3 -19
View File
@@ -69,7 +69,6 @@ import profileIcon from './icon--profile.png';
import remixIcon from './icon--remix.svg';
import dropdownCaret from './dropdown-caret.svg';
import languageIcon from '../language-selector/language-icon.svg';
import aboutIcon from './icon--about.svg';
import scratchLogo from './scratch-logo.svg';
@@ -142,19 +141,6 @@ MenuItemTooltip.propTypes = {
isRtl: PropTypes.bool
};
const AboutButton = props => (
<Button
className={classNames(styles.menuBarItem, styles.hoverable)}
iconClassName={styles.aboutIcon}
iconSrc={aboutIcon}
onClick={props.onClick}
/>
);
AboutButton.propTypes = {
onClick: PropTypes.func.isRequired
};
class MenuBar extends React.Component {
constructor (props) {
super(props);
@@ -325,8 +311,6 @@ class MenuBar extends React.Component {
{remixMessage}
</Button>
);
// Show the About button only if we have a handler for it (like in the desktop app)
const aboutButton = this.props.onClickAbout ? <AboutButton onClick={this.props.onClickAbout} /> : null;
return (
<Box
className={classNames(
@@ -411,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
@@ -510,6 +495,7 @@ class MenuBar extends React.Component {
>
<ProjectTitleInput
className={classNames(styles.titleFieldGrowable)}
onUpdateProjectTitle={this.props.onUpdateProjectTitle}
/>
</MenuBarItemTooltip>
</div>
@@ -705,8 +691,6 @@ class MenuBar extends React.Component {
</React.Fragment>
)}
</div>
{aboutButton}
</Box>
);
}
@@ -740,7 +724,6 @@ MenuBar.propTypes = {
locale: PropTypes.string.isRequired,
loginMenuOpen: PropTypes.bool,
logo: PropTypes.string,
onClickAbout: PropTypes.func,
onClickAccount: PropTypes.func,
onClickEdit: PropTypes.func,
onClickFile: PropTypes.func,
@@ -763,6 +746,7 @@ MenuBar.propTypes = {
onSeeCommunity: PropTypes.func,
onShare: PropTypes.func,
onToggleLoginOpen: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectTitle: PropTypes.string,
renderLogin: PropTypes.func,
sessionExists: PropTypes.bool,
+31 -21
View File
@@ -1,9 +1,9 @@
import classNames from 'classnames';
import {connect} from 'react-redux';
import PropTypes from 'prop-types';
import bindAll from 'lodash.bindall';
import React from 'react';
import {defineMessages, intlShape, injectIntl} from 'react-intl';
import {setProjectTitle} from '../../reducers/project-title';
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
import Input from '../forms/input.jsx';
@@ -19,27 +19,39 @@ const messages = defineMessages({
}
});
const ProjectTitleInput = ({
className,
intl,
onSubmit,
projectTitle
}) => (
<BufferedInput
className={classNames(styles.titleField, className)}
maxLength="100"
placeholder={intl.formatMessage(messages.projectTitlePlaceholder)}
tabIndex="0"
type="text"
value={projectTitle}
onSubmit={onSubmit}
/>
);
class ProjectTitleInput extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'handleUpdateProjectTitle'
]);
}
// call onUpdateProjectTitle if it is defined (only defined when gui
// is used within scratch-www)
handleUpdateProjectTitle (newTitle) {
if (this.props.onUpdateProjectTitle) {
this.props.onUpdateProjectTitle(newTitle);
}
}
render () {
return (
<BufferedInput
className={classNames(styles.titleField, this.props.className)}
maxLength="100"
placeholder={this.props.intl.formatMessage(messages.projectTitlePlaceholder)}
tabIndex="0"
type="text"
value={this.props.projectTitle}
onSubmit={this.handleUpdateProjectTitle}
/>
);
}
}
ProjectTitleInput.propTypes = {
className: PropTypes.string,
intl: intlShape.isRequired,
onSubmit: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectTitle: PropTypes.string
};
@@ -47,9 +59,7 @@ const mapStateToProps = state => ({
projectTitle: state.scratchGui.projectTitle
});
const mapDispatchToProps = dispatch => ({
onSubmit: title => dispatch(setProjectTitle(title))
});
const mapDispatchToProps = () => ({});
export default injectIntl(connect(
mapStateToProps,
+1 -3
View File
@@ -43,7 +43,6 @@
margin: 0 5px;
border-radius: calc($space / 2);
padding: 0 2px;
white-space: pre-wrap;
transform: translateZ(0); /* Fixes flickering in Safari */
}
@@ -54,7 +53,6 @@
text-align: center;
color: white;
font-size: 1rem;
white-space: pre-wrap;
transform: translateZ(0); /* Fixes flickering in Safari */
}
@@ -151,7 +149,7 @@
overflow: hidden; /* Don't let long values escape container */
text-overflow: ellipsis;
user-select: text; /* Allow selecting list values for 2.0 compatibility, only relevant in player */
white-space: pre;
white-space: nowrap;
}
.list-input {
+1 -3
View File
@@ -20,10 +20,8 @@
border: 1px solid $ui-black-transparent;
border-radius: 5px;
padding: 3px;
/* Force these to be on their own render layer because they update often */
transform: translateZ(0);
}
}
.meter-container {
margin-right: 5px;
height: 180px;
@@ -0,0 +1,139 @@
import PropTypes from 'prop-types';
import React from 'react';
import VisibilitySensor from 'react-visibility-sensor';
import storage from '../../lib/storage';
class ScratchImage extends React.PureComponent {
static init () {
this._maxParallelism = 6;
this._currentJobs = 0;
this._pendingImages = new Set();
}
static loadPendingImages () {
if (this._currentJobs >= this._maxParallelism) {
// already busy
return;
}
// Find the first visible image. If there aren't any, find the first non-visible image.
let nextImage;
for (const image of this._pendingImages) {
if (image.isVisible) {
nextImage = image;
break;
} else {
nextImage = nextImage || image;
}
}
// If we found an image to load:
// 1) Remove it from the queue
// 2) Load the image
// 3) Pump the queue again
if (nextImage) {
this._pendingImages.delete(nextImage);
const imageSource = nextImage.props.imageSource;
++this._currentJobs;
storage
.load(imageSource.assetType, imageSource.assetId)
.then(asset => {
if (!nextImage.wasUnmounted) {
const dataURI = asset.encodeDataURI();
nextImage.setState({
imageURI: dataURI
});
}
--this._currentJobs;
this.loadPendingImages();
});
}
}
constructor (props) {
super(props);
this.state = {};
Object.assign(this.state, this._loadImageSource(props.imageSource));
}
componentWillReceiveProps (nextProps) {
const newState = this._loadImageSource(nextProps.imageSource);
this.setState(newState);
}
componentWillUnmount () {
this.wasUnmounted = true;
ScratchImage._pendingImages.delete(this);
}
/**
* Calculate the state changes necessary to load the image specified in the provided source info. If the component
* is mounted, call setState() with the return value of this function. If the component has not yet mounted, use
* the return value of this function as initial state for the component.
*
* @param {object} imageSource - the new source for the image, including either assetId or URI
* @returns {object} - the new state values, if any.
*/
_loadImageSource (imageSource) {
if (imageSource) {
if (imageSource.uri) {
ScratchImage._pendingImages.delete(this);
return {
imageURI: imageSource.uri,
lastRequestedAsset: null
};
}
if (this.state.lastRequestedAsset !== imageSource.assetId) {
ScratchImage._pendingImages.add(this);
return {
lastRequestedAsset: imageSource.assetId
};
}
}
// Nothing to do - don't change any state.
return {};
}
render () {
const {
src: _src,
imageSource: _imageSource,
...imgProps
} = this.props;
return (
<VisibilitySensor
intervalCheck
scrollCheck
>
{
({isVisible}) => {
this.isVisible = isVisible;
ScratchImage.loadPendingImages();
return (
<img
src={this.state.imageURI}
{...imgProps}
/>
);
}
}
</VisibilitySensor>
);
}
}
ScratchImage.ImageSourcePropType = PropTypes.oneOfType([
PropTypes.shape({
assetId: PropTypes.string.isRequired,
assetType: PropTypes.oneOf(Object.values(storage.AssetType)).isRequired
}),
PropTypes.shape({
uri: PropTypes.string.isRequired
})
]);
ScratchImage.propTypes = {
imageSource: ScratchImage.ImageSourcePropType.isRequired
};
ScratchImage.init();
export default ScratchImage;
@@ -265,7 +265,6 @@ const SoundEditor = props => (
onClick={props.onSlower}
/>
<IconButton
disabled={props.tooLoud}
className={classNames(styles.effectButton, styles.flipInRtl)}
img={louderIcon}
title={<FormattedMessage {...messages.louder} />}
@@ -341,7 +340,6 @@ SoundEditor.propTypes = {
onUndo: PropTypes.func.isRequired,
playhead: PropTypes.number,
setRef: PropTypes.func,
tooLoud: PropTypes.bool.isRequired,
trimEnd: PropTypes.number,
trimStart: PropTypes.number
};
+1
View File
@@ -66,6 +66,7 @@
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
@@ -101,9 +101,6 @@ class TelemetryModal extends React.PureComponent {
privacyPolicyLink: (<a
className={styles.privacyPolicyLink}
href="https://scratch.mit.edu/privacy_policy/"
onClick={this.props.onShowPrivacyPolicy}
target="_blank"
rel="noopener noreferrer"
>
<FormattedMessage {...messages.privacyPolicyLink} />
</a>)
@@ -137,8 +134,7 @@ TelemetryModal.propTypes = {
onCancel: PropTypes.func,
onOptIn: PropTypes.func.isRequired,
onOptOut: PropTypes.func.isRequired,
onRequestClose: PropTypes.func,
onShowPrivacyPolicy: PropTypes.func
onRequestClose: PropTypes.func
};
export default injectIntl(TelemetryModal);
+3 -3
View File
@@ -340,9 +340,9 @@ class Blocks extends React.Component {
const targetSounds = target.getSounds();
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML,
targetCostumes[targetCostumes.length - 1].name,
stageCostumes[stageCostumes.length - 1].name,
targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : ''
targetCostumes[0].name,
stageCostumes[0].name,
targetSounds.length > 0 ? targetSounds[0].name : ''
);
} catch {
return null;
+30 -4
View File
@@ -4,13 +4,14 @@ import {compose} from 'redux';
import {connect} from 'react-redux';
import ReactModal from 'react-modal';
import VM from 'scratch-vm';
import {injectIntl, intlShape} from 'react-intl';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx';
import {
getIsError,
getIsShowingProject
} from '../reducers/project-state';
import {setProjectTitle} from '../reducers/project-title';
import {
activateTab,
BLOCKS_TAB_INDEX,
@@ -28,7 +29,6 @@ import {
import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
import LocalizationHOC from '../lib/localization-hoc.jsx';
import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
import TitledHOC from '../lib/titled-hoc.jsx';
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
import QueryParserHOC from '../lib/query-parser-hoc.jsx';
import storage from '../lib/storage';
@@ -39,9 +39,18 @@ import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx';
import GUIComponent from '../components/gui/gui.jsx';
import {setIsScratchDesktop} from '../lib/isScratchDesktop.js';
const messages = defineMessages({
defaultProjectTitle: {
id: 'gui.gui.defaultProjectTitle',
description: 'Default title for project',
defaultMessage: 'Scratch Project'
}
});
class GUI extends React.Component {
componentDidMount () {
setIsScratchDesktop(this.props.isScratchDesktop);
this.setReduxTitle(this.props.projectTitle);
this.props.onStorageInit(storage);
this.props.onVmInit(this.props.vm);
}
@@ -49,12 +58,24 @@ class GUI extends React.Component {
if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
this.props.onUpdateProjectId(this.props.projectId);
}
if (this.props.projectTitle !== prevProps.projectTitle) {
this.setReduxTitle(this.props.projectTitle);
}
if (this.props.isShowingProject && !prevProps.isShowingProject) {
// this only notifies container when a project changes from not yet loaded to loaded
// At this time the project view in www doesn't need to know when a project is unloaded
this.props.onProjectLoaded();
}
}
setReduxTitle (newTitle) {
if (newTitle === null || typeof newTitle === 'undefined') {
this.props.onUpdateReduxProjectTitle(
this.props.intl.formatMessage(messages.defaultProjectTitle)
);
} else {
this.props.onUpdateReduxProjectTitle(newTitle);
}
}
render () {
if (this.props.isError) {
throw new Error(
@@ -71,9 +92,11 @@ class GUI extends React.Component {
onProjectLoaded,
onStorageInit,
onUpdateProjectId,
onUpdateReduxProjectTitle,
onVmInit,
projectHost,
projectId,
projectTitle,
/* eslint-enable no-unused-vars */
children,
fetchingProject,
@@ -108,9 +131,12 @@ GUI.propTypes = {
onSeeCommunity: PropTypes.func,
onStorageInit: PropTypes.func,
onUpdateProjectId: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
onUpdateReduxProjectTitle: PropTypes.func,
onVmInit: PropTypes.func,
projectHost: PropTypes.string,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
projectTitle: PropTypes.string,
telemetryModalVisible: PropTypes.bool,
vm: PropTypes.instanceOf(VM).isRequired
};
@@ -160,7 +186,8 @@ const mapDispatchToProps = dispatch => ({
onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)),
onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()),
onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()),
onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal())
onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()),
onUpdateReduxProjectTitle: title => dispatch(setProjectTitle(title))
});
const ConnectedGUI = injectIntl(connect(
@@ -177,7 +204,6 @@ const WrappedGui = compose(
FontLoaderHOC,
QueryParserHOC,
ProjectFetcherHOC,
TitledHOC,
ProjectSaverHOC,
vmListenerHOC,
vmManagerHOC,
+9 -22
View File
@@ -91,23 +91,17 @@ class LibraryItem extends React.PureComponent {
const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length;
this.setState({iconIndex: nextIconIndex});
}
curIconMd5 () {
const iconMd5Prop = this.props.iconMd5;
curIconSource () {
if (this.props.icons &&
this.state.isRotatingIcon &&
this.state.iconIndex < this.props.icons.length) {
const icon = this.props.icons[this.state.iconIndex] || {};
return icon.md5ext || // 3.0 library format
icon.baseLayerMD5 || // 2.0 library format, TODO GH-5084
iconMd5Prop;
this.state.iconIndex < this.props.icons.length &&
this.props.icons[this.state.iconIndex]) {
return this.props.icons[this.state.iconIndex];
}
return iconMd5Prop;
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}
@@ -117,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}
@@ -149,14 +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, // 2.0 library format, TODO GH-5084
md5ext: PropTypes.string // 3.0 library format
})
),
iconSource: LibraryItemComponent.propTypes.iconSource, // single icon
icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons
id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
+4 -4
View File
@@ -44,10 +44,10 @@ class RecordingStep extends React.Component {
alert(this.props.intl.formatMessage(messages.alertMsg)); // eslint-disable-line no-alert
}
handleLevelUpdate (level) {
this.setState({
level: level,
levels: this.props.recording ? (this.state.levels || []).concat([level]) : this.state.levels
});
this.setState({level});
if (this.props.recording) {
this.setState({levels: (this.state.levels || []).concat([level])});
}
}
handleRecord () {
this.audioRecorder.startRecording();
+3 -5
View File
@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import {setProjectTitle} from '../reducers/project-title';
import log from '../lib/log';
import sharedMessages from '../lib/shared-messages';
@@ -132,7 +131,7 @@ class SBFileUploader extends React.Component {
// This is necessary in case the user wants to reload a project
if (filename) {
const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
this.props.onReceivedProjectTitle(uploadedProjectTitle);
this.props.onUpdateProjectTitle(uploadedProjectTitle);
}
this.resetFileInput();
})
@@ -180,9 +179,9 @@ SBFileUploader.propTypes = {
loadingState: PropTypes.oneOf(LoadingStates),
onLoadingFinished: PropTypes.func,
onLoadingStarted: PropTypes.func,
onUpdateProjectTitle: PropTypes.func,
projectChanged: PropTypes.bool,
requestProjectUpload: PropTypes.func,
onReceivedProjectTitle: PropTypes.func,
userOwnsProject: PropTypes.bool,
vm: PropTypes.shape({
loadProject: PropTypes.func
@@ -210,8 +209,7 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
dispatch(closeFileMenu());
},
requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)),
onLoadingStarted: () => dispatch(openLoadingProject()),
onReceivedProjectTitle: title => dispatch(setProjectTitle(title))
onLoadingStarted: () => dispatch(openLoadingProject())
});
// Allow incoming props to override redux-provided props. Used to mock in tests.
+84 -121
View File
@@ -6,12 +6,7 @@ import VM from 'scratch-vm';
import {connect} from 'react-redux';
import {
computeChunkedRMS,
encodeAndAddSoundToVM,
downsampleIfNeeded,
dropEveryOtherSample
} from '../lib/audio/audio-util.js';
import {computeChunkedRMS, encodeAndAddSoundToVM, SOUND_BYTE_LIMIT} from '../lib/audio/audio-util.js';
import AudioEffects from '../lib/audio/audio-effects.js';
import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx';
import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js';
@@ -19,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);
@@ -44,8 +37,7 @@ class SoundEditor extends React.Component {
'paste',
'handleKeyPress',
'handleContainerClick',
'setRef',
'resampleBufferToRate'
'setRef'
]);
this.state = {
copyBuffer: null,
@@ -138,32 +130,45 @@ class SoundEditor extends React.Component {
});
}
submitNewSamples (samples, sampleRate, skipUndo) {
return downsampleIfNeeded({samples, sampleRate}, this.resampleBufferToRate)
.then(({samples: newSamples, sampleRate: newSampleRate}) =>
WavEncoder.encode({
sampleRate: newSampleRate,
channelData: [newSamples]
}).then(wavBuffer => {
if (!skipUndo) {
this.redoStack = [];
if (this.undoStack.length >= UNDO_STACK_SIZE) {
this.undoStack.shift(); // Drop the first element off the array
}
this.undoStack.push(this.getUndoItem());
}
this.resetState(newSamples, newSampleRate);
this.props.vm.updateSoundBuffer(
this.props.soundIndex,
this.audioBufferPlayer.buffer,
new Uint8Array(wavBuffer));
return true; // Edit was successful
})
)
.catch(e => {
// Encoding failed, or the sound was too large to save so edit is rejected
log.error(`Encountered error while trying to encode sound update: ${e}`);
return false; // Edit was not applied
// Encode the new sound into a wav so that it can be stored
let wavBuffer = null;
try {
wavBuffer = WavEncoder.encode.sync({
sampleRate: sampleRate,
channelData: [samples]
});
if (wavBuffer.byteLength > SOUND_BYTE_LIMIT) {
// Cancel the sound update by setting to null
wavBuffer = null;
log.error(`Refusing to encode sound larger than ${SOUND_BYTE_LIMIT} bytes`);
}
} catch (e) {
// This error state is mostly for the mock sounds used during testing.
// Any incorrect sound buffer trying to get interpretd as a Wav file
// should yield this error.
// This can also happen if the sound is too be allocated in memory.
log.error(`Encountered error while trying to encode sound update: ${e}`);
}
// Do not submit sound if it could not be encoded (i.e. if too large)
if (wavBuffer) {
if (!skipUndo) {
this.redoStack = [];
if (this.undoStack.length >= UNDO_STACK_SIZE) {
this.undoStack.shift(); // Drop the first element off the array
}
this.undoStack.push(this.getUndoItem());
}
this.resetState(samples, sampleRate);
this.props.vm.updateSoundBuffer(
this.props.soundIndex,
this.audioBufferPlayer.buffer,
new Uint8Array(wavBuffer));
return true; // Update succeeded
}
return false; // Update failed
}
handlePlay () {
this.audioBufferPlayer.stop();
@@ -202,15 +207,13 @@ class SoundEditor extends React.Component {
newSamples.set(firstPart, 0);
newSamples.set(secondPart, firstPart.length);
}
this.submitNewSamples(newSamples, sampleRate).then(() => {
this.setState({
trimStart: null,
trimEnd: null
});
this.submitNewSamples(newSamples, sampleRate);
this.setState({
trimStart: null,
trimEnd: null
});
}
handleDeleteInverse () {
// Delete everything outside of the trimmers
const {samples, sampleRate} = this.copyCurrentBuffer();
const sampleCount = samples.length;
const startIndex = Math.floor(this.state.trimStart * sampleCount);
@@ -219,13 +222,10 @@ class SoundEditor extends React.Component {
if (clippedSamples.length === 0) {
clippedSamples = new Float32Array(1);
}
this.submitNewSamples(clippedSamples, sampleRate).then(success => {
if (success) {
this.setState({
trimStart: null,
trimEnd: null
});
}
this.submitNewSamples(clippedSamples, sampleRate);
this.setState({
trimStart: null,
trimEnd: null
});
}
handleUpdateTrim (trimStart, trimEnd) {
@@ -255,26 +255,16 @@ class SoundEditor extends React.Component {
effects.process((renderedBuffer, adjustedTrimStart, adjustedTrimEnd) => {
const samples = renderedBuffer.getChannelData(0);
const sampleRate = renderedBuffer.sampleRate;
this.submitNewSamples(samples, sampleRate).then(success => {
if (success) {
if (this.state.trimStart === null) {
this.handlePlay();
} else {
this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
}
const success = this.submitNewSamples(samples, sampleRate);
if (success) {
if (this.state.trimStart === null) {
this.handlePlay();
} else {
this.setState({trimStart: adjustedTrimStart, trimEnd: adjustedTrimEnd}, this.handlePlay);
}
});
}
});
}
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(),
@@ -286,22 +276,16 @@ class SoundEditor extends React.Component {
this.redoStack.push(this.getUndoItem());
const {samples, sampleRate, trimStart, trimEnd} = this.undoStack.pop();
if (samples) {
return this.submitNewSamples(samples, sampleRate, true).then(success => {
if (success) {
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
}
});
this.submitNewSamples(samples, sampleRate, true);
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
}
}
handleRedo () {
const {samples, sampleRate, trimStart, trimEnd} = this.redoStack.pop();
if (samples) {
this.undoStack.push(this.getUndoItem());
return this.submitNewSamples(samples, sampleRate, true).then(success => {
if (success) {
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
}
});
this.submitNewSamples(samples, sampleRate, true);
this.setState({trimStart: trimStart, trimEnd: trimEnd}, this.handlePlay);
}
}
handleCopy () {
@@ -327,39 +311,25 @@ class SoundEditor extends React.Component {
});
}
resampleBufferToRate (buffer, newRate) {
return new Promise((resolve, reject) => {
const sampleRateRatio = newRate / buffer.sampleRate;
const newLength = sampleRateRatio * buffer.samples.length;
let offlineContext;
// Try to use either OfflineAudioContext or webkitOfflineAudioContext to resample
// The constructors will throw if trying to resample at an unsupported rate
// (e.g. Safari/webkitOAC does not support lower than 44khz).
try {
if (window.OfflineAudioContext) {
offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
} else if (window.webkitOfflineAudioContext) {
offlineContext = new window.webkitOfflineAudioContext(1, newLength, newRate);
}
} catch {
// If no OAC available and downsampling by 2, downsample by dropping every other sample.
if (newRate === buffer.sampleRate / 2) {
return resolve(dropEveryOtherSample(buffer));
}
return reject('Could not resample');
return new Promise(resolve => {
if (window.OfflineAudioContext) {
const sampleRateRatio = newRate / buffer.sampleRate;
const newLength = sampleRateRatio * buffer.samples.length;
const offlineContext = new window.OfflineAudioContext(1, newLength, newRate);
const source = offlineContext.createBufferSource();
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
audioBuffer.getChannelData(0).set(buffer.samples);
source.buffer = audioBuffer;
source.connect(offlineContext.destination);
source.start();
offlineContext.startRendering();
offlineContext.oncomplete = ({renderedBuffer}) => {
resolve({
samples: renderedBuffer.getChannelData(0),
sampleRate: newRate
});
};
}
const source = offlineContext.createBufferSource();
const audioBuffer = offlineContext.createBuffer(1, buffer.samples.length, buffer.sampleRate);
audioBuffer.getChannelData(0).set(buffer.samples);
source.buffer = audioBuffer;
source.connect(offlineContext.destination);
source.start();
offlineContext.startRendering();
offlineContext.oncomplete = ({renderedBuffer}) => {
resolve({
samples: renderedBuffer.getChannelData(0),
sampleRate: newRate
});
};
});
}
paste () {
@@ -370,11 +340,8 @@ class SoundEditor extends React.Component {
const newSamples = new Float32Array(newLength);
newSamples.set(samples, 0);
newSamples.set(this.state.copyBuffer.samples, samples.length);
this.submitNewSamples(newSamples, this.props.sampleRate, false).then(success => {
if (success) {
this.handlePlay();
}
});
this.submitNewSamples(newSamples, this.props.sampleRate, false);
this.handlePlay();
} else {
// else replace the selection with the pasted sound
const trimStartSamples = this.state.trimStart * samples.length;
@@ -393,14 +360,11 @@ class SoundEditor extends React.Component {
const newDurationSeconds = newSamples.length / this.state.copyBuffer.sampleRate;
const adjustedTrimStart = trimStartSeconds / newDurationSeconds;
const adjustedTrimEnd = trimEndSeconds / newDurationSeconds;
this.submitNewSamples(newSamples, this.props.sampleRate, false).then(success => {
if (success) {
this.setState({
trimStart: adjustedTrimStart,
trimEnd: adjustedTrimEnd
}, this.handlePlay);
}
});
this.submitNewSamples(newSamples, this.props.sampleRate, false);
this.setState({
trimStart: adjustedTrimStart,
trimEnd: adjustedTrimEnd
}, this.handlePlay);
}
}
handlePaste () {
@@ -435,7 +399,6 @@ class SoundEditor extends React.Component {
name={this.props.name}
playhead={this.state.playhead}
setRef={this.setRef}
tooLoud={this.tooLoud()}
trimEnd={this.state.trimEnd}
trimStart={this.state.trimStart}
onChangeName={this.handleChangeName}
+2
View File
@@ -1,5 +1,6 @@
import GUI from './containers/gui.jsx';
import AppStateHOC from './lib/app-state-hoc.jsx';
import TitledHOC from './lib/titled-hoc.jsx';
import GuiReducer, {guiInitialState, guiMiddleware, initEmbedded, initFullScreen, initPlayer} from './reducers/gui';
import LocalesReducer, {localesInitialState, initLocale} from './reducers/locales';
import {ScratchPaintReducer} from 'scratch-paint';
@@ -16,6 +17,7 @@ const guiReducers = {
export {
GUI as default,
AppStateHOC,
TitledHOC,
setAppElement,
guiReducers,
guiInitialState,
+14 -14
View File
@@ -1,12 +1,12 @@
import 'get-float-time-domain-data';
import getUserMedia from 'get-user-media-promise';
import SharedAudioContext from './shared-audio-context.js';
import {computeRMS, computeChunkedRMS} from './audio-util.js';
import {computeRMS} from './audio-util.js';
class AudioRecorder {
constructor () {
this.audioContext = new SharedAudioContext();
this.bufferLength = 8192;
this.bufferLength = 1024;
this.userMediaStream = null;
this.mediaStreamSource = null;
@@ -51,7 +51,7 @@ class AudioRecorder {
this.userMediaStream = userMediaStream;
this.mediaStreamSource = this.audioContext.createMediaStreamSource(userMediaStream);
this.sourceNode = this.audioContext.createGain();
this.scriptProcessorNode = this.audioContext.createScriptProcessor(this.bufferLength, 1, 1);
this.scriptProcessorNode = this.audioContext.createScriptProcessor(this.bufferLength, 2, 2);
this.scriptProcessorNode.onaudioprocess = processEvent => {
if (this.recording && !this.disposed) {
@@ -68,9 +68,9 @@ class AudioRecorder {
const update = () => {
if (this.disposed) return;
requestAnimationFrame(update);
this.analyserNode.getFloatTimeDomainData(dataArray);
onUpdate(computeRMS(dataArray));
requestAnimationFrame(update);
};
requestAnimationFrame(update);
@@ -83,16 +83,7 @@ class AudioRecorder {
}
stop () {
const buffer = new Float32Array(this.buffers.length * this.bufferLength);
let offset = 0;
for (let i = 0; i < this.buffers.length; i++) {
const bufferChunk = this.buffers[i];
buffer.set(bufferChunk, offset);
offset += bufferChunk.length;
}
const chunkLevels = computeChunkedRMS(buffer);
const chunkLevels = this.buffers.map(buffer => computeRMS(buffer));
const maxRMS = Math.max.apply(null, chunkLevels);
const threshold = maxRMS / 8;
@@ -114,6 +105,15 @@ class AudioRecorder {
trimEnd = 1;
}
const buffer = new Float32Array(this.buffers.length * this.bufferLength);
let offset = 0;
for (let i = 0; i < this.buffers.length; i++) {
const bufferChunk = this.buffers[i];
buffer.set(bufferChunk, offset);
offset += bufferChunk.length;
}
return {
levels: chunkLevels,
samples: buffer,
+1 -49
View File
@@ -59,57 +59,9 @@ const encodeAndAddSoundToVM = function (vm, samples, sampleRate, name, callback)
});
};
/**
@typedef SoundBuffer
@type {Object}
@property {Float32Array} samples Array of audio samples
@property {number} sampleRate Audio sample rate
*/
/**
* Downsample the given buffer to try to reduce file size below SOUND_BYTE_LIMIT
* @param {SoundBuffer} buffer - Buffer to resample
* @param {function(SoundBuffer):Promise<SoundBuffer>} resampler - resampler function
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
*/
const downsampleIfNeeded = (buffer, resampler) => {
const {samples, sampleRate} = buffer;
const duration = samples.length / sampleRate;
const encodedByteLength = samples.length * 2; /* bitDepth 16 bit */
// Resolve immediately if already within byte limit
if (encodedByteLength < SOUND_BYTE_LIMIT) {
return Promise.resolve({samples, sampleRate});
}
// If encodeable at 22khz, resample and call submitNewSamples again
if (duration * 22050 * 2 < SOUND_BYTE_LIMIT) {
return resampler({samples, sampleRate}, 22050);
}
// Cannot save this sound at 22khz, refuse to edit
// In the future we could introduce further compression here
return Promise.reject('Sound too large to save, refusing to edit');
};
/**
* Drop every other sample of an audio buffer as a last-resort way of downsampling.
* @param {SoundBuffer} buffer - Buffer to resample
* @returns {SoundBuffer} Downsampled buffer with half the sample rate
*/
const dropEveryOtherSample = buffer => {
const newLength = Math.floor(buffer.samples.length / 2);
const newSamples = new Float32Array(newLength);
for (let i = 0; i < newLength; i++) {
newSamples[i] = buffer.samples[i * 2];
}
return {
samples: newSamples,
sampleRate: buffer.sampleRate / 2
};
};
export {
computeRMS,
computeChunkedRMS,
encodeAndAddSoundToVM,
downsampleIfNeeded,
dropEveryOtherSample
SOUND_BYTE_LIMIT
};
+6 -11
View File
@@ -3,20 +3,15 @@ const jpegThumbnail = dataUrl => new Promise((resolve, reject) => {
image.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const maxDimension = 96; // 3x the maximum displayed size of 32px
if (image.height > image.width) {
canvas.height = maxDimension;
canvas.width = (maxDimension / image.height) * image.width;
} else {
canvas.width = maxDimension;
canvas.height = (maxDimension / image.width) * image.height;
}
// TODO we may want to draw to a smaller, fixed size to optimize file size
canvas.width = image.width;
canvas.height = image.height;
ctx.fillStyle = 'white'; // Create white background, since jpeg doesn't have transparency
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
ctx.drawImage(image, 0, 0);
// TODO we can play with the `quality` option here to optimize file size
resolve(canvas.toDataURL('image/jpeg', 0.92 /* quality */)); // Default quality is 0.92
};
image.onerror = err => {
+12 -16
View File
@@ -54,10 +54,13 @@ const cloudManagerHOC = function (WrappedComponent) {
canUseCloud (props) {
return !!(props.cloudHost && props.username && props.vm && props.projectId && props.hasCloudPermission);
}
shouldNotModifyCloudData (props) {
return (props.hasEverEnteredEditor && !props.canSave);
}
shouldConnect (props) {
return !this.isConnected() && this.canUseCloud(props) &&
props.isShowingWithId && props.vm.runtime.hasCloudData() &&
props.canModifyCloudData;
!this.shouldNotModifyCloudData(props);
}
shouldDisconnect (props, prevProps) {
return this.isConnected() &&
@@ -67,7 +70,7 @@ const cloudManagerHOC = function (WrappedComponent) {
(props.projectId !== prevProps.projectId) ||
(props.username !== prevProps.username) ||
// Editing someone else's project
!props.canModifyCloudData
this.shouldNotModifyCloudData(props)
);
}
isConnected () {
@@ -99,11 +102,11 @@ const cloudManagerHOC = function (WrappedComponent) {
render () {
const {
/* eslint-disable no-unused-vars */
canModifyCloudData,
cloudHost,
projectId,
username,
hasCloudPermission,
hasEverEnteredEditor,
isShowingWithId,
onShowCloudInfo,
/* eslint-enable no-unused-vars */
@@ -121,30 +124,23 @@ const cloudManagerHOC = function (WrappedComponent) {
}
CloudManager.propTypes = {
canModifyCloudData: PropTypes.bool.isRequired,
canSave: PropTypes.bool.isRequired,
cloudHost: PropTypes.string,
hasCloudPermission: PropTypes.bool,
isShowingWithId: PropTypes.bool.isRequired,
hasEverEnteredEditor: PropTypes.bool,
isShowingWithId: PropTypes.bool,
onShowCloudInfo: PropTypes.func,
projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
username: PropTypes.string,
vm: PropTypes.instanceOf(VM).isRequired
};
CloudManager.defaultProps = {
cloudHost: null,
hasCloudPermission: false,
onShowCloudInfo: () => {},
username: null
};
const mapStateToProps = (state, ownProps) => {
const mapStateToProps = state => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
hasEverEnteredEditor: state.scratchGui.mode.hasEverEnteredEditor,
isShowingWithId: getIsShowingWithId(loadingState),
projectId: state.scratchGui.projectState.projectId,
// if you're editing someone else's project, you can't modify cloud data
canModifyCloudData: (!state.scratchGui.mode.hasEverEnteredEditor || ownProps.canSave)
projectId: state.scratchGui.projectState.projectId
};
};
+7 -19
View File
@@ -8,23 +8,11 @@ export default (filename, blob) => {
return;
}
if ('download' in HTMLAnchorElement.prototype) {
const url = window.URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.download = filename;
downloadLink.type = blob.type;
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(url);
} else {
// iOS Safari, open a new page and set href to data-uri
let popup = window.open('', '_blank');
const reader = new FileReader();
reader.onloadend = function () {
popup.location.href = reader.result;
popup = null;
};
reader.readAsDataURL(blob);
}
const url = window.URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.download = filename;
downloadLink.type = blob.type;
downloadLink.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(downloadLink);
};
+1 -1
View File
@@ -78,7 +78,7 @@ const DropAreaHOC = function (dragTypes) {
}
}
render () {
const componentProps = omit(this.props, ['onDrop', 'dragInfo', 'componentRef']);
const componentProps = omit(this.props, ['onDrop', 'dragInfo']);
return (
<WrappedComponent
containerRef={this.setRef}
+7 -7
View File
@@ -52,7 +52,7 @@
},
{
"name": "Basketball 1",
"md5": "ae21eac3d1814aee1d37ae82ea287816.svg",
"md5": "e494c4f44897d94e0541f7036a302449.svg",
"type": "backdrop",
"tags": [
"sports",
@@ -178,7 +178,7 @@
},
{
"name": "Blue Sky 2 ",
"md5": "8eb8790be5507fdccf73e7c1570bbbab.svg",
"md5": "5a906c2f272b77f59f0ef207857a8b3a.svg",
"type": "backdrop",
"tags": [
"outdoors",
@@ -286,7 +286,7 @@
},
{
"name": "Circles",
"md5": "c9847be305920807c5597d81576dd0c4.svg",
"md5": "4e29033ec2b891a8f1ca21242811d403.svg",
"type": "backdrop",
"tags": [
"patterns"
@@ -491,7 +491,7 @@
},
{
"name": "Hearts",
"md5": "f98526ccb0eec3ac7d6c8f8ab502825e.svg",
"md5": "26d3418b2fbc1af2c5fea82e1c3df1db.svg",
"type": "backdrop",
"tags": [
"patterns"
@@ -1050,7 +1050,7 @@
},
{
"name": "Underwater 1",
"md5": "d3344650f594bcecdf46aa4a9441badd.svg",
"md5": "fb907f72b310acc8b95cbf2d2cccabc9.svg",
"type": "backdrop",
"tags": [
"ocean",
@@ -1175,7 +1175,7 @@
},
{
"name": "Witch House",
"md5": "30085b2d27beb5acdbe895d8b3e64b04.svg",
"md5": "597b9a9813fe5d8d387283138a0b8f2b.svg",
"type": "backdrop",
"tags": [
"fantasy",
@@ -1190,7 +1190,7 @@
},
{
"name": "Woods",
"md5": "f3eb165d6f3fd23370f97079f2e631bf.svg",
"md5": "d26cfd278999c9e37d6af75c296a58df.svg",
"type": "backdrop",
"tags": [
"fantasy",
File diff suppressed because it is too large. Load diff
+36 -90
View File
@@ -3,7 +3,7 @@ import introMove from './steps/intro-move.en.gif';
import introSayHello from './steps/intro-say-hello.en.gif';
// Text to Speech
import speechAddExtension from './steps/talking-1-add-extension.en.gif';
import speechAddExtension from './steps/speech-add-extension.en.gif';
import speechSaySomething from './steps/speech-say-something.en.gif';
import speechSetVoice from './steps/speech-set-voice.en.gif';
import speechMoveAround from './steps/speech-move-around.en.gif';
@@ -15,12 +15,12 @@ import speechSpin from './steps/speech-spin.en.gif';
import speechGrowShrink from './steps/speech-grow-shrink.en.gif';
// Cartoon Network
import cnShowCharacter from './steps/cn-show-character.gif';
import cnShowCharacter from './steps/cn-show-character.en.gif';
import cnSay from './steps/cn-say.en.gif';
import cnGlide from './steps/cn-glide.en.gif';
import cnPickSprite from './steps/cn-pick-sprite.gif';
import cnPickSprite from './steps/cn-pick-sprite.en.gif';
import cnCollect from './steps/cn-collect.en.gif';
import cnVariable from './steps/add-variable.en.gif';
import cnVariable from './steps/cn-variable.en.gif';
import cnScore from './steps/cn-score.en.gif';
import cnBackdrop from './steps/cn-backdrop.en.gif';
@@ -43,20 +43,20 @@ import musicMakeBeat from './steps/music-make-beat.en.gif';
import musicMakeBeatbox from './steps/music-make-beatbox.en.gif';
// Chase-Game
import chaseGameAddBackdrop from './steps/chase-game-add-backdrop.gif';
import chaseGameAddBackdrop from './steps/chase-game-add-backdrop.en.gif';
import chaseGameAddSprite1 from './steps/chase-game-add-sprite1.en.gif';
import chaseGameRightLeft from './steps/chase-game-right-left.en.gif';
import chaseGameUpDown from './steps/chase-game-up-down.en.gif';
import chaseGameAddSprite2 from './steps/chase-game-add-sprite2.en.gif';
import chaseGameMoveRandomly from './steps/chase-game-move-randomly.en.gif';
import chaseGamePlaySound from './steps/chase-game-play-sound.en.gif';
import chaseGameAddVariable from './steps/add-variable.en.gif';
import chaseGameAddVariable from './steps/chase-game-add-variable.en.gif';
import chaseGameChangeScore from './steps/chase-game-change-score.en.gif';
// Clicker-Game (Pop Game)
import popGamePickSprite from './steps/pop-game-pick-sprite.en.gif';
import popGamePlaySound from './steps/pop-game-play-sound.en.gif';
import popGameAddScore from './steps/add-variable.en.gif';
import popGameAddScore from './steps/pop-game-add-score.en.gif';
import popGameChangeScore from './steps/pop-game-change-score.en.gif';
import popGameRandomPosition from './steps/pop-game-random-position.en.gif';
import popGameChangeColor from './steps/pop-game-change-color.en.gif';
@@ -73,22 +73,22 @@ import animateCharJump from './steps/animate-char-jump.en.gif';
import animateCharChangeColor from './steps/animate-char-change-color.en.gif';
// Tell A Story
import storyPickBackdrop from './steps/story-pick-backdrop.gif';
import storyPickSprite from './steps/story-pick-sprite.gif';
import storyPickBackdrop from './steps/story-pick-backdrop.en.gif';
import storyPickSprite from './steps/story-pick-sprite.en.gif';
import storySaySomething from './steps/story-say-something.en.gif';
import storyPickSprite2 from './steps/story-pick-sprite2.gif';
import storyPickSprite2 from './steps/story-pick-sprite2.en.gif';
import storyFlip from './steps/story-flip.en.gif';
import storyConversation from './steps/story-conversation.en.gif';
import storyPickBackdrop2 from './steps/story-pick-backdrop2.gif';
import storyPickBackdrop2 from './steps/story-pick-backdrop2.en.gif';
import storySwitchBackdrop from './steps/story-switch-backdrop.en.gif';
import storyHideCharacter from './steps/story-hide-character.en.gif';
import storyShowCharacter from './steps/story-show-character.en.gif';
// Video Sensing
import videoAddExtension from './steps/video-add-extension.en.gif';
import videoPet from './steps/video-pet.en.png';
import videoAnimate from './steps/video-animate.en.png';
import videoPop from './steps/video-pop.en.png';
import videoPet from './steps/video-pet.en.gif';
import videoAnimate from './steps/video-animate.en.gif';
import videoPop from './steps/video-pop.en.gif';
// Make it Fly
import flyChooseBackdrop from './steps/fly-choose-backdrop.en.gif';
@@ -98,7 +98,7 @@ import flyMoveArrows from './steps/fly-make-interactive.en.gif';
import flyChooseObject from './steps/fly-object-to-collect.en.gif';
import flyFlyingObject from './steps/fly-flying-heart.en.gif';
import flySelectFlyingSprite from './steps/fly-select-flyer.en.jpg';
import flyAddScore from './steps/add-variable.en.gif';
import flyAddScore from './steps/fly-add-score.en.gif';
import flyKeepScore from './steps/fly-keep-score.en.gif';
import flyAddScenery from './steps/fly-choose-scenery.en.gif';
import flyMoveScenery from './steps/fly-move-scenery.en.gif';
@@ -106,32 +106,32 @@ import flySwitchLooks from './steps/fly-switch-costume.en.gif';
// Pong
import pongAddBackdrop from './steps/pong-add-backdrop.en.png';
import pongAddBallSprite from './steps/pong-add-ball-sprite.png';
import pongAddBallSprite from './steps/pong-add-ball-sprite.en.png';
import pongBounceAround from './steps/pong-bounce-around.en.gif';
import pongAddPaddle from './steps/pong-add-a-paddle.en.gif';
import pongMoveThePaddle from './steps/pong-move-the-paddle.en.gif';
import pongSelectBallSprite from './steps/pong-select-ball.png';
import pongSelectBallSprite from './steps/pong-select-ball.en.png';
import pongAddMoreCodeToBall from './steps/pong-add-code-to-ball.en.png';
import pongAddAScore from './steps/add-variable.en.gif';
import pongAddAScore from './steps/pong-add-score.en.gif';
import pongChooseScoreFromMenu from './steps/pong-choose-score.en.png';
import pongInsertChangeScoreBlock from './steps/pong-insert-change-score.en.png';
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.png';
import imagineChooseSprite from './steps/imagine-choose-any-sprite.png';
import imagineChooseBackdrop from './steps/imagine-choose-backdrop.en.png';
import imagineChooseSprite from './steps/imagine-choose-any-sprite.en.png';
import imagineFlyAround from './steps/imagine-fly-around.en.png';
import imagineChooseAnotherSprite from './steps/imagine-choose-another-sprite.png';
import imagineChooseAnotherSprite from './steps/imagine-choose-another-sprite.en.png';
import imagineLeftRight from './steps/imagine-left-right.en.png';
import imagineUpDown from './steps/imagine-up-down.en.png';
import imagineChangeCostumes from './steps/imagine-change-costumes.en.png';
import imagineGlideToPoint from './steps/imagine-glide-to-point.en.png';
import imagineGrowShrink from './steps/imagine-grow-shrink.en.png';
import imagineChooseAnotherBackdrop from './steps/imagine-choose-another-backdrop.png';
import imagineChooseAnotherBackdrop from './steps/imagine-choose-another-backdrop.en.png';
import imagineSwitchBackdrops from './steps/imagine-switch-backdrops.en.gif';
import imagineRecordASound from './steps/imagine-record-a-sound.en.gif';
import imagineChooseSound from './steps/imagine-choose-sound.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.png';
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.png';
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};
+91 -306
View File
@@ -3,227 +3,119 @@ import introMove from './steps/intro-move.es.gif';
import introSayHello from './steps/intro-say-hello.es.gif';
// Text to Speech
import speechAddExtension from './steps/speech-add-extension.es.gif';
import speechSaySomething from './steps/speech-say-something.es.png';
import speechSetVoice from './steps/speech-set-voice.es.png';
import speechMoveAround from './steps/speech-move-around.es.png';
import speechAddBackdrop from './steps/speech-add-backdrop.gif';
import speechAddSprite from './steps/speech-add-sprite.gif';
import speechSong from './steps/speech-song.es.png';
import speechChangeColor from './steps/speech-change-color.es.png';
import speechSpin from './steps/speech-spin.es.png';
import speechGrowShrink from './steps/speech-grow-shrink.es.png';
// Cartoon Network
import cnShowCharacter from './steps/cn-show-character.gif';
import cnSay from './steps/cn-say.es.png';
import cnGlide from './steps/cn-glide.es.png';
import cnPickSprite from './steps/cn-pick-sprite.gif';
import cnCollect from './steps/cn-collect.es.png';
import cnVariable from './steps/add-variable.es.gif';
import cnScore from './steps/cn-score.es.png';
import cnBackdrop from './steps/cn-backdrop.es.png';
// import speechAddExtension from './steps/speech-add-extension.es.gif';
// import speechSaySomething from './steps/speech-say-something.es.gif';
// import speechSetVoice from './steps/speech-set-voice.es.gif';
// import speechMoveAround from './steps/speech-move-around.es.gif';
// import speechAddBackdrop from './steps/speech-add-backdrop.es.gif';
// import speechAddSprite from './steps/speech-add-sprite.es.gif';
// import speechSong from './steps/speech-song.es.gif';
// import speechChangeColor from './steps/speech-change-color.es.gif';
// import speechSpin from './steps/speech-spin.es.gif';
// import speechGrowShrink from './steps/speech-grow-shrink.es.gif';
//
// // Cartoon Network
// import cnShowCharacter from './steps/cn-show-character.es.gif';
// import cnSay from './steps/cn-say.es.gif';
// import cnGlide from './steps/cn-glide.es.gif';
// import cnPickSprite from './steps/cn-pick-sprite.es.gif';
// import cnCollect from './steps/cn-collect.es.gif';
// import cnVariable from './steps/cn-variable.es.gif';
// import cnScore from './steps/cn-score.es.gif';
// import cnBackdrop from './steps/cn-backdrop.es.gif';
// Add sprite
import addSprite from './steps/add-sprite.gif';
import addSprite from './steps/add-sprite.es.gif';
// Animate a name
import namePickLetter from './steps/name-pick-letter.gif';
import namePlaySound from './steps/name-play-sound.es.png';
import namePickLetter2 from './steps/name-pick-letter2.gif';
import nameChangeColor from './steps/name-change-color.es.png';
import nameSpin from './steps/name-spin.es.png';
import nameGrow from './steps/name-grow.es.png';
import namePickLetter from './steps/name-pick-letter.es.gif';
import namePlaySound from './steps/name-play-sound.es.gif';
import namePickLetter2 from './steps/name-pick-letter2.es.gif';
import nameChangeColor from './steps/name-change-color.es.gif';
import nameSpin from './steps/name-spin.es.gif';
import nameGrow from './steps/name-grow.es.gif';
// Make Music
import musicPickInstrument from './steps/music-pick-instrument.gif';
import musicPlaySound from './steps/music-play-sound.es.png';
import musicMakeSong from './steps/music-make-song.es.png';
import musicMakeBeat from './steps/music-make-beat.es.png';
import musicMakeBeatbox from './steps/music-make-beatbox.es.png';
// Make-Music
import musicPickInstrument from './steps/music-pick-instrument.es.gif';
import musicPlaySound from './steps/music-play-sound.es.gif';
import musicMakeSong from './steps/music-make-song.es.gif';
import musicMakeBeat from './steps/music-make-beat.es.gif';
import musicMakeBeatbox from './steps/music-make-beatbox.es.gif';
// Chase-Game
import chaseGameAddBackdrop from './steps/chase-game-add-backdrop.gif';
import chaseGameAddSprite1 from './steps/chase-game-add-sprite1.gif';
import chaseGameRightLeft from './steps/chase-game-right-left.es.png';
import chaseGameUpDown from './steps/chase-game-up-down.es.png';
import chaseGameAddSprite2 from './steps/chase-game-add-sprite2.gif';
import chaseGameMoveRandomly from './steps/chase-game-move-randomly.es.png';
import chaseGamePlaySound from './steps/chase-game-play-sound.es.png';
import chaseGameAddVariable from './steps/add-variable.es.gif';
import chaseGameChangeScore from './steps/chase-game-change-score.es.png';
import chaseGameAddBackdrop from './steps/chase-game-add-backdrop.es.gif';
import chaseGameAddSprite1 from './steps/chase-game-add-sprite1.es.gif';
import chaseGameRightLeft from './steps/chase-game-right-left.es.gif';
import chaseGameUpDown from './steps/chase-game-up-down.es.gif';
import chaseGameAddSprite2 from './steps/chase-game-add-sprite2.es.gif';
import chaseGameMoveRandomly from './steps/chase-game-move-randomly.es.gif';
import chaseGamePlaySound from './steps/chase-game-play-sound.es.gif';
import chaseGameAddVariable from './steps/chase-game-add-variable.es.gif';
import chaseGameChangeScore from './steps/chase-game-change-score.es.gif';
// Clicker-Game (Pop Game)
import popGamePickSprite from './steps/pop-game-pick-sprite.gif';
import popGamePlaySound from './steps/pop-game-play-sound.es.png';
import popGameAddScore from './steps/add-variable.es.gif';
import popGameChangeScore from './steps/pop-game-change-score.es.png';
import popGameRandomPosition from './steps/pop-game-random-position.es.png';
import popGameChangeColor from './steps/pop-game-change-color.es.png';
import popGameResetScore from './steps/pop-game-reset-score.es.png';
import popGamePickSprite from './steps/pop-game-pick-sprite.es.gif';
import popGamePlaySound from './steps/pop-game-play-sound.es.gif';
import popGameAddScore from './steps/pop-game-add-score.es.gif';
import popGameChangeScore from './steps/pop-game-change-score.es.gif';
import popGameRandomPosition from './steps/pop-game-random-position.es.gif';
import popGameChangeColor from './steps/pop-game-change-color.es.gif';
import popGameResetScore from './steps/pop-game-reset-score.es.gif';
// Animate A Character
import animateCharPickBackdrop from './steps/animate-char-pick-backdrop.gif';
import animateCharPickSprite from './steps/animate-char-pick-sprite.gif';
import animateCharSaySomething from './steps/animate-char-say-something.es.png';
import animateCharAddSound from './steps/animate-char-add-sound.es.png';
import animateCharTalk from './steps/animate-char-talk.es.png';
import animateCharMove from './steps/animate-char-move.es.png';
import animateCharJump from './steps/animate-char-jump.es.png';
import animateCharChangeColor from './steps/animate-char-change-color.es.png';
import animateCharPickBackdrop from './steps/animate-char-pick-backdrop.es.gif';
import animateCharPickSprite from './steps/animate-char-pick-sprite.es.gif';
import animateCharSaySomething from './steps/animate-char-say-something.es.gif';
import animateCharAddSound from './steps/animate-char-add-sound.es.gif';
import animateCharTalk from './steps/animate-char-talk.es.gif';
import animateCharMove from './steps/animate-char-move.es.gif';
import animateCharJump from './steps/animate-char-jump.es.gif';
import animateCharChangeColor from './steps/animate-char-change-color.es.gif';
// Tell A Story
import storyPickBackdrop from './steps/story-pick-backdrop.gif';
import storyPickSprite from './steps/story-pick-sprite.gif';
import storySaySomething from './steps/story-say-something.es.png';
import storyPickSprite2 from './steps/story-pick-sprite2.gif';
import storyPickBackdrop from './steps/story-pick-backdrop.es.gif';
import storyPickSprite from './steps/story-pick-sprite.es.gif';
import storySaySomething from './steps/story-say-something.es.gif';
import storyPickSprite2 from './steps/story-pick-sprite2.es.gif';
import storyFlip from './steps/story-flip.es.gif';
import storyConversation from './steps/story-conversation.es.png';
import storyPickBackdrop2 from './steps/story-pick-backdrop2.gif';
import storySwitchBackdrop from './steps/story-switch-backdrop.es.png';
import storyHideCharacter from './steps/story-hide-character.es.png';
import storyShowCharacter from './steps/story-show-character.es.png';
import storyConversation from './steps/story-conversation.es.gif';
import storyPickBackdrop2 from './steps/story-pick-backdrop2.es.gif';
import storySwitchBackdrop from './steps/story-switch-backdrop.es.gif';
import storyHideCharacter from './steps/story-hide-character.es.gif';
import storyShowCharacter from './steps/story-show-character.es.gif';
// Video Sensing
import videoAddExtension from './steps/video-add-extension.es.gif';
import videoPet from './steps/video-pet.es.png';
import videoAnimate from './steps/video-animate.es.png';
import videoPop from './steps/video-pop.es.png';
// Make it Fly
import flyChooseBackdrop from './steps/fly-choose-backdrop.gif';
import flyChooseCharacter from './steps/fly-choose-character.png';
import flySaySomething from './steps/fly-say-something.es.png';
import flyMoveArrows from './steps/fly-make-interactive.es.png';
import flyChooseObject from './steps/fly-object-to-collect.png';
import flyFlyingObject from './steps/fly-flying-heart.es.png';
import flySelectFlyingSprite from './steps/fly-select-flyer.png';
import flyAddScore from './steps/add-variable.es.gif';
import flyKeepScore from './steps/fly-keep-score.es.png';
import flyAddScenery from './steps/fly-choose-scenery.gif';
import flyMoveScenery from './steps/fly-move-scenery.es.png';
import flySwitchLooks from './steps/fly-switch-costume.es.png';
// Pong
import pongAddBackdrop from './steps/pong-add-backdrop.png';
import pongAddBallSprite from './steps/pong-add-ball-sprite.png';
import pongBounceAround from './steps/pong-bounce-around.es.png';
import pongAddPaddle from './steps/pong-add-a-paddle.gif';
import pongMoveThePaddle from './steps/pong-move-the-paddle.es.png';
import pongSelectBallSprite from './steps/pong-select-ball.png';
import pongAddMoreCodeToBall from './steps/pong-add-code-to-ball.es.png';
import pongAddAScore from './steps/add-variable.es.gif';
import pongChooseScoreFromMenu from './steps/pong-choose-score.es.png';
import pongInsertChangeScoreBlock from './steps/pong-insert-change-score.es.png';
import pongResetScore from './steps/pong-reset-score.es.png';
import pongAddLineSprite from './steps/pong-add-line.gif';
import pongGameOver from './steps/pong-game-over.es.png';
// Imagine a World
import imagineTypeWhatYouWant from './steps/imagine-type-what-you-want.es.png';
import imagineClickGreenFlag from './steps/imagine-click-green-flag.es.png';
import imagineChooseBackdrop from './steps/imagine-choose-backdrop.png';
import imagineChooseSprite from './steps/imagine-choose-any-sprite.png';
import imagineFlyAround from './steps/imagine-fly-around.es.png';
import imagineChooseAnotherSprite from './steps/imagine-choose-another-sprite.png';
import imagineLeftRight from './steps/imagine-left-right.es.png';
import imagineUpDown from './steps/imagine-up-down.es.png';
import imagineChangeCostumes from './steps/imagine-change-costumes.es.png';
import imagineGlideToPoint from './steps/imagine-glide-to-point.es.png';
import imagineGrowShrink from './steps/imagine-grow-shrink.es.png';
import imagineChooseAnotherBackdrop from './steps/imagine-choose-another-backdrop.png';
import imagineSwitchBackdrops from './steps/imagine-switch-backdrops.es.png';
import imagineRecordASound from './steps/imagine-record-a-sound.es.gif';
import imagineChooseSound from './steps/imagine-choose-sound.es.png';
// Add a Backdrop
import addBackdrop from './steps/add-backdrop.png';
// Add Effects
import addEffects from './steps/add-effects.es.png';
// Hide and Show
import hideAndShow from './steps/hide-show.es.png';
// Switch Costumes
import switchCostumes from './steps/switch-costumes.es.png';
// Change Size
import changeSize from './steps/change-size.es.png';
// Spin
import spinTurn from './steps/spin-turn.es.png';
import spinPointInDirection from './steps/spin-point-in-direction.es.png';
// Record a Sound
import recordASoundSoundsTab from './steps/record-a-sound-sounds-tab.es.png';
import recordASoundClickRecord from './steps/record-a-sound-click-record.es.png';
import recordASoundPressRecordButton from './steps/record-a-sound-press-record-button.es.png';
import recordASoundChooseSound from './steps/record-a-sound-choose-sound.es.png';
import recordASoundPlayYourSound from './steps/record-a-sound-play-your-sound.es.png';
// Use Arrow Keys
import moveArrowKeysLeftRight from './steps/move-arrow-keys-left-right.es.png';
import moveArrowKeysUpDown from './steps/move-arrow-keys-up-down.es.png';
// Glide Around
import glideAroundBackAndForth from './steps/glide-around-back-and-forth.es.png';
import glideAroundPoint from './steps/glide-around-point.es.png';
// Code a Cartoon
import codeCartoonSaySomething from './steps/code-cartoon-01-say-something.es.png';
import codeCartoonAnimate from './steps/code-cartoon-02-animate.es.png';
import codeCartoonSelectDifferentCharacter from './steps/code-cartoon-03-select-different-character.png';
import codeCartoonUseMinusSign from './steps/code-cartoon-04-use-minus-sign.es.png';
import codeCartoonGrowShrink from './steps/code-cartoon-05-grow-shrink.es.png';
import codeCartoonSelectDifferentCharacter2 from './steps/code-cartoon-06-select-another-different-character.png';
import codeCartoonJump from './steps/code-cartoon-07-jump.es.png';
import codeCartoonChangeScenes from './steps/code-cartoon-08-change-scenes.es.png';
import codeCartoonGlideAround from './steps/code-cartoon-09-glide-around.es.png';
import codeCartoonChangeCostumes from './steps/code-cartoon-10-change-costumes.es.png';
import codeCartoonChooseMoreCharacters from './steps/code-cartoon-11-choose-more-characters.png';
// Talking Tales
import talesAddExtension from './steps/speech-add-extension.es.gif';
import talesChooseSprite from './steps/talking-2-choose-sprite.png';
import talesSaySomething from './steps/talking-3-say-something.es.png';
import talesChooseBackdrop from './steps/talking-4-choose-backdrop.png';
import talesSwitchBackdrop from './steps/talking-5-switch-backdrop.es.png';
import talesChooseAnotherSprite from './steps/talking-6-choose-another-sprite.png';
import talesMoveAround from './steps/talking-7-move-around.es.png';
import talesChooseAnotherBackdrop from './steps/talking-8-choose-another-backdrop.png';
import talesAnimateTalking from './steps/talking-9-animate.es.png';
import talesChooseThirdBackdrop from './steps/talking-10-choose-third-backdrop.png';
import talesChooseSound from './steps/talking-11-choose-sound.es.gif';
import talesDanceMoves from './steps/talking-12-dance-moves.es.png';
import talesAskAnswer from './steps/talking-13-ask-and-answer.es.png';
import videoPet from './steps/video-pet.es.gif';
import videoAnimate from './steps/video-animate.es.gif';
import videoPop from './steps/video-pop.es.gif';
const esImages = {
// Intro
introMove: introMove,
introSayHello: introSayHello,
// Text to Speech
speechAddExtension: speechAddExtension,
speechSaySomething: speechSaySomething,
speechSetVoice: speechSetVoice,
speechMoveAround: speechMoveAround,
speechAddBackdrop: speechAddBackdrop,
speechAddSprite: speechAddSprite,
speechSong: speechSong,
speechChangeColor: speechChangeColor,
speechSpin: speechSpin,
speechGrowShrink: speechGrowShrink,
// Cartoon Network
cnShowCharacter: cnShowCharacter,
cnSay: cnSay,
cnGlide: cnGlide,
cnPickSprite: cnPickSprite,
cnCollect: cnCollect,
cnVariable: cnVariable,
cnScore: cnScore,
cnBackdrop: cnBackdrop,
// // Text to Speech
// speechAddExtension: speechAddExtension,
// speechSaySomething: speechSaySomething,
// speechSetVoice: speechSetVoice,
// speechMoveAround: speechMoveAround,
// speechAddBackdrop: speechAddBackdrop,
// speechAddSprite: speechAddSprite,
// speechSong: speechSong,
// speechChangeColor: speechChangeColor,
// speechSpin: speechSpin,
// speechGrowShrink: speechGrowShrink,
//
// // Cartoon Network
// cnShowCharacter: cnShowCharacter,
// cnSay: cnSay,
// cnGlide: cnGlide,
// cnPickSprite: cnPickSprite,
// cnCollect: cnCollect,
// cnVariable: cnVariable,
// cnScore: cnScore,
// cnBackdrop: cnBackdrop,
// Add sprite
addSprite: addSprite,
@@ -289,115 +181,8 @@ const esImages = {
videoAddExtension: videoAddExtension,
videoPet: videoPet,
videoAnimate: videoAnimate,
videoPop: videoPop,
videoPop: videoPop
// Make it Fly
flyChooseBackdrop: flyChooseBackdrop,
flyChooseCharacter: flyChooseCharacter,
flySaySomething: flySaySomething,
flyMoveArrows: flyMoveArrows,
flyChooseObject: flyChooseObject,
flyFlyingObject: flyFlyingObject,
flySelectFlyingSprite: flySelectFlyingSprite,
flyAddScore: flyAddScore,
flyKeepScore: flyKeepScore,
flyAddScenery: flyAddScenery,
flyMoveScenery: flyMoveScenery,
flySwitchLooks: flySwitchLooks,
// Pong
pongAddBackdrop: pongAddBackdrop,
pongAddBallSprite: pongAddBallSprite,
pongBounceAround: pongBounceAround,
pongAddPaddle: pongAddPaddle,
pongMoveThePaddle: pongMoveThePaddle,
pongSelectBallSprite: pongSelectBallSprite,
pongAddMoreCodeToBall: pongAddMoreCodeToBall,
pongAddAScore: pongAddAScore,
pongChooseScoreFromMenu: pongChooseScoreFromMenu,
pongInsertChangeScoreBlock: pongInsertChangeScoreBlock,
pongResetScore: pongResetScore,
pongAddLineSprite: pongAddLineSprite,
pongGameOver: pongGameOver,
// Imagine a World
imagineTypeWhatYouWant: imagineTypeWhatYouWant,
imagineClickGreenFlag: imagineClickGreenFlag,
imagineChooseBackdrop: imagineChooseBackdrop,
imagineChooseSprite: imagineChooseSprite,
imagineFlyAround: imagineFlyAround,
imagineChooseAnotherSprite: imagineChooseAnotherSprite,
imagineLeftRight: imagineLeftRight,
imagineUpDown: imagineUpDown,
imagineChangeCostumes: imagineChangeCostumes,
imagineGlideToPoint: imagineGlideToPoint,
imagineGrowShrink: imagineGrowShrink,
imagineChooseAnotherBackdrop: imagineChooseAnotherBackdrop,
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,
recordASoundPressRecordButton: recordASoundPressRecordButton,
recordASoundChooseSound: recordASoundChooseSound,
recordASoundPlayYourSound: recordASoundPlayYourSound,
// Use Arrow Keys
moveArrowKeysLeftRight: moveArrowKeysLeftRight,
moveArrowKeysUpDown: moveArrowKeysUpDown,
// 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
};
export {esImages};
File diff suppressed because it is too large. Load diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 718 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

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