use project state system to load initial project

This commit is contained in:
Christopher Willis-Ford 2020-09-15 14:32:36 -07:00
parent 05e8b26a34
commit a9933242e0

View file

@ -1,16 +1,33 @@
import {ipcRenderer, shell} from 'electron'; import {ipcRenderer, shell} from 'electron';
import bindAll from 'lodash.bindall'; import bindAll from 'lodash.bindall';
import omit from 'lodash.omit';
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {connect} from 'react-redux';
import {compose} from 'redux'; import {compose} from 'redux';
import GUI, {AppStateHOC} from 'scratch-gui'; import GUI from 'scratch-gui/src/index';
import VM from 'scratch-vm';
import AppStateHOC from 'scratch-gui/src/lib/app-state-hoc.jsx';
import {
LoadingStates,
onFetchedProjectData,
onLoadedProject,
defaultProjectId,
requestNewProject,
requestProjectUpload,
setProjectId
} from 'scratch-gui/src/reducers/project-state';
import {
openLoadingProject,
closeLoadingProject
} from 'scratch-gui/src/reducers/modals';
import ElectronStorageHelper from '../common/ElectronStorageHelper'; import ElectronStorageHelper from '../common/ElectronStorageHelper';
import styles from './app.css'; import styles from './app.css';
const defaultProjectId = 0;
// override window.open so that it uses the OS's default browser, not an electron browser // override window.open so that it uses the OS's default browser, not an electron browser
window.open = function (url, target) { window.open = function (url, target) {
if (target === '_blank') { if (target === '_blank') {
@ -38,16 +55,29 @@ const ScratchDesktopHOC = function (WrappedComponent) {
'handleTelemetryModalOptOut', 'handleTelemetryModalOptOut',
'handleUpdateProjectTitle' 'handleUpdateProjectTitle'
]); ]);
this.state = { this.props.onLoadingStarted();
projectTitle: null, ipcRenderer.invoke('get-initial-project-data').then(initialProjectData => {
projectLoading: true const hasInitialProject = initialProjectData && (initialProjectData.length > 0);
}; this.props.onHasInitialProject(hasInitialProject, this.props.loadingState);
if (!hasInitialProject) {
this.props.onLoadingCompleted();
return;
}
this.props.vm.loadProject(initialProjectData).then(
() => {
this.props.onLoadingCompleted();
this.props.onLoadedProject(this.props.loadingState, true);
},
e => {
this.props.onLoadingCompleted();
console.error(e); // TODO: dialog box reporting the error?
ipcRenderer.invoke('get-initial-project-data').then(projectData => { // abandon this load and instead fetch+load the default project
this.setState({ // WARNING: this stuff doesn't currently work correctly!
projectData, this.props.onLoadedProject(this.props.loadingState, false);
projectLoading: false this.props.onRequestNewProject();
}); }
);
}); });
} }
componentDidMount () { componentDidMount () {
@ -80,15 +110,12 @@ const ScratchDesktopHOC = function (WrappedComponent) {
render () { render () {
const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean'); const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean');
if (this.state.projectLoading) { const childProps = omit(this.props, Object.keys(ScratchDesktopComponent.propTypes));
return <p className="splash">Loading File...</p>;
}
return (<WrappedComponent return (<WrappedComponent
canEditTitle canEditTitle
canModifyCloudData={false}
isScratchDesktop isScratchDesktop
projectId={defaultProjectId}
projectTitle={this.state.projectTitle}
showTelemetryModal={shouldShowTelemetryModal} showTelemetryModal={shouldShowTelemetryModal}
onClickLogo={this.handleClickLogo} onClickLogo={this.handleClickLogo}
onProjectTelemetryEvent={this.handleProjectTelemetryEvent} onProjectTelemetryEvent={this.handleProjectTelemetryEvent}
@ -97,25 +124,60 @@ const ScratchDesktopHOC = function (WrappedComponent) {
onTelemetryModalOptOut={this.handleTelemetryModalOptOut} onTelemetryModalOptOut={this.handleTelemetryModalOptOut}
onUpdateProjectTitle={this.handleUpdateProjectTitle} onUpdateProjectTitle={this.handleUpdateProjectTitle}
// completely omit the projectData prop if we have no project data
// passing an empty projectData causes a GUI error
{...(this.state.projectData ? {projectData: this.state.projectData} : {})}
// allow passed-in props to override any of the above // allow passed-in props to override any of the above
{...this.props} {...childProps}
/>); />);
} }
} }
return ScratchDesktopComponent; ScratchDesktopComponent.propTypes = {
loadingState: PropTypes.oneOf(LoadingStates),
onFetchedInitialProjectData: PropTypes.func,
onHasInitialProject: PropTypes.func,
onLoadedProject: PropTypes.func,
onLoadingCompleted: PropTypes.func,
onLoadingStarted: PropTypes.func,
onRequestNewProject: PropTypes.func,
vm: PropTypes.instanceOf(VM).isRequired
};
const mapStateToProps = state => {
const loadingState = state.scratchGui.projectState.loadingState;
return {
loadingState: loadingState,
vm: state.scratchGui.vm
};
};
const mapDispatchToProps = dispatch => ({
onLoadingStarted: () => dispatch(openLoadingProject()),
onLoadingCompleted: () => dispatch(closeLoadingProject()),
onHasInitialProject: (hasInitialProject, loadingState) => {
if (hasInitialProject) {
// emulate sb-file-uploader
return dispatch(requestProjectUpload(loadingState));
}
// `createProject()` might seem more appropriate but it's not a valid state transition here
// setting the default project ID is a valid transition from NOT_LOADED and acts like "create new"
return dispatch(setProjectId(defaultProjectId));
},
onFetchedInitialProjectData: (projectData, loadingState) =>
dispatch(onFetchedProjectData(projectData, loadingState)),
onLoadedProject: (loadingState, loadSuccess) => {
const canSaveToServer = false;
return dispatch(onLoadedProject(loadingState, canSaveToServer, loadSuccess));
},
onRequestNewProject: () => dispatch(requestNewProject(false))
});
return connect(mapStateToProps, mapDispatchToProps)(ScratchDesktopComponent);
}; };
// note that redux's 'compose' function is just being used as a general utility to make // note that redux's 'compose' function is just being used as a general utility to make
// the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's // the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's
// ability to compose reducers. // ability to compose reducers.
const WrappedGui = compose( const WrappedGui = compose(
ScratchDesktopHOC, AppStateHOC,
AppStateHOC ScratchDesktopHOC
)(GUI); )(GUI);
ReactDOM.render(<WrappedGui />, appTarget); ReactDOM.render(<WrappedGui />, appTarget);