import classNames from 'classnames'; import omit from 'lodash.omit'; import PropTypes from 'prop-types'; import React from 'react'; import ReactDOM from 'react-dom'; import Draggable from "react-draggable"; import {ContextMenuTrigger} from 'react-contextmenu'; import {BorderedMenuItem, ContextMenu, DangerousMenuItem, MenuItem} from '../context-menu/context-menu.jsx'; 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 Renderer from 'scratch-render'; import Blocks from '../../containers/blocks.jsx'; import CostumeTab from '../../containers/costume-tab.jsx'; import TargetPane from '../../containers/target-pane.jsx'; import SoundTab from '../../containers/sound-tab.jsx'; import VariablesTab from '../../containers/variables-tab.jsx'; import FilesTab from '../../containers/files-tab.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 PMExtensionModals from '../../containers/pm-extension-modals.jsx'; import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants'; import {resolveStageSize} from '../../lib/screen-utils'; import {isRendererSupported, isBrowserSupported} from '../../lib/tw-environment-support-prober'; import styles from './gui.css'; import plusIcon from './add-tab.svg'; import addExtensionIcon from './icon--extensions.svg'; import codeIcon from './icon--code.svg'; import costumesIcon from './icon--costumes.svg'; import soundsIcon from './icon--sounds.svg'; import variablesIcon from './icon--variables.svg'; import filesIcon from './icon--files.svg'; const urlParams = new URLSearchParams(location.search); const IsLocal = String(window.location.href).startsWith(`http://localhost:`); const IsLiveTests = urlParams.has('livetests'); const messages = defineMessages({ addExtension: { id: 'gui.gui.addExtension', description: 'Button to add an extension in the target pane', defaultMessage: 'Add Extension' } }); 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 safeJSONParse = (json, defaul, mustBeArray) => { try { const parsed = JSON.parse(json); if (mustBeArray && !Array.isArray(parsed)) throw 'Not array'; return parsed; } catch { return defaul; } }; const fullscreenBackgroundColor = getFullscreenBackgroundColor(); const GUIComponent = props => { const { accountNavOpen, activeTabIndex, alertsVisible, authorId, authorThumbnailUrl, authorUsername, basePath, backdropLibraryVisible, backpackHost, backpackVisible, blocksTabVisible, cardsVisible, canChangeLanguage, canCreateNew, canEditTitle, canManageFiles, canRemix, canSave, canCreateCopy, canShare, canUseCloud, children, connectionModalVisible, costumeLibraryVisible, costumesTabVisible, customStageSize, enableCommunity, intl, isCreating, isDark, isEmbedded, isFullScreen, isPlayerOnly, isRtl, isShared, isWindowFullScreen, isTelemetryEnabled, loading, logo, renderLogin, onClickAbout, onClickAccountNav, onCloseAccountNav, onClickAddonSettings, onClickNewWindow, onClickTheme, onClickPackager, onLogOut, onOpenRegistration, onToggleLoginOpen, onActivateCostumesTab, onActivateSoundsTab, onActivateVariablesTab, onActivateFilesTab, onActivateTab, onClickLogo, onExtensionButtonClick, onProjectTelemetryEvent, onRequestCloseBackdropLibrary, onRequestCloseCostumeLibrary, onRequestCloseTelemetryModal, onSeeCommunity, onShare, onShowPrivacyPolicy, onStartSelectingFileUpload, onTelemetryModalCancel, onTelemetryModalOptIn, onTelemetryModalOptOut, showComingSoon, soundsTabVisible, variablesTabVisible, filesTabVisible, stageSizeMode, targetIsStage, telemetryModalVisible, tipsLibraryVisible, usernameModalVisible, settingsModalVisible, customExtensionModalVisible, fontsModalVisible, isPlayground, 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) }; // We can't move this into it's own component or it'll break the selected tab styles & disable switching to the code tab // Moving the whole TabList element will also break the code panel from resizing properly const getTabOrder = () => { const tabOrderStr = localStorage.getItem('pm:taborder') || '["code", "costume", "sound"]'; const tabOrder = safeJSONParse(tabOrderStr, [], true); return tabOrder; }; const tabOrder = getTabOrder(); const ContextMenuWrapTab = ({ children, ...props }) => { const {tabId} = props; const disabled = tabId === 'code'; return (<> {children} {ReactDOM.createPortal( removeTabFromEditor(tabId)}> , document.body)} ); }; // currently each tab can decide whether or not its hidden, remove this once rearranging tabs is supported const codeTab = ( ); const costumesTab = ( {targetIsStage ? ( ) : ( )} ); const soundsTab = ( ); const variablesTab = ( ); const filesTab = ( ); const tabPairs = { code: codeTab, costume: costumesTab, sound: soundsTab, variable: variablesTab, // file: filesTab, }; // For now, rearranging tabs is not supported const organizedTabs = Object.values(tabPairs); // const organizedTabs = (() => { // const enabledTabs = []; // // Either add in rearranged order // // for (const tabId of tabOrder) { // // enabledTabs.push(tabPairs[tabId] || codeTab) // // } // // or we can add tabs in order of table inclusion // // for (const key in tabPairs) { // // const tab = tabPairs[key]; // // if (tabOrder.includes(key)) { // // enabledTabs.push(tab); // // } // // } // return enabledTabs; // })(); const addTabButtonDisabled = tabOrder.length >= Object.keys(tabPairs).length; const addTabToEditor = (tabId) => { const tabOrder = getTabOrder(); tabOrder.push(tabId); localStorage.setItem('pm:taborder', JSON.stringify(tabOrder)); const tabKeys = Object.keys(tabPairs); const tabIndex = tabKeys.indexOf(tabId); if (tabIndex === -1) { return onActivateTab(0); } onActivateTab(tabIndex); }; const removeTabFromEditor = (tabId) => { setTimeout(() => { // sometimes clicking delete will switch to the deleted tab const tabOrder = getTabOrder(); const idx = tabOrder.indexOf(tabId); if (idx === -1) return; tabOrder.splice(idx, 1); localStorage.setItem('pm:taborder', JSON.stringify(tabOrder)); if (tabId !== 'code') { return onActivateTab(0); } const tabKeys = Object.keys(tabPairs); const firstTab = tabOrder[0]; const firstTabIdx = tabKeys.indexOf(firstTab); if (firstTabIdx !== -1) { onActivateTab(firstTabIdx); } }); }; const minWidth = layout.fullSizeMinWidth + Math.max(0, customStageSize.width - layout.referenceWidth); return ({isFullSize => { const stageSize = resolveStageSize(stageSizeMode, isFullSize); const alwaysEnabledModals = ( {usernameModalVisible && } {settingsModalVisible && } {customExtensionModalVisible && } {fontsModalVisible && } ); 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} {(!isPlayground) ? ( ) : null} {organizedTabs} {!tabOrder.includes('code') && addTabToEditor('code')}>
} {!tabOrder.includes('costume') && addTabToEditor('costume')}>
} {!tabOrder.includes('sound') && addTabToEditor('sound')}>
} {!tabOrder.includes('variable') && addTabToEditor('variable')}>
} {/* {!tabOrder.includes('file') && addTabToEditor('file')}>
} */}
{costumesTabVisible ? : null} {soundsTabVisible ? : null} {variablesTabVisible ? : 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, canChangeLanguage: 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, intl: intlShape.isRequired, isCreating: PropTypes.bool, isDark: PropTypes.bool, isEmbedded: PropTypes.bool, isFullScreen: PropTypes.bool, isPlayerOnly: PropTypes.bool, isRtl: PropTypes.bool, isShared: PropTypes.bool, isWindowFullScreen: PropTypes.bool, loading: PropTypes.bool, logo: PropTypes.string, onActivateCostumesTab: PropTypes.func, onActivateSoundsTab: PropTypes.func, onActivateVariablesTab: PropTypes.func, onActivateFilesTab: PropTypes.func, onActivateTab: PropTypes.func, onClickAccountNav: PropTypes.func, onClickAddonSettings: PropTypes.func, onClickNewWindow: PropTypes.func, onClickTheme: PropTypes.func, onClickPackager: PropTypes.func, onClickLogo: PropTypes.func, onCloseAccountNav: PropTypes.func, onExtensionButtonClick: PropTypes.func, onLogOut: PropTypes.func, onOpenRegistration: PropTypes.func, onRequestCloseBackdropLibrary: PropTypes.func, onRequestCloseCostumeLibrary: 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, showComingSoon: PropTypes.bool, soundsTabVisible: PropTypes.bool, variablesTabVisible: PropTypes.bool, filesTabVisible: PropTypes.bool, stageSizeMode: PropTypes.oneOf(Object.keys(STAGE_SIZE_MODES)), targetIsStage: PropTypes.bool, telemetryModalVisible: PropTypes.bool, tipsLibraryVisible: PropTypes.bool, usernameModalVisible: PropTypes.bool, settingsModalVisible: PropTypes.bool, customExtensionModalVisible: PropTypes.bool, fontsModalVisible: PropTypes.bool, vm: PropTypes.instanceOf(VM).isRequired }; GUIComponent.defaultProps = { backpackHost: null, backpackVisible: false, basePath: './', canChangeLanguage: true, canCreateNew: false, canEditTitle: false, canManageFiles: true, canRemix: false, canSave: false, canCreateCopy: false, canShare: false, canUseCloud: false, enableCommunity: false, isCreating: false, isShared: 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 stageSizeMode: state.scratchGui.stageSize.stageSize }); export default injectIntl(connect( mapStateToProps )(GUIComponent));