Compare commits

...
Author SHA1 Message Date
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
9 changed files with 244 additions and 43 deletions

No files matched your search

+9
View File
@@ -10961,6 +10961,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,
+50 -7
View File
@@ -10,6 +10,7 @@ import Divider from '../divider/divider.jsx';
import Filter from '../filter/filter.jsx';
import TagButton from '../../containers/tag-button.jsx';
import Spinner from '../spinner/spinner.jsx';
import storage from '../../lib/storage';
import styles from './library.css';
@@ -29,6 +30,47 @@ const messages = defineMessages({
const ALL_TAG = {tag: 'all', intlLabel: messages.allTag};
const tagListPrefix = [ALL_TAG];
/**
* Find the AssetType which corresponds to a particular file extension. For example, 'png' => AssetType.ImageBitmap.
* @param {string} fileExtension - the file extension to look up.
* @returns {AssetType} - the AssetType corresponding to the extension, if any.
*/
const getAssetTypeForFileExtension = function (fileExtension) {
const compareOptions = {
sensitivity: 'accent',
usage: 'search'
};
for (const assetTypeId in storage.AssetType) {
const assetType = storage.AssetType[assetTypeId];
if (fileExtension.localeCompare(assetType.runtimeFormat, compareOptions) === 0) {
return assetType;
}
}
};
/**
* Figure out an `imageSource` (URI or asset ID & type) for a library item's icon.
* @param {object} item - either a library item or one of a library item's costumes.
* @returns {object} - an `imageSource` ready to be passed to a `ScratchImage`.
*/
const getItemImageSource = function (item) {
if (item.rawURL) {
return {
uri: item.rawURL
};
}
// TODO: adjust libraries to be more storage-friendly; don't use split() here.
const 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 +250,10 @@ class LibraryComponent extends React.Component {
})}
ref={this.setFilteredDataRef}
>
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => (
<LibraryItem
{this.state.loaded ? this.getFilteredData().map((dataItem, index) => {
const iconSource = getItemImageSource(dataItem);
const icons = dataItem.json && dataItem.json.costumes.map(getItemImageSource);
return (<LibraryItem
bluetoothRequired={dataItem.bluetoothRequired}
collaborator={dataItem.collaborator}
description={dataItem.description}
@@ -217,9 +261,8 @@ class LibraryComponent extends React.Component {
extensionId={dataItem.extensionId}
featured={dataItem.featured}
hidden={dataItem.hidden}
iconMd5={dataItem.md5}
iconRawURL={dataItem.rawURL}
icons={dataItem.json && dataItem.json.costumes}
iconSource={iconSource}
icons={icons}
id={index}
insetIconURL={dataItem.insetIconURL}
internetConnectionRequired={dataItem.internetConnectionRequired}
@@ -230,8 +273,8 @@ class LibraryComponent extends React.Component {
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
onSelect={this.handleSelect}
/>
)) : (
/>);
}) : (
<div className={styles.spinnerWrapper}>
<Spinner
large
@@ -0,0 +1,139 @@
import PropTypes from 'prop-types';
import React from 'react';
import VisibilitySensor from 'react-visibility-sensor';
import storage from '../../lib/storage';
class ScratchImage extends React.PureComponent {
static init () {
this._maxParallelism = 6;
this._currentJobs = 0;
this._pendingImages = new Set();
}
static loadPendingImages () {
if (this._currentJobs >= this._maxParallelism) {
// already busy
return;
}
// Find the first visible image. If there aren't any, find the first non-visible image.
let nextImage;
for (const image of this._pendingImages) {
if (image.isVisible) {
nextImage = image;
break;
} else {
nextImage = nextImage || image;
}
}
// If we found an image to load:
// 1) Remove it from the queue
// 2) Load the image
// 3) Pump the queue again
if (nextImage) {
this._pendingImages.delete(nextImage);
const imageSource = nextImage.props.imageSource;
++this._currentJobs;
storage
.load(imageSource.assetType, imageSource.assetId)
.then(asset => {
if (!nextImage.wasUnmounted) {
const dataURI = asset.encodeDataURI();
nextImage.setState({
imageURI: dataURI
});
}
--this._currentJobs;
this.loadPendingImages();
});
}
}
constructor (props) {
super(props);
this.state = {};
Object.assign(this.state, this._loadImageSource(props.imageSource));
}
componentWillReceiveProps (nextProps) {
const newState = this._loadImageSource(nextProps.imageSource);
this.setState(newState);
}
componentWillUnmount () {
this.wasUnmounted = true;
ScratchImage._pendingImages.delete(this);
}
/**
* Calculate the state changes necessary to load the image specified in the provided source info. If the component
* is mounted, call setState() with the return value of this function. If the component has not yet mounted, use
* the return value of this function as initial state for the component.
*
* @param {object} imageSource - the new source for the image, including either assetId or URI
* @returns {object} - the new state values, if any.
*/
_loadImageSource (imageSource) {
if (imageSource) {
if (imageSource.uri) {
ScratchImage._pendingImages.delete(this);
return {
imageURI: imageSource.uri,
lastRequestedAsset: null
};
}
if (this.state.lastRequestedAsset !== imageSource.assetId) {
ScratchImage._pendingImages.add(this);
return {
lastRequestedAsset: imageSource.assetId
};
}
}
// Nothing to do - don't change any state.
return {};
}
render () {
const {
src: _src,
imageSource: _imageSource,
...imgProps
} = this.props;
return (
<VisibilitySensor
intervalCheck
scrollCheck
>
{
({isVisible}) => {
this.isVisible = isVisible;
ScratchImage.loadPendingImages();
return (
<img
src={this.state.imageURI}
{...imgProps}
/>
);
}
}
</VisibilitySensor>
);
}
}
ScratchImage.ImageSourcePropType = PropTypes.oneOfType([
PropTypes.shape({
assetId: PropTypes.string.isRequired,
assetType: PropTypes.oneOf(Object.values(storage.AssetType)).isRequired
}),
PropTypes.shape({
uri: PropTypes.string.isRequired
})
]);
ScratchImage.propTypes = {
imageSource: ScratchImage.ImageSourcePropType.isRequired
};
ScratchImage.init();
export default ScratchImage;
+9 -22
View File
@@ -91,23 +91,17 @@ class LibraryItem extends React.PureComponent {
const nextIconIndex = (this.state.iconIndex + 1) % this.props.icons.length;
this.setState({iconIndex: nextIconIndex});
}
curIconMd5 () {
const iconMd5Prop = this.props.iconMd5;
curIconSource () {
if (this.props.icons &&
this.state.isRotatingIcon &&
this.state.iconIndex < this.props.icons.length) {
const icon = this.props.icons[this.state.iconIndex] || {};
return icon.md5ext || // 3.0 library format
icon.baseLayerMD5 || // 2.0 library format, TODO GH-5084
iconMd5Prop;
this.state.iconIndex < this.props.icons.length &&
this.props.icons[this.state.iconIndex]) {
return this.props.icons[this.state.iconIndex];
}
return iconMd5Prop;
return this.props.iconSource;
}
render () {
const iconMd5 = this.curIconMd5();
const iconURL = iconMd5 ?
`https://cdn.assets.scratch.mit.edu/internalapi/asset/${iconMd5}/get/` :
this.props.iconRawURL;
const iconSource = this.curIconSource();
return (
<LibraryItemComponent
bluetoothRequired={this.props.bluetoothRequired}
@@ -117,8 +111,7 @@ class LibraryItem extends React.PureComponent {
extensionId={this.props.extensionId}
featured={this.props.featured}
hidden={this.props.hidden}
iconURL={iconURL}
icons={this.props.icons}
iconSource={iconSource}
id={this.props.id}
insetIconURL={this.props.insetIconURL}
internetConnectionRequired={this.props.internetConnectionRequired}
@@ -149,14 +142,8 @@ LibraryItem.propTypes = {
extensionId: PropTypes.string,
featured: PropTypes.bool,
hidden: PropTypes.bool,
iconMd5: PropTypes.string,
iconRawURL: PropTypes.string,
icons: PropTypes.arrayOf(
PropTypes.shape({
baseLayerMD5: PropTypes.string, // 2.0 library format, TODO GH-5084
md5ext: PropTypes.string // 3.0 library format
})
),
iconSource: LibraryItemComponent.propTypes.iconSource, // single icon
icons: PropTypes.arrayOf(LibraryItemComponent.propTypes.iconSource), // rotating icons
id: PropTypes.number.isRequired,
insetIconURL: PropTypes.string,
internetConnectionRequired: PropTypes.bool,
+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([]);
});