soiz1's picture
Update src/components/menu-bar/menu-bar.jsx
0a8b9cf verified
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);