import PropTypes from 'prop-types'; import React from 'react'; import {compose} from 'redux'; import {connect} from 'react-redux'; import ReactModal from 'react-modal'; import VM from 'scratch-vm'; import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl'; import ErrorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; import { getIsError, getIsShowingProject } from '../reducers/project-state'; import { activateTab, BLOCKS_TAB_INDEX, COSTUMES_TAB_INDEX, SOUNDS_TAB_INDEX } from '../reducers/editor-tab'; import { closeCostumeLibrary, closeBackdropLibrary, closeTelemetryModal, openExtensionLibrary, closeExtensionLibrary, openCustomExtensionModal } from '../reducers/modals'; 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 SB3PostMessageHOC from '../lib/sb3-postmessage-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'; import storage from '../lib/storage'; import vmListenerHOC from '../lib/vm-listener-hoc.jsx'; import vmManagerHOC from '../lib/vm-manager-hoc.jsx'; import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx'; import GUIComponent from '../components/gui/gui.jsx'; import {setIsScratchDesktop} from '../lib/isScratchDesktop.js'; import TWFullScreenResizerHOC from '../lib/tw-fullscreen-resizer-hoc.jsx'; import TWThemeManagerHOC from './tw-theme-manager-hoc.jsx'; const {RequestMetadata, setMetadata, unsetMetadata} = storage.scratchFetch; const setProjectIdMetadata = projectId => { // If project ID is '0' or zero, it's not a real project ID. In that case, remove the project ID metadata. // Same if it's null undefined. if (projectId && projectId !== '0') { setMetadata(RequestMetadata.ProjectId, projectId); } else { unsetMetadata(RequestMetadata.ProjectId); } }; class GUI extends React.Component { componentDidMount () { setIsScratchDesktop(this.props.isScratchDesktop); this.props.onStorageInit(storage); this.props.onVmInit(this.props.vm); setProjectIdMetadata(this.props.projectId); } componentDidUpdate (prevProps) { if (this.props.projectId !== prevProps.projectId) { if (this.props.projectId !== null) { this.props.onUpdateProjectId(this.props.projectId); } setProjectIdMetadata(this.props.projectId); } if (this.props.isShowingProject && !prevProps.isShowingProject) { // this only notifies container when a project changes from not yet loaded to loaded // At this time the project view in www doesn't need to know when a project is unloaded // Log total loading time if (window.MISTWARP_LOAD_START_TIME) { const totalLoadTime = Date.now() - window.MISTWARP_LOAD_START_TIME; console.log(`🚀 MistWarp project loaded in ${totalLoadTime}ms (${(totalLoadTime / 1000).toFixed(2)}s)`); // Also use Performance API if available if (window.performance && window.performance.mark && window.performance.measure) { window.performance.mark('mistwarp-load-end'); window.performance.measure('mistwarp-total-load', 'mistwarp-load-start', 'mistwarp-load-end'); } } this.props.onProjectLoaded(); } } render () { if (this.props.isError) { throw this.props.error; } const { /* eslint-disable no-unused-vars */ assetHost, cloudHost, error, isError, isScratchDesktop, isShowingProject, onProjectLoaded, onStorageInit, onUpdateProjectId, onVmInit, projectHost, projectId, /* eslint-enable no-unused-vars */ children, fetchingProject, isLoading, loadingStateVisible, ...componentProps } = this.props; return ( {children} ); } } GUI.propTypes = { assetHost: PropTypes.string, children: PropTypes.node, cloudHost: PropTypes.string, error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), fetchingProject: PropTypes.bool, intl: intlShape, isError: PropTypes.bool, isEmbedded: PropTypes.bool, isFullScreen: PropTypes.bool, isLoading: PropTypes.bool, isScratchDesktop: PropTypes.bool, isShowingProject: PropTypes.bool, isTotallyNormal: PropTypes.bool, loadingStateVisible: PropTypes.bool, onProjectLoaded: PropTypes.func, onSeeCommunity: PropTypes.func, onStorageInit: PropTypes.func, onUpdateProjectId: PropTypes.func, onVmInit: PropTypes.func, projectHost: PropTypes.string, projectId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), telemetryModalVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; GUI.defaultProps = { isScratchDesktop: false, isTotallyNormal: false, onStorageInit: storageInstance => storageInstance.addOfficialScratchWebStores(), onProjectLoaded: () => {}, onUpdateProjectId: () => {}, onVmInit: (/* vm */) => {} }; const mapStateToProps = state => { const loadingState = state.scratchGui.projectState.loadingState; return { activeTabIndex: state.scratchGui.editorTab.activeTabIndex, alertsVisible: state.scratchGui.alerts.visible, backdropLibraryVisible: state.scratchGui.modals.backdropLibrary, blocksTabVisible: state.scratchGui.editorTab.activeTabIndex === BLOCKS_TAB_INDEX, cardsVisible: state.scratchGui.cards.visible, connectionModalVisible: state.scratchGui.modals.connectionModal, costumeLibraryVisible: state.scratchGui.modals.costumeLibrary, costumesTabVisible: state.scratchGui.editorTab.activeTabIndex === COSTUMES_TAB_INDEX, error: state.scratchGui.projectState.error, extensionLibraryVisible: state.scratchGui.modals.extensionLibrary, isError: getIsError(loadingState), isEmbedded: state.scratchGui.mode.isEmbedded, isFullScreen: state.scratchGui.mode.isFullScreen || state.scratchGui.mode.isEmbedded, isPlayerOnly: state.scratchGui.mode.isPlayerOnly, isRtl: state.locales.isRtl, isShowingProject: getIsShowingProject(loadingState), loadingStateVisible: state.scratchGui.modals.loadingProject, projectId: state.scratchGui.projectState.projectId, soundsTabVisible: state.scratchGui.editorTab.activeTabIndex === SOUNDS_TAB_INDEX, targetIsStage: ( state.scratchGui.targets.stage && state.scratchGui.targets.stage.id === state.scratchGui.targets.editingTarget ), telemetryModalVisible: state.scratchGui.modals.telemetryModal, tipsLibraryVisible: state.scratchGui.modals.tipsLibrary, usernameModalVisible: state.scratchGui.modals.usernameModal, settingsModalVisible: state.scratchGui.modals.settingsModal, customExtensionModalVisible: state.scratchGui.modals.customExtensionModal, fontsModalVisible: state.scratchGui.modals.fontsModal, unknownPlatformModalVisible: state.scratchGui.modals.unknownPlatformModal, invalidProjectModalVisible: state.scratchGui.modals.invalidProjectModal, vm: state.scratchGui.vm }; }; const mapDispatchToProps = dispatch => ({ onActivateTab: tab => dispatch(activateTab(tab)), onActivateCostumesTab: () => dispatch(activateTab(COSTUMES_TAB_INDEX)), onActivateSoundsTab: () => dispatch(activateTab(SOUNDS_TAB_INDEX)), onOpenExtensionLibrary: () => dispatch(openExtensionLibrary()), onOpenCustomExtensionModal: () => dispatch(openCustomExtensionModal()), onRequestCloseBackdropLibrary: () => dispatch(closeBackdropLibrary()), onRequestCloseCostumeLibrary: () => dispatch(closeCostumeLibrary()), onRequestCloseExtensionLibrary: () => dispatch(closeExtensionLibrary()), onRequestCloseTelemetryModal: () => dispatch(closeTelemetryModal()) }); const ConnectedGUI = injectIntl(connect( mapStateToProps, mapDispatchToProps )(GUI)); // 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( LocalizationHOC, ErrorBoundaryHOC('Top Level App'), TWThemeManagerHOC, // componentDidUpdate() needs to run very early for icons to update immediately TWFullScreenResizerHOC, FontLoaderHOC, // QueryParserHOC, // tw: HOC is unused ProjectFetcherHOC, SB3PostMessageHOC, // Handle postMessage events for SB3 loading TitledHOC, ProjectSaverHOC, vmListenerHOC, vmManagerHOC, SBFileUploaderHOC, cloudManagerHOC )(ConnectedGUI); WrappedGui.setAppElement = ReactModal.setAppElement; export default WrappedGui;