Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/components/gui/gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,6 @@ const GUIComponent = props => {
onRequestCloseTelemetryModal,
onSeeCommunity,
onShare,
onStartSelectingFileUpload,
onTelemetryModalCancel,
onTelemetryModalOptIn,
onTelemetryModalOptOut,
Expand Down Expand Up @@ -227,7 +226,6 @@ const GUIComponent = props => {
onProjectTelemetryEvent={onProjectTelemetryEvent}
onSeeCommunity={onSeeCommunity}
onShare={onShare}
onStartSelectingFileUpload={onStartSelectingFileUpload}
onToggleLoginOpen={onToggleLoginOpen}
/>
<Box className={styles.bodyWrapper}>
Expand Down Expand Up @@ -401,7 +399,6 @@ GUIComponent.propTypes = {
onRequestCloseTelemetryModal: PropTypes.func,
onSeeCommunity: PropTypes.func,
onShare: PropTypes.func,
onStartSelectingFileUpload: PropTypes.func,
onTabSelect: PropTypes.func,
onTelemetryModalCancel: PropTypes.func,
onTelemetryModalOptIn: PropTypes.func,
Expand Down
21 changes: 16 additions & 5 deletions src/components/menu-bar/menu-bar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {ComingSoonTooltip} from '../coming-soon/coming-soon.jsx';
import Divider from '../divider/divider.jsx';
import LanguageSelector from '../../containers/language-selector.jsx';
import SaveStatus from './save-status.jsx';
import SBFileUploader from '../../containers/sb-file-uploader.jsx';
import ProjectWatcher from '../../containers/project-watcher.jsx';
import MenuBarMenu from './menu-bar-menu.jsx';
import {MenuItem, MenuSection} from '../menu/menu.jsx';
Expand Down Expand Up @@ -391,11 +392,22 @@ class MenuBar extends React.Component {
</MenuSection>
)}
<MenuSection>
<MenuItem
onClick={this.props.onStartSelectingFileUpload}
<SBFileUploader
canSave={this.props.canSave}
userOwnsProject={this.props.userOwnsProject}
>
{this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
</MenuItem>
{(className, renderFileInput, handleLoadProject) => (
<MenuItem
className={className}
onClick={handleLoadProject}
>
{/* eslint-disable max-len */}
{this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)}
{/* eslint-enable max-len */}
{renderFileInput()}
</MenuItem>
)}
</SBFileUploader>
<SB3Downloader>{(className, downloadProjectCallback) => (
<MenuItem
className={className}
Expand Down Expand Up @@ -731,7 +743,6 @@ MenuBar.propTypes = {
onRequestCloseLogin: PropTypes.func,
onSeeCommunity: PropTypes.func,
onShare: PropTypes.func,
onStartSelectingFileUpload: PropTypes.func,
onToggleLoginOpen: PropTypes.func,
projectTitle: PropTypes.string,
renderLogin: PropTypes.func,
Expand Down
2 changes: 0 additions & 2 deletions src/containers/gui.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {

import FontLoaderHOC from '../lib/font-loader-hoc.jsx';
import LocalizationHOC from '../lib/localization-hoc.jsx';
import SBFileUploaderHOC from '../lib/sb-file-uploader-hoc.jsx';
import ProjectFetcherHOC from '../lib/project-fetcher-hoc.jsx';
import TitledHOC from '../lib/titled-hoc.jsx';
import ProjectSaverHOC from '../lib/project-saver-hoc.jsx';
Expand Down Expand Up @@ -182,7 +181,6 @@ const WrappedGui = compose(
ProjectSaverHOC,
vmListenerHOC,
vmManagerHOC,
SBFileUploaderHOC,
cloudManagerHOC
)(ConnectedGUI);

Expand Down
226 changes: 226 additions & 0 deletions src/containers/sb-file-uploader.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import bindAll from 'lodash.bindall';
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import {defineMessages, injectIntl, intlShape} from 'react-intl';
import {setProjectTitle} from '../reducers/project-title';

import log from '../lib/log';
import sharedMessages from '../lib/shared-messages';

import {
LoadingStates,
getIsLoadingUpload,
getIsShowingWithoutId,
onLoadedProject,
requestProjectUpload
} from '../reducers/project-state';

import {
openLoadingProject,
closeLoadingProject
} from '../reducers/modals';
import {
closeFileMenu
} from '../reducers/menus';

/**
* SBFileUploader component passes a file input, load handler and props to its child.
* It expects this child to be a function with the signature
* function (renderFileInput, handleLoadProject) {}
* The component can then be used to attach project loading functionality
* to any other component:
*
* <SBFileUploader>{(className, renderFileInput, handleLoadProject) => (
* <MyCoolComponent
* className={className}
* onClick={handleLoadProject}
* >
* {renderFileInput()}
* </MyCoolComponent>
* )}</SBFileUploader>
*/

const messages = defineMessages({
loadError: {
id: 'gui.projectLoader.loadError',
defaultMessage: 'The project file that was selected failed to load.',
description: 'An error that displays when a local project file fails to load.'
}
});

class SBFileUploader extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'getProjectTitleFromFilename',
'renderFileInput',
'setFileInput',
'handleChange',
'handleClick',
'onload',
'resetFileInput'
]);
}
componentWillMount () {
this.reader = new FileReader();
this.reader.onload = this.onload;
this.resetFileInput();
}
componentDidUpdate (prevProps) {
if (this.props.isLoadingUpload && !prevProps.isLoadingUpload && this.fileToUpload && this.reader) {
this.reader.readAsArrayBuffer(this.fileToUpload);
}
}
componentWillUnmount () {
this.reader = null;
this.resetFileInput();
}
resetFileInput () {
this.fileToUpload = null;
if (this.fileInput) {
this.fileInput.value = null;
}
}
getProjectTitleFromFilename (fileInputFilename) {
if (!fileInputFilename) return '';
// only parse title with valid scratch project extensions
// (.sb, .sb2, and .sb3)
const matches = fileInputFilename.match(/^(.*)\.sb[23]?$/);
if (!matches) return '';
return matches[1].substring(0, 100); // truncate project title to max 100 chars
}
// called when user has finished selecting a file to upload
handleChange (e) {
const {
intl,
isShowingWithoutId,
loadingState,
projectChanged,
userOwnsProject
} = this.props;

const thisFileInput = e.target;
if (thisFileInput.files) { // Don't attempt to load if no file was selected
this.fileToUpload = thisFileInput.files[0];

// If user owns the project, or user has changed the project,
// we must confirm with the user that they really intend to replace it.
// (If they don't own the project and haven't changed it, no need to confirm.)
let uploadAllowed = true;
if (userOwnsProject || (projectChanged && isShowingWithoutId)) {
uploadAllowed = confirm( // eslint-disable-line no-alert
intl.formatMessage(sharedMessages.replaceProjectWarning)
);
}
if (uploadAllowed) {
this.props.requestProjectUpload(loadingState);
} else {
this.props.closeFileMenu();
}
}
}
// called when file upload raw data is available in the reader
onload () {
if (this.reader) {
this.props.onLoadingStarted();
const filename = this.fileToUpload && this.fileToUpload.name;
this.props.vm.loadProject(this.reader.result)
.then(() => {
this.props.onLoadingFinished(this.props.loadingState, true);
// Reset the file input after project is loaded
// This is necessary in case the user wants to reload a project
if (filename) {
const uploadedProjectTitle = this.getProjectTitleFromFilename(filename);
this.props.onReceivedProjectTitle(uploadedProjectTitle);
}
this.resetFileInput();
})
.catch(error => {
log.warn(error);
alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert
this.props.onLoadingFinished(this.props.loadingState, false);
// Reset the file input after project is loaded
// This is necessary in case the user wants to reload a project
this.resetFileInput();
});
}
}
handleClick () {
// open filesystem browsing window
this.fileInput.click();
}
setFileInput (input) {
this.fileInput = input;
}
renderFileInput () {
return (
<input
accept=".sb,.sb2,.sb3"
ref={this.setFileInput}
style={{display: 'none'}}
type="file"
onChange={this.handleChange}
/>
);
}
render () {
return this.props.children(this.props.className, this.renderFileInput, this.handleClick);
}
}

SBFileUploader.propTypes = {
canSave: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types
children: PropTypes.func,
className: PropTypes.string,
closeFileMenu: PropTypes.func,
intl: intlShape.isRequired,
isLoadingUpload: PropTypes.bool,
isShowingWithoutId: PropTypes.bool,
loadingState: PropTypes.oneOf(LoadingStates),
onLoadingFinished: PropTypes.func,
onLoadingStarted: PropTypes.func,
projectChanged: PropTypes.bool,
requestProjectUpload: PropTypes.func,
onReceivedProjectTitle: PropTypes.func,
userOwnsProject: PropTypes.bool,
vm: PropTypes.shape({
loadProject: PropTypes.func
})
};
SBFileUploader.defaultProps = {
className: ''
};
const mapStateToProps = state => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
isLoadingUpload: getIsLoadingUpload(loadingState),
isShowingWithoutId: getIsShowingWithoutId(loadingState),
loadingState: loadingState,
projectChanged: state.scratchGui.projectChanged,
vm: state.scratchGui.vm
};
};

const mapDispatchToProps = (dispatch, ownProps) => ({
closeFileMenu: () => dispatch(closeFileMenu()),
onLoadingFinished: (loadingState, success) => {
dispatch(onLoadedProject(loadingState, ownProps.canSave, success));
dispatch(closeLoadingProject());
dispatch(closeFileMenu());
},
requestProjectUpload: loadingState => dispatch(requestProjectUpload(loadingState)),
onLoadingStarted: () => dispatch(openLoadingProject()),
onReceivedProjectTitle: title => dispatch(setProjectTitle(title))
});

// Allow incoming props to override redux-provided props. Used to mock in tests.
const mergeProps = (stateProps, dispatchProps, ownProps) => Object.assign(
{}, stateProps, dispatchProps, ownProps
);

export default connect(
mapStateToProps,
mapDispatchToProps,
mergeProps
)(injectIntl(SBFileUploader));
Loading