Skip to content

Commit

Permalink
feat: sandbox on store
Browse files Browse the repository at this point in the history
  • Loading branch information
teramotodaiki committed Nov 19, 2019
1 parent 89a54f1 commit cde65f2
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 67 deletions.
23 changes: 1 addition & 22 deletions src/CodeCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,9 @@ export interface CodeCellProps {
value: string;
title?: string;
onUpdate: OnUpdate;
onGame: () => void;
}

export function CodeCell({
id,
value,
title,
onUpdate,
onGame
}: CodeCellProps) {
export function CodeCell({ id, value, title, onUpdate }: CodeCellProps) {
const rootRef = React.useRef<HTMLDivElement>(null);
const editorRef = React.useRef<monaco.editor.IStandaloneCodeEditor>();
const [floating, setFloating] = React.useState(false);
Expand Down Expand Up @@ -70,21 +63,7 @@ export function CodeCell({
setFloating(true);
});

let previousCode = value;
let blurTimerHandle = 0;
editor.onDidBlurEditorText(() => {
window.cancelIdleCallback(blurTimerHandle);
blurTimerHandle = window.requestIdleCallback(
() => {
const e = document.activeElement;
if (!e || e.tagName !== 'IFRAME') return;
const coffee = editor.getValue();
if (previousCode === coffee) return;
onGame();
previousCode = coffee;
},
{ timeout: 2000 }
);
setFloating(false);
});

Expand Down
68 changes: 33 additions & 35 deletions src/CodeView.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import classNames from 'classnames';
import { CoffeeScript } from 'coffeescript';
import * as React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { appendEmptyLine } from './append-empty-line';
import { build } from './build';
import { blockify, cellify, ICodeCell } from './cellify';
import { CodeCell } from './CodeCell';
import './completion';
import { Sandbox } from './sandbox';
import { actions, SS } from './store';
import style from './styles/code-view.scss';
import { TextCell } from './TextCell';
import flex from './styles/flex.scss';
import { TextCell } from './TextCell';

export interface CodeViewProps {
code: string;
handleRun: () => void;
}

export type OnUpdate = (payload: { id: string; value: string }) => void;

let sandbox = new Sandbox();
export function CodeView({ code }: CodeViewProps) {
export function CodeView({ code, handleRun }: CodeViewProps) {
const dispatch = useDispatch();
const cellsRef = React.useRef(cellify(code)); // Notice: mutable

for (let cell of cellsRef.current) {
Expand Down Expand Up @@ -48,22 +50,28 @@ export function CodeView({ code }: CodeViewProps) {
cell.value = value;
cell.nodes = blockify(value);
}
}, []);

const onGame = React.useCallback(() => {
// Build && Run
sandbox.update([
{
name: 'modules/プレイヤー.js',
type: 'application/javascript',
code: build(cellsRef.current)
}
]);
sandbox.run();
dispatch(
actions.writeFiles([
{
name: 'modules/プレイヤー.js',
type: 'application/javascript',
code: build(cells)
}
])
);
}, []);

React.useEffect(() => {
onGame();
dispatch(
actions.writeFiles([
{
name: 'modules/プレイヤー.js',
type: 'application/javascript',
code: build(cellsRef.current)
}
])
);
}, []);

const fileInfo = {
Expand All @@ -79,7 +87,7 @@ export function CodeView({ code }: CodeViewProps) {
<div className={classNames(style.header, flex.horizontal)}>
<img src={fileInfo.iconUrl} alt="" className={style.icon} />
<div className={style.name}>{fileInfo.name.ja}</div>
<PaperPlane />
<PaperPlane handleRun={handleRun} />
</div>
{cellsRef.current.map(cell =>
cell.type === 'code' ? (
Expand All @@ -89,7 +97,6 @@ export function CodeView({ code }: CodeViewProps) {
value={cell.value}
title={cell.meta}
onUpdate={onUpdate}
onGame={onGame}
/>
) : (
<TextCell
Expand All @@ -108,29 +115,20 @@ function formatCodeCell(codeCell: ICodeCell) {
codeCell.value = appendEmptyLine(codeCell.value);
}

function PaperPlane({}) {
const [sent, setSent] = React.useState(false);
const [edited, setEdited] = React.useState(true);
interface PaperPlaneProps {
handleRun: () => void;
}

const onClick = React.useCallback(() => {
setSent(true);
setTimeout(() => {
setSent(false);
}, 2000);
}, []);
function PaperPlane({ handleRun }: PaperPlaneProps) {
const version = useSelector((state: SS) => state.sandbox.version);
const rv = useSelector((state: SS) => state.sandbox.runningVersion);

return (
<div
className={classNames(
style.plane,
sent && style.sent,
!edited && style.hidden
)}
>
<div className={classNames(style.plane, version === rv && style.sent)}>
<img
src={require('./resources/paperPlane.svg')}
alt="✈"
onClick={onClick}
onClick={handleRun}
/>
</div>
);
Expand Down
14 changes: 8 additions & 6 deletions src/root.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,32 @@
import classNames from 'classnames';
import * as React from 'react';
import { useSelector } from 'react-redux';
import { useDispatch, useSelector } from 'react-redux';
import { CodeView } from './CodeView';
import flex from './styles/flex.scss';
import font from './styles/font.scss';
import region from './styles/region.scss';
import { FloorView } from './FloorView';
import { Footer } from './Footer';
import { Header } from './Header';
import { MapView } from './MapView';
import { EditorMode, SS } from './store';
import { actions, EditorMode, SS } from './store';
import { StoreView } from './StoreView';
import flex from './styles/flex.scss';
import font from './styles/font.scss';
import region from './styles/region.scss';
import { Transition } from './Transition';

export interface RootProps {
code: string;
}

export function Root({ code }: RootProps) {
const dispatch = useDispatch();
const iframeRef = React.useRef<HTMLIFrameElement>(null);
const [bringFront, setBringFront] = React.useState(false);
const focusGame = React.useCallback(() => {
if (iframeRef.current) {
iframeRef.current.focus();
}
setBringFront(true);
dispatch(actions.runSanbox());
}, []);
const unfocusGame = React.useCallback(() => {
setBringFront(false);
Expand Down Expand Up @@ -63,7 +65,7 @@ export function Root({ code }: RootProps) {
className={region.codeView}
exiting={region.exiting}
>
<CodeView code={code} />
<CodeView code={code} handleRun={focusGame} />
<div className={region.outputCover} onClick={focusGame}></div>
</Transition>
</div>
Expand Down
17 changes: 13 additions & 4 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { applyMiddleware, combineReducers, createStore, Reducer } from 'redux';
import { combineEpics, createEpicMiddleware } from 'redux-observable';
import {
combineEpics,
createEpicMiddleware,
StateObservable
} from 'redux-observable';
import * as floor from './floor';
import * as mode from './mode';
import * as sandbox from './sandbox';

export * from './enums';

Expand All @@ -12,17 +17,21 @@ export type SS = StoreState; // alias

export type Store = ReturnType<typeof createGamebookStore>;

export type S$ = StateObservable<SS>;

export const rootReducer = combineReducers({
...floor.reducers,
...mode.reducers
...mode.reducers,
...sandbox.reducers
});

export const actions = {
...floor.actions,
...mode.actions
...mode.actions,
...sandbox.actions
};

export const rootEpic = combineEpics(floor.epic);
export const rootEpic = combineEpics(floor.epic, sandbox.epic);

export function createGamebookStore() {
const epicMiddleware = createEpicMiddleware();
Expand Down
61 changes: 61 additions & 0 deletions src/store/sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { combineEpics } from 'redux-observable';
import { NEVER } from 'rxjs';
import {
distinctUntilChanged,
filter,
map,
mergeMap,
tap
} from 'rxjs/operators';
import actionCreatorFactory from 'typescript-fsa';
import { S$ } from '.';
import { IFile, Sandbox } from '../sandbox';
import { reducerWithImmer } from './reducerWithImmer';

export const initialState = {
instance: new Sandbox(),
runningVersion: 0,
version: 0
};

const actionCreator = actionCreatorFactory('gamebook');

export const actions = {
runSanbox: actionCreator<void>('RUN_SANDBOX'),
writeFiles: actionCreator<IFile[]>('WRITE_FILES')
};

const sandbox = reducerWithImmer(initialState)
.case(actions.runSanbox, draft => {
draft.runningVersion = draft.version;
})
.case(actions.writeFiles, draft => {
draft.version += 1;
})
.toReducer();

export default sandbox;

export const reducers = {
sandbox
};

export const epic = combineEpics(
(action$, store$: S$) =>
action$.pipe(
filter(actions.writeFiles.match),
tap(action => {
store$.value.sandbox.instance.update(action.payload);
}),
mergeMap(() => NEVER)
),
(action$, store$: S$) =>
store$.pipe(
map(state => state.sandbox.runningVersion),
distinctUntilChanged(),
tap(() => {
store$.value.sandbox.instance.run();
}),
mergeMap(() => NEVER)
)
);

0 comments on commit cde65f2

Please # to comment.