Spaces:
Runtime error
Runtime error
import classNames from 'classnames'; | |
import omit from 'lodash.omit'; | |
import PropTypes from 'prop-types'; | |
import React, { useCallback } from 'react'; | |
import {defineMessages, FormattedMessage, injectIntl, intlShape} from 'react-intl'; | |
import {connect} from 'react-redux'; | |
import MediaQuery from 'react-responsive'; | |
import {Tab, Tabs, TabList, TabPanel} from 'react-tabs'; | |
import tabStyles from 'react-tabs/style/react-tabs.css'; | |
import VM from 'scratch-vm'; | |
import Blocks from '../../containers/blocks.jsx'; | |
import CostumeTab from '../../containers/costume-tab.jsx'; | |
import SoundTab from '../../containers/sound-tab.jsx'; | |
import ExtensionLibrary from '../../containers/extension-library.jsx'; | |
import TargetPane from '../../containers/target-pane.jsx'; | |
import StageWrapper from '../../containers/stage-wrapper.jsx'; | |
import Loader from '../loader/loader.jsx'; | |
import Box from '../box/box.jsx'; | |
import MenuBar from '../menu-bar/menu-bar.jsx'; | |
import CostumeLibrary from '../../containers/costume-library.jsx'; | |
import BackdropLibrary from '../../containers/backdrop-library.jsx'; | |
import Watermark from '../../containers/watermark.jsx'; | |
import Backpack from '../../containers/backpack.jsx'; | |
import BrowserModal from '../browser-modal/browser-modal.jsx'; | |
import TipsLibrary from '../../containers/tips-library.jsx'; | |
import Cards from '../../containers/cards.jsx'; | |
import Alerts from '../../containers/alerts.jsx'; | |
import DragLayer from '../../containers/drag-layer.jsx'; | |
import ConnectionModal from '../../containers/connection-modal.jsx'; | |
import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx'; | |
import TWUsernameModal from '../../containers/tw-username-modal.jsx'; | |
import TWSettingsModal from '../../containers/tw-settings-modal.jsx'; | |
import TWSecurityManager from '../../containers/tw-security-manager.jsx'; | |
import TWCustomExtensionModal from '../../containers/tw-custom-extension-modal.jsx'; | |
import TWRestorePointManager from '../../containers/tw-restore-point-manager.jsx'; | |
import TWFontsModal from '../../containers/tw-fonts-modal.jsx'; | |
import TWUnknownPlatformModal from '../../containers/tw-unknown-platform-modal.jsx'; | |
import TWInvalidProjectModal from '../../containers/tw-invalid-project-modal.jsx'; | |
import AddonHooks from '../../addons/hooks.js'; | |
import {STAGE_SIZE_MODES, FIXED_WIDTH, UNCONSTRAINED_NON_STAGE_WIDTH} from '../../lib/layout-constants'; | |
import {resolveStageSize} from '../../lib/screen-utils'; | |
import {Theme} from '../../lib/themes'; | |
import {isRendererSupported, isBrowserSupported} from '../../lib/tw-environment-support-prober'; | |
import styles from './gui.css'; | |
import { ReactComponent as AddExtensionIcon } from './icon--extensions.svg'; | |
import { ReactComponent as CodeIcon } from './icon--code.svg'; | |
import { ReactComponent as CostumesIcon } from './icon--costumes.svg'; | |
import { ReactComponent as SoundsIcon } from './icon--sounds.svg'; | |
const getFullscreenBackgroundColor = () => { | |
const params = new URLSearchParams(location.search); | |
if (params.has('fullscreen-background')) { | |
return params.get('fullscreen-background'); | |
} | |
if (window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
return '#111'; | |
} | |
return 'white'; | |
}; | |
const fullscreenBackgroundColor = getFullscreenBackgroundColor(); | |
const GUIComponent = props => { | |
const handleEnableProcedureReturns = useCallback(() => { | |
try { | |
const workspace = AddonHooks.blocklyWorkspace; | |
if (workspace && workspace.enableProcedureReturns) { | |
workspace.enableProcedureReturns(); | |
// Force toolbox refresh | |
if (workspace.refreshToolboxSelection_) { | |
workspace.refreshToolboxSelection_(); | |
} | |
} | |
} catch (error) { | |
console.error('Error enabling procedure returns:', error); | |
} | |
}, []); | |
const { | |
accountNavOpen, | |
activeTabIndex, | |
alertsVisible, | |
authorId, | |
authorThumbnailUrl, | |
authorUsername, | |
basePath, | |
backdropLibraryVisible, | |
backpackHost, | |
backpackVisible, | |
blocksId, | |
blocksTabVisible, | |
cardsVisible, | |
canChangeLanguage, | |
canChangeTheme, | |
canCreateNew, | |
canEditTitle, | |
canManageFiles, | |
canRemix, | |
canSave, | |
canCreateCopy, | |
canShare, | |
canUseCloud, | |
children, | |
connectionModalVisible, | |
costumeLibraryVisible, | |
costumesTabVisible, | |
customStageSize, | |
enableCommunity, | |
extensionLibraryVisible, | |
intl, | |
isCreating, | |
isEmbedded, | |
isFullScreen, | |
isPlayerOnly, | |
isRtl, | |
isShared, | |
isWindowFullScreen, | |
isTelemetryEnabled, | |
isTotallyNormal, | |
loading, | |
logo, | |
renderLogin, | |
onClickAbout, | |
onClickAccountNav, | |
onCloseAccountNav, | |
onClickAddonSettings, | |
onClickDesktopSettings, | |
onClickNewWindow, | |
onClickPackager, | |
onLogOut, | |
onOpenExtensionLibrary, | |
onOpenRegistration, | |
onToggleLoginOpen, | |
onActivateCostumesTab, | |
onActivateSoundsTab, | |
onActivateTab, | |
onClickLogo, | |
onExtensionButtonClick, | |
onOpenCustomExtensionModal, | |
onProjectTelemetryEvent, | |
onRequestCloseBackdropLibrary, | |
onRequestCloseCostumeLibrary, | |
onRequestCloseExtensionLibrary, | |
onRequestCloseTelemetryModal, | |
onSeeCommunity, | |
onShare, | |
onShowPrivacyPolicy, | |
onStartSelectingFileUpload, | |
onTelemetryModalCancel, | |
onTelemetryModalOptIn, | |
onTelemetryModalOptOut, | |
securityManager, | |
showComingSoon, | |
showOpenFilePicker, | |
showSaveFilePicker, | |
soundsTabVisible, | |
stageSizeMode, | |
targetIsStage, | |
telemetryModalVisible, | |
theme, | |
tipsLibraryVisible, | |
usernameModalVisible, | |
settingsModalVisible, | |
customExtensionModalVisible, | |
fontsModalVisible, | |
unknownPlatformModalVisible, | |
invalidProjectModalVisible, | |
vm, | |
...componentProps | |
} = omit(props, 'dispatch'); | |
if (children) { | |
return <Box {...componentProps}>{children}</Box>; | |
} | |
const tabClassNames = { | |
tabs: styles.tabs, | |
tab: classNames(tabStyles.reactTabsTab, styles.tab), | |
tabList: classNames(tabStyles.reactTabsTabList, styles.tabList), | |
tabPanel: classNames(tabStyles.reactTabsTabPanel, styles.tabPanel), | |
tabPanelSelected: classNames(tabStyles.reactTabsTabPanelSelected, styles.isSelected), | |
tabSelected: classNames(tabStyles.reactTabsTabSelected, styles.isSelected) | |
}; | |
const unconstrainedWidth = ( | |
UNCONSTRAINED_NON_STAGE_WIDTH + | |
FIXED_WIDTH + | |
Math.max(0, customStageSize.width - FIXED_WIDTH) | |
); | |
return (<MediaQuery minWidth={unconstrainedWidth}>{isUnconstrained => { | |
const stageSize = resolveStageSize(stageSizeMode, isUnconstrained); | |
const alwaysEnabledModals = ( | |
<React.Fragment> | |
<TWSecurityManager securityManager={securityManager} /> | |
<TWRestorePointManager /> | |
{usernameModalVisible && <TWUsernameModal visible={usernameModalVisible} />} | |
{settingsModalVisible && ( | |
<TWSettingsModal | |
isRtl={isRtl} | |
visible={settingsModalVisible} | |
/> | |
)} | |
{customExtensionModalVisible && <TWCustomExtensionModal />} | |
{fontsModalVisible && <TWFontsModal />} | |
{unknownPlatformModalVisible && <TWUnknownPlatformModal />} | |
{invalidProjectModalVisible && <TWInvalidProjectModal />} | |
</React.Fragment> | |
); | |
return isPlayerOnly ? ( | |
<React.Fragment> | |
{/* TW: When the window is fullscreen, use an element to display the background color */} | |
{/* The default color for transparency is inconsistent between browsers and there isn't an existing */} | |
{/* element for us to style that fills the entire screen. */} | |
{isWindowFullScreen ? ( | |
<div | |
className={styles.fullscreenBackground} | |
style={{ | |
backgroundColor: fullscreenBackgroundColor | |
}} | |
/> | |
) : null} | |
<StageWrapper | |
isFullScreen={isFullScreen} | |
isEmbedded={isEmbedded} | |
isRendererSupported={isRendererSupported()} | |
isRtl={isRtl} | |
loading={loading} | |
stageSize={STAGE_SIZE_MODES.full} | |
vm={vm} | |
> | |
{alertsVisible ? ( | |
<Alerts className={styles.alertsContainer} /> | |
) : null} | |
</StageWrapper> | |
{alwaysEnabledModals} | |
</React.Fragment> | |
) : ( | |
<Box | |
className={styles.pageWrapper} | |
dir={isRtl ? 'rtl' : 'ltr'} | |
style={{ | |
minWidth: 1024 + Math.max(0, customStageSize.width - 480), | |
minHeight: 640 + Math.max(0, customStageSize.height - 360) | |
}} | |
{...componentProps} | |
> | |
{alwaysEnabledModals} | |
{telemetryModalVisible ? ( | |
<TelemetryModal | |
isRtl={isRtl} | |
isTelemetryEnabled={isTelemetryEnabled} | |
onCancel={onTelemetryModalCancel} | |
onOptIn={onTelemetryModalOptIn} | |
onOptOut={onTelemetryModalOptOut} | |
onRequestClose={onRequestCloseTelemetryModal} | |
onShowPrivacyPolicy={onShowPrivacyPolicy} | |
/> | |
) : null} | |
{loading ? ( | |
<Loader isFullScreen /> | |
) : null} | |
{isCreating ? ( | |
<Loader | |
isFullScreen | |
messageId="gui.loader.creating" | |
/> | |
) : null} | |
{isBrowserSupported() ? null : ( | |
<BrowserModal isRtl={isRtl} /> | |
)} | |
{tipsLibraryVisible ? ( | |
<TipsLibrary /> | |
) : null} | |
{cardsVisible ? ( | |
<Cards /> | |
) : null} | |
{alertsVisible ? ( | |
<Alerts className={styles.alertsContainer} /> | |
) : null} | |
{connectionModalVisible ? ( | |
<ConnectionModal | |
vm={vm} | |
/> | |
) : null} | |
{costumeLibraryVisible ? ( | |
<CostumeLibrary | |
vm={vm} | |
onRequestClose={onRequestCloseCostumeLibrary} | |
/> | |
) : null} | |
{backdropLibraryVisible ? ( | |
<BackdropLibrary | |
vm={vm} | |
onRequestClose={onRequestCloseBackdropLibrary} | |
/> | |
) : null} | |
<MenuBar | |
accountNavOpen={accountNavOpen} | |
authorId={authorId} | |
authorThumbnailUrl={authorThumbnailUrl} | |
authorUsername={authorUsername} | |
canChangeLanguage={canChangeLanguage} | |
canChangeTheme={canChangeTheme} | |
canCreateCopy={canCreateCopy} | |
canCreateNew={canCreateNew} | |
canEditTitle={canEditTitle} | |
canManageFiles={canManageFiles} | |
canRemix={canRemix} | |
canSave={canSave} | |
canShare={canShare} | |
className={styles.menuBarPosition} | |
enableCommunity={enableCommunity} | |
isShared={isShared} | |
isTotallyNormal={isTotallyNormal} | |
logo={logo} | |
renderLogin={renderLogin} | |
showComingSoon={showComingSoon} | |
showOpenFilePicker={showOpenFilePicker} | |
showSaveFilePicker={showSaveFilePicker} | |
onClickAbout={onClickAbout} | |
onClickAccountNav={onClickAccountNav} | |
onClickAddonSettings={onClickAddonSettings} | |
onClickDesktopSettings={onClickDesktopSettings} | |
onClickNewWindow={onClickNewWindow} | |
onClickPackager={onClickPackager} | |
onClickLogo={onClickLogo} | |
onCloseAccountNav={onCloseAccountNav} | |
onLogOut={onLogOut} | |
onOpenExtensionLibrary={onOpenExtensionLibrary} | |
onOpenRegistration={onOpenRegistration} | |
onProjectTelemetryEvent={onProjectTelemetryEvent} | |
onSeeCommunity={onSeeCommunity} | |
onShare={onShare} | |
onStartSelectingFileUpload={onStartSelectingFileUpload} | |
onToggleLoginOpen={onToggleLoginOpen} | |
/> | |
<Box className={styles.bodyWrapper}> | |
<Box className={styles.flexWrapper}> | |
<Box className={styles.editorWrapper}> | |
<Tabs | |
forceRenderTabPanel | |
className={tabClassNames.tabs} | |
selectedIndex={activeTabIndex} | |
selectedTabClassName={tabClassNames.tabSelected} | |
selectedTabPanelClassName={tabClassNames.tabPanelSelected} | |
onSelect={onActivateTab} | |
> | |
<TabList className={tabClassNames.tabList}> | |
<Tab className={tabClassNames.tab}> | |
<CodeIcon /> | |
<FormattedMessage | |
defaultMessage="Code" | |
description="Button to get to the code panel" | |
id="gui.gui.codeTab" | |
/> | |
</Tab> | |
<Tab | |
className={tabClassNames.tab} | |
onClick={onActivateCostumesTab} | |
> | |
<CostumesIcon /> | |
{targetIsStage ? ( | |
<FormattedMessage | |
defaultMessage="Backdrops" | |
description="Button to get to the backdrops panel" | |
id="gui.gui.backdropsTab" | |
/> | |
) : ( | |
<FormattedMessage | |
defaultMessage="Costumes" | |
description="Button to get to the costumes panel" | |
id="gui.gui.costumesTab" | |
/> | |
)} | |
</Tab> | |
<Tab | |
className={tabClassNames.tab} | |
onClick={onActivateSoundsTab} | |
> | |
<SoundsIcon /> | |
<FormattedMessage | |
defaultMessage="Sounds" | |
description="Button to get to the sounds panel" | |
id="gui.gui.soundsTab" | |
/> | |
</Tab> | |
</TabList> | |
<TabPanel className={tabClassNames.tabPanel}> | |
<Box className={styles.blocksWrapper}> | |
<Blocks | |
key={`${blocksId}/${theme.id}`} | |
canUseCloud={canUseCloud} | |
grow={1} | |
isVisible={blocksTabVisible} | |
options={{ | |
media: `${basePath}static/${theme.getBlocksMediaFolder()}/` | |
}} | |
stageSize={stageSize} | |
onOpenCustomExtensionModal={onOpenCustomExtensionModal} | |
theme={theme} | |
vm={vm} | |
/> | |
</Box> | |
<Box className={styles.watermark}> | |
<Watermark /> | |
</Box> | |
</TabPanel> | |
<TabPanel className={tabClassNames.tabPanel}> | |
{costumesTabVisible ? <CostumeTab | |
vm={vm} | |
/> : null} | |
</TabPanel> | |
<TabPanel className={tabClassNames.tabPanel}> | |
{soundsTabVisible ? <SoundTab vm={vm} /> : null} | |
</TabPanel> | |
</Tabs> | |
{backpackVisible ? ( | |
<Backpack host={backpackHost} /> | |
) : null} | |
</Box> | |
<Box className={classNames(styles.stageAndTargetWrapper, styles[stageSize])}> | |
<StageWrapper | |
isFullScreen={isFullScreen} | |
isRendererSupported={isRendererSupported()} | |
isRtl={isRtl} | |
stageSize={stageSize} | |
vm={vm} | |
/> | |
<Box className={styles.targetWrapper}> | |
<TargetPane | |
stageSize={stageSize} | |
vm={vm} | |
/> | |
</Box> | |
</Box> | |
</Box> | |
</Box> | |
<ExtensionLibrary | |
vm={vm} | |
visible={extensionLibraryVisible} | |
onRequestClose={onRequestCloseExtensionLibrary} | |
onOpenCustomExtensionModal={onOpenCustomExtensionModal} | |
onEnableProcedureReturns={handleEnableProcedureReturns} | |
/> | |
<DragLayer /> | |
</Box> | |
); | |
}}</MediaQuery>); | |
}; | |
GUIComponent.propTypes = { | |
accountNavOpen: PropTypes.bool, | |
activeTabIndex: PropTypes.number, | |
authorId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), // can be false | |
authorThumbnailUrl: PropTypes.string, | |
authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), // can be false | |
backdropLibraryVisible: PropTypes.bool, | |
backpackHost: PropTypes.string, | |
backpackVisible: PropTypes.bool, | |
basePath: PropTypes.string, | |
blocksTabVisible: PropTypes.bool, | |
blocksId: PropTypes.string, | |
canChangeLanguage: PropTypes.bool, | |
canChangeTheme: PropTypes.bool, | |
canCreateCopy: PropTypes.bool, | |
canCreateNew: PropTypes.bool, | |
canEditTitle: PropTypes.bool, | |
canManageFiles: PropTypes.bool, | |
canRemix: PropTypes.bool, | |
canSave: PropTypes.bool, | |
canShare: PropTypes.bool, | |
canUseCloud: PropTypes.bool, | |
cardsVisible: PropTypes.bool, | |
children: PropTypes.node, | |
costumeLibraryVisible: PropTypes.bool, | |
costumesTabVisible: PropTypes.bool, | |
customStageSize: PropTypes.shape({ | |
width: PropTypes.number, | |
height: PropTypes.number | |
}), | |
enableCommunity: PropTypes.bool, | |
extensionLibraryVisible: PropTypes.bool, | |
intl: intlShape.isRequired, | |
isCreating: PropTypes.bool, | |
isEmbedded: PropTypes.bool, | |
isFullScreen: PropTypes.bool, | |
isPlayerOnly: PropTypes.bool, | |
isRtl: PropTypes.bool, | |
isShared: PropTypes.bool, | |
isWindowFullScreen: PropTypes.bool, | |
isTotallyNormal: PropTypes.bool, | |
loading: PropTypes.bool, | |
logo: PropTypes.string, | |
onActivateCostumesTab: PropTypes.func, | |
onActivateSoundsTab: PropTypes.func, | |
onActivateTab: PropTypes.func, | |
onClickAccountNav: PropTypes.func, | |
onClickAddonSettings: PropTypes.func, | |
onClickDesktopSettings: PropTypes.func, | |
onClickNewWindow: PropTypes.func, | |
onClickPackager: PropTypes.func, | |
onClickLogo: PropTypes.func, | |
onCloseAccountNav: PropTypes.func, | |
onExtensionButtonClick: PropTypes.func, | |
onOpenCustomExtensionModal: PropTypes.func, | |
onLogOut: PropTypes.func, | |
onOpenExtensionLibrary: PropTypes.func, | |
onOpenRegistration: PropTypes.func, | |
onRequestCloseBackdropLibrary: PropTypes.func, | |
onRequestCloseCostumeLibrary: PropTypes.func, | |
onRequestCloseExtensionLibrary: PropTypes.func, | |
onRequestCloseTelemetryModal: PropTypes.func, | |
onSeeCommunity: PropTypes.func, | |
onShare: PropTypes.func, | |
onShowPrivacyPolicy: PropTypes.func, | |
onStartSelectingFileUpload: PropTypes.func, | |
onTabSelect: PropTypes.func, | |
onTelemetryModalCancel: PropTypes.func, | |
onTelemetryModalOptIn: PropTypes.func, | |
onTelemetryModalOptOut: PropTypes.func, | |
onToggleLoginOpen: PropTypes.func, | |
renderLogin: PropTypes.func, | |
securityManager: PropTypes.shape({}), | |
showComingSoon: PropTypes.bool, | |
showOpenFilePicker: PropTypes.func, | |
showSaveFilePicker: PropTypes.func, | |
soundsTabVisible: PropTypes.bool, | |
stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)), | |
targetIsStage: PropTypes.bool, | |
telemetryModalVisible: PropTypes.bool, | |
theme: PropTypes.instanceOf(Theme), | |
tipsLibraryVisible: PropTypes.bool, | |
usernameModalVisible: PropTypes.bool, | |
settingsModalVisible: PropTypes.bool, | |
customExtensionModalVisible: PropTypes.bool, | |
fontsModalVisible: PropTypes.bool, | |
unknownPlatformModalVisible: PropTypes.bool, | |
invalidProjectModalVisible: PropTypes.bool, | |
vm: PropTypes.instanceOf(VM).isRequired | |
}; | |
GUIComponent.defaultProps = { | |
backpackHost: null, | |
backpackVisible: false, | |
basePath: './', | |
blocksId: 'original', | |
canChangeLanguage: true, | |
canChangeTheme: true, | |
canCreateNew: false, | |
canEditTitle: false, | |
canManageFiles: true, | |
canRemix: false, | |
canSave: false, | |
canCreateCopy: false, | |
canShare: false, | |
canUseCloud: false, | |
enableCommunity: false, | |
isCreating: false, | |
isShared: false, | |
isTotallyNormal: false, | |
loading: false, | |
showComingSoon: false, | |
stageSizeMode: STAGE_SIZE_MODES.large | |
}; | |
const mapStateToProps = state => ({ | |
customStageSize: state.scratchGui.customStageSize, | |
isWindowFullScreen: state.scratchGui.tw.isWindowFullScreen, | |
// This is the button's mode, as opposed to the actual current state | |
blocksId: state.scratchGui.timeTravel.year.toString(), | |
stageSizeMode: state.scratchGui.stageSize.stageSize, | |
theme: state.scratchGui.theme.theme | |
}); | |
export default injectIntl(connect( | |
mapStateToProps | |
)(GUIComponent)); | |