Compare commits
263
Commits
No files matched your search
@@ -12,6 +12,7 @@
|
||||
*.svg binary
|
||||
|
||||
# Prefer LF for most file types
|
||||
*.css text eol=lf
|
||||
*.frag text eol=lf
|
||||
*.htm text eol=lf
|
||||
*.html text eol=lf
|
||||
|
||||
+56
-3
@@ -1,11 +1,64 @@
|
||||
## Contributing
|
||||
The development of Scratch is an ongoing process, and we love to have people in the Scratch and open source communities help us along the way.
|
||||
|
||||
If you're interested in contributing, please take a look at the [issues](https://github.com/LLK/scratch-gui/issues) on this repository.
|
||||
Two great ways of helping are by identifying bugs and documenting them as issues, or fixing issues and creating pull requests. When looking for bugs to fix, please look for the ["Help Wanted" label](https://github.com/LLK/scratch-gui/issues?q=label%3A%22help+wanted%22). Bugs with this label have been specifically set aside for Open Source contributors. Issues without the label can also be worked on but we ask that you comment on the issue prior to starting work. When submitting pull requests please be patient -- it can take a while to find time to review them. The organization and class structures can't be radically changed without significant coordination and collaboration from the Scratch Team, so these types of changes should be avoided.
|
||||
### Ways to Help
|
||||
|
||||
It's been said that the Scratch Team spends about one hour of design discussion for every pixel in Scratch, but some think that estimate is a little low. While we welcome suggestions for new features in our [suggestions forum](https://scratch.mit.edu/discuss/1/) (especially ones that come with mockups), we are unlikely to accept PRs with new features that haven't been thought through and discussed as a group. Why? Because we have a strong belief in the value of keeping things simple for new users. To learn more about our design philosophy, see [the Scratch Developers page](https://scratch.mit.edu/developers), or [this paper](http://web.media.mit.edu/~mres/papers/Scratch-CACM-final.pdf).
|
||||
* **Documenting bugs**
|
||||
* If you've identified a bug in Scratch you should first check to see if it's been filed as an issue, if not you can file one. Make sure you follow the issue template.
|
||||
* It's important that we can consistently reproduce issues. When writing an issue, be sure to follow our [reproduction step guidelines](https://github.com/LLK/scratch-gui/wiki/Writing-good-repro-steps).
|
||||
* Some issues are marked "Needs Repro". Adding a comment with good reproduction steps to those issues is a great way to help.
|
||||
* If you don't have an issue in mind already, you can look through the [Bugs & Glitches forum.](https://scratch.mit.edu/discuss/3/) Look for users reporting problems, reproduce the problem yourself, and file new issues following our guidelines.
|
||||
|
||||
* **Fixing bugs**
|
||||
* You can request to fix a bug in a comment on the issue if you at mention the repo coordinator, who for this repo is @chrisgarrity.
|
||||
* If the issue is marked "Help Wanted" you can go ahead and start working on it!
|
||||
* **We will only accept Pull Requests for bugs that have an issue filed that has a priority label**
|
||||
* If you're interested in fixing a bug with no issue, file the issue first and wait for it to have a priority added to it.
|
||||
|
||||
* We are not looking for Pull Requests ("PR") for every issue and may deny a PR if it doesn't fit our criteria.
|
||||
* We are far more likely to accept a PR if it is for an issue marked with Help Wanted.
|
||||
* We will not accept PRs for issues marked with "Needs Discussion" or "Needs Design."
|
||||
* Wait until the Repo Coordinator assigns the issue to you before you begin work or submit a PR.
|
||||
|
||||
### Learning Git and Github
|
||||
|
||||
If you want to work on fixing issues, you should be familiar with Git and Github.
|
||||
|
||||
* [Learn Git branching](https://learngitbranching.js.org/) includes an introduction to basic git commands and useful branching features.
|
||||
* Here's a general introduction to [contributing to an open source project](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github).
|
||||
|
||||
**Important:** we follow the [Github Flow process](https://guides.github.com/introduction/flow/) as our development process.
|
||||
|
||||
### How to Fix Bugs
|
||||
1. Identify which Github issue you are working on. Leave a comment on the issue to let us (and other contributors) know you're working on it.
|
||||
2. Make sure you have a fork of this repo (see [Github's forking a repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) for details)
|
||||
3. Switch to the `develop` branch, and pull down the latest changes from upstream
|
||||
4. Run the code, and reproduce the problem
|
||||
5. Create your branch from the `develop` branch
|
||||
6. Make code changes to fix the problem
|
||||
7. Run `npm test` to make sure that your changes pass our tests
|
||||
8. Commit your changes
|
||||
9. Push your branch to your fork
|
||||
10. Create your pull request
|
||||
1. Make sure to follow the template in the PR description
|
||||
1. Remember to check the “[Allow edits from maintainers](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)” box
|
||||
|
||||
When submitting pull requests keep in mind:
|
||||
* please be patient -- it can take a while to find time to review them
|
||||
* try to change the least amount of code necessary to fix the bug
|
||||
* the code can't be radically changed without significant coordination with the Scratch Team, so these types of changes should be avoided
|
||||
* if you find yourself changing a substantial amount of code or considering radical changes, please ask for clarification -- we may have envisioned a different approach, or underestimated the amount of effort
|
||||
|
||||
### Suggestions
|
||||

|
||||
|
||||
Please note: **_we are unlikely to accept PRs with new features that haven't been thought through and discussed as a group_**.
|
||||
|
||||
Why? Because we have a strong belief in the value of keeping things simple for new users. It's been said that the Scratch Team spends about one hour of design discussion for every pixel in Scratch. To learn more about our design philosophy, see [the Scratch Developers page](https://scratch.mit.edu/developers), or [this paper](http://web.media.mit.edu/~mres/papers/Scratch-CACM-final.pdf).
|
||||
|
||||
We welcome suggestions! If you want to suggest a feature, please post in our [suggestions forum](https://scratch.mit.edu/discuss/1/). Your suggestion will be helped if you include a mockup design; this can be simple, even hand-drawn.
|
||||
|
||||
### Other resources
|
||||
Beyond this repo, there are also some other resources that you might want to take a look at:
|
||||
* [Community Guidelines](https://github.com/LLK/scratch-www/wiki/Community-Guidelines) (we find it important to maintain a constructive and welcoming community, just like on Scratch)
|
||||
* [Open Source forum](https://scratch.mit.edu/discuss/49/) on Scratch
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ dist: xenial
|
||||
addons:
|
||||
chrome: stable
|
||||
node_js:
|
||||
- 8
|
||||
- 10
|
||||
env:
|
||||
global:
|
||||
- CHROMEDRIVER_VERSION=LATEST
|
||||
@@ -54,7 +54,7 @@ deploy:
|
||||
- provider: script
|
||||
on:
|
||||
all_branches: true
|
||||
condition: $TRAVIS_EVENT_TYPE != cron && ! $TRAVIS_BRANCH =~ ^greenkeeper/
|
||||
condition: $TRAVIS_EVENT_TYPE != cron && ! $TRAVIS_BRANCH =~ ^dependabot/
|
||||
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
|
||||
|
||||
@@ -191,6 +191,13 @@ npm install --no-optional --save-dev react-responsive@^5.0.0
|
||||
|
||||
Further reading: [Stack Overflow](https://stackoverflow.com/questions/46602286/npm-requires-a-peer-of-but-all-peers-are-in-package-json-and-node-modules)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you run into npm install errors, try these steps:
|
||||
1. run `npm cache clean --force`
|
||||
2. Delete the node_modules directory
|
||||
3. Delete package-lock.json
|
||||
4. run `npm install` again
|
||||
|
||||
## Publishing to GitHub Pages
|
||||
You can publish the GUI to github.io so that others on the Internet can view it.
|
||||
|
||||
Generated
+5282
-769
File diff suppressed because it is too large.
Load diff
+12
-11
@@ -4,7 +4,7 @@
|
||||
"description": "GraphicaL User Interface for creating and running Scratch 3.0 projects",
|
||||
"main": "./dist/scratch-gui.js",
|
||||
"scripts": {
|
||||
"build": "npm run clean && webpack --progress --colors --bail",
|
||||
"build": "npm run clean && webpack --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]\"",
|
||||
"prune": "./prune-gh-pages.sh",
|
||||
@@ -16,7 +16,7 @@
|
||||
"test:lint": "eslint . --ext .js,.jsx",
|
||||
"test:unit": "jest test[\\\\/]unit",
|
||||
"test:smoke": "jest --runInBand test[\\\\/]smoke",
|
||||
"watch": "webpack --progress --colors --watch"
|
||||
"watch": "webpack --colors --watch"
|
||||
},
|
||||
"author": "Massachusetts Institute of Technology",
|
||||
"license": "BSD-3-Clause",
|
||||
@@ -44,7 +44,7 @@
|
||||
"babel-loader": "^8.0.4",
|
||||
"base64-loader": "1.0.0",
|
||||
"bowser": "1.9.4",
|
||||
"chromedriver": "80.0.0",
|
||||
"chromedriver": "83.0.0",
|
||||
"classnames": "2.2.6",
|
||||
"computed-style-to-inline-style": "3.0.0",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
@@ -107,14 +107,14 @@
|
||||
"redux-mock-store": "^1.2.3",
|
||||
"redux-throttle": "0.1.1",
|
||||
"rimraf": "^2.6.1",
|
||||
"scratch-audio": "0.1.0-prerelease.20190925183642",
|
||||
"scratch-blocks": "0.1.0-prerelease.20200507190920",
|
||||
"scratch-l10n": "3.8.20200505213509",
|
||||
"scratch-paint": "0.2.0-prerelease.20200507195338",
|
||||
"scratch-render": "0.1.0-prerelease.20200511203322",
|
||||
"scratch-audio": "0.1.0-prerelease.20200528195344",
|
||||
"scratch-blocks": "0.1.0-prerelease.20200714135908",
|
||||
"scratch-l10n": "3.10.20200715030910",
|
||||
"scratch-paint": "0.2.0-prerelease.20200717024459",
|
||||
"scratch-render": "0.1.0-prerelease.20200716193608",
|
||||
"scratch-storage": "1.3.3",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20200507183648",
|
||||
"scratch-vm": "0.2.0-prerelease.20200402182733",
|
||||
"scratch-svg-renderer": "0.2.0-prerelease.20200610220938",
|
||||
"scratch-vm": "0.2.0-prerelease.20200714185213",
|
||||
"selenium-webdriver": "3.6.0",
|
||||
"startaudiocontext": "1.2.1",
|
||||
"style-loader": "^0.23.0",
|
||||
@@ -139,7 +139,8 @@
|
||||
],
|
||||
"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"
|
||||
"\\.(css|less)$": "<rootDir>/test/__mocks__/styleMock.js",
|
||||
"editor-msgs(\\.js)?$": "<rootDir>/test/__mocks__/editor-msgs-mock.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
padding-right: 1rem;
|
||||
margin-bottom: 7px;
|
||||
min-height: 1.5rem;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.alert.warn {
|
||||
|
||||
+14
-15
@@ -34,7 +34,17 @@
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
flex-basis: 600px;
|
||||
/*
|
||||
This is carefully balanced-- the minimum width at which the GUI will be displayed is 1024px.
|
||||
At that size, the stage pane is 408px wide, with $space of padding to each side.
|
||||
However, we must also add the border width to the stage pane. All-in-all, the stage pane's final width is
|
||||
408px + ($space + $stage-standard-border-width * 2) (one border & padding per left/right side).
|
||||
|
||||
@todo This is in place to prevent "doubling up" of horizontal scrollbars in narrow windows, but there are likely
|
||||
much better ways to solve that (e.g. undo #2124, remove this flex-basis entirely). However, they run their own
|
||||
risks of breaking things, so let's just leave this as-is for the time being.
|
||||
*/
|
||||
flex-basis: calc(1024px - 408px - (($space + $stage-standard-border-width) * 2));
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
@@ -197,21 +207,9 @@
|
||||
/* pad entire wrapper to the left and right; allow children to fill width */
|
||||
padding-left: $space;
|
||||
padding-right: $space;
|
||||
}
|
||||
|
||||
.stage-and-target-wrapper.large {
|
||||
/* Fix the max width to max large stage size (defined in layout_constants.js) + gutter size */
|
||||
max-width: calc(480px + calc($space * 2));
|
||||
}
|
||||
|
||||
.stage-and-target-wrapper.large-constrained {
|
||||
/* Fix the max width to max largeConstrained stage size (defined in layout_constants.js) + gutter size */
|
||||
max-width: calc(408px + calc($space * 2));
|
||||
}
|
||||
|
||||
.stage-and-target-wrapper.small {
|
||||
/* Fix the max width to max small stage size (defined in layout_constants.js) + gutter size */
|
||||
max-width: calc(240px + calc($space * 2));
|
||||
/* this will only ever be as wide as the stage */
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.target-wrapper {
|
||||
@@ -309,6 +307,7 @@ $fade-out-distance: 15px;
|
||||
z-index: $z-index-alerts;
|
||||
position: absolute;
|
||||
margin-top: 53px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -89,6 +89,7 @@ const GUIComponent = props => {
|
||||
loading,
|
||||
logo,
|
||||
renderLogin,
|
||||
onClickAbout,
|
||||
onClickAccountNav,
|
||||
onCloseAccountNav,
|
||||
onLogOut,
|
||||
@@ -105,6 +106,7 @@ const GUIComponent = props => {
|
||||
onRequestCloseTelemetryModal,
|
||||
onSeeCommunity,
|
||||
onShare,
|
||||
onShowPrivacyPolicy,
|
||||
onTelemetryModalCancel,
|
||||
onTelemetryModalOptIn,
|
||||
onTelemetryModalOptOut,
|
||||
@@ -162,6 +164,7 @@ const GUIComponent = props => {
|
||||
onOptIn={onTelemetryModalOptIn}
|
||||
onOptOut={onTelemetryModalOptOut}
|
||||
onRequestClose={onRequestCloseTelemetryModal}
|
||||
onShowPrivacyPolicy={onShowPrivacyPolicy}
|
||||
/>
|
||||
) : null}
|
||||
{loading ? (
|
||||
@@ -218,6 +221,7 @@ const GUIComponent = props => {
|
||||
logo={logo}
|
||||
renderLogin={renderLogin}
|
||||
showComingSoon={showComingSoon}
|
||||
onClickAbout={onClickAbout}
|
||||
onClickAccountNav={onClickAccountNav}
|
||||
onClickLogo={onClickLogo}
|
||||
onCloseAccountNav={onCloseAccountNav}
|
||||
@@ -332,6 +336,7 @@ const GUIComponent = props => {
|
||||
|
||||
<Box className={classNames(styles.stageAndTargetWrapper, styles[stageSize])}>
|
||||
<StageWrapper
|
||||
isFullScreen={isFullScreen}
|
||||
isRendererSupported={isRendererSupported}
|
||||
isRtl={isRtl}
|
||||
stageSize={stageSize}
|
||||
@@ -388,6 +393,7 @@ GUIComponent.propTypes = {
|
||||
onActivateCostumesTab: PropTypes.func,
|
||||
onActivateSoundsTab: PropTypes.func,
|
||||
onActivateTab: PropTypes.func,
|
||||
onClickAbout: PropTypes.func,
|
||||
onClickAccountNav: PropTypes.func,
|
||||
onClickLogo: PropTypes.func,
|
||||
onCloseAccountNav: PropTypes.func,
|
||||
@@ -399,6 +405,7 @@ GUIComponent.propTypes = {
|
||||
onRequestCloseTelemetryModal: PropTypes.func,
|
||||
onSeeCommunity: PropTypes.func,
|
||||
onShare: PropTypes.func,
|
||||
onShowPrivacyPolicy: PropTypes.func,
|
||||
onTabSelect: PropTypes.func,
|
||||
onTelemetryModalCancel: PropTypes.func,
|
||||
onTelemetryModalOptIn: PropTypes.func,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -218,3 +218,9 @@
|
||||
.mystuff > a {
|
||||
background-image: url("/images/mystuff.png");
|
||||
}
|
||||
|
||||
.about-icon {
|
||||
height: 1.25rem;
|
||||
margin: 0.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -69,6 +69,7 @@ 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';
|
||||
|
||||
@@ -141,6 +142,19 @@ 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);
|
||||
@@ -311,6 +325,8 @@ 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(
|
||||
@@ -689,6 +705,8 @@ class MenuBar extends React.Component {
|
||||
</React.Fragment>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{aboutButton}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -722,6 +740,7 @@ MenuBar.propTypes = {
|
||||
locale: PropTypes.string.isRequired,
|
||||
loginMenuOpen: PropTypes.bool,
|
||||
logo: PropTypes.string,
|
||||
onClickAbout: PropTypes.func,
|
||||
onClickAccount: PropTypes.func,
|
||||
onClickEdit: PropTypes.func,
|
||||
onClickFile: PropTypes.func,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "../../css/units.css";
|
||||
@import "../../css/colors.css";
|
||||
@import "../../css/z-index.css";
|
||||
|
||||
.stage-wrapper * {
|
||||
box-sizing: border-box;
|
||||
@@ -9,3 +10,21 @@
|
||||
/* Hides negative space between edge of rounded corners + container, when selected */
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.stage-wrapper.full-screen {
|
||||
position: fixed;
|
||||
top: $stage-menu-height;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $z-index-stage-wrapper-overlay;
|
||||
background-color: $ui-white;
|
||||
/* spacing between stage and control bar (on the top), or between
|
||||
stage and window edges (on left/right/bottom) */
|
||||
padding: $stage-full-screen-stage-padding;
|
||||
|
||||
/* this centers the stage */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import VM from 'scratch-vm';
|
||||
|
||||
import Box from '../box/box.jsx';
|
||||
@@ -22,7 +23,10 @@ const StageWrapperComponent = function (props) {
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={styles.stageWrapper}
|
||||
className={classNames(
|
||||
styles.stageWrapper,
|
||||
{[styles.fullScreen]: isFullScreen}
|
||||
)}
|
||||
dir={isRtl ? 'rtl' : 'ltr'}
|
||||
>
|
||||
<Box className={styles.stageMenuWrapper}>
|
||||
|
||||
@@ -21,6 +21,13 @@
|
||||
|
||||
/* Make sure border is not included in size calculation */
|
||||
box-sizing: content-box !important;
|
||||
|
||||
/* enforce overflow + reset position of absolutely-positioned children */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stage.full-screen {
|
||||
border: $stage-full-screen-border-width solid rgb(126, 133, 151);
|
||||
}
|
||||
|
||||
.with-color-picker {
|
||||
@@ -43,62 +50,30 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stage-wrapper-overlay {
|
||||
position: fixed;
|
||||
top: $stage-menu-height;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: $z-index-stage-wrapper-overlay;
|
||||
background-color: $ui-white;
|
||||
/* spacing between stage and control bar (on the top), or between
|
||||
stage and window edges (on left/right/bottom) */
|
||||
padding: $stage-full-screen-stage-padding;
|
||||
}
|
||||
|
||||
/* wraps only main content of overlay player, not monitors */
|
||||
.stage-overlay-content {
|
||||
outline: none;
|
||||
margin: auto;
|
||||
border: $stage-full-screen-border-width solid rgb(126, 133, 151);
|
||||
padding: 0;
|
||||
border-radius: $space;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stage-overlay-content-border-override {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* adjust monitors when stage is standard size:
|
||||
shift them down and right to compensate for the stage's border */
|
||||
.stage-wrapper .monitor-wrapper {
|
||||
/* we want stage overlays to all be positioned in the same spot as the stage, but can't put them inside the border
|
||||
because we want their overflow to be visible, and the bordered element must have overflow: hidden set so that the
|
||||
stage doesn't "spill" out from under its rounded corners. instead, shift these over by the border width. */
|
||||
.stage-overlays {
|
||||
position: absolute;
|
||||
top: $stage-standard-border-width;
|
||||
left: $stage-standard-border-width;
|
||||
|
||||
/* the overlay itself should not capture pointer events; only its child elements can do that */
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* adjust monitors when stage is full screen:
|
||||
.stage-wrapper-overlay uses position: fixed instead of relative, so we need
|
||||
to adjust for the border using a different method */
|
||||
.stage-wrapper-overlay .monitor-wrapper {
|
||||
padding-top: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width);
|
||||
padding-bottom: calc($stage-full-screen-stage-padding + $stage-full-screen-border-width);
|
||||
.stage-overlays.full-screen {
|
||||
top: $stage-full-screen-border-width;
|
||||
left: $stage-full-screen-border-width;
|
||||
}
|
||||
|
||||
.monitor-wrapper,
|
||||
.color-picker-wrapper,
|
||||
.frame-wrapper,
|
||||
.green-flag-overlay-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dragging-sprite {
|
||||
@@ -107,7 +82,7 @@ to adjust for the border using a different method */
|
||||
left: 0;
|
||||
z-index: $z-index-dragging-sprite;
|
||||
filter: drop-shadow(5px 5px 5px $ui-black-transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.stage-bottom-wrapper {
|
||||
position: absolute;
|
||||
@@ -115,6 +90,7 @@ to adjust for the border using a different method */
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -132,7 +108,6 @@ to adjust for the border using a different method */
|
||||
}
|
||||
|
||||
.frame {
|
||||
box-sizing: content-box !important;
|
||||
background: $motion-transparent;
|
||||
border: 2px solid $motion-primary;
|
||||
border-radius: 0.5rem;
|
||||
@@ -142,6 +117,8 @@ to adjust for the border using a different method */
|
||||
}
|
||||
|
||||
.green-flag-overlay-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@@ -35,23 +35,17 @@ const StageComponent = props => {
|
||||
const stageDimensions = getStageDimensions(stageSize, isFullScreen);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<React.Fragment>
|
||||
<Box
|
||||
className={classNames({
|
||||
[styles.stageWrapper]: !isFullScreen,
|
||||
[styles.stageWrapperOverlay]: isFullScreen,
|
||||
[styles.withColorPicker]: !isFullScreen && isColorPicking
|
||||
})}
|
||||
style={{
|
||||
minHeight: stageDimensions.height,
|
||||
minWidth: stageDimensions.width
|
||||
}}
|
||||
className={classNames(
|
||||
styles.stageWrapper,
|
||||
{[styles.withColorPicker]: !isFullScreen && isColorPicking})}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
<Box
|
||||
className={classNames(
|
||||
styles.stage,
|
||||
{[styles.stageOverlayContent]: isFullScreen}
|
||||
{[styles.fullScreen]: isFullScreen}
|
||||
)}
|
||||
style={{
|
||||
height: stageDimensions.height,
|
||||
@@ -66,18 +60,61 @@ const StageComponent = props => {
|
||||
}}
|
||||
{...boxProps}
|
||||
/>
|
||||
<Box className={styles.monitorWrapper}>
|
||||
<MonitorList
|
||||
draggable={useEditorDragStyle}
|
||||
stageSize={stageDimensions}
|
||||
/>
|
||||
</Box>
|
||||
<Box className={styles.frameWrapper}>
|
||||
<TargetHighlight
|
||||
className={styles.frame}
|
||||
stageHeight={stageDimensions.height}
|
||||
stageWidth={stageDimensions.width}
|
||||
/>
|
||||
</Box>
|
||||
{isColorPicking && colorInfo ? (
|
||||
<Loupe colorInfo={colorInfo} />
|
||||
) : null}
|
||||
</Box>
|
||||
<Box className={styles.monitorWrapper}>
|
||||
<MonitorList
|
||||
draggable={useEditorDragStyle}
|
||||
stageSize={stageDimensions}
|
||||
/>
|
||||
</Box>
|
||||
<Box className={styles.frameWrapper}>
|
||||
<TargetHighlight
|
||||
className={styles.frame}
|
||||
stageHeight={stageDimensions.height}
|
||||
stageWidth={stageDimensions.width}
|
||||
|
||||
{/* `stageOverlays` is for items that should *not* have their overflow contained within the stage */}
|
||||
<Box
|
||||
className={classNames(
|
||||
styles.stageOverlays,
|
||||
{[styles.fullScreen]: isFullScreen}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={styles.stageBottomWrapper}
|
||||
style={{
|
||||
width: stageDimensions.width,
|
||||
height: stageDimensions.height
|
||||
}}
|
||||
>
|
||||
{micIndicator ? (
|
||||
<MicIndicator
|
||||
className={styles.micIndicator}
|
||||
stageSize={stageDimensions}
|
||||
/>
|
||||
) : null}
|
||||
{question === null ? null : (
|
||||
<div
|
||||
className={styles.questionWrapper}
|
||||
style={{width: stageDimensions.width}}
|
||||
>
|
||||
<Question
|
||||
question={question}
|
||||
onQuestionAnswered={onQuestionAnswered}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<canvas
|
||||
className={styles.draggingSprite}
|
||||
height={0}
|
||||
ref={dragRef}
|
||||
width={0}
|
||||
/>
|
||||
</Box>
|
||||
{isStarted ? null : (
|
||||
@@ -86,44 +123,6 @@ const StageComponent = props => {
|
||||
wrapperClass={styles.greenFlagOverlayWrapper}
|
||||
/>
|
||||
)}
|
||||
{isColorPicking && colorInfo ? (
|
||||
<Box className={styles.colorPickerWrapper}>
|
||||
<Loupe colorInfo={colorInfo} />
|
||||
</Box>
|
||||
) : null}
|
||||
<div
|
||||
className={styles.stageBottomWrapper}
|
||||
style={{
|
||||
width: stageDimensions.width,
|
||||
height: stageDimensions.height,
|
||||
left: '50%',
|
||||
marginLeft: stageDimensions.width * -0.5
|
||||
}}
|
||||
>
|
||||
{micIndicator ? (
|
||||
<MicIndicator
|
||||
className={styles.micIndicator}
|
||||
stageSize={stageDimensions}
|
||||
/>
|
||||
) : null}
|
||||
{question === null ? null : (
|
||||
<div
|
||||
className={styles.questionWrapper}
|
||||
style={{width: stageDimensions.width}}
|
||||
>
|
||||
<Question
|
||||
question={question}
|
||||
onQuestionAnswered={onQuestionAnswered}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<canvas
|
||||
className={styles.draggingSprite}
|
||||
height={0}
|
||||
ref={dragRef}
|
||||
width={0}
|
||||
/>
|
||||
</Box>
|
||||
{isColorPicking ? (
|
||||
<Box
|
||||
@@ -131,7 +130,7 @@ const StageComponent = props => {
|
||||
onClick={onDeactivateColorPicker}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
StageComponent.propTypes = {
|
||||
|
||||
@@ -101,6 +101,9 @@ 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>)
|
||||
@@ -134,7 +137,8 @@ TelemetryModal.propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
onOptIn: PropTypes.func.isRequired,
|
||||
onOptOut: PropTypes.func.isRequired,
|
||||
onRequestClose: PropTypes.func
|
||||
onRequestClose: PropTypes.func,
|
||||
onShowPrivacyPolicy: PropTypes.func
|
||||
};
|
||||
|
||||
export default injectIntl(TelemetryModal);
|
||||
@@ -338,8 +338,8 @@ class Blocks extends React.Component {
|
||||
const stageCostumes = stage.getCostumes();
|
||||
const targetCostumes = target.getCostumes();
|
||||
const targetSounds = target.getSounds();
|
||||
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML();
|
||||
return makeToolboxXML(target.isStage, target.id, dynamicBlocksXML,
|
||||
const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(target);
|
||||
return makeToolboxXML(false, target.isStage, target.id, dynamicBlocksXML,
|
||||
targetCostumes[targetCostumes.length - 1].name,
|
||||
stageCostumes[stageCostumes.length - 1].name,
|
||||
targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : ''
|
||||
|
||||
@@ -21,8 +21,6 @@ class GreenFlagOverlay extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
if (this.props.isStarted) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={this.props.wrapperClass}
|
||||
@@ -42,13 +40,11 @@ class GreenFlagOverlay extends React.Component {
|
||||
|
||||
GreenFlagOverlay.propTypes = {
|
||||
className: PropTypes.string,
|
||||
isStarted: PropTypes.bool,
|
||||
vm: PropTypes.instanceOf(VM),
|
||||
wrapperClass: PropTypes.string
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isStarted: state.scratchGui.vmStatus.started,
|
||||
vm: state.scratchGui.vm
|
||||
});
|
||||
|
||||
|
||||
@@ -362,6 +362,7 @@ class Stage extends React.Component {
|
||||
this.drawDragCanvas(drawableData);
|
||||
this.positionDragCanvas(x, y);
|
||||
this.props.vm.postSpriteInfo({visible: false});
|
||||
this.props.vm.renderer.draw();
|
||||
}
|
||||
}
|
||||
onStopDrag (mouseX, mouseY) {
|
||||
@@ -387,12 +388,9 @@ class Stage extends React.Component {
|
||||
}
|
||||
this.props.vm.postSpriteInfo(spriteInfo);
|
||||
// Then clear the dragging canvas and stop drag (potentially slow if selecting sprite)
|
||||
setTimeout(() => {
|
||||
this.clearDragCanvas();
|
||||
setTimeout(() => {
|
||||
commonStopDragActions();
|
||||
}, 30);
|
||||
}, 30);
|
||||
this.clearDragCanvas();
|
||||
commonStopDragActions();
|
||||
this.props.vm.renderer.draw();
|
||||
} else {
|
||||
commonStopDragActions();
|
||||
}
|
||||
|
||||
+1
-1
@@ -41,4 +41,4 @@ $extensions-tertiary: hsla(163, 85%, 30%, 1); /* #0B8E69 */
|
||||
$extensions-transparent: hsla(163, 85%, 40%, 0.35); /* 35% transparent version of extensions-primary */
|
||||
$extensions-light: hsla(163, 57%, 85%, 1); /* opaque version of extensions-transparent, on white bg */
|
||||
|
||||
$drop-highlight: hsla(215, 100%, 77%); /* lighter than motion-primary */
|
||||
$drop-highlight: hsla(215, 100%, 77%, 1); /* lighter than motion-primary */
|
||||
+19
-6
@@ -1,10 +1,23 @@
|
||||
import GoogleAnalytics from 'react-ga';
|
||||
|
||||
GoogleAnalytics.initialize(process.env.GA_ID || window.GA_ID, {
|
||||
debug: (process.env.NODE_ENV !== 'production'),
|
||||
titleCase: true,
|
||||
sampleRate: (process.env.NODE_ENV === 'production') ? 100 : 0,
|
||||
forceSSL: true
|
||||
});
|
||||
import log from './log';
|
||||
|
||||
const GA_ID = (process.env.GA_ID || window.GA_ID);
|
||||
if (GA_ID) {
|
||||
GoogleAnalytics.initialize(GA_ID, {
|
||||
debug: (process.env.NODE_ENV !== 'production'),
|
||||
titleCase: true,
|
||||
sampleRate: (process.env.NODE_ENV === 'production') ? 100 : 0,
|
||||
forceSSL: true
|
||||
});
|
||||
} else {
|
||||
log.info('Disabling GA because GA_ID is not set.');
|
||||
window.ga = () => {
|
||||
// The `react-ga` module calls this function to implement all Google Analytics calls. Providing an empty
|
||||
// function effectively disables `react-ga`. This is similar to the `testModeAPI` feature of `react-ga` except
|
||||
// that `testModeAPI` logs the arguments of every call into an array. That's nice for testing purposes but
|
||||
// would look like a memory leak in a live program.
|
||||
};
|
||||
}
|
||||
|
||||
export default GoogleAnalytics;
|
||||
@@ -2,10 +2,22 @@ import StartAudioContext from 'startaudiocontext';
|
||||
import bowser from 'bowser';
|
||||
|
||||
let AUDIO_CONTEXT;
|
||||
if (!bowser.msie) {
|
||||
AUDIO_CONTEXT = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
StartAudioContext(AUDIO_CONTEXT);
|
||||
if (!bowser.msie) {
|
||||
/**
|
||||
* AudioContext can be initialized only when user interaction event happens
|
||||
*/
|
||||
const event =
|
||||
typeof document.ontouchstart === 'undefined' ?
|
||||
'mousedown' :
|
||||
'touchstart';
|
||||
const initAudioContext = () => {
|
||||
document.removeEventListener(event, initAudioContext);
|
||||
AUDIO_CONTEXT = new (window.AudioContext ||
|
||||
window.webkitAudioContext)();
|
||||
StartAudioContext(AUDIO_CONTEXT);
|
||||
};
|
||||
document.addEventListener(event, initAudioContext);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,10 +14,13 @@ export default (filename, blob) => {
|
||||
downloadLink.download = filename;
|
||||
downloadLink.type = blob.type;
|
||||
downloadLink.click();
|
||||
document.body.removeChild(downloadLink);
|
||||
window.URL.revokeObjectURL(url);
|
||||
// remove the link after a timeout to prevent a crash on iOS 13 Safari
|
||||
window.setTimeout(() => {
|
||||
document.body.removeChild(downloadLink);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}, 1000);
|
||||
} else {
|
||||
// iOS Safari, open a new page and set href to data-uri
|
||||
// iOS 12 Safari, open a new page and set href to data-uri
|
||||
let popup = window.open('', '_blank');
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = function () {
|
||||
|
||||
@@ -78,7 +78,7 @@ const DropAreaHOC = function (dragTypes) {
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const componentProps = omit(this.props, ['onDrop', 'dragInfo']);
|
||||
const componentProps = omit(this.props, ['onDrop', 'dragInfo', 'componentRef']);
|
||||
return (
|
||||
<WrappedComponent
|
||||
containerRef={this.setRef}
|
||||
|
||||
+452
-452
@@ -104,6 +104,82 @@ export default {
|
||||
urlId: 'getStarted'
|
||||
},
|
||||
|
||||
'animate-a-name': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Animate a Name"
|
||||
description="Name for the 'Animate a Name' how-to"
|
||||
id="gui.howtos.animate-a-name.name"
|
||||
/>
|
||||
),
|
||||
img: libraryAnimate,
|
||||
tags: ['animation', 'art', 'spin', 'grow'],
|
||||
steps: [{
|
||||
video: 'animate-a-name'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Pick a Letter Sprite"
|
||||
description="Step name for 'Pick a Letter Sprite' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimatePickLetter"
|
||||
/>
|
||||
),
|
||||
image: 'namePickLetter'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Play a Sound When Clicked"
|
||||
description="Step name for 'Play a Sound When Clicked' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimatePlaySound"
|
||||
/>
|
||||
),
|
||||
image: 'namePlaySound'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Pick Another Letter Sprite"
|
||||
description="Step name for 'Pick Another Letter Sprite"
|
||||
id="gui.howtos.animate-a-name.step_AnimatePickLetter2"
|
||||
/>
|
||||
),
|
||||
image: 'namePickLetter2'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Change color"
|
||||
description="Step name for 'Change color' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimateChangeColor"
|
||||
/>
|
||||
),
|
||||
image: 'nameChangeColor'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Pick Another Letter Sprite & Make It Spin"
|
||||
description="Step name for 'Pick Another Letter Sprite & Make It Spin' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimateSpin"
|
||||
/>
|
||||
),
|
||||
image: 'nameSpin'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Pick Another Letter Sprite & Make It Grow"
|
||||
description="Step name for 'Pick Another Letter Sprite & Make It Grow!' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimateGrow"
|
||||
/>
|
||||
),
|
||||
image: 'nameGrow'
|
||||
}, {
|
||||
deckIds: [
|
||||
'add-a-backdrop',
|
||||
'glide-around'
|
||||
]
|
||||
}
|
||||
],
|
||||
urlId: 'name'
|
||||
},
|
||||
|
||||
'imagine': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
@@ -260,458 +336,7 @@ export default {
|
||||
],
|
||||
urlId: 'imagine'
|
||||
},
|
||||
|
||||
'code-cartoon': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Code a Cartoon"
|
||||
description="Name for the 'Code a Cartoon' how-to"
|
||||
id="gui.howtos.code-cartoon"
|
||||
/>
|
||||
),
|
||||
tags: ['code-cartoon'],
|
||||
requiredProjectId: '331474033',
|
||||
img: libraryCodeCartoon,
|
||||
steps: [{
|
||||
video: 'code-cartoon'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Say Something When You Click the Green Flag"
|
||||
description="Step name for 'Say Something When You Click the Green Flag' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonSaySomething"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonSaySomething'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Animate a Character When You Click It"
|
||||
description="Step name for 'Animate a Character When You Click It' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonAnimate"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonAnimate'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a Different Character"
|
||||
description="Step name for 'Select a Different Character' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonSelectDifferentCharacter"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonSelectDifferentCharacter'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Use a Minus Sign to Get Smaller"
|
||||
description="Step name for 'Use a Minus Sign to Get Smaller' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonUseMinusSign"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonUseMinusSign'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Make a Character Grow and Shrink"
|
||||
description="Step name for 'Make a Character Grow and Shrink' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonGrowShrink"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonGrowShrink'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a Different Character"
|
||||
description="Step name for 'Select a Different Character' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonSelectDifferentCharacter2"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonSelectDifferentCharacter2'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Jump Up and Down"
|
||||
description="Step name for 'Jump Up and Down' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonJump"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonJump'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Click a Character to Change Scenes"
|
||||
description="Step name for 'Click a Character to Change Scenes' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonChangeScenes"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonChangeScenes'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Glide Around"
|
||||
description="Step name for 'Glide Around' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonGlideAround"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonGlideAround'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Change Costumes"
|
||||
description="Step name for 'Change Costumes' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonChangeCostumes"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonChangeCostumes'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose More Characters to Add to Your Cartoon"
|
||||
description="Step name for 'Choose More Characters to Add to Your Cartoon' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonChooseMoreCharacters"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonChooseMoreCharacters'
|
||||
}, {
|
||||
deckIds: [
|
||||
'Chase-Game',
|
||||
'Tell-A-Story'
|
||||
]
|
||||
}
|
||||
],
|
||||
urlId: 'code-cartoon'
|
||||
},
|
||||
|
||||
'say-it-out-loud': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Create Animations That Talk"
|
||||
description="Name for the 'Create Animations That Talk' how-to"
|
||||
id="gui.howtos.say-it-out-loud"
|
||||
/>
|
||||
),
|
||||
img: libraryTXTSpeech,
|
||||
steps: [{
|
||||
video: 'k54n8uwcty',
|
||||
trackingPixel: (
|
||||
<img src="https://code.org/api/hour/begin_scratch_talk.png" />
|
||||
)
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Add the Text to Speech blocks"
|
||||
description="Step name for 'Add the Text to Speech blocks' step"
|
||||
id="gui.howtos.say-it-out-loud.step_AddTXTextension"
|
||||
/>
|
||||
),
|
||||
image: 'speechAddExtension'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Say Something"
|
||||
description="Step name for 'Say Something' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTSpeech"
|
||||
/>
|
||||
),
|
||||
image: 'speechSaySomething'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Set a Voice"
|
||||
description="Step name for 'Set a Voice"
|
||||
id="gui.howtos.say-it-out-loud_TXTSetVoice"
|
||||
/>
|
||||
),
|
||||
image: 'speechSetVoice'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Move Around"
|
||||
description="Step name for 'Move Around' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTMove"
|
||||
/>
|
||||
),
|
||||
image: 'speechMoveAround'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a Backdrop"
|
||||
description="Step name for 'Add a Backdrop' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'speechAddBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Add Another Character"
|
||||
description="Step name for 'Add Another Character' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTAddSprite"
|
||||
/>
|
||||
),
|
||||
image: 'speechAddSprite'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Perform a Song"
|
||||
description="Step name for 'Perform a Song' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTSong"
|
||||
/>
|
||||
),
|
||||
image: 'speechSong'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Change Color"
|
||||
description="Step name for 'Change Color' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTColor"
|
||||
/>
|
||||
),
|
||||
image: 'speechChangeColor'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Spin Around"
|
||||
description="Step name for 'Spin Around"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTSpin"
|
||||
/>
|
||||
),
|
||||
image: 'speechSpin'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Grow and Shrink"
|
||||
description="Step name for 'Grow and Shrink' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTGrow"
|
||||
/>
|
||||
),
|
||||
image: 'speechGrowShrink'
|
||||
}, {
|
||||
deckIds: [
|
||||
'animate-a-name',
|
||||
'Make-Music'
|
||||
]
|
||||
}
|
||||
],
|
||||
urlId: 'animations-that-talk'
|
||||
},
|
||||
|
||||
'talking': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Talking Tales"
|
||||
description="Name for the 'Talking Tales' how-to"
|
||||
id="gui.howtos.talking"
|
||||
/>
|
||||
),
|
||||
tags: ['talking'],
|
||||
img: libraryTalking,
|
||||
steps: [{
|
||||
video: 'talking'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Click to Add the Text-to-Speech Blocks"
|
||||
description="Step name for 'Click to Add the Text-to-Speech Blocks' step"
|
||||
id="gui.howtos.talking.step_talesAddExtension"
|
||||
/>
|
||||
),
|
||||
image: 'talesAddExtension'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose a Sprite"
|
||||
description="Step name for 'Choose a Sprite' step"
|
||||
id="gui.howtos.talking.step_talesChooseSprite"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseSprite'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Make a Character Speak"
|
||||
description="Step name for 'Make a Character Speak' step"
|
||||
id="gui.howtos.talking.step_talesSaySomething"
|
||||
/>
|
||||
),
|
||||
image: 'talesSaySomething'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose a Backdrop"
|
||||
description="Step name for 'Choose a Backdrop' step"
|
||||
id="gui.howtos.talking.step_talesChooseBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Click a Character to Go to the Next Backdrop"
|
||||
description="Step name for 'Click a Character to Go to the Next Backdrop' step"
|
||||
id="gui.howtos.talking.step_talesSwitchBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'talesSwitchBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose Another Sprite"
|
||||
description="Step name for 'Choose Another Sprite' step"
|
||||
id="gui.howtos.talking.step_talesChooseAnotherSprite"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseAnotherSprite'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Move Around"
|
||||
description="Step name for 'Move Around' step"
|
||||
id="gui.howtos.talking.step_talesMoveAround"
|
||||
/>
|
||||
),
|
||||
image: 'talesMoveAround'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose Another Backdrop"
|
||||
description="Step name for 'Choose Another Backdrop' step"
|
||||
id="gui.howtos.talking.step_talesChooseAnotherBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseAnotherBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Animate Talking"
|
||||
description="Step name for 'Animate Talking' step"
|
||||
id="gui.howtos.talking.step_talesAnimateTalking"
|
||||
/>
|
||||
),
|
||||
image: 'talesAnimateTalking'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose Another Backdrop"
|
||||
description="Step name for 'Choose Another Backdrop' step"
|
||||
id="gui.howtos.talking.step_talesChooseThirdBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseThirdBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose a Song to Dance To"
|
||||
description="Step name for 'Choose a Song to Dance To' step"
|
||||
id="gui.howtos.talking.step_talesChooseSound"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseSound'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Dance Moves"
|
||||
description="Step name for 'Dance Moves' step"
|
||||
id="gui.howtos.talking.step_talesDanceMoves"
|
||||
/>
|
||||
),
|
||||
image: 'talesDanceMoves'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Get the Ask and Answer Blocks from the Sensing Category"
|
||||
description="Step name for 'Get the Ask and Answer Blocks from the Sensing Category' step"
|
||||
id="gui.howtos.talking.step_talesAskAnswer"
|
||||
/>
|
||||
),
|
||||
image: 'talesAskAnswer'
|
||||
}, {
|
||||
deckIds: [
|
||||
'Tell-A-Story',
|
||||
'Animate-A-Character'
|
||||
]
|
||||
}
|
||||
],
|
||||
urlId: 'talking'
|
||||
},
|
||||
|
||||
'animate-a-name': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Animate a Name"
|
||||
description="Name for the 'Animate a Name' how-to"
|
||||
id="gui.howtos.animate-a-name.name"
|
||||
/>
|
||||
),
|
||||
img: libraryAnimate,
|
||||
tags: ['animation', 'art', 'spin', 'grow'],
|
||||
steps: [{
|
||||
video: 'animate-a-name'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Pick a Letter Sprite"
|
||||
description="Step name for 'Pick a Letter Sprite' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimatePickLetter"
|
||||
/>
|
||||
),
|
||||
image: 'namePickLetter'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Play a Sound When Clicked"
|
||||
description="Step name for 'Play a Sound When Clicked' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimatePlaySound"
|
||||
/>
|
||||
),
|
||||
image: 'namePlaySound'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Pick Another Letter Sprite"
|
||||
description="Step name for 'Pick Another Letter Sprite"
|
||||
id="gui.howtos.animate-a-name.step_AnimatePickLetter2"
|
||||
/>
|
||||
),
|
||||
image: 'namePickLetter2'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Change color"
|
||||
description="Step name for 'Change color' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimateChangeColor"
|
||||
/>
|
||||
),
|
||||
image: 'nameChangeColor'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Pick Another Letter Sprite & Make It Spin"
|
||||
description="Step name for 'Pick Another Letter Sprite & Make It Spin' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimateSpin"
|
||||
/>
|
||||
),
|
||||
image: 'nameSpin'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Pick Another Letter Sprite & Make It Grow"
|
||||
description="Step name for 'Pick Another Letter Sprite & Make It Grow!' step"
|
||||
id="gui.howtos.animate-a-name.step_AnimateGrow"
|
||||
/>
|
||||
),
|
||||
image: 'nameGrow'
|
||||
}, {
|
||||
deckIds: [
|
||||
'add-a-backdrop',
|
||||
'glide-around'
|
||||
]
|
||||
}
|
||||
],
|
||||
urlId: 'name'
|
||||
},
|
||||
|
||||
|
||||
'Make-Music': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
@@ -1450,6 +1075,128 @@ export default {
|
||||
urlId: 'pong'
|
||||
},
|
||||
|
||||
'code-cartoon': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Code a Cartoon"
|
||||
description="Name for the 'Code a Cartoon' how-to"
|
||||
id="gui.howtos.code-cartoon"
|
||||
/>
|
||||
),
|
||||
tags: ['code-cartoon'],
|
||||
requiredProjectId: '331474033',
|
||||
img: libraryCodeCartoon,
|
||||
steps: [{
|
||||
video: 'code-cartoon'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Say Something When You Click the Green Flag"
|
||||
description="Step name for 'Say Something When You Click the Green Flag' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonSaySomething"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonSaySomething'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Animate a Character When You Click It"
|
||||
description="Step name for 'Animate a Character When You Click It' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonAnimate"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonAnimate'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a Different Character"
|
||||
description="Step name for 'Select a Different Character' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonSelectDifferentCharacter"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonSelectDifferentCharacter'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Use a Minus Sign to Get Smaller"
|
||||
description="Step name for 'Use a Minus Sign to Get Smaller' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonUseMinusSign"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonUseMinusSign'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Make a Character Grow and Shrink"
|
||||
description="Step name for 'Make a Character Grow and Shrink' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonGrowShrink"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonGrowShrink'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Select a Different Character"
|
||||
description="Step name for 'Select a Different Character' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonSelectDifferentCharacter2"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonSelectDifferentCharacter2'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Jump Up and Down"
|
||||
description="Step name for 'Jump Up and Down' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonJump"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonJump'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Click a Character to Change Scenes"
|
||||
description="Step name for 'Click a Character to Change Scenes' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonChangeScenes"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonChangeScenes'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Glide Around"
|
||||
description="Step name for 'Glide Around' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonGlideAround"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonGlideAround'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Change Costumes"
|
||||
description="Step name for 'Change Costumes' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonChangeCostumes"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonChangeCostumes'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose More Characters to Add to Your Cartoon"
|
||||
description="Step name for 'Choose More Characters to Add to Your Cartoon' step"
|
||||
id="gui.howtos.code-cartoon.step_codeCartoonChooseMoreCharacters"
|
||||
/>
|
||||
),
|
||||
image: 'codeCartoonChooseMoreCharacters'
|
||||
}, {
|
||||
deckIds: [
|
||||
'Chase-Game',
|
||||
'Tell-A-Story'
|
||||
]
|
||||
}
|
||||
],
|
||||
urlId: 'code-cartoon'
|
||||
},
|
||||
|
||||
'cartoon-network': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
@@ -1609,6 +1356,259 @@ export default {
|
||||
urlId: 'video-sensing'
|
||||
},
|
||||
|
||||
'say-it-out-loud': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Create Animations That Talk"
|
||||
description="Name for the 'Create Animations That Talk' how-to"
|
||||
id="gui.howtos.say-it-out-loud"
|
||||
/>
|
||||
),
|
||||
img: libraryTXTSpeech,
|
||||
steps: [{
|
||||
video: 'k54n8uwcty',
|
||||
trackingPixel: (
|
||||
<img src="https://code.org/api/hour/begin_scratch_talk.png" />
|
||||
)
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Add the Text to Speech blocks"
|
||||
description="Step name for 'Add the Text to Speech blocks' step"
|
||||
id="gui.howtos.say-it-out-loud.step_AddTXTextension"
|
||||
/>
|
||||
),
|
||||
image: 'speechAddExtension'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Say Something"
|
||||
description="Step name for 'Say Something' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTSpeech"
|
||||
/>
|
||||
),
|
||||
image: 'speechSaySomething'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Set a Voice"
|
||||
description="Step name for 'Set a Voice"
|
||||
id="gui.howtos.say-it-out-loud_TXTSetVoice"
|
||||
/>
|
||||
),
|
||||
image: 'speechSetVoice'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Move Around"
|
||||
description="Step name for 'Move Around' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTMove"
|
||||
/>
|
||||
),
|
||||
image: 'speechMoveAround'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Add a Backdrop"
|
||||
description="Step name for 'Add a Backdrop' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'speechAddBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Add Another Character"
|
||||
description="Step name for 'Add Another Character' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTAddSprite"
|
||||
/>
|
||||
),
|
||||
image: 'speechAddSprite'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Perform a Song"
|
||||
description="Step name for 'Perform a Song' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTSong"
|
||||
/>
|
||||
),
|
||||
image: 'speechSong'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Change Color"
|
||||
description="Step name for 'Change Color' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTColor"
|
||||
/>
|
||||
),
|
||||
image: 'speechChangeColor'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Spin Around"
|
||||
description="Step name for 'Spin Around"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTSpin"
|
||||
/>
|
||||
),
|
||||
image: 'speechSpin'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Grow and Shrink"
|
||||
description="Step name for 'Grow and Shrink' step"
|
||||
id="gui.howtos.say-it-out-loud.step_TXTGrow"
|
||||
/>
|
||||
),
|
||||
image: 'speechGrowShrink'
|
||||
}, {
|
||||
deckIds: [
|
||||
'animate-a-name',
|
||||
'Make-Music'
|
||||
]
|
||||
}
|
||||
],
|
||||
urlId: 'animations-that-talk'
|
||||
},
|
||||
|
||||
'talking': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Talking Tales"
|
||||
description="Name for the 'Talking Tales' how-to"
|
||||
id="gui.howtos.talking"
|
||||
/>
|
||||
),
|
||||
tags: ['talking'],
|
||||
img: libraryTalking,
|
||||
steps: [{
|
||||
video: 'talking'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Click to Add the Text-to-Speech Blocks"
|
||||
description="Step name for 'Click to Add the Text-to-Speech Blocks' step"
|
||||
id="gui.howtos.talking.step_talesAddExtension"
|
||||
/>
|
||||
),
|
||||
image: 'talesAddExtension'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose a Sprite"
|
||||
description="Step name for 'Choose a Sprite' step"
|
||||
id="gui.howtos.talking.step_talesChooseSprite"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseSprite'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Make a Character Speak"
|
||||
description="Step name for 'Make a Character Speak' step"
|
||||
id="gui.howtos.talking.step_talesSaySomething"
|
||||
/>
|
||||
),
|
||||
image: 'talesSaySomething'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose a Backdrop"
|
||||
description="Step name for 'Choose a Backdrop' step"
|
||||
id="gui.howtos.talking.step_talesChooseBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Click a Character to Go to the Next Backdrop"
|
||||
description="Step name for 'Click a Character to Go to the Next Backdrop' step"
|
||||
id="gui.howtos.talking.step_talesSwitchBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'talesSwitchBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose Another Sprite"
|
||||
description="Step name for 'Choose Another Sprite' step"
|
||||
id="gui.howtos.talking.step_talesChooseAnotherSprite"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseAnotherSprite'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Move Around"
|
||||
description="Step name for 'Move Around' step"
|
||||
id="gui.howtos.talking.step_talesMoveAround"
|
||||
/>
|
||||
),
|
||||
image: 'talesMoveAround'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose Another Backdrop"
|
||||
description="Step name for 'Choose Another Backdrop' step"
|
||||
id="gui.howtos.talking.step_talesChooseAnotherBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseAnotherBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Animate Talking"
|
||||
description="Step name for 'Animate Talking' step"
|
||||
id="gui.howtos.talking.step_talesAnimateTalking"
|
||||
/>
|
||||
),
|
||||
image: 'talesAnimateTalking'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose Another Backdrop"
|
||||
description="Step name for 'Choose Another Backdrop' step"
|
||||
id="gui.howtos.talking.step_talesChooseThirdBackdrop"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseThirdBackdrop'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Choose a Song to Dance To"
|
||||
description="Step name for 'Choose a Song to Dance To' step"
|
||||
id="gui.howtos.talking.step_talesChooseSound"
|
||||
/>
|
||||
),
|
||||
image: 'talesChooseSound'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Dance Moves"
|
||||
description="Step name for 'Dance Moves' step"
|
||||
id="gui.howtos.talking.step_talesDanceMoves"
|
||||
/>
|
||||
),
|
||||
image: 'talesDanceMoves'
|
||||
}, {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
defaultMessage="Get the Ask and Answer Blocks from the Sensing Category"
|
||||
description="Step name for 'Get the Ask and Answer Blocks from the Sensing Category' step"
|
||||
id="gui.howtos.talking.step_talesAskAnswer"
|
||||
/>
|
||||
),
|
||||
image: 'talesAskAnswer'
|
||||
}, {
|
||||
deckIds: [
|
||||
'Tell-A-Story',
|
||||
'Animate-A-Character'
|
||||
]
|
||||
}
|
||||
],
|
||||
urlId: 'talking'
|
||||
},
|
||||
|
||||
'add-sprite': {
|
||||
name: (
|
||||
<FormattedMessage
|
||||
|
||||
+74
-64
@@ -4,7 +4,8 @@ const categorySeparator = '<sep gap="36"/>';
|
||||
|
||||
const blockSeparator = '<sep gap="36"/>'; // At default scale, about 28px
|
||||
|
||||
const motion = function (isStage, targetId) {
|
||||
/* eslint-disable no-unused-vars */
|
||||
const motion = function (isInitialSetup, isStage, targetId) {
|
||||
const stageSelected = ScratchBlocks.ScratchMsgs.translate(
|
||||
'MOTION_STAGE_SELECTED',
|
||||
'Stage selected: no motion blocks'
|
||||
@@ -150,7 +151,7 @@ const xmlEscape = function (unsafe) {
|
||||
});
|
||||
};
|
||||
|
||||
const looks = function (isStage, targetId, costumeName, backdropName) {
|
||||
const looks = function (isInitialSetup, isStage, targetId, costumeName, backdropName) {
|
||||
const hello = ScratchBlocks.ScratchMsgs.translate('LOOKS_HELLO', 'Hello!');
|
||||
const hmm = ScratchBlocks.ScratchMsgs.translate('LOOKS_HMM', 'Hmm...');
|
||||
return `
|
||||
@@ -287,7 +288,7 @@ const looks = function (isStage, targetId, costumeName, backdropName) {
|
||||
`;
|
||||
};
|
||||
|
||||
const sound = function (isStage, targetId, soundName) {
|
||||
const sound = function (isInitialSetup, isStage, targetId, soundName) {
|
||||
return `
|
||||
<category name="%{BKY_CATEGORY_SOUND}" id="sound" colour="#D65CD6" secondaryColour="#BD42BD">
|
||||
<block id="${targetId}_sound_playuntildone" type="sound_playuntildone">
|
||||
@@ -342,7 +343,7 @@ const sound = function (isStage, targetId, soundName) {
|
||||
`;
|
||||
};
|
||||
|
||||
const events = function (isStage) {
|
||||
const events = function (isInitialSetup, isStage) {
|
||||
return `
|
||||
<category name="%{BKY_CATEGORY_EVENTS}" id="events" colour="#FFD500" secondaryColour="#CC9900">
|
||||
<block type="event_whenflagclicked"/>
|
||||
@@ -381,7 +382,7 @@ const events = function (isStage) {
|
||||
`;
|
||||
};
|
||||
|
||||
const control = function (isStage) {
|
||||
const control = function (isInitialSetup, isStage) {
|
||||
return `
|
||||
<category name="%{BKY_CATEGORY_CONTROL}" id="control" colour="#FFAB19" secondaryColour="#CF8B17">
|
||||
<block type="control_wait">
|
||||
@@ -428,7 +429,7 @@ const control = function (isStage) {
|
||||
`;
|
||||
};
|
||||
|
||||
const sensing = function (isStage) {
|
||||
const sensing = function (isInitialSetup, isStage) {
|
||||
const name = ScratchBlocks.ScratchMsgs.translate('SENSING_ASK_TEXT', 'What\'s your name?');
|
||||
return `
|
||||
<category name="%{BKY_CATEGORY_SENSING}" id="sensing" colour="#4CBFE6" secondaryColour="#2E8EB8">
|
||||
@@ -458,13 +459,15 @@ const sensing = function (isStage) {
|
||||
</block>
|
||||
${blockSeparator}
|
||||
`}
|
||||
<block id="askandwait" type="sensing_askandwait">
|
||||
<value name="QUESTION">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${name}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
${isInitialSetup ? '' : `
|
||||
<block id="askandwait" type="sensing_askandwait">
|
||||
<value name="QUESTION">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${name}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
`}
|
||||
<block id="answer" type="sensing_answer"/>
|
||||
${blockSeparator}
|
||||
<block type="sensing_keypressed">
|
||||
@@ -501,7 +504,7 @@ const sensing = function (isStage) {
|
||||
`;
|
||||
};
|
||||
|
||||
const operators = function () {
|
||||
const operators = function (isInitialSetup) {
|
||||
const apple = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_APPLE', 'apple');
|
||||
const banana = ScratchBlocks.ScratchMsgs.translate('OPERATORS_JOIN_BANANA', 'banana');
|
||||
const letter = ScratchBlocks.ScratchMsgs.translate('OPERATORS_LETTEROF_APPLE', 'a');
|
||||
@@ -610,49 +613,51 @@ const operators = function () {
|
||||
<block type="operator_or"/>
|
||||
<block type="operator_not"/>
|
||||
${blockSeparator}
|
||||
<block type="operator_join">
|
||||
<value name="STRING1">
|
||||
${isInitialSetup ? '' : `
|
||||
<block type="operator_join">
|
||||
<value name="STRING1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${apple} </field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="STRING2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${banana}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_letter_of">
|
||||
<value name="LETTER">
|
||||
<shadow type="math_whole_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="STRING">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${apple}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_length">
|
||||
<value name="STRING">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${apple}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_contains" id="operator_contains">
|
||||
<value name="STRING1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${apple} </field>
|
||||
<field name="TEXT">${apple}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="STRING2">
|
||||
</value>
|
||||
<value name="STRING2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${banana}</field>
|
||||
<field name="TEXT">${letter}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_letter_of">
|
||||
<value name="LETTER">
|
||||
<shadow type="math_whole_number">
|
||||
<field name="NUM">1</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="STRING">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${apple}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_length">
|
||||
<value name="STRING">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${apple}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
<block type="operator_contains" id="operator_contains">
|
||||
<value name="STRING1">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${apple}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
<value name="STRING2">
|
||||
<shadow type="text">
|
||||
<field name="TEXT">${letter}</field>
|
||||
</shadow>
|
||||
</value>
|
||||
</block>
|
||||
</value>
|
||||
</block>
|
||||
`}
|
||||
${blockSeparator}
|
||||
<block type="operator_mod">
|
||||
<value name="NUM1">
|
||||
@@ -709,12 +714,16 @@ const myBlocks = function () {
|
||||
</category>
|
||||
`;
|
||||
};
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
const xmlOpen = '<xml style="display: none">';
|
||||
const xmlClose = '</xml>';
|
||||
|
||||
/**
|
||||
* @param {!boolean} isStage - Whether the toolbox is for a stage-type target.
|
||||
* @param {!boolean} isInitialSetup - Whether the toolbox is for initial setup. If the mode is "initial setup",
|
||||
* blocks with localized default parameters (e.g. ask and wait) should not be loaded. (LLK/scratch-gui#5445)
|
||||
* @param {?boolean} isStage - Whether the toolbox is for a stage-type target. This is always set to true
|
||||
* when isInitialSetup is true.
|
||||
* @param {?string} targetId - The current editing target
|
||||
* @param {?Array.<object>} categoriesXML - optional array of `{id,xml}` for categories. This can include both core
|
||||
* and other extensions: core extensions will be placed in the normal Scratch order; others will go at the bottom.
|
||||
@@ -725,8 +734,9 @@ const xmlClose = '</xml>';
|
||||
* @param {?string} soundName - The name of the default selected sound dropdown.
|
||||
* @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox.
|
||||
*/
|
||||
const makeToolboxXML = function (isStage, targetId, categoriesXML = [],
|
||||
const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categoriesXML = [],
|
||||
costumeName = '', backdropName = '', soundName = '') {
|
||||
isStage = isInitialSetup || isStage;
|
||||
const gap = [categorySeparator];
|
||||
|
||||
costumeName = xmlEscape(costumeName);
|
||||
@@ -743,15 +753,15 @@ const makeToolboxXML = function (isStage, targetId, categoriesXML = [],
|
||||
}
|
||||
// return `undefined`
|
||||
};
|
||||
const motionXML = moveCategory('motion') || motion(isStage, targetId);
|
||||
const looksXML = moveCategory('looks') || looks(isStage, targetId, costumeName, backdropName);
|
||||
const soundXML = moveCategory('sound') || sound(isStage, targetId, soundName);
|
||||
const eventsXML = moveCategory('event') || events(isStage, targetId);
|
||||
const controlXML = moveCategory('control') || control(isStage, targetId);
|
||||
const sensingXML = moveCategory('sensing') || sensing(isStage, targetId);
|
||||
const operatorsXML = moveCategory('operators') || operators(isStage, targetId);
|
||||
const variablesXML = moveCategory('data') || variables(isStage, targetId);
|
||||
const myBlocksXML = moveCategory('procedures') || myBlocks(isStage, targetId);
|
||||
const motionXML = moveCategory('motion') || motion(isInitialSetup, isStage, targetId);
|
||||
const looksXML = moveCategory('looks') || looks(isInitialSetup, isStage, targetId, costumeName, backdropName);
|
||||
const soundXML = moveCategory('sound') || sound(isInitialSetup, isStage, targetId, soundName);
|
||||
const eventsXML = moveCategory('event') || events(isInitialSetup, isStage, targetId);
|
||||
const controlXML = moveCategory('control') || control(isInitialSetup, isStage, targetId);
|
||||
const sensingXML = moveCategory('sensing') || sensing(isInitialSetup, isStage, targetId);
|
||||
const operatorsXML = moveCategory('operators') || operators(isInitialSetup, isStage, targetId);
|
||||
const variablesXML = moveCategory('data') || variables(isInitialSetup, isStage, targetId);
|
||||
const myBlocksXML = moveCategory('procedures') || myBlocks(isInitialSetup, isStage, targetId);
|
||||
|
||||
const everything = [
|
||||
xmlOpen,
|
||||
|
||||
@@ -251,6 +251,7 @@ const ProjectSaverHOC = function (WrappedComponent) {
|
||||
if (id && this.props.onUpdateProjectThumbnail) {
|
||||
this.storeProjectThumbnail(id);
|
||||
}
|
||||
this.reportTelemetryEvent('projectDidSave');
|
||||
return response;
|
||||
})
|
||||
.catch(err => {
|
||||
@@ -291,9 +292,15 @@ const ProjectSaverHOC = function (WrappedComponent) {
|
||||
*/
|
||||
// TODO make a telemetry HOC and move this stuff there
|
||||
reportTelemetryEvent (event) {
|
||||
if (this.props.onProjectTelemetryEvent) {
|
||||
const metadata = collectMetadata(this.props.vm, this.props.reduxProjectTitle, this.props.locale);
|
||||
this.props.onProjectTelemetryEvent(event, metadata);
|
||||
try {
|
||||
if (this.props.onProjectTelemetryEvent) {
|
||||
const metadata = collectMetadata(this.props.vm, this.props.reduxProjectTitle, this.props.locale);
|
||||
this.props.onProjectTelemetryEvent(event, metadata);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Telemetry error', event, e);
|
||||
// This is intentionally fire/forget because a failure
|
||||
// to report telemetry should not block saving
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
en: {}
|
||||
};
|
||||
@@ -8,7 +8,8 @@ const {
|
||||
getLogs,
|
||||
loadUri,
|
||||
scope,
|
||||
rightClickText
|
||||
rightClickText,
|
||||
findByText
|
||||
} = new SeleniumHelper();
|
||||
|
||||
const uri = path.resolve(__dirname, '../../build/index.html');
|
||||
@@ -25,16 +26,12 @@ describe('Localization', () => {
|
||||
});
|
||||
|
||||
test('Switching languages', async () => {
|
||||
await driver.quit();
|
||||
driver = getDriver();
|
||||
await loadUri(uri);
|
||||
|
||||
// Add a sprite to make sure it stays when switching languages
|
||||
await clickText('Costumes');
|
||||
await clickXpath('//button[@aria-label="Choose a Sprite"]');
|
||||
await clickText('Apple', scope.modal); // Closes modal
|
||||
|
||||
await clickText('Code');
|
||||
await clickXpath('//*[@aria-label="language selector"]');
|
||||
await clickText('Deutsch');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks refresh
|
||||
@@ -66,4 +63,15 @@ describe('Localization', () => {
|
||||
const logs = await getLogs();
|
||||
await expect(logs).toEqual([]);
|
||||
});
|
||||
|
||||
// test for #5445
|
||||
test('Loading with locale shows correct translation for string length block parameter', async () => {
|
||||
await loadUri(`${uri}?locale=ja`);
|
||||
await clickText('演算'); // Operators category in Japanese
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // wait for blocks to scroll
|
||||
await clickText('の長さ', scope.blocksTab); // Click "length <apple>" block
|
||||
await findByText('3', scope.reportedValue); // Tooltip with result
|
||||
const logs = await getLogs();
|
||||
await expect(logs).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import {mountWithIntl} from '../../helpers/intl-helpers';
|
||||
import MenuBar from '../../../src/components/menu-bar/menu-bar';
|
||||
import {menuInitialState} from '../../../src/reducers/menus';
|
||||
import {LoadingState} from '../../../src/reducers/project-state';
|
||||
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {Provider} from 'react-redux';
|
||||
import VM from 'scratch-vm';
|
||||
|
||||
|
||||
describe('MenuBar Component', () => {
|
||||
const store = configureStore()({
|
||||
locales: {
|
||||
isRtl: false,
|
||||
locale: 'en-US'
|
||||
},
|
||||
scratchGui: {
|
||||
menus: menuInitialState,
|
||||
projectState: {
|
||||
loadingState: LoadingState.NOT_LOADED
|
||||
},
|
||||
vm: new VM()
|
||||
}
|
||||
});
|
||||
|
||||
const getComponent = function (props = {}) {
|
||||
return <Provider store={store}><MenuBar {...props} /></Provider>;
|
||||
};
|
||||
|
||||
test('menu bar with no About handler has no About button', () => {
|
||||
const menuBar = mountWithIntl(getComponent());
|
||||
const button = menuBar.find('AboutButton');
|
||||
expect(button.exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('menu bar with an About handler has an About button', () => {
|
||||
const onClickAbout = jest.fn();
|
||||
const menuBar = mountWithIntl(getComponent({onClickAbout}));
|
||||
const button = menuBar.find('AboutButton');
|
||||
expect(button.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('clicking on About button calls the handler', () => {
|
||||
const onClickAbout = jest.fn();
|
||||
const menuBar = mountWithIntl(getComponent({onClickAbout}));
|
||||
const button = menuBar.find('AboutButton');
|
||||
expect(onClickAbout).toHaveBeenCalledTimes(0);
|
||||
button.simulate('click');
|
||||
expect(onClickAbout).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'web-audio-test-api';
|
||||
import SharedAudioContext from '../../../src/lib/audio/shared-audio-context';
|
||||
|
||||
describe('Shared Audio Context', () => {
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
test('returns empty object without user gesture', () => {
|
||||
const sharedAudioContext = new SharedAudioContext();
|
||||
expect(sharedAudioContext).toMatchObject({});
|
||||
});
|
||||
|
||||
test('returns AudioContext when mousedown is triggered', () => {
|
||||
const sharedAudioContext = new SharedAudioContext();
|
||||
const event = new Event('mousedown');
|
||||
document.dispatchEvent(event);
|
||||
expect(sharedAudioContext).toMatchObject(audioContext);
|
||||
});
|
||||
|
||||
test('returns AudioContext when touchstart is triggered', () => {
|
||||
const sharedAudioContext = new SharedAudioContext();
|
||||
const event = new Event('touchstart');
|
||||
document.dispatchEvent(event);
|
||||
expect(sharedAudioContext).toMatchObject(audioContext);
|
||||
});
|
||||
});
|
||||
@@ -95,6 +95,10 @@ const base = {
|
||||
plugins: []
|
||||
};
|
||||
|
||||
if (!process.env.CI) {
|
||||
base.plugins.push(new webpack.ProgressPlugin());
|
||||
}
|
||||
|
||||
module.exports = [
|
||||
// to run editor examples
|
||||
defaultsDeep({}, base, {
|
||||
|
||||
Reference in new issue
Block a user