diff --git a/package.json b/package.json index 2bf0c55357ef60946dc439914b76548e442ad6a0..d33b9d1db30728963df5b502e48fd3c5d3918330 100644 --- a/package.json +++ b/package.json @@ -99,10 +99,10 @@ "scratch-blocks": "0.1.0-prerelease.1535662135", "scratch-l10n": "3.0.20180830210150", "scratch-paint": "0.2.0-prerelease.20180918203812", - "scratch-render": "0.1.0-prerelease.20180907144714", + "scratch-render": "0.1.0-prerelease.20180918201144", "scratch-storage": "1.0.2", "scratch-svg-renderer": "0.2.0-prerelease.20180907141232", - "scratch-vm": "0.2.0-prerelease.20180912222010", + "scratch-vm": "0.2.0-prerelease.20180918201814", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.23.0", diff --git a/src/components/language-selector/language-selector.jsx b/src/components/language-selector/language-selector.jsx index fd4caef5f93590ddcd5878920ca4c084d292545c..864c4848fd774a32c9bd712fb2d0883d185b47aa 100644 --- a/src/components/language-selector/language-selector.jsx +++ b/src/components/language-selector/language-selector.jsx @@ -5,7 +5,7 @@ import locales from 'scratch-l10n'; import styles from './language-selector.css'; // supported languages to exclude from the menu, but allow as a URL option -const ignore = ['he']; +const ignore = []; const LanguageSelector = ({currentLocale, label, onChange}) => ( <select diff --git a/src/containers/sprite-selector-item.jsx b/src/containers/sprite-selector-item.jsx index 01e617c8e77385244507977b037c7f31a918a26d..9b154c59b0ae97a85522865db8cec792fd1ad65b 100644 --- a/src/containers/sprite-selector-item.jsx +++ b/src/containers/sprite-selector-item.jsx @@ -32,7 +32,17 @@ class SpriteSelectorItem extends React.Component { ]); this.svgRenderer = new SVGRenderer(); // Asset ID of the SVG currently in SVGRenderer - this.svgRendererAssetId = null; + this.decodedAssetId = null; + } + shouldComponentUpdate (nextProps) { + // Ignore dragPayload due to https://github.com/LLK/scratch-gui/issues/3172. + // This function should be removed once the issue is fixed. + for (const property in nextProps) { + if (property !== 'dragPayload' && this.props[property] !== nextProps[property]) { + return true; + } + } + return false; } getCostumeUrl () { if (this.props.costumeURL) return this.props.costumeURL; @@ -44,18 +54,20 @@ class SpriteSelectorItem extends React.Component { // Avoid parsing the SVG when possible, since it's expensive. if (asset.assetType === storage.AssetType.ImageVector) { // If the asset ID has not changed, no need to re-parse - if (this.svgRendererAssetId === this.props.assetId) { + if (this.decodedAssetId === this.props.assetId) { + // @todo consider caching more than one URL. return this.cachedUrl; } - + this.decodedAssetId = this.props.assetId; const svgString = this.props.vm.runtime.storage.get(this.props.assetId).decodeText(); if (svgString.match(HAS_FONT_REGEXP)) { - this.svgRendererAssetId = this.props.assetId; this.svgRenderer.loadString(svgString); const svgText = this.svgRenderer.toString(true /* shouldInjectFonts */); this.cachedUrl = `data:image/svg+xml;utf8,${encodeURIComponent(svgText)}`; - return this.cachedUrl; + } else { + this.cachedUrl = this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI(); } + return this.cachedUrl; } return this.props.vm.runtime.storage.get(this.props.assetId).encodeDataURI(); } diff --git a/src/containers/stage.jsx b/src/containers/stage.jsx index 1ab84412eed16a676b3e9e8fcf04d2b9cd0c6f78..0aa3f2a4b64ef883e29cf0b47ff0a4f1d8d93cd3 100644 --- a/src/containers/stage.jsx +++ b/src/containers/stage.jsx @@ -52,9 +52,9 @@ class Stage extends React.Component { colorInfo: null, question: null }; - if (this.props.vm.runtime.renderer) { - this.renderer = this.props.vm.runtime.renderer; - this.canvas = this.props.vm.runtime.renderer._gl.canvas; + if (this.props.vm.renderer) { + this.renderer = this.props.vm.renderer; + this.canvas = this.renderer.canvas; } else { this.canvas = document.createElement('canvas'); this.renderer = new Renderer(this.canvas); diff --git a/src/lib/app-state-hoc.jsx b/src/lib/app-state-hoc.jsx index fe7ac023eab97c5df5aea8e5829a6769d28392b5..93d97180ef38526ac05feaf019338aa32ed4496b 100644 --- a/src/lib/app-state-hoc.jsx +++ b/src/lib/app-state-hoc.jsx @@ -10,6 +10,7 @@ import {setPlayer, setFullScreen} from '../reducers/mode.js'; import locales from 'scratch-l10n'; import {detectLocale} from './detect-locale'; +import {detectTutorialId} from './tutorial-from-url'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; @@ -50,16 +51,27 @@ const AppStateHOC = function (WrappedComponent, localesOnly) { guiInitialState, guiMiddleware, initFullScreen, - initPlayer + initPlayer, + initTutorialCard } = guiRedux; const {ScratchPaintReducer} = require('scratch-paint'); let initializedGui = guiInitialState; - if (props.isFullScreen) { - initializedGui = initFullScreen(initializedGui); - } - if (props.isPlayerOnly) { - initializedGui = initPlayer(initializedGui); + if (props.isFullScreen || props.isPlayerOnly) { + if (props.isFullScreen) { + initializedGui = initFullScreen(initializedGui); + } + if (props.isPlayerOnly) { + initializedGui = initPlayer(initializedGui); + } + } else { + const tutorialId = detectTutorialId(); + if (tutorialId !== null) { + // When loading a tutorial from the URL, + // load w/o preview modal + // open requested tutorial card + initializedGui = initTutorialCard(initializedGui, tutorialId); + } } reducers = { locales: localesReducer, diff --git a/src/lib/libraries/decks/index.jsx b/src/lib/libraries/decks/index.jsx index 096ca15224a7882aa7fe243efad76bf5b8a736cc..f8324f3ae73068221fe0b86bc68b5eb0757a9db5 100644 --- a/src/lib/libraries/decks/index.jsx +++ b/src/lib/libraries/decks/index.jsx @@ -99,7 +99,8 @@ export default { 'add-sprite' ] } - ] + ], + urlId: 1 }, 'animate-a-name': { name: ( @@ -172,7 +173,8 @@ export default { 'glide-around' ] } - ] + ], + urlId: 2 }, 'Make-Music': { name: ( @@ -239,7 +241,8 @@ export default { 'add-sprite' ] } - ] + ], + urlId: 3 }, 'Make-A-Game': { name: ( @@ -323,7 +326,8 @@ export default { 'move-around-with-arrow-keys' ] } - ] + ], + urlId: 4 }, 'Chase-Game': { @@ -425,7 +429,8 @@ export default { 'move-around-with-arrow-keys' ] } - ] + ], + urlId: 5 }, 'add-sprite': { name: ( @@ -453,7 +458,8 @@ export default { 'switch-costume' ] } - ] + ], + urlId: 6 }, 'add-a-backdrop': { name: ( @@ -471,7 +477,8 @@ export default { 'change-size', 'switch-costume' ] - }] + }], + urlId: 7 }, 'change-size': { name: ( @@ -489,7 +496,8 @@ export default { 'glide-around', 'spin-video' ] - }] + }], + urlId: 8 }, 'glide-around': { name: ( @@ -507,7 +515,8 @@ export default { 'add-a-backdrop', 'switch-costume' ] - }] + }], + urlId: 9 }, 'record-a-sound': { @@ -526,8 +535,8 @@ export default { 'Make-Music', 'switch-costume' ] - }] - + }], + urlId: 10 }, 'spin-video': { name: ( @@ -545,7 +554,8 @@ export default { 'add-a-backdrop', 'switch-costume' ] - }] + }], + urlId: 11 }, 'hide-and-show': { name: ( @@ -563,7 +573,8 @@ export default { 'add-a-backdrop', 'switch-costume' ] - }] + }], + urlId: 12 }, 'switch-costume': { @@ -582,7 +593,8 @@ export default { 'add-a-backdrop', 'add-effects' ] - }] + }], + urlId: 13 }, 'move-around-with-arrow-keys': { @@ -601,7 +613,8 @@ export default { 'add-a-backdrop', 'switch-costume' ] - }] + }], + urlId: 14 }, 'add-effects': { name: ( @@ -619,6 +632,7 @@ export default { 'add-a-backdrop', 'switch-costume' ] - }] + }], + urlId: 15 } }; diff --git a/src/lib/tutorial-from-url.js b/src/lib/tutorial-from-url.js new file mode 100644 index 0000000000000000000000000000000000000000..c627b8db7abf1a709719c12add3b6eec64f13257 --- /dev/null +++ b/src/lib/tutorial-from-url.js @@ -0,0 +1,48 @@ +/** + * @fileoverview + * Utility function to detect tutorial id from query paramenter on the URL. + */ + +import tutorials from './libraries/decks/index.jsx'; +import analytics from './analytics'; + +/** + * Get the tutorial id from the given numerical id (representing the + * url id of the tutorial). + * @param {number} urlId The URL Id for the tutorial + * @returns {string} The string id for the tutorial, or null if the URL ID + * was not found. + */ +const getDeckIdFromUrlId = urlId => { + for (const deckId in tutorials) { + if (tutorials[deckId].urlId === urlId) { + analytics.event({ + category: 'how-to', + action: 'load from url', + label: `${deckId}` + }); + return deckId; + } + } + return null; +}; + +/** + * Check if there's a tutorial id provided as a query parameter in the URL. + * Return the corresponding tutorial id or null if not found. + * @return {string} The ID of the requested tutorial or null if no tutorial was + * requested or found. + */ +const detectTutorialId = () => { + if (window.location.search.indexOf('tutorial=') !== -1) { + const urlTutorialId = window.location.search.match(/(?:tutorial)=(\d+)/)[1]; + if (urlTutorialId) { + return getDeckIdFromUrlId(Number(urlTutorialId)); + } + } + return null; +}; + +export { + detectTutorialId +}; diff --git a/src/reducers/gui.js b/src/reducers/gui.js index cfad8f6825ddad3a07a56b4c62757ffbc3a97909..8869c1a35031e1d46b8dd28f4f408d2500946840 100644 --- a/src/reducers/gui.js +++ b/src/reducers/gui.js @@ -21,6 +21,8 @@ import vmReducer, {vmInitialState} from './vm'; import vmStatusReducer, {vmStatusInitialState} from './vm-status'; import throttle from 'redux-throttle'; +import decks from '../lib/libraries/decks/index.jsx'; + const guiMiddleware = compose(applyMiddleware(throttle(300, {leading: true, trailing: true}))); const guiInitialState = { @@ -67,6 +69,27 @@ const initFullScreen = function (currentState) { ); }; +const initTutorialCard = function (currentState, deckId) { + return Object.assign( + {}, + currentState, + { + modals: { + previewInfo: false + }, + cards: { + visible: true, + content: decks, + activeDeckId: deckId, + step: 0, + x: 0, + y: 0, + dragging: false + } + } + ); +}; + const guiReducer = combineReducers({ assetDrag: assetDragReducer, blockDrag: blockDragReducer, @@ -95,5 +118,6 @@ export { guiInitialState, guiMiddleware, initFullScreen, - initPlayer + initPlayer, + initTutorialCard }; diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js index 62199cdcbb0cf2885b969f00a082d9c54b4245b2..f851188c45d705915b946b2a16e6eed5cf0f97af 100644 --- a/test/integration/project-loading.test.js +++ b/test/integration/project-loading.test.js @@ -72,7 +72,7 @@ describe('Loading scratch gui', () => { const projectId = '96708228'; await loadUri(`${uri}#${projectId}`); await clickXpath('//button[@title="Try It"]'); - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise(resolve => setTimeout(resolve, 3000)); await clickXpath('//img[@title="Go"]'); await new Promise(resolve => setTimeout(resolve, 2000)); await clickXpath('//img[@title="Stop"]');