Spaces:
Runtime error
Runtime error
| import classNames from 'classnames'; | |
| import { connect } from 'react-redux'; | |
| import { compose } from 'redux'; | |
| import { defineMessages, FormattedMessage, injectIntl, intlShape } from 'react-intl'; | |
| import PropTypes from 'prop-types'; | |
| import bindAll from 'lodash.bindall'; | |
| import bowser from 'bowser'; | |
| import React from 'react'; | |
| import VM from 'scratch-vm'; | |
| import Box from '../box/box.jsx'; | |
| import Button from '../button/button.jsx'; | |
| import CommunityButton from './community-button.jsx'; | |
| import ShareButton from './share-button.jsx'; | |
| import GoogleDriveSave from './google-drive-save.jsx'; | |
| import { ComingSoonTooltip } from '../coming-soon/coming-soon.jsx'; | |
| import Divider from '../divider/divider.jsx'; | |
| import LanguageSelector from '../../containers/language-selector.jsx'; | |
| import ProjectWatcher from '../../containers/project-watcher.jsx'; | |
| import MenuBarMenu from './menu-bar-menu.jsx'; | |
| import { MenuItem, MenuSection } from '../menu/menu.jsx'; | |
| import ProjectTitleInput from './project-title-input.jsx'; | |
| import AuthorInfo from './author-info.jsx'; | |
| import SB3Downloader from '../../containers/sb3-downloader.jsx'; | |
| import DeletionRestorer from '../../containers/deletion-restorer.jsx'; | |
| import TurboMode from '../../containers/turbo-mode.jsx'; | |
| import MenuBarHOC from '../../containers/menu-bar-hoc.jsx'; | |
| import FramerateChanger from '../../containers/tw-framerate-changer.jsx'; | |
| import ChangeUsername from '../../containers/tw-change-username.jsx'; | |
| import CloudVariablesToggler from '../../containers/tw-cloud-toggler.jsx'; | |
| import TWSaveStatus from './tw-save-status.jsx'; | |
| import { openTipsLibrary, openSettingsModal, openRestorePointModal } from '../../reducers/modals'; | |
| import { setPlayer } from '../../reducers/mode'; | |
| import { | |
| autoUpdateProject, | |
| getIsUpdating, | |
| getIsShowingProject, | |
| manualUpdateProject, | |
| requestNewProject, | |
| remixProject, | |
| saveProjectAsCopy | |
| } from '../../reducers/project-state'; | |
| import { | |
| openAboutMenu, | |
| closeAboutMenu, | |
| aboutMenuOpen, | |
| openAccountMenu, | |
| closeAccountMenu, | |
| accountMenuOpen, | |
| openFileMenu, | |
| closeFileMenu, | |
| fileMenuOpen, | |
| openEditMenu, | |
| closeEditMenu, | |
| editMenuOpen, | |
| openErrorsMenu, | |
| closeErrorsMenu, | |
| errorsMenuOpen, | |
| openLanguageMenu, | |
| closeLanguageMenu, | |
| languageMenuOpen, | |
| openLoginMenu, | |
| closeLoginMenu, | |
| loginMenuOpen | |
| } from '../../reducers/menus'; | |
| import { setFileHandle } from '../../reducers/tw.js'; | |
| import collectMetadata from '../../lib/collect-metadata'; | |
| import styles from './menu-bar.css'; | |
| import remixIcon from './icon--remix.svg'; | |
| import dropdownCaret from './dropdown-caret.svg'; | |
| import languageIcon from '../language-selector/language-icon.svg'; | |
| import aboutIcon from './icon--about.svg'; | |
| import errorIcon from './tw-error.svg'; | |
| import themeIcon from './tw-moon.svg'; | |
| import s4sicon from './Glow-4.svg' | |
| import scratchLogo from './scratch-logo.svg'; | |
| import sharedMessages from '../../lib/shared-messages'; | |
| import { consoleLogs } from '../../lib/pm-log-capture.js'; | |
| import SeeInsideButton from './tw-see-inside.jsx'; | |
| import { notScratchDesktop } from '../../lib/isScratchDesktop.js'; | |
| const ariaMessages = defineMessages({ | |
| language: { | |
| id: 'gui.menuBar.LanguageSelector', | |
| defaultMessage: 'language selector', | |
| description: 'accessibility text for the language selection menu' | |
| }, | |
| tutorials: { | |
| id: 'gui.menuBar.tutorialsLibrary', | |
| defaultMessage: 'Tutorials', | |
| description: 'accessibility text for the tutorials button' | |
| } | |
| }); | |
| const twMessages = defineMessages({ | |
| compileError: { | |
| id: 'tw.menuBar.compileError', | |
| defaultMessage: '{sprite}: {error}', | |
| description: 'Error message in error menu' | |
| } | |
| }); | |
| const MenuBarItemTooltip = ({ | |
| children, | |
| className, | |
| enable, | |
| id, | |
| place = 'bottom' | |
| }) => { | |
| if (enable) { | |
| return ( | |
| <React.Fragment> | |
| {children} | |
| </React.Fragment> | |
| ); | |
| } | |
| return ( | |
| <ComingSoonTooltip | |
| className={classNames(styles.comingSoon, className)} | |
| place={place} | |
| tooltipClassName={styles.comingSoonTooltip} | |
| tooltipId={id} | |
| > | |
| {children} | |
| </ComingSoonTooltip> | |
| ); | |
| }; | |
| MenuBarItemTooltip.propTypes = { | |
| children: PropTypes.node, | |
| className: PropTypes.string, | |
| enable: PropTypes.bool, | |
| id: PropTypes.string, | |
| place: PropTypes.oneOf(['top', 'bottom', 'left', 'right']) | |
| }; | |
| const MenuItemTooltip = ({ id, isRtl, children, className }) => ( | |
| <ComingSoonTooltip | |
| className={classNames(styles.comingSoon, className)} | |
| isRtl={isRtl} | |
| place={isRtl ? 'left' : 'right'} | |
| tooltipClassName={styles.comingSoonTooltip} | |
| tooltipId={id} | |
| > | |
| {children} | |
| </ComingSoonTooltip> | |
| ); | |
| MenuItemTooltip.propTypes = { | |
| children: PropTypes.node, | |
| className: PropTypes.string, | |
| id: PropTypes.string, | |
| isRtl: PropTypes.bool | |
| }; | |
| const AboutButton = props => ( | |
| <Button | |
| className={classNames(styles.menuBarItem, styles.hoverable)} | |
| iconClassName={styles.aboutIcon} | |
| iconSrc={aboutIcon} | |
| onClick={props.onClick} | |
| /> | |
| ); | |
| AboutButton.propTypes = { | |
| onClick: PropTypes.func.isRequired | |
| }; | |
| // Unlike <MenuItem href="">, this uses an actual <a> | |
| const MenuItemLink = props => ( | |
| <a | |
| href={props.href} | |
| // _blank is safe because of noopener | |
| // eslint-disable-next-line react/jsx-no-target-blank | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className={styles.menuItemLink} | |
| > | |
| <MenuItem>{props.children}</MenuItem> | |
| </a> | |
| ); | |
| MenuItemLink.propTypes = { | |
| children: PropTypes.node.isRequired, | |
| href: PropTypes.string.isRequired | |
| }; | |
| class MenuBar extends React.Component { | |
| constructor(props) { | |
| super(props); | |
| bindAll(this, [ | |
| 'handleClickSeeInside', | |
| 'handleClickNew', | |
| 'handleClickNewWindow', | |
| 'handleClickRemix', | |
| 'handleClickSave', | |
| 'handleClickSaveAsCopy', | |
| 'handleClickPackager', | |
| 'handleClickRestorePoints', | |
| 'handleClickSeeCommunity', | |
| 'handleClickShare', | |
| 'handleKeyPress', | |
| 'handleLanguageMouseUp', | |
| 'handleRestoreOption', | |
| 'getSaveToComputerHandler', | |
| 'restoreOptionMessage' | |
| ]); | |
| } | |
| componentDidMount() { | |
| document.addEventListener('keydown', this.handleKeyPress); | |
| } | |
| componentWillUnmount() { | |
| document.removeEventListener('keydown', this.handleKeyPress); | |
| } | |
| handleClickNew() { | |
| // if the project is dirty, and user owns the project, we will autosave. | |
| // but if they are not logged in and can't save, user should consider | |
| // downloading or logging in first. | |
| // Note that if user is logged in and editing someone else's project, | |
| // they'll lose their work. | |
| const readyToReplaceProject = this.props.confirmReadyToReplaceProject( | |
| this.props.intl.formatMessage(sharedMessages.replaceProjectWarning) | |
| ); | |
| this.props.onRequestCloseFile(); | |
| if (readyToReplaceProject) { | |
| this.props.onClickNew(this.props.canSave && this.props.canCreateNew); | |
| } | |
| this.props.onRequestCloseFile(); | |
| } | |
| handleClickNewWindow() { | |
| this.props.onClickNewWindow(); | |
| this.props.onRequestCloseFile(); | |
| } | |
| handleClickRemix() { | |
| this.props.onClickRemix(); | |
| this.props.onRequestCloseFile(); | |
| } | |
| handleClickSave() { | |
| this.props.onClickSave(); | |
| this.props.onRequestCloseFile(); | |
| } | |
| handleClickSaveAsCopy() { | |
| this.props.onClickSaveAsCopy(); | |
| this.props.onRequestCloseFile(); | |
| } | |
| handleClickPackager() { | |
| this.props.onClickPackager(); | |
| this.props.onRequestCloseFile(); | |
| } | |
| handleClickRestorePoints() { | |
| this.props.onClickRestorePoints(); | |
| this.props.onRequestCloseFile(); | |
| } | |
| handleClickSeeCommunity(waitForUpdate) { | |
| if (this.props.shouldSaveBeforeTransition()) { | |
| this.props.autoUpdateProject(); // save before transitioning to project page | |
| waitForUpdate(true); // queue the transition to project page | |
| } else { | |
| waitForUpdate(false); // immediately transition to project page | |
| } | |
| } | |
| handleClickShare(waitForUpdate) { | |
| if (!this.props.isShared) { | |
| if (this.props.canShare) { // save before transitioning to project page | |
| this.props.onShare(); | |
| } | |
| if (this.props.canSave) { // save before transitioning to project page | |
| this.props.autoUpdateProject(); | |
| waitForUpdate(true); // queue the transition to project page | |
| } else { | |
| waitForUpdate(false); // immediately transition to project page | |
| } | |
| } | |
| } | |
| handleRestoreOption(restoreFun) { | |
| return () => { | |
| restoreFun(); | |
| this.props.onRequestCloseEdit(); | |
| }; | |
| } | |
| handleKeyPress(event) { | |
| const modifier = bowser.mac ? event.metaKey : event.ctrlKey; | |
| if (modifier && event.key.toLowerCase() === 's') { | |
| this.props.handleSaveProject(); | |
| event.preventDefault(); | |
| } | |
| } | |
| getSaveToComputerHandler(downloadProjectCallback) { | |
| return () => { | |
| this.props.onRequestCloseFile(); | |
| downloadProjectCallback(); | |
| if (this.props.onProjectTelemetryEvent) { | |
| const metadata = collectMetadata(this.props.vm, this.props.projectTitle, this.props.locale); | |
| this.props.onProjectTelemetryEvent('projectDidSave', metadata); | |
| } | |
| }; | |
| } | |
| handleLanguageMouseUp(e) { | |
| if (!this.props.languageMenuOpen) { | |
| this.props.onClickLanguage(e); | |
| } | |
| } | |
| handleClickMode(effect) { | |
| const body = document.body; | |
| body.style = ''; | |
| if (!effect) return; | |
| // fix some weird sizing, just applies on effects | |
| body.style = "width:100%;height:100%;position:fixed;overflow:hidden;"; | |
| switch (effect) { | |
| case 'night': | |
| body.style.filter = 'brightness(90%) sepia(100%) hue-rotate(340deg) saturate(400%)'; | |
| break; | |
| case 'blur': | |
| body.style.filter = 'blur(4px)'; | |
| break; | |
| case 'comic': | |
| body.style.filter = 'brightness(70%) contrast(1000%) grayscale(100%)'; | |
| break; | |
| case 'toxic': | |
| body.style.filter = 'sepia(100%) hue-rotate(58deg) saturate(400%)'; | |
| break; | |
| case 'uhd': | |
| body.style.filter = 'url("./bloomfilter.svg#bloom")'; | |
| break; | |
| case 'upsidedown': | |
| body.style.transform = 'rotateX(180deg) rotateY(180deg)'; | |
| break; | |
| } | |
| } | |
| restoreOptionMessage(deletedItem) { | |
| switch (deletedItem) { | |
| case 'Sprite': | |
| return (<FormattedMessage | |
| defaultMessage="Restore Sprite" | |
| description="Menu bar item for restoring the last deleted sprite." | |
| id="gui.menuBar.restoreSprite" | |
| />); | |
| case 'Sound': | |
| return (<FormattedMessage | |
| defaultMessage="Restore Sound" | |
| description="Menu bar item for restoring the last deleted sound." | |
| id="gui.menuBar.restoreSound" | |
| />); | |
| case 'Costume': | |
| return (<FormattedMessage | |
| defaultMessage="Restore Costume" | |
| description="Menu bar item for restoring the last deleted costume." | |
| id="gui.menuBar.restoreCostume" | |
| />); | |
| default: { | |
| return (<FormattedMessage | |
| defaultMessage="Restore" | |
| description="Menu bar item for restoring the last deleted item in its disabled state." /* eslint-disable-line max-len */ | |
| id="gui.menuBar.restore" | |
| />); | |
| } | |
| } | |
| } | |
| handleClickSeeInside() { | |
| this.props.onClickSeeInside(); | |
| } | |
| buildAboutMenu(onClickAbout) { | |
| if (!onClickAbout) { | |
| // hide the button | |
| return null; | |
| } | |
| if (typeof onClickAbout === 'function') { | |
| // make a button which calls a function | |
| return <AboutButton onClick={onClickAbout} />; | |
| } | |
| // assume it's an array of objects | |
| // each item must have a 'title' FormattedMessage and a 'handleClick' function | |
| // generate a menu with items for each object in the array | |
| return ( | |
| <div | |
| className={classNames(styles.menuBarItem, styles.hoverable, { | |
| [styles.active]: this.props.aboutMenuOpen | |
| })} | |
| onMouseUp={this.props.onRequestOpenAbout} | |
| > | |
| <img | |
| className={styles.aboutIcon} | |
| src={aboutIcon} | |
| draggable={false} | |
| /> | |
| <MenuBarMenu | |
| className={classNames(styles.menuBarMenu)} | |
| open={this.props.aboutMenuOpen} | |
| place={this.props.isRtl ? 'right' : 'left'} | |
| onRequestClose={this.props.onRequestCloseAbout} | |
| > | |
| { | |
| onClickAbout.map(itemProps => ( | |
| <MenuItem | |
| key={itemProps.title} | |
| isRtl={this.props.isRtl} | |
| onClick={this.wrapAboutMenuCallback(itemProps.onClick)} | |
| > | |
| {itemProps.title} | |
| </MenuItem> | |
| )) | |
| } | |
| </MenuBarMenu> | |
| </div> | |
| ); | |
| } | |
| wrapAboutMenuCallback(callback) { | |
| return () => { | |
| callback(); | |
| this.props.onRequestCloseAbout(); | |
| }; | |
| } | |
| handleClickDownloadLogs() { | |
| const str = JSON.stringify(consoleLogs); | |
| const a = document.createElement('a'); | |
| a.style.display = 'none'; | |
| document.body.append(a); | |
| const url = window.URL.createObjectURL(new Blob([str])); | |
| a.href = url; | |
| a.download = 'pm-log-trace.json'; | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| a.remove(); | |
| } | |
| render() { | |
| const saveNowMessage = ( | |
| <FormattedMessage | |
| defaultMessage="Save now" | |
| description="Menu bar item for saving now" | |
| id="gui.menuBar.saveNow" | |
| /> | |
| ); | |
| const createCopyMessage = ( | |
| <FormattedMessage | |
| defaultMessage="Save as a copy" | |
| description="Menu bar item for saving as a copy" | |
| id="gui.menuBar.saveAsCopy" | |
| /> | |
| ); | |
| const remixMessage = ( | |
| <FormattedMessage | |
| defaultMessage="Remix" | |
| description="Menu bar item for remixing" | |
| id="gui.menuBar.remix" | |
| /> | |
| ); | |
| const newProjectMessage = ( | |
| <FormattedMessage | |
| defaultMessage="New" | |
| description="Menu bar item for creating a new project" | |
| id="gui.menuBar.new" | |
| /> | |
| ); | |
| const remixButton = ( | |
| <Button | |
| className={classNames( | |
| styles.menuBarButton, | |
| styles.remixButton | |
| )} | |
| iconClassName={styles.remixButtonIcon} | |
| iconSrc={remixIcon} | |
| onClick={this.handleClickRemix} | |
| > | |
| {remixMessage} | |
| </Button> | |
| ); | |
| // Show the About button only if we have a handler for it (like in the desktop app) | |
| const aboutButton = this.buildAboutMenu(this.props.onClickAbout); | |
| return ( | |
| <Box | |
| className={classNames( | |
| this.props.className, | |
| styles.menuBar | |
| )} | |
| > | |
| <div className={styles.mainMenu}> | |
| <div className={styles.fileGroup}> | |
| {this.props.onClickLogo ? ( | |
| <div className={classNames(styles.menuBarItem)}> | |
| <img | |
| alt="Scratch" | |
| className={classNames(styles.scratchLogo, { | |
| [styles.clickable]: typeof this.props.onClickLogo !== 'undefined' | |
| })} | |
| draggable={false} | |
| src={s4sicon} | |
| onClick={this.props.onClickLogo} | |
| /> | |
| </div> | |
| ) : null} | |
| {(this.props.canChangeLanguage) && (<div | |
| className={classNames(styles.menuBarItem, styles.hoverable, styles.languageMenu)} | |
| > | |
| <div> | |
| <img | |
| className={styles.languageIcon} | |
| src={languageIcon} | |
| width="24" | |
| height="24" | |
| /> | |
| <img | |
| className={styles.languageCaret} | |
| src={dropdownCaret} | |
| width="8" | |
| height="5" | |
| /> | |
| </div> | |
| <LanguageSelector label={this.props.intl.formatMessage(ariaMessages.language)} /> | |
| </div>)} | |
| {/* tw: theme toggler */} | |
| {this.props.onClickTheme && ( | |
| <div | |
| className={classNames(styles.menuBarItem, styles.hoverable)} | |
| onMouseUp={this.props.onClickTheme} | |
| > | |
| <img | |
| src={themeIcon} | |
| width="24" | |
| height="24" | |
| draggable={false} | |
| /> | |
| </div> | |
| )} | |
| {/* tw: display compile errors */} | |
| {this.props.compileErrors.length > 0 && <div> | |
| <div | |
| className={classNames(styles.menuBarItem, styles.hoverable, { | |
| [styles.active]: this.props.errorsMenuOpen | |
| })} | |
| onMouseUp={this.props.onClickErrors} | |
| > | |
| <div className={classNames(styles.errorsMenu)}> | |
| <img | |
| className={styles.languageIcon} | |
| src={errorIcon} | |
| /> | |
| <img | |
| className={styles.languageCaret} | |
| src={dropdownCaret} | |
| /> | |
| </div> | |
| <MenuBarMenu | |
| className={classNames(styles.menuBarMenu)} | |
| open={this.props.errorsMenuOpen} | |
| place={this.props.isRtl ? 'left' : 'right'} | |
| onRequestClose={this.props.onRequestCloseErrors} | |
| > | |
| <MenuSection> | |
| <MenuItemLink href="https://discord.gg/NZ9MBMYTZh"> | |
| <FormattedMessage | |
| defaultMessage="Some scripts could not be compiled." | |
| description="Link in error menu" | |
| id="tw.menuBar.reportError1" | |
| /> | |
| </MenuItemLink> | |
| <MenuItemLink href="https://discord.gg/NZ9MBMYTZh"> | |
| <FormattedMessage | |
| defaultMessage="This is a bug. Please report it." | |
| description="Link in error menu" | |
| id="tw.menuBar.reportError2" | |
| /> | |
| </MenuItemLink> | |
| </MenuSection> | |
| <MenuSection> | |
| {this.props.compileErrors.map(({ id, sprite, error }) => ( | |
| <MenuItem key={id}> | |
| {this.props.intl.formatMessage(twMessages.compileError, { | |
| sprite, | |
| error | |
| })} | |
| </MenuItem> | |
| ))} | |
| </MenuSection> | |
| </MenuBarMenu> | |
| </div> | |
| </div>} | |
| {(this.props.canManageFiles) && ( | |
| <div | |
| className={classNames(styles.menuBarItem, styles.hoverable, { | |
| [styles.active]: this.props.fileMenuOpen | |
| })} | |
| onMouseUp={this.props.onClickFile} | |
| > | |
| <FormattedMessage | |
| defaultMessage="File" | |
| description="Text for file dropdown menu" | |
| id="gui.menuBar.file" | |
| /> | |
| <MenuBarMenu | |
| className={classNames(styles.menuBarMenu)} | |
| open={this.props.fileMenuOpen} | |
| place={this.props.isRtl ? 'left' : 'right'} | |
| onRequestClose={this.props.onRequestCloseFile} | |
| > | |
| <MenuSection> | |
| <MenuItem | |
| isRtl={this.props.isRtl} | |
| onClick={this.handleClickNew} | |
| > | |
| {newProjectMessage} | |
| </MenuItem> | |
| </MenuSection> | |
| {this.props.onClickNewWindow && ( | |
| <MenuItem | |
| isRtl={this.props.isRtl} | |
| onClick={this.handleClickNewWindow} | |
| > | |
| <FormattedMessage | |
| defaultMessage="New window" | |
| // eslint-disable-next-line max-len | |
| description="Part of desktop app. Menu bar item that creates a new window." | |
| id="tw.menuBar.newWindow" | |
| /> | |
| </MenuItem> | |
| )} | |
| {(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && ( | |
| <MenuSection> | |
| {this.props.canSave && ( | |
| <MenuItem onClick={this.handleClickSave}> | |
| {saveNowMessage} | |
| </MenuItem> | |
| )} | |
| {this.props.canCreateCopy && ( | |
| <MenuItem onClick={this.handleClickSaveAsCopy}> | |
| {createCopyMessage} | |
| </MenuItem> | |
| )} | |
| {this.props.canRemix && ( | |
| <MenuItem onClick={this.handleClickRemix}> | |
| {remixMessage} | |
| </MenuItem> | |
| )} | |
| </MenuSection> | |
| )} | |
| <MenuSection> | |
| <MenuItem | |
| onClick={this.props.onStartSelectingFileUpload} | |
| > | |
| {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)} | |
| </MenuItem> | |
| <SB3Downloader>{(_className, downloadProject, extended) => ( | |
| <React.Fragment> | |
| {extended.available && ( | |
| <React.Fragment> | |
| {extended.name !== null && ( | |
| // eslint-disable-next-line max-len | |
| <MenuItem onClick={this.getSaveToComputerHandler(extended.saveToLastFile)}> | |
| <FormattedMessage | |
| defaultMessage="Save to {file}" | |
| // eslint-disable-next-line max-len | |
| description="Menu bar item to save project to an existing file on the user's computer" | |
| id="tw.saveTo" | |
| values={{ | |
| file: extended.name | |
| }} | |
| /> | |
| </MenuItem> | |
| )} | |
| {/* eslint-disable-next-line max-len */} | |
| <MenuItem onClick={this.getSaveToComputerHandler(extended.saveAsNew)}> | |
| <FormattedMessage | |
| defaultMessage="Save as..." | |
| // eslint-disable-next-line max-len | |
| description="Menu bar item to select a new file to save the project as" | |
| id="tw.saveAs" | |
| /> | |
| </MenuItem> | |
| </React.Fragment> | |
| )} | |
| {notScratchDesktop() && ( | |
| <MenuItem onClick={this.getSaveToComputerHandler(downloadProject)}> | |
| {extended.available ? ( | |
| <FormattedMessage | |
| defaultMessage="Save to separate file..." | |
| // eslint-disable-next-line max-len | |
| description="Download the project once, without being able to easily save to the same spot" | |
| id="tw.oldDownload" | |
| /> | |
| ) : ( | |
| <FormattedMessage | |
| defaultMessage="Save to your computer" | |
| description="Menu bar item for downloading a project to your computer" // eslint-disable-line max-len | |
| id="gui.menuBar.downloadToComputer" | |
| /> | |
| )} | |
| </MenuItem> | |
| )} | |
| </React.Fragment> | |
| )}</SB3Downloader> | |
| </MenuSection> | |
| {this.props.isDirectoryPickerSupported && ( | |
| <MenuSection> | |
| <MenuItem | |
| onClick={this.props.onStartFolderUpload} | |
| > | |
| {"Load from a folder"} | |
| </MenuItem> | |
| <SB3Downloader>{(_className, downloadProject, extended) => ( | |
| <React.Fragment> | |
| <MenuItem | |
| onClick={this.getSaveToComputerHandler(extended.saveAsFolder)} | |
| > | |
| {"Export project to folder"} | |
| </MenuItem> | |
| </React.Fragment> | |
| )}</SB3Downloader> | |
| </MenuSection> | |
| )} | |
| {this.props.onClickPackager && ( | |
| <MenuSection> | |
| <MenuItem | |
| onClick={this.handleClickPackager} | |
| > | |
| <FormattedMessage | |
| defaultMessage="Package project" | |
| // eslint-disable-next-line max-len | |
| description="Menu bar item to open the current project in the packager" | |
| id="tw.menuBar.package" | |
| /> | |
| </MenuItem> | |
| </MenuSection> | |
| )} | |
| <MenuSection> | |
| <MenuItem onClick={this.handleClickRestorePoints}> | |
| <FormattedMessage | |
| defaultMessage="Restore points" | |
| description="Menu bar item to manage restore points" | |
| id="tw.menuBar.restorePoints" | |
| /> | |
| </MenuItem> | |
| </MenuSection> | |
| <MenuSection> | |
| <MenuItem onClick={this.handleClickDownloadLogs}> | |
| {'Download Logs'} | |
| </MenuItem> | |
| </MenuSection> | |
| </MenuBarMenu> | |
| </div> | |
| )} | |
| <div | |
| className={classNames(styles.menuBarItem, styles.hoverable, { | |
| [styles.active]: this.props.editMenuOpen | |
| })} | |
| onMouseUp={this.props.onClickEdit} | |
| > | |
| <div className={classNames(styles.editMenu)}> | |
| <FormattedMessage | |
| defaultMessage="Edit" | |
| description="Text for edit dropdown menu" | |
| id="gui.menuBar.edit" | |
| /> | |
| </div> | |
| <MenuBarMenu | |
| className={classNames(styles.menuBarMenu)} | |
| open={this.props.editMenuOpen} | |
| place={this.props.isRtl ? 'left' : 'right'} | |
| onRequestClose={this.props.onRequestCloseEdit} | |
| > | |
| {this.props.isPlayerOnly ? null : ( | |
| <DeletionRestorer>{(handleRestore, { restorable, deletedItem }) => ( | |
| <MenuItem | |
| className={classNames({ [styles.disabled]: !restorable })} | |
| onClick={this.handleRestoreOption(handleRestore)} | |
| > | |
| {this.restoreOptionMessage(deletedItem)} | |
| </MenuItem> | |
| )}</DeletionRestorer> | |
| )} | |
| <MenuSection> | |
| <TurboMode>{(toggleTurboMode, { turboMode }) => ( | |
| <MenuItem onClick={toggleTurboMode}> | |
| {turboMode ? ( | |
| <FormattedMessage | |
| defaultMessage="Turn off Turbo Mode" | |
| description="Menu bar item for turning off turbo mode" | |
| id="gui.menuBar.turboModeOff" | |
| /> | |
| ) : ( | |
| <FormattedMessage | |
| defaultMessage="Turn on Turbo Mode" | |
| description="Menu bar item for turning on turbo mode" | |
| id="gui.menuBar.turboModeOn" | |
| /> | |
| )} | |
| </MenuItem> | |
| )}</TurboMode> | |
| <FramerateChanger>{(changeFramerate, { framerate }) => ( | |
| <MenuItem onClick={changeFramerate}> | |
| {framerate === 60 ? ( | |
| <FormattedMessage | |
| defaultMessage="Turn off 60 FPS Mode" | |
| description="Menu bar item for turning off 60 FPS mode" | |
| id="tw.menuBar.60off" | |
| /> | |
| ) : ( | |
| <FormattedMessage | |
| defaultMessage="Turn on 60 FPS Mode" | |
| description="Menu bar item for turning on 60 FPS mode" | |
| id="tw.menuBar.60on" | |
| /> | |
| )} | |
| </MenuItem> | |
| )}</FramerateChanger> | |
| <ChangeUsername>{changeUsername => ( | |
| <MenuItem | |
| className={classNames({ [styles.disabled]: this.props.usernameLoggedIn })} | |
| onClick={this.props.usernameLoggedIn ? () => {} : changeUsername} | |
| > | |
| <FormattedMessage | |
| defaultMessage="Change Username" | |
| description="Menu bar item for changing the username" | |
| id="tw.menuBar.changeUsername" | |
| /> | |
| </MenuItem> | |
| )}</ChangeUsername> | |
| <CloudVariablesToggler>{(toggleCloudVariables, { enabled, canUseCloudVariables }) => ( | |
| <MenuItem | |
| className={classNames({ [styles.disabled]: !canUseCloudVariables })} | |
| onClick={toggleCloudVariables} | |
| > | |
| {canUseCloudVariables ? ( | |
| enabled ? ( | |
| <FormattedMessage | |
| defaultMessage="Disable Cloud Variables" | |
| description="Menu bar item for disabling cloud variables" | |
| id="tw.menuBar.cloudOff" | |
| /> | |
| ) : ( | |
| <FormattedMessage | |
| defaultMessage="Enable Cloud Variables" | |
| description="Menu bar item for enabling cloud variables" | |
| id="tw.menuBar.cloudOn" | |
| /> | |
| ) | |
| ) : ( | |
| <FormattedMessage | |
| defaultMessage="Cloud Variables are not Available" | |
| description="Menu bar item for when cloud variables are not available" | |
| id="tw.menuBar.cloudUnavailable" | |
| /> | |
| )} | |
| </MenuItem> | |
| )}</CloudVariablesToggler> | |
| </MenuSection> | |
| <MenuSection> | |
| <MenuItem onClick={this.props.onClickSettings}> | |
| <FormattedMessage | |
| defaultMessage="Gameplay Settings" | |
| description="Menu bar item for gameplay settings" | |
| id="pm.menuBar.moreSettings" | |
| /> | |
| </MenuItem> | |
| </MenuSection> | |
| </MenuBarMenu> | |
| </div> | |
| {this.props.onClickAddonSettings && ( | |
| <div | |
| className={classNames(styles.menuBarItem, styles.hoverable)} | |
| onMouseUp={this.props.onClickAddonSettings} | |
| > | |
| <div> | |
| <FormattedMessage | |
| // Note: this string is used by scratch-vm for the addons blocks category | |
| defaultMessage="Addons" | |
| description="Menu bar item for addon settings" | |
| id="tw.menuBar.addons" | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| <div | |
| className={classNames(styles.menuBarItem, styles.hoverable)} | |
| onMouseUp={this.props.onClickSettings} | |
| > | |
| <div> | |
| <FormattedMessage | |
| defaultMessage="Settings" | |
| description="Text for gameplay settings menu item" | |
| id="pm.menuBar.gameplaySettings" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| <Divider className={classNames(styles.divider)} /> | |
| {/* {(this.props.authorUsername && this.props.authorUsername !== this.props.username) ? ( | |
| <AuthorInfo | |
| className={styles.authorInfo} | |
| imageUrl={this.props.authorThumbnailUrl} | |
| projectId={this.props.projectId} | |
| // projectTitle={this.props.projectTitle} | |
| userId={this.props.authorId} | |
| username={this.props.authorUsername} | |
| /> | |
| ) : null} */} | |
| {this.props.canEditTitle ? ( | |
| <div className={classNames(styles.menuBarItem, styles.growable)}> | |
| <MenuBarItemTooltip | |
| enable | |
| id="title-field" | |
| > | |
| <ProjectTitleInput | |
| className={classNames(styles.titleFieldGrowable)} | |
| /> | |
| </MenuBarItemTooltip> | |
| </div> | |
| ) : null} | |
| <div className={classNames(styles.menuBarItem)}> | |
| {this.props.canRemix ? remixButton : []} | |
| </div> | |
| <div className={classNames(styles.menuBarItem, styles.communityButtonWrapper)}> | |
| {this.props.enableCommunity ? ( | |
| (this.props.isShowingProject || this.props.isUpdating) && ( | |
| <ProjectWatcher onDoneUpdating={this.props.onSeeCommunity}> | |
| { | |
| waitForUpdate => ( | |
| <CommunityButton | |
| className={styles.menuBarButton} | |
| /* eslint-disable react/jsx-no-bind */ | |
| onClick={() => { | |
| this.handleClickSeeCommunity(waitForUpdate); | |
| }} | |
| /* eslint-enable react/jsx-no-bind */ | |
| /> | |
| ) | |
| } | |
| </ProjectWatcher> | |
| ) | |
| ) : (this.props.showComingSoon ? ( | |
| <MenuBarItemTooltip id="community-button"> | |
| <CommunityButton className={styles.menuBarButton} /> | |
| </MenuBarItemTooltip> | |
| ) : (this.props.enableSeeInside ? ( | |
| <SeeInsideButton | |
| className={styles.menuBarButton} | |
| onClick={this.handleClickSeeInside} | |
| /> | |
| ) : []))} | |
| </div> | |
| <GoogleDriveSave /> | |
| <div className={styles.menuBarItem}> | |
| {this.props.isShowingProject && this.props.canEditTitle ? | |
| (<ShareButton | |
| className={styles.menuBarButton} | |
| isShared={this.props.isShared} | |
| />) | |
| : (null)} | |
| </div> | |
| <div className={styles.menuBarItem}> | |
| <a | |
| className={styles.feedbackLink} | |
| href="https://penguinmod.com" | |
| rel="noopener noreferrer" | |
| target="_blank" | |
| > | |
| <Button className={styles.feedbackButton}> | |
| <FormattedMessage | |
| defaultMessage="Back to Home" | |
| description="Button to go back to the home page" | |
| id="pm.backToHomeButton" | |
| /> | |
| </Button> | |
| </a> | |
| </div> | |
| </div> | |
| <div className={styles.accountInfoGroup}> | |
| <div className={styles.menuBarItem}> | |
| <TWSaveStatus /> | |
| </div> | |
| </div> | |
| {aboutButton} | |
| </Box> | |
| ); | |
| } | |
| } | |
| MenuBar.propTypes = { | |
| enableSeeInside: PropTypes.bool, | |
| onClickSeeInside: PropTypes.func, | |
| aboutMenuOpen: PropTypes.bool, | |
| accountMenuOpen: PropTypes.bool, | |
| authorId: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), | |
| authorThumbnailUrl: PropTypes.string, | |
| authorUsername: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), | |
| autoUpdateProject: PropTypes.func, | |
| canChangeLanguage: PropTypes.bool, | |
| canCreateCopy: PropTypes.bool, | |
| canCreateNew: PropTypes.bool, | |
| canEditTitle: PropTypes.bool, | |
| canManageFiles: PropTypes.bool, | |
| canRemix: PropTypes.bool, | |
| canSave: PropTypes.bool, | |
| canShare: PropTypes.bool, | |
| className: PropTypes.string, | |
| compileErrors: PropTypes.arrayOf(PropTypes.shape({ | |
| sprite: PropTypes.string, | |
| error: PropTypes.string, | |
| id: PropTypes.number | |
| })), | |
| confirmReadyToReplaceProject: PropTypes.func, | |
| editMenuOpen: PropTypes.bool, | |
| enableCommunity: PropTypes.bool, | |
| fileMenuOpen: PropTypes.bool, | |
| handleSaveProject: PropTypes.func, | |
| intl: intlShape, | |
| isDirectoryPickerSupported: PropTypes.bool, | |
| isPlayerOnly: PropTypes.bool, | |
| isRtl: PropTypes.bool, | |
| isShared: PropTypes.bool, | |
| isShowingProject: PropTypes.bool, | |
| isUpdating: PropTypes.bool, | |
| languageMenuOpen: PropTypes.bool, | |
| locale: PropTypes.string.isRequired, | |
| loginMenuOpen: PropTypes.bool, | |
| logo: PropTypes.string, | |
| onClickAbout: PropTypes.oneOfType([ | |
| PropTypes.func, // button mode: call this callback when the About button is clicked | |
| PropTypes.arrayOf( // menu mode: list of items in the About menu | |
| PropTypes.shape({ | |
| title: PropTypes.string, // text for the menu item | |
| onClick: PropTypes.func // call this callback when the menu item is clicked | |
| }) | |
| ) | |
| ]), | |
| onClickAccount: PropTypes.func, | |
| onClickAddonSettings: PropTypes.func, | |
| onClickTheme: PropTypes.func, | |
| onClickPackager: PropTypes.func, | |
| onClickRestorePoints: PropTypes.func, | |
| onClickEdit: PropTypes.func, | |
| onClickFile: PropTypes.func, | |
| onClickLanguage: PropTypes.func, | |
| onClickLogin: PropTypes.func, | |
| onClickLogo: PropTypes.func, | |
| onClickNew: PropTypes.func, | |
| onClickNewWindow: PropTypes.func, | |
| onClickRemix: PropTypes.func, | |
| onClickSave: PropTypes.func, | |
| onClickSaveAsCopy: PropTypes.func, | |
| onClickSettings: PropTypes.func, | |
| onClickErrors: PropTypes.func, | |
| onRequestCloseErrors: PropTypes.func, | |
| onLogOut: PropTypes.func, | |
| onOpenRegistration: PropTypes.func, | |
| onOpenTipLibrary: PropTypes.func, | |
| onProjectTelemetryEvent: PropTypes.func, | |
| onRequestOpenAbout: PropTypes.func, | |
| onRequestCloseAbout: PropTypes.func, | |
| onRequestCloseAccount: PropTypes.func, | |
| onRequestCloseEdit: PropTypes.func, | |
| onRequestCloseFile: PropTypes.func, | |
| onRequestCloseLanguage: PropTypes.func, | |
| onRequestCloseLogin: PropTypes.func, | |
| onSeeCommunity: PropTypes.func, | |
| onShare: PropTypes.func, | |
| onStartSelectingFileUpload: PropTypes.func, | |
| onStartFolderUpload: PropTypes.func, | |
| onToggleLoginOpen: PropTypes.func, | |
| projectId: PropTypes.string, | |
| projectTitle: PropTypes.string, | |
| renderLogin: PropTypes.func, | |
| sessionExists: PropTypes.bool, | |
| errorsMenuOpen: PropTypes.bool, | |
| shouldSaveBeforeTransition: PropTypes.func, | |
| showComingSoon: PropTypes.bool, | |
| userOwnsProject: PropTypes.bool, | |
| username: PropTypes.string, | |
| usernameLoggedIn: PropTypes.bool.isRequired, | |
| vm: PropTypes.instanceOf(VM).isRequired | |
| }; | |
| MenuBar.defaultProps = { | |
| logo: scratchLogo, | |
| usernameLoggedIn: false, | |
| onShare: () => { } | |
| }; | |
| const mapStateToProps = (state, ownProps) => { | |
| const loadingState = state.scratchGui.projectState.loadingState; | |
| const user = state.session && state.session.session && state.session.session.user; | |
| return { | |
| aboutMenuOpen: aboutMenuOpen(state), | |
| accountMenuOpen: accountMenuOpen(state), | |
| authorThumbnailUrl: state.scratchGui.tw.author.thumbnail, | |
| authorUsername: state.scratchGui.tw.author.username, | |
| compileErrors: state.scratchGui.tw.compileErrors, | |
| fileMenuOpen: fileMenuOpen(state), | |
| editMenuOpen: editMenuOpen(state), | |
| isPlayerOnly: state.scratchGui.mode.isPlayerOnly, | |
| isRtl: state.locales.isRtl, | |
| isUpdating: getIsUpdating(loadingState), | |
| isShowingProject: getIsShowingProject(loadingState), | |
| languageMenuOpen: languageMenuOpen(state), | |
| locale: state.locales.locale, | |
| loginMenuOpen: loginMenuOpen(state), | |
| projectId: state.scratchGui.projectState.projectId, | |
| projectTitle: state.scratchGui.projectTitle, | |
| sessionExists: state.session && typeof state.session.session !== 'undefined', | |
| errorsMenuOpen: errorsMenuOpen(state), | |
| username: user ? user.username : null, | |
| usernameLoggedIn: state.scratchGui.tw.usernameLoggedIn, | |
| userOwnsProject: ownProps.authorUsername && user && | |
| (ownProps.authorUsername === user.username), | |
| vm: state.scratchGui.vm | |
| }; | |
| }; | |
| const mapDispatchToProps = dispatch => ({ | |
| onClickSeeInside: () => dispatch(setPlayer(false)), | |
| autoUpdateProject: () => dispatch(autoUpdateProject()), | |
| onOpenTipLibrary: () => dispatch(openTipsLibrary()), | |
| onClickAccount: () => dispatch(openAccountMenu()), | |
| onRequestCloseAccount: () => dispatch(closeAccountMenu()), | |
| onClickFile: () => dispatch(openFileMenu()), | |
| onRequestCloseFile: () => dispatch(closeFileMenu()), | |
| onClickEdit: () => dispatch(openEditMenu()), | |
| onRequestCloseEdit: () => dispatch(closeEditMenu()), | |
| onClickLanguage: () => dispatch(openLanguageMenu()), | |
| onRequestCloseLanguage: () => dispatch(closeLanguageMenu()), | |
| onClickLogin: () => dispatch(openLoginMenu()), | |
| onRequestCloseLogin: () => dispatch(closeLoginMenu()), | |
| onClickErrors: () => dispatch(openErrorsMenu()), | |
| onRequestCloseErrors: () => dispatch(closeErrorsMenu()), | |
| onRequestOpenAbout: () => dispatch(openAboutMenu()), | |
| onRequestCloseAbout: () => dispatch(closeAboutMenu()), | |
| onClickNew: needSave => { | |
| dispatch(requestNewProject(needSave)); | |
| dispatch(setFileHandle(null)); | |
| }, | |
| onClickRemix: () => dispatch(remixProject()), | |
| onClickSave: () => dispatch(manualUpdateProject()), | |
| onClickSaveAsCopy: () => dispatch(saveProjectAsCopy()), | |
| onClickRestorePoints: () => dispatch(openRestorePointModal()), | |
| onClickSettings: () => { | |
| dispatch(openSettingsModal()); | |
| dispatch(closeEditMenu()); | |
| }, | |
| onSeeCommunity: () => dispatch(setPlayer(true)) | |
| }); | |
| export default compose( | |
| injectIntl, | |
| MenuBarHOC, | |
| connect( | |
| mapStateToProps, | |
| mapDispatchToProps | |
| ) | |
| )(MenuBar); | |