Files
scratch-gui/src/containers/project-loader.jsx
T

143 lines
4.5 KiB
React

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 analytics from '../lib/analytics';
import log from '../lib/log';
import {setProjectTitle} from '../reducers/project-title';
import {
openLoadingProject,
closeLoadingProject
} from '../reducers/modals';
/**
* Project loader 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, loadProject, props) {}
* The component can then be used to attach project loading functionality
* to any other component:
*
* <ProjectLoader>{(renderFileInput, loadProject, props) => (
* <MyCoolComponent
* onClick={loadProject}
* {...props}
* >
* {renderFileInput()}
* </MyCoolComponent>
* )}</ProjectLoader>
*/
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 ProjectLoader extends React.Component {
constructor (props) {
super(props);
bindAll(this, [
'renderFileInput',
'setFileInput',
'handleChange',
'handleClick'
]);
}
handleChange (e) {
// Remove the hash if any (without triggering a hash change event or a reload)
history.replaceState({}, document.title, '.');
const reader = new FileReader();
const thisFileInput = e.target;
reader.onload = () => this.props.vm.loadProject(reader.result)
.then(() => {
analytics.event({
category: 'project',
action: 'Import Project File',
nonInteraction: true
});
this.props.closeLoadingState();
// Reset the file input after project is loaded
// This is necessary in case the user wants to reload a project
thisFileInput.value = null;
})
.catch(error => {
log.warn(error);
alert(this.props.intl.formatMessage(messages.loadError)); // eslint-disable-line no-alert
this.props.closeLoadingState();
// Reset the file input after project is loaded
// This is necessary in case the user wants to reload a project
thisFileInput.value = null;
});
if (thisFileInput.files) { // Don't attempt to load if no file was selected
this.props.openLoadingState();
reader.readAsArrayBuffer(thisFileInput.files[0]);
if (thisFileInput.files[0].name) {
const matches = thisFileInput.files[0].name.match(/^(.*)\.sb3$/);
if (matches) {
this.props.onSetProjectTitle(matches[1].substring(0, 100));
}
}
}
}
handleClick () {
this.fileInput.click();
}
setFileInput (input) {
this.fileInput = input;
}
renderFileInput () {
return (
<input
accept=".sb2,.sb3"
ref={this.setFileInput}
style={{display: 'none'}}
type="file"
onChange={this.handleChange}
/>
);
}
render () {
const {
/* eslint-disable no-unused-vars */
children,
closeLoadingState,
openLoadingState,
vm,
/* eslint-enable no-unused-vars */
...props
} = this.props;
return this.props.children(this.renderFileInput, this.handleClick, props);
}
}
ProjectLoader.propTypes = {
children: PropTypes.func,
closeLoadingState: PropTypes.func,
intl: intlShape.isRequired,
onSetProjectTitle: PropTypes.func,
openLoadingState: PropTypes.func,
vm: PropTypes.shape({
loadProject: PropTypes.func
})
};
const mapStateToProps = state => ({
vm: state.scratchGui.vm
});
const mapDispatchToProps = dispatch => ({
closeLoadingState: () => dispatch(closeLoadingProject()),
onSetProjectTitle: title => dispatch(setProjectTitle(title)),
openLoadingState: () => dispatch(openLoadingProject())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(injectIntl(ProjectLoader));