diff --git a/client/common/icons.jsx b/client/common/icons.jsx index b0521f0c..f91e4dc7 100644 --- a/client/common/icons.jsx +++ b/client/common/icons.jsx @@ -13,6 +13,9 @@ import DropdownArrow from '../images/down-filled-triangle.svg'; import Preferences from '../images/preferences.svg'; import Play from '../images/triangle-arrow-right.svg'; import More from '../images/more.svg'; +import Code from '../images/code.svg'; +import Terminal from '../images/terminal.svg'; + // HOC that adds the right web accessibility props // https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html @@ -76,3 +79,4 @@ export const DropdownArrowIcon = withLabel(DropdownArrow); export const PreferencesIcon = withLabel(Preferences); export const PlayIcon = withLabel(Play); export const MoreIcon = withLabel(More); +export const TerminalIcon = withLabel(Terminal); diff --git a/client/components/mobile/ActionStrip.jsx b/client/components/mobile/ActionStrip.jsx new file mode 100644 index 00000000..0d75a579 --- /dev/null +++ b/client/components/mobile/ActionStrip.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import styled from 'styled-components'; +import { bindActionCreators } from 'redux'; +import { useDispatch, useSelector } from 'react-redux'; +import { remSize } from '../../theme'; +import IconButton from './IconButton'; +import { TerminalIcon } from '../../common/icons'; +import * as IDEActions from '../../modules/IDE/actions/ide'; + +const BottomBarContent = styled.h2` + padding: ${remSize(8)}; + + svg { + max-height: ${remSize(32)}; + } +`; + +export default () => { + const { expandConsole, collapseConsole } = bindActionCreators(IDEActions, useDispatch()); + const { consoleIsExpanded } = useSelector(state => state.ide); + + const actions = [{ icon: TerminalIcon, aria: 'Say Something', action: consoleIsExpanded ? collapseConsole : expandConsole }]; + + return ( + + {actions.map(({ icon, aria, action }) => + ( action()} + />))} + + ); +}; diff --git a/client/components/mobile/Footer.jsx b/client/components/mobile/Footer.jsx index 25f10c1d..ff19eee1 100644 --- a/client/components/mobile/Footer.jsx +++ b/client/components/mobile/Footer.jsx @@ -1,6 +1,6 @@ import React from 'react'; import styled from 'styled-components'; -import { prop } from '../../theme'; +import { prop, grays } from '../../theme'; const background = prop('MobilePanel.default.background'); @@ -12,4 +12,6 @@ export default styled.div` bottom: 0; background: ${background}; color: ${textColor}; + + & > * + * { border-top: dashed 1px ${prop('Separator')} } `; diff --git a/client/components/mobile/Header.jsx b/client/components/mobile/Header.jsx index 1f7f7a29..cf727b7c 100644 --- a/client/components/mobile/Header.jsx +++ b/client/components/mobile/Header.jsx @@ -3,14 +3,14 @@ import styled from 'styled-components'; import PropTypes from 'prop-types'; import { prop, remSize } from '../../theme'; -const background = prop('MobilePanel.default.background'); +const background = transparent => prop(transparent ? 'backgroundColor' : 'MobilePanel.default.background'); const textColor = prop('primaryTextColor'); const HeaderDiv = styled.div` position: fixed; width: 100%; - background: ${props => (props.transparent ? 'transparent' : background)}; + background: ${props => background(props.transparent === true)}; color: ${textColor}; padding: ${remSize(12)}; padding-left: ${remSize(16)}; @@ -23,7 +23,6 @@ const HeaderDiv = styled.div` justify-content: flex-start; align-items: center; - // TODO: svg { max-height: ${remSize(32)}; padding: ${remSize(4)} diff --git a/client/images/terminal.svg b/client/images/terminal.svg new file mode 100644 index 00000000..57f1f52c --- /dev/null +++ b/client/images/terminal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/modules/IDE/components/Console.jsx b/client/modules/IDE/components/Console.jsx index 2bd3af7e..ddb6d55e 100644 --- a/client/modules/IDE/components/Console.jsx +++ b/client/modules/IDE/components/Console.jsx @@ -1,6 +1,8 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import { useSelector, useDispatch, connect } from 'react-redux'; +import React, { useRef } from 'react'; + +import { bindActionCreators } from 'redux'; + +import { useSelector, useDispatch } from 'react-redux'; import classNames from 'classnames'; import { Console as ConsoleFeed } from 'console-feed'; import { @@ -25,172 +27,118 @@ import DownArrowIcon from '../../../images/down-arrow.svg'; import * as IDEActions from '../../IDE/actions/ide'; import * as ConsoleActions from '../../IDE/actions/console'; +import { useDidUpdate } from '../../../utils/custom-hooks'; -class ConsoleComponent extends React.Component { - componentDidUpdate(prevProps) { - this.consoleMessages.scrollTop = this.consoleMessages.scrollHeight; - if (this.props.theme !== prevProps.theme) { - this.props.clearConsole(); - this.props.dispatchConsoleEvent(this.props.consoleEvents); - } +const getConsoleFeedStyle = (theme, times, fontSize) => { + const style = {}; + const CONSOLE_FEED_LIGHT_ICONS = { + LOG_WARN_ICON: `url(${warnLightUrl})`, + LOG_ERROR_ICON: `url(${errorLightUrl})`, + LOG_DEBUG_ICON: `url(${debugLightUrl})`, + LOG_INFO_ICON: `url(${infoLightUrl})` + }; + const CONSOLE_FEED_DARK_ICONS = { + LOG_WARN_ICON: `url(${warnDarkUrl})`, + LOG_ERROR_ICON: `url(${errorDarkUrl})`, + LOG_DEBUG_ICON: `url(${debugDarkUrl})`, + LOG_INFO_ICON: `url(${infoDarkUrl})` + }; + const CONSOLE_FEED_CONTRAST_ICONS = { + LOG_WARN_ICON: `url(${warnContrastUrl})`, + LOG_ERROR_ICON: `url(${errorContrastUrl})`, + LOG_DEBUG_ICON: `url(${debugContrastUrl})`, + LOG_INFO_ICON: `url(${infoContrastUrl})` + }; + const CONSOLE_FEED_SIZES = { + TREENODE_LINE_HEIGHT: 1.2, + BASE_FONT_SIZE: fontSize, + ARROW_FONT_SIZE: fontSize, + LOG_ICON_WIDTH: fontSize, + LOG_ICON_HEIGHT: 1.45 * fontSize, + }; - if (this.props.fontSize !== prevProps.fontSize) { - this.props.clearConsole(); - this.props.dispatchConsoleEvent(this.props.consoleEvents); - } + if (times > 1) { + Object.assign(style, CONSOLE_FEED_WITHOUT_ICONS); } - - getConsoleFeedStyle(theme, times) { - const style = {}; - const CONSOLE_FEED_LIGHT_ICONS = { - LOG_WARN_ICON: `url(${warnLightUrl})`, - LOG_ERROR_ICON: `url(${errorLightUrl})`, - LOG_DEBUG_ICON: `url(${debugLightUrl})`, - LOG_INFO_ICON: `url(${infoLightUrl})` - }; - const CONSOLE_FEED_DARK_ICONS = { - LOG_WARN_ICON: `url(${warnDarkUrl})`, - LOG_ERROR_ICON: `url(${errorDarkUrl})`, - LOG_DEBUG_ICON: `url(${debugDarkUrl})`, - LOG_INFO_ICON: `url(${infoDarkUrl})` - }; - const CONSOLE_FEED_CONTRAST_ICONS = { - LOG_WARN_ICON: `url(${warnContrastUrl})`, - LOG_ERROR_ICON: `url(${errorContrastUrl})`, - LOG_DEBUG_ICON: `url(${debugContrastUrl})`, - LOG_INFO_ICON: `url(${infoContrastUrl})` - }; - const CONSOLE_FEED_SIZES = { - TREENODE_LINE_HEIGHT: 1.2, - BASE_FONT_SIZE: this.props.fontSize, - ARROW_FONT_SIZE: this.props.fontSize, - LOG_ICON_WIDTH: this.props.fontSize, - LOG_ICON_HEIGHT: 1.45 * this.props.fontSize, - }; - - if (times > 1) { - Object.assign(style, CONSOLE_FEED_WITHOUT_ICONS); - } - switch (theme) { - case 'light': - return Object.assign(CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_LIGHT_ICONS, CONSOLE_FEED_SIZES, style); - case 'dark': - return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style); - case 'contrast': - return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_CONTRAST_ICONS, CONSOLE_FEED_SIZES, style); - default: - return ''; - } + switch (theme) { + case 'light': + return Object.assign(CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_LIGHT_ICONS, CONSOLE_FEED_SIZES, style); + case 'dark': + return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style); + case 'contrast': + return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_CONTRAST_ICONS, CONSOLE_FEED_SIZES, style); + default: + return ''; } - - render() { - const consoleClass = classNames({ - 'preview-console': true, - 'preview-console--collapsed': !this.props.isExpanded - }); - - return ( -
-
-

Console

-
- - - -
-
-
{ this.consoleMessages = element; }} className="preview-console__messages"> - {this.props.consoleEvents.map((consoleEvent) => { - const { method, times } = consoleEvent; - const { theme } = this.props; - return ( -
- { times > 1 && -
- {times} -
- } - -
- ); - })} -
-
- ); - } -} - -ConsoleComponent.propTypes = { - consoleEvents: PropTypes.arrayOf(PropTypes.shape({ - method: PropTypes.string.isRequired, - args: PropTypes.arrayOf(PropTypes.string) - })), - isExpanded: PropTypes.bool.isRequired, - collapseConsole: PropTypes.func.isRequired, - expandConsole: PropTypes.func.isRequired, - clearConsole: PropTypes.func.isRequired, - dispatchConsoleEvent: PropTypes.func.isRequired, - theme: PropTypes.string.isRequired, - fontSize: PropTypes.number.isRequired }; -ConsoleComponent.defaultProps = { - consoleEvents: [] -}; - -// TODO: Use Hooks implementation. Requires react-redux 7.1.0 -/* const Console = () => { const consoleEvents = useSelector(state => state.console); - const { consoleIsExpanded } = useSelector(state => state.ide); + const isExpanded = useSelector(state => state.ide.consoleIsExpanded); const { theme, fontSize } = useSelector(state => state.preferences); - const dispatch = useDispatch(); + const { + collapseConsole, expandConsole, clearConsole, dispatchConsoleEvent + } = bindActionCreators({ ...IDEActions, ...ConsoleActions }, useDispatch()); + + useDidUpdate(() => { + clearConsole(); + dispatchConsoleEvent(consoleEvents); + }, [theme, fontSize]); + + const cm = useRef({}); + + useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; }); + + const consoleClass = classNames({ + 'preview-console': true, + 'preview-console--collapsed': !isExpanded + }); return ( - dispatch({})} - expandConsole={() => dispatch({})} - clearConsole={() => dispatch({})} - dispatchConsoleEvent={() => dispatch({})} - /> +
+
+

Console

+
+ + + +
+
+
+ {consoleEvents.map((consoleEvent) => { + const { method, times } = consoleEvent; + return ( +
+ { times > 1 && +
+ {times} +
+ } + +
+ ); + })} +
+
); }; - */ -const Console = connect( - state => ({ - consoleEvents: state.console, - isExpanded: state.ide.consoleIsExpanded, - theme: state.preferences.theme, - fontSize: state.preferences.fontSize - }), - dispatch => ({ - collapseConsole: () => dispatch(IDEActions.collapseConsole()), - expandConsole: () => dispatch(IDEActions.expandConsole()), - clearConsole: () => dispatch(ConsoleActions.clearConsole()), - dispatchConsoleEvent: msgs => dispatch(ConsoleActions.dispatchConsoleEvent(msgs)), - }) -)(ConsoleComponent); export default Console; diff --git a/client/modules/IDE/components/Preferences/PreferenceCreators.jsx b/client/modules/IDE/components/Preferences/PreferenceCreators.jsx new file mode 100644 index 00000000..9b977e09 --- /dev/null +++ b/client/modules/IDE/components/Preferences/PreferenceCreators.jsx @@ -0,0 +1,26 @@ +export const optionsOnOff = (name, onLabel = 'On', offLabel = 'Off') => [ + { + value: true, label: onLabel, ariaLabel: `${name} on`, name: `${name}`, id: `${name}-on`.replace(' ', '-') + }, + { + value: false, label: offLabel, ariaLabel: `${name} off`, name: `${name}`, id: `${name}-off`.replace(' ', '-') + }, +]; + +export const optionsPickOne = (name, ...options) => options.map(option => ({ + value: option, + label: option, + ariaLabel: `${option} ${name} on`, + name: `${option} ${name}`, + id: `${option}-${name}-on`.replace(' ', '-') +})); + +const nameToValueName = x => (x && x.toLowerCase().replace(/#|_|-/g, ' ')); + +// preferenceOnOff: name, value and onSelect are mandatory. propname is optional +export const preferenceOnOff = (name, value, onSelect, propname) => ({ + title: name, + value, + options: optionsOnOff(propname || nameToValueName(name)), + onSelect +}); diff --git a/client/modules/IDE/components/Preferences.jsx b/client/modules/IDE/components/Preferences/index.jsx similarity index 98% rename from client/modules/IDE/components/Preferences.jsx rename to client/modules/IDE/components/Preferences/index.jsx index 1a658c3c..5fd5147d 100644 --- a/client/modules/IDE/components/Preferences.jsx +++ b/client/modules/IDE/components/Preferences/index.jsx @@ -7,9 +7,9 @@ import { withTranslation } from 'react-i18next'; // import { connect } from 'react-redux'; // import * as PreferencesActions from '../actions/preferences'; -import PlusIcon from '../../../images/plus.svg'; -import MinusIcon from '../../../images/minus.svg'; -import beepUrl from '../../../sounds/audioAlert.mp3'; +import PlusIcon from '../../../../images/plus.svg'; +import MinusIcon from '../../../../images/minus.svg'; +import beepUrl from '../../../../sounds/audioAlert.mp3'; class Preferences extends React.Component { constructor(props) { diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index fbcecf1b..b28e505f 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -10,7 +10,7 @@ import Editor from '../components/Editor'; import Sidebar from '../components/Sidebar'; import PreviewFrame from '../components/PreviewFrame'; import Toolbar from '../components/Toolbar'; -import Preferences from '../components/Preferences'; +import Preferences from '../components/Preferences/index'; import NewFileModal from '../components/NewFileModal'; import NewFolderModal from '../components/NewFolderModal'; import UploadFileModal from '../components/UploadFileModal'; diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index 15f211cd..190791d7 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -29,12 +29,13 @@ import IDEWrapper from '../../../components/mobile/IDEWrapper'; import Console from '../components/Console'; import { remSize } from '../../../theme'; import Dropdown from '../../../components/Dropdown'; +import ActionStrip from '../../../components/mobile/ActionStrip'; const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id); -const BottomBarContent = styled.h2` - padding: ${remSize(12)}; - padding-left: ${remSize(32)}; + +const Expander = styled.div` + height: ${props => (props.expanded ? remSize(160) : remSize(27))}; `; @@ -116,8 +117,8 @@ const MobileIDEView = (props) => { /> {/* Overlays */} diff --git a/client/modules/Mobile/MobilePreferences.jsx b/client/modules/Mobile/MobilePreferences.jsx index a9dc9f34..c7991c2f 100644 --- a/client/modules/Mobile/MobilePreferences.jsx +++ b/client/modules/Mobile/MobilePreferences.jsx @@ -1,6 +1,6 @@ import React from 'react'; import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; +import { connect, useSelector, useDispatch } from 'react-redux'; import { withRouter } from 'react-router'; import PropTypes from 'prop-types'; import styled from 'styled-components'; @@ -14,17 +14,13 @@ import Header from '../../components/mobile/Header'; import PreferencePicker from '../../components/mobile/PreferencePicker'; import { ExitIcon } from '../../common/icons'; import { remSize, prop } from '../../theme'; +import { optionsOnOff, optionsPickOne, preferenceOnOff } from '../IDE/components/Preferences/PreferenceCreators'; const Content = styled.div` z-index: 0; margin-top: ${remSize(68)}; `; - -const SettingsHeader = styled(Header)` - background: transparent; -`; - const SectionHeader = styled.h2` color: ${prop('primaryTextColor')}; padding-top: ${remSize(32)}; @@ -35,143 +31,46 @@ const SectionSubeader = styled.h3` `; -const MobilePreferences = (props) => { +const MobilePreferences = () => { + // Props const { - setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, lineNumbers, lintWarning - } = props; + theme, autosave, linewrap, textOutput, gridOutput, soundOutput, lineNumbers, lintWarning + } = useSelector(state => state.preferences); + + // Actions const { - theme, autosave, linewrap, textOutput, gridOutput, soundOutput, setLineNumbers, setLintWarning - } = props; + setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, setLineNumbers, setLintWarning, + } = bindActionCreators({ ...PreferencesActions, ...IdeActions }, useDispatch()); + const generalSettings = [ { title: 'Theme', value: theme, - options: [ - { - value: 'light', label: 'light', ariaLabel: 'light theme on', name: 'light theme', id: 'light-theme-on' - }, - { - value: 'dark', label: 'dark', ariaLabel: 'dark theme on', name: 'dark theme', id: 'dark-theme-on' - }, - { - value: 'contrast', - label: 'contrast', - ariaLabel: 'contrast theme on', - name: 'contrast theme', - id: 'contrast-theme-on' - } - ], + options: optionsPickOne('theme', 'light', 'dark', 'contrast'), onSelect: x => setTheme(x) // setTheme }, - - { - title: 'Autosave', - value: autosave, - options: [ - { - value: true, label: 'On', ariaLabel: 'autosave on', name: 'autosave', id: 'autosave-on' - }, - { - value: false, label: 'Off', ariaLabel: 'autosave off', name: 'autosave', id: 'autosave-off' - }, - ], - onSelect: x => setAutosave(x) // setAutosave - }, - - { - title: 'Word Wrap', - value: linewrap, - options: [ - { - value: true, label: 'On', ariaLabel: 'linewrap on', name: 'linewrap', id: 'linewrap-on' - }, - { - value: false, label: 'Off', ariaLabel: 'linewrap off', name: 'linewrap', id: 'linewrap-off' - }, - ], - onSelect: x => setLinewrap(x) - } + preferenceOnOff('Autosave', autosave, setAutosave, 'autosave'), + preferenceOnOff('Word Wrap', linewrap, setLinewrap, 'linewrap') ]; const outputSettings = [ - { - title: 'Plain-text', - value: textOutput, - options: [ - { - value: true, label: 'On', ariaLabel: 'text output on', name: 'text output', id: 'text-output-on' - }, - { - value: false, label: 'Off', ariaLabel: 'text output off', name: 'text output', id: 'text-output-off' - }, - ], - onSelect: x => setTextOutput(x) - }, - { - title: 'Table-text', - value: gridOutput, - options: [ - { - value: true, label: 'On', ariaLabel: 'table output on', name: 'table output', id: 'table-output-on' - }, - { - value: false, label: 'Off', ariaLabel: 'table output off', name: 'table output', id: 'table-output-off' - }, - ], - onSelect: x => setGridOutput(x) - }, - { - title: 'Sound', - value: soundOutput, - options: [ - { - value: true, label: 'On', ariaLabel: 'sound output on', name: 'sound output', id: 'sound-output-on' - }, - { - value: false, label: 'Off', ariaLabel: 'sound output off', name: 'sound output', id: 'sound-output-off' - }, - ], - onSelect: x => setSoundOutput(x) - }, + preferenceOnOff('Plain-text', textOutput, setTextOutput, 'text output'), + preferenceOnOff('Table-text', gridOutput, setGridOutput, 'table output'), + preferenceOnOff('Lint Warning Sound', soundOutput, setSoundOutput, 'sound output') ]; const accessibilitySettings = [ - { - title: 'Line Numbers', - value: lineNumbers, - options: [ - { - value: true, label: 'On', ariaLabel: 'line numbers on', name: 'line numbers', id: 'line-numbers-on' - }, - { - value: false, label: 'Off', ariaLabel: 'line numbers off', name: 'line numbers', id: 'line-numbers-off' - }, - ], - onSelect: x => setLineNumbers(x) - }, - { - title: 'Lint Warning Sound', - value: lintWarning, - options: [ - { - value: true, label: 'On', ariaLabel: 'lint warning on', name: 'lint warning', id: 'lint-warning-on' - }, - { - value: false, label: 'Off', ariaLabel: 'lint warning off', name: 'lint warning', id: 'lint-warning-off' - }, - ], - onSelect: x => setLintWarning(x) - }, + preferenceOnOff('Line Numbers', lineNumbers, setLineNumbers), + preferenceOnOff('Lint Warning Sound', lintWarning, setLintWarning) ]; return (
- - +
- +
General Settings @@ -190,37 +89,4 @@ const MobilePreferences = (props) => { ); }; - -MobilePreferences.propTypes = { - fontSize: PropTypes.number.isRequired, - lineNumbers: PropTypes.bool.isRequired, - autosave: PropTypes.bool.isRequired, - linewrap: PropTypes.bool.isRequired, - textOutput: PropTypes.bool.isRequired, - gridOutput: PropTypes.bool.isRequired, - soundOutput: PropTypes.bool.isRequired, - lintWarning: PropTypes.bool.isRequired, - theme: PropTypes.string.isRequired, - - setLinewrap: PropTypes.func.isRequired, - setLintWarning: PropTypes.func.isRequired, - setTheme: PropTypes.func.isRequired, - setFontSize: PropTypes.func.isRequired, - setLineNumbers: PropTypes.func.isRequired, - setAutosave: PropTypes.func.isRequired, - setTextOutput: PropTypes.func.isRequired, - setGridOutput: PropTypes.func.isRequired, - setSoundOutput: PropTypes.func.isRequired, -}; - -const mapStateToProps = state => ({ - ...state.preferences, -}); - -const mapDispatchToProps = dispatch => bindActionCreators({ - ...PreferencesActions, - ...IdeActions -}, dispatch); - - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobilePreferences)); +export default withRouter(MobilePreferences); diff --git a/client/modules/Mobile/MobileSketchView.jsx b/client/modules/Mobile/MobileSketchView.jsx index 9c80c88d..da4e9829 100644 --- a/client/modules/Mobile/MobileSketchView.jsx +++ b/client/modules/Mobile/MobileSketchView.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; +import { connect, useSelector, useDispatch } from 'react-redux'; import styled from 'styled-components'; import Header from '../../components/mobile/Header'; import IconButton from '../../components/mobile/IconButton'; @@ -16,44 +16,35 @@ import * as FilesActions from '../IDE/actions/files'; import { getHTMLFile } from '../IDE/reducers/files'; - import { ExitIcon } from '../../common/icons'; import { remSize } from '../../theme'; import Footer from '../../components/mobile/Footer'; - const Content = styled.div` z-index: 0; margin-top: ${remSize(68)}; `; const MobileSketchView = (props) => { - // TODO: useSelector requires react-redux ^7.1.0 - // const htmlFile = useSelector(state => getHTMLFile(state.files)); - // const jsFiles = useSelector(state => getJSFiles(state.files)); - // const cssFiles = useSelector(state => getCSSFiles(state.files)); - // const files = useSelector(state => state.files); + const { files, ide, preferences } = useSelector(state => state); + + const htmlFile = useSelector(state => getHTMLFile(state.files)); + const projectName = useSelector(state => state.project.name); + const selectedFile = useSelector(state => state.files.find(file => file.isSelectedFile) || + state.files.find(file => file.name === 'sketch.js') || + state.files.find(file => file.name !== 'root')); const { - htmlFile, files, selectedFile, projectName - } = props; - - // Actions - const { - setTextOutput, setGridOutput, setSoundOutput, - endSketchRefresh, stopSketch, - dispatchConsoleEvent, expandConsole, clearConsole, - setBlobUrl, - } = props; - - const { preferences, ide } = props; + setTextOutput, setGridOutput, setSoundOutput, dispatchConsoleEvent, + endSketchRefresh, stopSketch, setBlobUrl, expandConsole, clearConsole + } = bindActionCreators({ + ...ProjectActions, ...IDEActions, ...PreferencesActions, ...ConsoleActions, ...FilesActions + }, useDispatch()); return (
- } + leftButton={} title={projectName} /> @@ -90,99 +81,4 @@ const MobileSketchView = (props) => { ); }; -MobileSketchView.propTypes = { - params: PropTypes.shape({ - project_id: PropTypes.string, - username: PropTypes.string - }).isRequired, - - htmlFile: PropTypes.shape({ - id: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - }).isRequired, - files: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - })).isRequired, - - selectedFile: PropTypes.shape({ - id: PropTypes.string.isRequired, - content: PropTypes.string.isRequired, - name: PropTypes.string.isRequired - }).isRequired, - - preferences: PropTypes.shape({ - fontSize: PropTypes.number.isRequired, - autosave: PropTypes.bool.isRequired, - linewrap: PropTypes.bool.isRequired, - lineNumbers: PropTypes.bool.isRequired, - lintWarning: PropTypes.bool.isRequired, - textOutput: PropTypes.bool.isRequired, - gridOutput: PropTypes.bool.isRequired, - soundOutput: PropTypes.bool.isRequired, - theme: PropTypes.string.isRequired, - autorefresh: PropTypes.bool.isRequired - }).isRequired, - - ide: PropTypes.shape({ - isPlaying: PropTypes.bool.isRequired, - isAccessibleOutputPlaying: PropTypes.bool.isRequired, - consoleEvent: PropTypes.array, - modalIsVisible: PropTypes.bool.isRequired, - sidebarIsExpanded: PropTypes.bool.isRequired, - consoleIsExpanded: PropTypes.bool.isRequired, - preferencesIsVisible: PropTypes.bool.isRequired, - projectOptionsVisible: PropTypes.bool.isRequired, - newFolderModalVisible: PropTypes.bool.isRequired, - shareModalVisible: PropTypes.bool.isRequired, - shareModalProjectId: PropTypes.string.isRequired, - shareModalProjectName: PropTypes.string.isRequired, - shareModalProjectUsername: PropTypes.string.isRequired, - editorOptionsVisible: PropTypes.bool.isRequired, - keyboardShortcutVisible: PropTypes.bool.isRequired, - unsavedChanges: PropTypes.bool.isRequired, - infiniteLoop: PropTypes.bool.isRequired, - previewIsRefreshing: PropTypes.bool.isRequired, - infiniteLoopMessage: PropTypes.string.isRequired, - projectSavedTime: PropTypes.string, - previousPath: PropTypes.string.isRequired, - justOpenedProject: PropTypes.bool.isRequired, - errorType: PropTypes.string, - runtimeErrorWarningVisible: PropTypes.bool.isRequired, - }).isRequired, - - projectName: PropTypes.string.isRequired, - - setTextOutput: PropTypes.func.isRequired, - setGridOutput: PropTypes.func.isRequired, - setSoundOutput: PropTypes.func.isRequired, - dispatchConsoleEvent: PropTypes.func.isRequired, - endSketchRefresh: PropTypes.func.isRequired, - stopSketch: PropTypes.func.isRequired, - setBlobUrl: PropTypes.func.isRequired, - expandConsole: PropTypes.func.isRequired, - clearConsole: PropTypes.func.isRequired, -}; - -function mapStateToProps(state) { - return { - htmlFile: getHTMLFile(state.files), - projectName: state.project.name, - files: state.files, - ide: state.ide, - preferences: state.preferences, - selectedFile: state.files.find(file => file.isSelectedFile) || - state.files.find(file => file.name === 'sketch.js') || - state.files.find(file => file.name !== 'root'), - }; -} - -function mapDispatchToProps(dispatch) { - return bindActionCreators({ - ...ProjectActions, ...IDEActions, ...PreferencesActions, ...ConsoleActions, ...FilesActions - }, dispatch); -} - -export default connect(mapStateToProps, mapDispatchToProps)(MobileSketchView); +export default MobileSketchView; diff --git a/client/theme.js b/client/theme.js index 5dba9b88..45f2772e 100644 --- a/client/theme.js +++ b/client/theme.js @@ -62,6 +62,7 @@ export default { colors, ...common, primaryTextColor: grays.dark, + backgroundColor: grays.lighter, Button: { default: { @@ -95,12 +96,14 @@ export default { background: grays.light, border: grays.middleLight, }, - } + }, + Separator: grays.middleLight, }, [Theme.dark]: { colors, ...common, primaryTextColor: grays.lightest, + backgroundColor: grays.darker, Button: { default: { @@ -134,12 +137,14 @@ export default { background: grays.dark, border: grays.middleDark, }, - } + }, + Separator: grays.middleDark, }, [Theme.contrast]: { colors, ...common, primaryTextColor: grays.lightest, + backgroundColor: grays.darker, Button: { default: { @@ -173,6 +178,7 @@ export default { background: grays.dark, border: grays.middleDark, }, - } + }, + Separator: grays.middleDark, }, }; diff --git a/client/utils/custom-hooks.js b/client/utils/custom-hooks.js new file mode 100644 index 00000000..7e0a7b8f --- /dev/null +++ b/client/utils/custom-hooks.js @@ -0,0 +1,15 @@ +import React, { useEffect, useRef } from 'react'; + +export const noop = () => {}; + +export const useDidUpdate = (callback, deps) => { + const hasMount = useRef(false); + + useEffect(() => { + if (hasMount.current) { + callback(); + } else { + hasMount.current = true; + } + }, deps); +};