Merge pull request #1544 from ghalestrilo/feature/mobile-save-sketch
Save Sketches on mobile
This commit is contained in:
commit
6cb9968099
10 changed files with 329 additions and 287 deletions
|
@ -14,6 +14,7 @@ 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 Save from '../images/save.svg';
|
||||
import Terminal from '../images/terminal.svg';
|
||||
|
||||
import Folder from '../images/folder-padded.svg';
|
||||
|
@ -87,6 +88,7 @@ export const PlayIcon = withLabel(Play);
|
|||
export const MoreIcon = withLabel(More);
|
||||
export const TerminalIcon = withLabel(Terminal);
|
||||
export const CodeIcon = withLabel(Code);
|
||||
export const SaveIcon = withLabel(Save);
|
||||
|
||||
export const FolderIcon = withLabel(Folder);
|
||||
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { remSize, prop } from '../../theme';
|
||||
import IconButton from './IconButton';
|
||||
import { TerminalIcon, FolderIcon } from '../../common/icons';
|
||||
import * as IDEActions from '../../modules/IDE/actions/ide';
|
||||
|
||||
const BottomBarContent = styled.div`
|
||||
padding: ${remSize(8)};
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8,1fr);
|
||||
|
||||
svg {
|
||||
max-height: ${remSize(32)};
|
||||
|
||||
}
|
||||
|
||||
path { fill: ${prop('primaryTextColor')} !important }
|
||||
|
@ -25,42 +21,28 @@ const BottomBarContent = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
// Maybe this component shouldn't be connected, and instead just receive the `actions` prop
|
||||
const ActionStrip = ({ toggleExplorer }) => {
|
||||
const { expandConsole, collapseConsole } = bindActionCreators(IDEActions, useDispatch());
|
||||
const { consoleIsExpanded } = useSelector(state => state.ide);
|
||||
|
||||
const actions = [
|
||||
{
|
||||
icon: TerminalIcon, inverted: true, aria: 'Open terminal console', action: consoleIsExpanded ? collapseConsole : expandConsole
|
||||
},
|
||||
{ icon: FolderIcon, aria: 'Open files explorer', action: toggleExplorer }
|
||||
];
|
||||
|
||||
return (
|
||||
<BottomBarContent>
|
||||
{actions.map(({
|
||||
icon, aria, action, inverted
|
||||
}) =>
|
||||
(
|
||||
<IconButton
|
||||
inverted={inverted}
|
||||
className={inverted && 'inverted'}
|
||||
icon={icon}
|
||||
aria-label={aria}
|
||||
key={`bottom-bar-${aria}`}
|
||||
onClick={() => action()}
|
||||
/>))}
|
||||
</BottomBarContent>
|
||||
);
|
||||
};
|
||||
const ActionStrip = ({ actions }) => (
|
||||
<BottomBarContent>
|
||||
{actions.map(({
|
||||
icon, aria, action, inverted
|
||||
}) =>
|
||||
(<IconButton
|
||||
inverted={inverted}
|
||||
className={inverted && 'inverted'}
|
||||
icon={icon}
|
||||
aria-label={aria}
|
||||
key={`bottom-bar-${aria}`}
|
||||
onClick={action}
|
||||
/>))}
|
||||
</BottomBarContent>);
|
||||
|
||||
ActionStrip.propTypes = {
|
||||
toggleExplorer: PropTypes.func
|
||||
};
|
||||
|
||||
ActionStrip.defaultProps = {
|
||||
toggleExplorer: () => {}
|
||||
actions: PropTypes.arrayOf(PropTypes.shape({
|
||||
icon: PropTypes.any,
|
||||
aria: PropTypes.string.isRequired,
|
||||
action: PropTypes.func.isRequired,
|
||||
inverted: PropTypes.bool
|
||||
})).isRequired
|
||||
};
|
||||
|
||||
export default ActionStrip;
|
||||
|
|
|
@ -35,6 +35,12 @@ const HeaderDiv = styled.div`
|
|||
}
|
||||
|
||||
& svg path { fill: ${textColor} !important; }
|
||||
|
||||
.editor__unsaved-changes svg {
|
||||
width: ${remSize(16)};
|
||||
padding: 0;
|
||||
vertical-align: top
|
||||
}
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
|
@ -71,8 +77,9 @@ const Header = ({
|
|||
</HeaderDiv>
|
||||
);
|
||||
|
||||
|
||||
Header.propTypes = {
|
||||
title: PropTypes.string,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
subtitle: PropTypes.string,
|
||||
leftButton: PropTypes.element,
|
||||
children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
|
||||
|
|
3
client/images/save.svg
Normal file
3
client/images/save.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="-4 -4 32 32" width="24">
|
||||
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 249 B |
|
@ -126,7 +126,7 @@ function getSynchedProject(currentState, responseProject) {
|
|||
};
|
||||
}
|
||||
|
||||
export function saveProject(selectedFile = null, autosave = false) {
|
||||
export function saveProject(selectedFile = null, autosave = false, mobile = false) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
if (state.project.isSaving) {
|
||||
|
@ -185,16 +185,15 @@ export function saveProject(selectedFile = null, autosave = false) {
|
|||
.then((response) => {
|
||||
dispatch(endSavingProject());
|
||||
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
|
||||
|
||||
dispatch(setNewProject(synchedProject));
|
||||
dispatch(setUnsavedChanges(false));
|
||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
||||
|
||||
if (hasChanges) {
|
||||
dispatch(setNewProject(synchedProject));
|
||||
dispatch(setUnsavedChanges(false));
|
||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
||||
dispatch(setUnsavedChanges(true));
|
||||
} else {
|
||||
dispatch(setNewProject(synchedProject));
|
||||
dispatch(setUnsavedChanges(false));
|
||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
||||
}
|
||||
|
||||
dispatch(projectSaveSuccess());
|
||||
if (!autosave) {
|
||||
if (state.preferences.autosave) {
|
||||
|
@ -222,9 +221,9 @@ export function saveProject(selectedFile = null, autosave = false) {
|
|||
};
|
||||
}
|
||||
|
||||
export function autosaveProject() {
|
||||
export function autosaveProject(mobile = false) {
|
||||
return (dispatch, getState) => {
|
||||
saveProject(null, true)(dispatch, getState);
|
||||
saveProject(null, true, mobile)(dispatch, getState);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ import { CSSLint } from 'csslint';
|
|||
import { HTMLHint } from 'htmlhint';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import '../../../utils/htmlmixed';
|
||||
import '../../../utils/p5-javascript';
|
||||
import '../../../utils/webGL-clike';
|
||||
|
@ -40,6 +42,16 @@ import beepUrl from '../../../sounds/audioAlert.mp3';
|
|||
import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg';
|
||||
import RightArrowIcon from '../../../images/right-arrow.svg';
|
||||
import LeftArrowIcon from '../../../images/left-arrow.svg';
|
||||
import { getHTMLFile } from '../reducers/files';
|
||||
|
||||
import * as FileActions from '../actions/files';
|
||||
import * as IDEActions from '../actions/ide';
|
||||
import * as ProjectActions from '../actions/project';
|
||||
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
|
||||
import * as PreferencesActions from '../actions/preferences';
|
||||
import * as UserActions from '../../User/actions';
|
||||
import * as ToastActions from '../actions/toast';
|
||||
import * as ConsoleActions from '../actions/console';
|
||||
|
||||
search(CodeMirror);
|
||||
|
||||
|
@ -413,4 +425,47 @@ Editor.defaultProps = {
|
|||
consoleEvents: [],
|
||||
};
|
||||
|
||||
export default withTranslation()(Editor);
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
files: state.files,
|
||||
file:
|
||||
state.files.find(file => file.isSelectedFile) ||
|
||||
state.files.find(file => file.name === 'sketch.js') ||
|
||||
state.files.find(file => file.name !== 'root'),
|
||||
htmlFile: getHTMLFile(state.files),
|
||||
ide: state.ide,
|
||||
preferences: state.preferences,
|
||||
editorAccessibility: state.editorAccessibility,
|
||||
user: state.user,
|
||||
project: state.project,
|
||||
toast: state.toast,
|
||||
console: state.console,
|
||||
|
||||
...state.preferences,
|
||||
...state.ide,
|
||||
...state.project,
|
||||
...state.editorAccessibility,
|
||||
isExpanded: state.ide.consoleIsExpanded,
|
||||
projectSavedTime: state.project.updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
Object.assign(
|
||||
{},
|
||||
EditorAccessibilityActions,
|
||||
FileActions,
|
||||
ProjectActions,
|
||||
IDEActions,
|
||||
PreferencesActions,
|
||||
UserActions,
|
||||
ToastActions,
|
||||
ConsoleActions
|
||||
),
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
|
||||
export default withTranslation()(connect(mapStateToProps, mapDispatchToProps)(Editor));
|
||||
|
|
|
@ -343,46 +343,7 @@ class IDEView extends React.Component {
|
|||
allowResize={this.props.ide.consoleIsExpanded}
|
||||
className="editor-preview-subpanel"
|
||||
>
|
||||
<Editor
|
||||
lintWarning={this.props.preferences.lintWarning}
|
||||
linewrap={this.props.preferences.linewrap}
|
||||
lintMessages={this.props.editorAccessibility.lintMessages}
|
||||
updateLintMessage={this.props.updateLintMessage}
|
||||
clearLintMessage={this.props.clearLintMessage}
|
||||
file={this.props.selectedFile}
|
||||
updateFileContent={this.props.updateFileContent}
|
||||
fontSize={this.props.preferences.fontSize}
|
||||
lineNumbers={this.props.preferences.lineNumbers}
|
||||
files={this.props.files}
|
||||
editorOptionsVisible={this.props.ide.editorOptionsVisible}
|
||||
showEditorOptions={this.props.showEditorOptions}
|
||||
closeEditorOptions={this.props.closeEditorOptions}
|
||||
showKeyboardShortcutModal={
|
||||
this.props.showKeyboardShortcutModal
|
||||
}
|
||||
setUnsavedChanges={this.props.setUnsavedChanges}
|
||||
isPlaying={this.props.ide.isPlaying}
|
||||
theme={this.props.preferences.theme}
|
||||
startRefreshSketch={this.props.startRefreshSketch}
|
||||
stopSketch={this.props.stopSketch}
|
||||
autorefresh={this.props.preferences.autorefresh}
|
||||
unsavedChanges={this.props.ide.unsavedChanges}
|
||||
projectSavedTime={this.props.project.updatedAt}
|
||||
isExpanded={this.props.ide.sidebarIsExpanded}
|
||||
expandSidebar={this.props.expandSidebar}
|
||||
collapseSidebar={this.props.collapseSidebar}
|
||||
isUserOwner={isUserOwner(this.props)}
|
||||
clearConsole={this.props.clearConsole}
|
||||
consoleEvents={this.props.console}
|
||||
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
|
||||
hideRuntimeErrorWarning={this.props.hideRuntimeErrorWarning}
|
||||
runtimeErrorWarningVisible={
|
||||
this.props.ide.runtimeErrorWarningVisible
|
||||
}
|
||||
provideController={(ctl) => {
|
||||
this.cmController = ctl;
|
||||
}}
|
||||
/>
|
||||
<Editor provideController={(ctl) => { this.cmController = ctl; }} />
|
||||
<Console />
|
||||
</SplitPane>
|
||||
<section className="preview-frame-holder">
|
||||
|
@ -533,31 +494,25 @@ IDEView.propTypes = {
|
|||
}).isRequired,
|
||||
saveProject: PropTypes.func.isRequired,
|
||||
ide: PropTypes.shape({
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||
consoleEvent: PropTypes.array, // eslint-disable-line
|
||||
modalIsVisible: PropTypes.bool.isRequired,
|
||||
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||
preferencesIsVisible: PropTypes.bool.isRequired,
|
||||
projectOptionsVisible: PropTypes.bool.isRequired,
|
||||
newFolderModalVisible: PropTypes.bool.isRequired,
|
||||
errorType: PropTypes.string,
|
||||
keyboardShortcutVisible: 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,
|
||||
previewIsRefreshing: PropTypes.bool.isRequired,
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||
projectOptionsVisible: PropTypes.bool.isRequired,
|
||||
preferencesIsVisible: PropTypes.bool.isRequired,
|
||||
modalIsVisible: PropTypes.bool.isRequired,
|
||||
uploadFileModalVisible: PropTypes.bool.isRequired,
|
||||
newFolderModalVisible: PropTypes.bool.isRequired,
|
||||
justOpenedProject: PropTypes.bool.isRequired,
|
||||
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||
unsavedChanges: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
stopSketch: PropTypes.func.isRequired,
|
||||
project: PropTypes.shape({
|
||||
|
@ -572,11 +527,9 @@ IDEView.propTypes = {
|
|||
editorAccessibility: PropTypes.shape({
|
||||
lintMessages: PropTypes.array.isRequired, // eslint-disable-line
|
||||
}).isRequired,
|
||||
updateLintMessage: PropTypes.func.isRequired,
|
||||
clearLintMessage: PropTypes.func.isRequired,
|
||||
preferences: PropTypes.shape({
|
||||
fontSize: PropTypes.number.isRequired,
|
||||
autosave: PropTypes.bool.isRequired,
|
||||
fontSize: PropTypes.number.isRequired,
|
||||
linewrap: PropTypes.bool.isRequired,
|
||||
lineNumbers: PropTypes.bool.isRequired,
|
||||
lintWarning: PropTypes.bool.isRequired,
|
||||
|
@ -602,7 +555,6 @@ IDEView.propTypes = {
|
|||
name: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
updateFileContent: PropTypes.func.isRequired,
|
||||
selectedFile: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
|
@ -630,9 +582,6 @@ IDEView.propTypes = {
|
|||
closeNewFileModal: PropTypes.func.isRequired,
|
||||
createFolder: PropTypes.func.isRequired,
|
||||
closeShareModal: PropTypes.func.isRequired,
|
||||
showEditorOptions: PropTypes.func.isRequired,
|
||||
closeEditorOptions: PropTypes.func.isRequired,
|
||||
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||
closeKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||
toast: PropTypes.shape({
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
|
@ -642,22 +591,14 @@ IDEView.propTypes = {
|
|||
setRouteLeaveHook: PropTypes.func,
|
||||
}).isRequired,
|
||||
route: PropTypes.oneOfType([PropTypes.object, PropTypes.element]).isRequired,
|
||||
setUnsavedChanges: PropTypes.func.isRequired,
|
||||
setTheme: PropTypes.func.isRequired,
|
||||
endSketchRefresh: PropTypes.func.isRequired,
|
||||
startRefreshSketch: PropTypes.func.isRequired,
|
||||
setBlobUrl: PropTypes.func.isRequired,
|
||||
setPreviousPath: PropTypes.func.isRequired,
|
||||
console: PropTypes.arrayOf(PropTypes.shape({
|
||||
method: PropTypes.string.isRequired,
|
||||
args: PropTypes.arrayOf(PropTypes.string),
|
||||
})).isRequired,
|
||||
clearConsole: PropTypes.func.isRequired,
|
||||
showErrorModal: PropTypes.func.isRequired,
|
||||
hideErrorModal: PropTypes.func.isRequired,
|
||||
clearPersistedState: PropTypes.func.isRequired,
|
||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
startSketch: PropTypes.func.isRequired,
|
||||
openUploadFileModal: PropTypes.func.isRequired,
|
||||
closeUploadFileModal: PropTypes.func.isRequired,
|
||||
|
|
|
@ -8,19 +8,17 @@ import styled from 'styled-components';
|
|||
// Imports to be Refactored
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import * as FileActions from '../actions/files';
|
||||
import * as IDEActions from '../actions/ide';
|
||||
import * as ProjectActions from '../actions/project';
|
||||
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
|
||||
import * as PreferencesActions from '../actions/preferences';
|
||||
import * as UserActions from '../../User/actions';
|
||||
import * as ToastActions from '../actions/toast';
|
||||
import * as ConsoleActions from '../actions/console';
|
||||
import { getHTMLFile } from '../reducers/files';
|
||||
import * as PreferencesActions from '../actions/preferences';
|
||||
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
|
||||
|
||||
// Local Imports
|
||||
import Editor from '../components/Editor';
|
||||
import { PlayIcon, MoreIcon } from '../../../common/icons';
|
||||
|
||||
import { PlayIcon, MoreIcon, FolderIcon, PreferencesIcon, TerminalIcon, SaveIcon } from '../../../common/icons';
|
||||
import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg';
|
||||
|
||||
import IconButton from '../../../components/mobile/IconButton';
|
||||
import Header from '../../../components/mobile/Header';
|
||||
|
@ -33,28 +31,25 @@ import { remSize } from '../../../theme';
|
|||
|
||||
import ActionStrip from '../../../components/mobile/ActionStrip';
|
||||
import useAsModal from '../../../components/useAsModal';
|
||||
import { PreferencesIcon } from '../../../common/icons';
|
||||
import Dropdown from '../../../components/Dropdown';
|
||||
|
||||
|
||||
import { useEffectWithComparison, useEventListener } from '../../../utils/custom-hooks';
|
||||
|
||||
import * as device from '../../../utils/device';
|
||||
|
||||
const withChangeDot = (title, unsavedChanges = false) => (
|
||||
<span>
|
||||
{title}
|
||||
<span className="editor__unsaved-changes">
|
||||
{unsavedChanges &&
|
||||
<UnsavedChangesDotIcon role="img" aria-label="Sketch has unsaved changes" focusable="false" />}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
const getRootFile = files => files && files.filter(file => file.name === 'root')[0];
|
||||
const getRootFileID = files => (root => root && root.id)(getRootFile(files));
|
||||
|
||||
const isUserOwner = ({ project, user }) =>
|
||||
project.owner && project.owner.id === user.id;
|
||||
|
||||
|
||||
// const userCanEditProject = (props) => {
|
||||
// let canEdit;
|
||||
// if (!props.owner) {
|
||||
// canEdit = true;
|
||||
// } else if (props.user.authenticated && props.owner.id === props.user.id) {
|
||||
// canEdit = true;
|
||||
// } else {
|
||||
// canEdit = false;
|
||||
// }
|
||||
// return canEdit;
|
||||
// };
|
||||
|
||||
const Expander = styled.div`
|
||||
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
|
||||
`;
|
||||
|
@ -80,24 +75,135 @@ const getNavOptions = (username = undefined, logoutUser = () => {}, toggleForceD
|
|||
]
|
||||
);
|
||||
|
||||
const MobileIDEView = (props) => {
|
||||
|
||||
const isUserOwner = ({ project, user }) =>
|
||||
project && project.owner && project.owner.id === user.id;
|
||||
|
||||
const canSaveProject = (project, user) =>
|
||||
isUserOwner({ project, user }) || (user.authenticated && !project.owner);
|
||||
|
||||
// TODO: This could go into <Editor />
|
||||
const handleGlobalKeydown = (props, cmController) => (e) => {
|
||||
const {
|
||||
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
|
||||
selectedFile, updateFileContent, files, user, params,
|
||||
closeEditorOptions, showEditorOptions, logoutUser,
|
||||
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
||||
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState, setUnsavedChanges,
|
||||
toggleForceDesktop
|
||||
user, project, ide,
|
||||
setAllAccessibleOutput,
|
||||
saveProject, cloneProject, showErrorModal, startSketch, stopSketch,
|
||||
expandSidebar, collapseSidebar, expandConsole, collapseConsole,
|
||||
closeNewFolderModal, closeUploadFileModal, closeNewFileModal
|
||||
} = props;
|
||||
|
||||
const [tmController, setTmController] = useState(null); // eslint-disable-line
|
||||
|
||||
const isMac = device.isMac();
|
||||
|
||||
// const ctrlDown = (e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac);
|
||||
const ctrlDown = (isMac ? e.metaKey : e.ctrlKey);
|
||||
|
||||
if (ctrlDown) {
|
||||
if (e.shiftKey) {
|
||||
if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
stopSketch();
|
||||
} else if (e.keyCode === 13) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
startSketch();
|
||||
// 50 === 2
|
||||
} else if (e.keyCode === 50
|
||||
) {
|
||||
e.preventDefault();
|
||||
setAllAccessibleOutput(false);
|
||||
// 49 === 1
|
||||
} else if (e.keyCode === 49) {
|
||||
e.preventDefault();
|
||||
setAllAccessibleOutput(true);
|
||||
}
|
||||
} else if (e.keyCode === 83) {
|
||||
// 83 === s
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (canSaveProject(project, user)) saveProject(cmController.getContent(), false, true);
|
||||
else if (user.authenticated) cloneProject();
|
||||
else showErrorModal('forceAuthentication');
|
||||
|
||||
// 13 === enter
|
||||
} else if (e.keyCode === 66) {
|
||||
e.preventDefault();
|
||||
if (!ide.sidebarIsExpanded) expandSidebar();
|
||||
else collapseSidebar();
|
||||
}
|
||||
} else if (e.keyCode === 192 && e.ctrlKey) {
|
||||
e.preventDefault();
|
||||
if (ide.consoleIsExpanded) collapseConsole();
|
||||
else expandConsole();
|
||||
} else if (e.keyCode === 27) {
|
||||
if (ide.newFolderModalVisible) closeNewFolderModal();
|
||||
else if (ide.uploadFileModalVisible) closeUploadFileModal();
|
||||
else if (ide.modalIsVisible) closeNewFileModal();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const autosave = (autosaveInterval, setAutosaveInterval) => (props, prevProps) => {
|
||||
const {
|
||||
autosaveProject, preferences, ide, selectedFile: file, project
|
||||
} = props;
|
||||
|
||||
const { selectedFile: oldFile } = prevProps;
|
||||
|
||||
const doAutosave = () => autosaveProject(true);
|
||||
|
||||
if (isUserOwner(props) && project.id) {
|
||||
if (preferences.autosave && ide.unsavedChanges && !ide.justOpenedProject) {
|
||||
if (file.name === oldFile.name && file.content !== oldFile.content) {
|
||||
if (autosaveInterval) {
|
||||
clearTimeout(autosaveInterval);
|
||||
}
|
||||
console.log('will save project in 20 seconds');
|
||||
setAutosaveInterval(setTimeout(doAutosave, 20000));
|
||||
}
|
||||
} else if (autosaveInterval && !preferences.autosave) {
|
||||
clearTimeout(autosaveInterval);
|
||||
setAutosaveInterval(null);
|
||||
}
|
||||
} else if (autosaveInterval) {
|
||||
clearTimeout(autosaveInterval);
|
||||
setAutosaveInterval(null);
|
||||
}
|
||||
};
|
||||
|
||||
// ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole,
|
||||
// stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files
|
||||
|
||||
const MobileIDEView = (props) => {
|
||||
// const {
|
||||
// preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
|
||||
// selectedFile, updateFileContent, files, user, params,
|
||||
// closeEditorOptions, showEditorOptions, logoutUser,
|
||||
// startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
||||
// showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch, getProject, clearPersistedState, setUnsavedChanges,
|
||||
// toggleForceDesktop
|
||||
// } = props;
|
||||
|
||||
const {
|
||||
ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole,
|
||||
stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files,
|
||||
toggleForceDesktop, logoutUser
|
||||
} = props;
|
||||
|
||||
|
||||
const [cmController, setCmController] = useState(null); // eslint-disable-line
|
||||
|
||||
const { username } = user;
|
||||
|
||||
const { consoleIsExpanded } = ide;
|
||||
const { name: filename } = selectedFile;
|
||||
|
||||
// Force state reset
|
||||
useEffect(clearPersistedState, []);
|
||||
useEffect(stopSketch, []);
|
||||
useEffect(() => {
|
||||
stopSketch();
|
||||
collapseConsole();
|
||||
}, []);
|
||||
|
||||
// Load Project
|
||||
const [currentProjectID, setCurrentProjectID] = useState(null);
|
||||
|
@ -124,12 +230,28 @@ const MobileIDEView = (props) => {
|
|||
onPressClose={toggle}
|
||||
/>), true);
|
||||
|
||||
// TODO: This behavior could move to <Editor />
|
||||
const [autosaveInterval, setAutosaveInterval] = useState(null);
|
||||
useEffectWithComparison(autosave(autosaveInterval, setAutosaveInterval), {
|
||||
autosaveProject, preferences, ide, selectedFile, project, user
|
||||
});
|
||||
|
||||
useEventListener('keydown', handleGlobalKeydown(props, cmController), false, [props]);
|
||||
|
||||
const projectActions =
|
||||
[{
|
||||
icon: TerminalIcon, aria: 'Toggle console open/closed', action: consoleIsExpanded ? collapseConsole : expandConsole, inverted: true
|
||||
},
|
||||
{ icon: SaveIcon, aria: 'Save project', action: () => saveProject(cmController.getContent(), false, true) },
|
||||
{ icon: FolderIcon, aria: 'Open files explorer', action: toggleExplorer }
|
||||
];
|
||||
|
||||
return (
|
||||
<Screen fullscreen>
|
||||
<Explorer />
|
||||
<Header
|
||||
title={project.name}
|
||||
subtitle={selectedFile.name}
|
||||
title={withChangeDot(project.name, unsavedChanges)}
|
||||
subtitle={filename}
|
||||
>
|
||||
<NavItem>
|
||||
|
||||
|
@ -146,97 +268,43 @@ const MobileIDEView = (props) => {
|
|||
</Header>
|
||||
|
||||
<IDEWrapper>
|
||||
<Editor
|
||||
lintWarning={preferences.lintWarning}
|
||||
linewrap={preferences.linewrap}
|
||||
lintMessages={editorAccessibility.lintMessages}
|
||||
updateLintMessage={updateLintMessage}
|
||||
clearLintMessage={clearLintMessage}
|
||||
file={selectedFile}
|
||||
updateFileContent={updateFileContent}
|
||||
fontSize={preferences.fontSize}
|
||||
lineNumbers={preferences.lineNumbers}
|
||||
files={files}
|
||||
editorOptionsVisible={ide.editorOptionsVisible}
|
||||
showEditorOptions={showEditorOptions}
|
||||
closeEditorOptions={closeEditorOptions}
|
||||
showKeyboard={ide.isPlaying}
|
||||
theme={preferences.theme}
|
||||
startRefreshSketch={startRefreshSketch}
|
||||
stopSketch={stopSketch}
|
||||
autorefresh={preferences.autorefresh}
|
||||
unsavedChanges={ide.unsavedChanges}
|
||||
projectSavedTime={project.updatedAt}
|
||||
isExpanded={ide.sidebarIsExpanded}
|
||||
expandSidebar={expandSidebar}
|
||||
collapseSidebar={collapseSidebar}
|
||||
isUserOwner={isUserOwner(props)}
|
||||
clearConsole={clearConsole}
|
||||
consoleEvents={console}
|
||||
showRuntimeErrorWarning={showRuntimeErrorWarning}
|
||||
hideRuntimeErrorWarning={hideRuntimeErrorWarning}
|
||||
runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible}
|
||||
provideController={setTmController}
|
||||
setUnsavedChanges={setUnsavedChanges}
|
||||
/>
|
||||
<Editor provideController={setCmController} />
|
||||
</IDEWrapper>
|
||||
|
||||
<Footer>
|
||||
{ide.consoleIsExpanded && (
|
||||
{consoleIsExpanded && (
|
||||
<Expander expanded>
|
||||
<Console />
|
||||
</Expander>
|
||||
)}
|
||||
<ActionStrip toggleExplorer={toggleExplorer} />
|
||||
<ActionStrip actions={projectActions} />
|
||||
</Footer>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
const handleGlobalKeydownProps = {
|
||||
expandConsole: PropTypes.func.isRequired,
|
||||
collapseConsole: PropTypes.func.isRequired,
|
||||
expandSidebar: PropTypes.func.isRequired,
|
||||
collapseSidebar: PropTypes.func.isRequired,
|
||||
|
||||
setAllAccessibleOutput: PropTypes.func.isRequired,
|
||||
saveProject: PropTypes.func.isRequired,
|
||||
cloneProject: PropTypes.func.isRequired,
|
||||
showErrorModal: PropTypes.func.isRequired,
|
||||
|
||||
closeNewFolderModal: PropTypes.func.isRequired,
|
||||
closeUploadFileModal: PropTypes.func.isRequired,
|
||||
closeNewFileModal: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
MobileIDEView.propTypes = {
|
||||
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,
|
||||
uploadFileModalVisible: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
editorAccessibility: PropTypes.shape({
|
||||
lintMessages: PropTypes.array.isRequired,
|
||||
preferences: PropTypes.shape({
|
||||
}).isRequired,
|
||||
|
||||
project: PropTypes.shape({
|
||||
|
@ -246,14 +314,8 @@ MobileIDEView.propTypes = {
|
|||
username: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
}),
|
||||
updatedAt: PropTypes.string,
|
||||
}).isRequired,
|
||||
|
||||
startSketch: PropTypes.func.isRequired,
|
||||
|
||||
updateLintMessage: PropTypes.func.isRequired,
|
||||
|
||||
clearLintMessage: PropTypes.func.isRequired,
|
||||
|
||||
selectedFile: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
|
@ -261,36 +323,12 @@ MobileIDEView.propTypes = {
|
|||
name: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
updateFileContent: PropTypes.func.isRequired,
|
||||
|
||||
files: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
})).isRequired,
|
||||
|
||||
closeEditorOptions: PropTypes.func.isRequired,
|
||||
|
||||
showEditorOptions: PropTypes.func.isRequired,
|
||||
|
||||
startRefreshSketch: PropTypes.func.isRequired,
|
||||
|
||||
stopSketch: PropTypes.func.isRequired,
|
||||
|
||||
expandSidebar: PropTypes.func.isRequired,
|
||||
|
||||
collapseSidebar: PropTypes.func.isRequired,
|
||||
|
||||
clearConsole: PropTypes.func.isRequired,
|
||||
|
||||
console: PropTypes.arrayOf(PropTypes.shape({
|
||||
method: PropTypes.string.isRequired,
|
||||
args: PropTypes.arrayOf(PropTypes.string),
|
||||
})).isRequired,
|
||||
|
||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
|
||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
toggleForceDesktop: PropTypes.func.isRequired,
|
||||
|
||||
user: PropTypes.shape({
|
||||
|
@ -301,26 +339,32 @@ MobileIDEView.propTypes = {
|
|||
|
||||
logoutUser: PropTypes.func.isRequired,
|
||||
|
||||
setUnsavedChanges: PropTypes.func.isRequired,
|
||||
getProject: PropTypes.func.isRequired,
|
||||
clearPersistedState: PropTypes.func.isRequired,
|
||||
params: PropTypes.shape({
|
||||
project_id: PropTypes.string,
|
||||
username: PropTypes.string
|
||||
}).isRequired,
|
||||
|
||||
startSketch: PropTypes.func.isRequired,
|
||||
|
||||
unsavedChanges: PropTypes.bool.isRequired,
|
||||
autosaveProject: PropTypes.func.isRequired,
|
||||
|
||||
|
||||
...handleGlobalKeydownProps
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
files: state.files,
|
||||
selectedFile:
|
||||
state.files.find(file => file.isSelectedFile) ||
|
||||
state.files.find(file => file.name === 'sketch.js') ||
|
||||
state.files.find(file => file.name !== 'root'),
|
||||
htmlFile: getHTMLFile(state.files),
|
||||
ide: state.ide,
|
||||
files: state.files,
|
||||
unsavedChanges: state.ide.unsavedChanges,
|
||||
preferences: state.preferences,
|
||||
editorAccessibility: state.editorAccessibility,
|
||||
user: state.user,
|
||||
project: state.project,
|
||||
toast: state.toast,
|
||||
|
@ -328,21 +372,12 @@ function mapStateToProps(state) {
|
|||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
Object.assign(
|
||||
{},
|
||||
EditorAccessibilityActions,
|
||||
FileActions,
|
||||
ProjectActions,
|
||||
IDEActions,
|
||||
PreferencesActions,
|
||||
UserActions,
|
||||
ToastActions,
|
||||
ConsoleActions
|
||||
),
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
const mapDispatchToProps = dispatch => bindActionCreators({
|
||||
...ProjectActions,
|
||||
...IDEActions,
|
||||
...ConsoleActions,
|
||||
...PreferencesActions,
|
||||
...EditorAccessibilityActions
|
||||
}, dispatch);
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));
|
||||
|
|
|
@ -40,3 +40,20 @@ export const useModalBehavior = (hideOverlay) => {
|
|||
|
||||
return [visible, trigger, setRef];
|
||||
};
|
||||
|
||||
// Usage: useEffectWithComparison((props, prevProps) => { ... }, { prop1, prop2 })
|
||||
// This hook basically applies useEffect but keeping track of the last value of relevant props
|
||||
// So you can passa a 2-param function to capture new and old values and do whatever with them.
|
||||
export const useEffectWithComparison = (fn, props) => {
|
||||
const [prevProps, update] = useState({});
|
||||
|
||||
return useEffect(() => {
|
||||
fn(props, prevProps);
|
||||
update(props);
|
||||
}, Object.values(props));
|
||||
};
|
||||
|
||||
export const useEventListener = (event, callback, useCapture = false, list = []) => useEffect(() => {
|
||||
document.addEventListener(event, callback, useCapture);
|
||||
return () => document.removeEventListener(event, callback, useCapture);
|
||||
}, list);
|
||||
|
|
1
client/utils/device.js
Normal file
1
client/utils/device.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const isMac = () => navigator.userAgent.toLowerCase().indexOf('mac') !== -1; // eslint-disable-line
|
Loading…
Reference in a new issue