From a9933242e0daa417fd9ccb2492b33f83f77d5954 Mon Sep 17 00:00:00 2001 From: Christopher Willis-Ford <7019101+cwillisf@users.noreply.github.com> Date: Tue, 15 Sep 2020 14:32:36 -0700 Subject: [PATCH] use project state system to load initial project --- src/renderer/app.jsx | 112 +++++++++++++++++++++++++++++++++---------- 1 file changed, 87 insertions(+), 25 deletions(-) diff --git a/src/renderer/app.jsx b/src/renderer/app.jsx index 9ad78c0..0e73fff 100644 --- a/src/renderer/app.jsx +++ b/src/renderer/app.jsx @@ -1,16 +1,33 @@ import {ipcRenderer, shell} from 'electron'; import bindAll from 'lodash.bindall'; +import omit from 'lodash.omit'; +import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; +import {connect} from 'react-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 styles from './app.css'; -const defaultProjectId = 0; - // override window.open so that it uses the OS's default browser, not an electron browser window.open = function (url, target) { if (target === '_blank') { @@ -38,16 +55,29 @@ const ScratchDesktopHOC = function (WrappedComponent) { 'handleTelemetryModalOptOut', 'handleUpdateProjectTitle' ]); - this.state = { - projectTitle: null, - projectLoading: true - }; + this.props.onLoadingStarted(); + ipcRenderer.invoke('get-initial-project-data').then(initialProjectData => { + 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 => { - this.setState({ - projectData, - projectLoading: false - }); + // abandon this load and instead fetch+load the default project + // WARNING: this stuff doesn't currently work correctly! + this.props.onLoadedProject(this.props.loadingState, false); + this.props.onRequestNewProject(); + } + ); }); } componentDidMount () { @@ -80,15 +110,12 @@ const ScratchDesktopHOC = function (WrappedComponent) { render () { const shouldShowTelemetryModal = (typeof ipcRenderer.sendSync('getTelemetryDidOptIn') !== 'boolean'); - if (this.state.projectLoading) { - return <p className="splash">Loading File...</p>; - } + const childProps = omit(this.props, Object.keys(ScratchDesktopComponent.propTypes)); return (<WrappedComponent canEditTitle + canModifyCloudData={false} isScratchDesktop - projectId={defaultProjectId} - projectTitle={this.state.projectTitle} showTelemetryModal={shouldShowTelemetryModal} onClickLogo={this.handleClickLogo} onProjectTelemetryEvent={this.handleProjectTelemetryEvent} @@ -97,25 +124,60 @@ const ScratchDesktopHOC = function (WrappedComponent) { onTelemetryModalOptOut={this.handleTelemetryModalOptOut} 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 - {...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 // the hierarchy of HOC constructor calls clearer here; it has nothing to do with redux's // ability to compose reducers. const WrappedGui = compose( - ScratchDesktopHOC, - AppStateHOC + AppStateHOC, + ScratchDesktopHOC )(GUI); ReactDOM.render(<WrappedGui />, appTarget);