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') && }
{!tabOrder.includes('costume') && }
{!tabOrder.includes('sound') && }
{!tabOrder.includes('variable') && }
{/* {!tabOrder.includes('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));