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 ( {children} ); } return ( {children} ); }; 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 }) => ( {children} ); MenuItemTooltip.propTypes = { children: PropTypes.node, className: PropTypes.string, id: PropTypes.string, isRtl: PropTypes.bool }; const AboutButton = props => ( ); // 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 (
{this.props.onClickLogo ? (
Scratch
) : null} {(this.props.canChangeLanguage) && (
)} {/* tw: theme toggler */} {this.props.onClickTheme && (
)} {/* tw: display compile errors */} {this.props.compileErrors.length > 0 &&
{this.props.compileErrors.map(({ id, sprite, error }) => ( {this.props.intl.formatMessage(twMessages.compileError, { sprite, error })} ))}
} {(this.props.canManageFiles) && (
{newProjectMessage} {this.props.onClickNewWindow && ( )} {(this.props.canSave || this.props.canCreateCopy || this.props.canRemix) && ( {this.props.canSave && ( {saveNowMessage} )} {this.props.canCreateCopy && ( {createCopyMessage} )} {this.props.canRemix && ( {remixMessage} )} )} {this.props.intl.formatMessage(sharedMessages.loadFromComputerTitle)} {(_className, downloadProject, extended) => ( {extended.available && ( {extended.name !== null && ( // eslint-disable-next-line max-len )} {/* eslint-disable-next-line max-len */} )} {notScratchDesktop() && ( {extended.available ? ( ) : ( )} )} )} {this.props.onClickPackager && ( )}
)}
{this.props.isPlayerOnly ? null : ( {(handleRestore, { restorable, deletedItem }) => ( {this.restoreOptionMessage(deletedItem)} )} )} {(toggleTurboMode, { turboMode }) => ( {turboMode ? ( ) : ( )} )} {(changeFramerate, { framerate }) => ( {framerate === 60 ? ( ) : ( )} )} {changeUsername => ( {} : changeUsername} > )} {(toggleCloudVariables, { enabled, canUseCloudVariables }) => ( {canUseCloudVariables ? ( enabled ? ( ) : ( ) ) : ( )} )}
{this.props.onClickAddonSettings && (
)}
{/* {(this.props.authorUsername && this.props.authorUsername !== this.props.username) ? ( ) : null} */} {this.props.canEditTitle ? (
) : null}
{this.props.canRemix ? remixButton : []}
{this.props.enableCommunity ? ( (this.props.isShowingProject || this.props.isUpdating) && ( { waitForUpdate => ( { this.handleClickSeeCommunity(waitForUpdate); }} /* eslint-enable react/jsx-no-bind */ /> ) } ) ) : (this.props.showComingSoon ? ( ) : (this.props.enableSeeInside ? ( ) : []))}
{this.props.isShowingProject && this.props.canEditTitle ? () : (null)}
{aboutButton}
); } } 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);