Spaces:
Running
Running
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 { 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 scratchLogo from './scratch-logo.svg'; | |
import sharedMessages from '../../lib/shared-messages'; | |
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(); | |
}; | |
} | |
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={this.props.logo} | |
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" | |
hidden="true" | |
/> | |
</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.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> | |
</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> | |
<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, | |
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, | |
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); | |