soiz1's picture
Update src/components/gui/gui.jsx
91b0f30 verified
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));