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 {children}; } 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 ({isUnconstrained => { const stageSize = resolveStageSize(stageSizeMode, isUnconstrained); const alwaysEnabledModals = ( {usernameModalVisible && } {settingsModalVisible && ( )} {customExtensionModalVisible && } {fontsModalVisible && } {unknownPlatformModalVisible && } {invalidProjectModalVisible && } ); return isPlayerOnly ? ( {/* 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 ? (
) : null} {alertsVisible ? ( ) : null} {alwaysEnabledModals} ) : ( {alwaysEnabledModals} {telemetryModalVisible ? ( ) : null} {loading ? ( ) : null} {isCreating ? ( ) : null} {isBrowserSupported() ? null : ( )} {tipsLibraryVisible ? ( ) : null} {cardsVisible ? ( ) : null} {alertsVisible ? ( ) : null} {connectionModalVisible ? ( ) : null} {costumeLibraryVisible ? ( ) : null} {backdropLibraryVisible ? ( ) : null} {targetIsStage ? ( ) : ( )} {costumesTabVisible ? : null} {soundsTabVisible ? : null} {backpackVisible ? ( ) : null} ); }}); }; 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));