Compare commits

...
Author SHA1 Message Date
Christopher Willis-Ford e2ce299604 Merge tag '0.1.0-prerelease.20200527204130' into scratch-desktop 2020-06-03 17:20:43 -07:00
Christopher Willis-Ford 3e1f3b743b Merge branch 'develop' into scratch-desktop 2020-05-15 16:29:02 -07:00
Chris Willis-Ford 8412cfc46e Merge pull request #5558 from cwillisf/fix-costume-animation
fix costume animation in sprite library
2020-05-13 14:18:01 -07:00
Christopher Willis-Ford b77b3464db Merge tag '0.1.0-prerelease.20200508195134' into scratch-desktop
Includes new people sprites from #5731
2020-05-12 01:03:15 -07:00
Chris Willis-Fordandadroitwhiz ae71ac1941 remove obsolete TODO comment
Co-authored-by: adroitwhiz <adroitwhiz@protonmail.com>
2020-05-06 16:22:42 -07:00
Chris Willis-Fordandapple502j 6eb0f57736 use Array.isArray to detect array
Co-Authored-By: apple502j <33279053+apple502j@users.noreply.github.com>
2020-04-28 13:28:06 -07:00
Christopher Willis-Ford c1d4b8745a Merge tag '0.1.0-prerelease.20200425022051' into scratch-desktop 2020-04-28 13:22:54 -07:00
Christopher Willis-Ford 9219af1db1 future-proof through simplification
`getItemIcons` (formerly `getItemImageSource`) now handles the current
library formats as well as upcoming library changes in which library
items will no longer have a `json` property. Or, at least, it should
work...

As part of that effort, `getItemIcons` now handles library items with
single or multiple icons internally, meaning that the caller no longer
needs to do extra work to check. Likewise, the `LibraryItem` container
now has just one `iconSource` property instead of an `icon` property for
the one-icon case and an `icons` property for the multi-icon case. The
LibraryItem component (not container) still only takes one icon, and the
container is still responsible for managing icon rotation.
2020-04-27 21:20:10 -07:00
Christopher Willis-Ford cf8a2e41df fix lint error with code that's arguably better anyway 2020-04-02 16:52:34 -07:00
Christopher Willis-Ford 25986f0f31 fix costume animation in sprite library 2020-04-02 16:52:34 -07:00
Christopher Willis-Ford 230503cb27 REVERT ME: un-exclude /src for scratch-desktop
This is necessary for the current scratch-desktop build process but when
we merge this branch into develop we should revert this change.
2020-04-02 16:11:52 -07:00
Christopher Willis-Ford 988ce22121 Merge tag '0.1.0-prerelease.20200319183521' into scratch-desktop 2020-03-26 17:57:43 -07:00
Christopher Willis-Ford b9518788dc Merge branch 'init-ga-only-if-id' into scratch-desktop 2020-02-28 11:31:50 -08:00
Christopher Willis-Ford b941d99f5f log a message if we skip GA init 2020-02-28 11:19:40 -08:00
Christopher Willis-Ford 528e8c9c2e Merge tag '0.1.0-prerelease.20200213175628' into scratch-desktop 2020-02-28 10:47:01 -08:00
Christopher Willis-Ford 5ac16672e5 provide empty window.ga(...) when GA_ID is absent 2020-01-28 13:28:21 -08:00
Christopher Willis-Ford 2cf2d4c455 initialize Google Analytics only if GA_ID is set 2020-01-27 17:28:15 -08:00
Christopher Willis-Ford dcf2b8e07c Merge tag '0.1.0-prerelease.20200113203847' into scratch-desktop 2020-01-15 10:37:16 -08:00
Christopher Willis-Ford 062e1ff4cc Merge tag '0.1.0-prerelease.20191120175131' into scratch-desktop 2019-11-25 18:20:20 -08:00
Christopher Willis-Ford dc3b177219 Merge tag '0.1.0-prerelease.20191114160340' into scratch-desktop 2019-11-14 11:18:28 -08:00
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
10 changed files with 272 additions and 49 deletions

No files matched your search

+5 -2
View File
@@ -5,8 +5,11 @@
/node_modules
npm-*
# Double copies of all the static assets and tutorial gifs
/src
# TO REVERT: the scratch-desktop branch wants to build scratch-gui from source.
# To support that we need to un-ignore the /src directory.
# Once we merge the scratch-desktop branch into develop we should re-ignore /src here.
# We can also do that by omitting or reverting this commit during the merge.
# /src
# Testing
/.nyc_output
+9
View File
@@ -13018,6 +13018,15 @@
"react-lifecycles-compat": "^3.0.4"
}
},
"react-visibility-sensor": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/react-visibility-sensor/-/react-visibility-sensor-5.0.2.tgz",
"integrity": "sha512-7+1lr7oQOO2vKr5u/uxoDGFWAAy7lsDy2aJU3Zy1/OymI+TRqOBk8m2L8YE1B0UyfCDWxVAWO+aifVGqNOvqeQ==",
"dev": true,
"requires": {
"prop-types": "^15.6.2"
}
},
"read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+1
View File
@@ -103,6 +103,7 @@
"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",
+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,
+62 -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,61 @@ 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 one or more icon(s) for a library item.
* If it's an animated thumbnail, this will return an array of `imageSource`.
* Otherwise it'll return just one `imageSource`.
* @param {object} item - either a library item or one of a library item's costumes.
* The latter is used internally as part of processing an animated thumbnail.
* @returns {LibraryItem.PropTypes.icons} - an `imageSource` or array of them, ready for `LibraryItem` & `ScratchImage`
*/
const getItemIcons = function (item) {
const costumes = (item.json && item.json.costumes) || item.costumes;
if (costumes) {
return costumes.map(getItemIcons);
}
if (item.rawURL) {
return {
uri: item.rawURL
};
}
if (item.assetId && item.dataFormat) {
return {
assetId: item.assetId,
assetType: getAssetTypeForFileExtension(item.dataFormat)
};
}
const md5ext = item.md5ext || item.md5 || item.baseLayerMD5;
if (md5ext) {
const [assetId, fileExtension] = md5ext.split('.');
return {
assetId: assetId,
assetType: getAssetTypeForFileExtension(fileExtension)
};
}
};
class LibraryComponent extends React.Component {
constructor (props) {
super(props);
@@ -208,8 +264,9 @@ 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 icons = getItemIcons(dataItem);
return (<LibraryItem
bluetoothRequired={dataItem.bluetoothRequired}
collaborator={dataItem.collaborator}
description={dataItem.description}
@@ -217,9 +274,7 @@ 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}
icons={icons}
id={index}
insetIconURL={dataItem.insetIconURL}
internetConnectionRequired={dataItem.internetConnectionRequired}
@@ -230,8 +285,8 @@ class LibraryComponent extends React.Component {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
)) : (
/>);
}) : (
<div className={styles.spinnerWrapper}>
<Spinner
large
@@ -0,0 +1,138 @@
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 {
imageSource: _imageSource,
...imgProps
} = this.props;
return (
<VisibilitySensor
intervalCheck
scrollCheck
>
{
({isVisible}) => {
this.isVisible = isVisible;
ScratchImage.loadPendingImages();
return (
<img
{...imgProps} // do this first in case it contains `src`
src={this.state.imageURI} // overrides imgProps.src if present
/>
);
}
}
</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;
+21 -26
View File
@@ -21,6 +21,7 @@ class LibraryItem extends React.PureComponent {
'startRotatingIcons',
'stopRotatingIcons'
]);
this.hasIconsArray = Array.isArray(props.icons);
this.state = {
iconIndex: 0,
isRotatingIcon: false
@@ -53,7 +54,7 @@ class LibraryItem extends React.PureComponent {
// only show hover effects on the item if not showing a play button
if (!this.props.showPlayButton) {
this.props.onMouseEnter(this.props.id);
if (this.props.icons && this.props.icons.length) {
if (this.hasIconsArray) {
this.stopRotatingIcons();
this.setState({
isRotatingIcon: true
@@ -65,7 +66,7 @@ class LibraryItem extends React.PureComponent {
// only show hover effects on the item if not showing a play button
if (!this.props.showPlayButton) {
this.props.onMouseLeave(this.props.id);
if (this.props.icons && this.props.icons.length) {
if (this.hasIconsArray) {
this.setState({
isRotatingIcon: false
}, this.stopRotatingIcons);
@@ -91,23 +92,22 @@ 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;
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;
curIconSource () {
if (this.hasIconsArray) {
if (this.state.isRotatingIcon &&
this.state.iconIndex < this.props.icons.length &&
this.props.icons[this.state.iconIndex]) {
// multiple icons, currently animating: show current frame
return this.props.icons[this.state.iconIndex];
}
// multiple icons, not currently animating: show first frame
return this.props.icons[0];
}
return iconMd5Prop;
// single icon
return this.props.icons;
}
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 +117,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 +148,10 @@ 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
})
),
icons: PropTypes.oneOfType([
LibraryItemComponent.propTypes.iconSource, // single icon
PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource) // rotating icons
]),
id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
+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,
+19 -6
View File
@@ -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;
+5 -1
View File
@@ -193,7 +193,11 @@ describe('Working with costumes', () => {
.mouseMove(abbyElement)
.perform();
// wait for one of Abby's alternate costumes to appear
await findByXpath('//img[@src="https://cdn.assets.scratch.mit.edu/internalapi/asset/45de34b47a2ce22f6f5d28bb35a44ff5.svg/get/"]');
const src1 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
await driver.sleep(300);
const src2 = await abbyElement.findElement({css: 'img'}).getAttribute('src');
const sourcesMatch = (src1 === src2);
await expect(sourcesMatch).toBeFalsy(); // 'src' attribute should have changed by now
const logs = await getLogs();
await expect(logs).toEqual([]);
});