2018-12-22 17:20:04 +03:00
|
|
|
import React, { Component, PureComponent } from 'react';
|
|
|
|
import PropTypes from 'prop-types';
|
2020-09-10 17:10:53 +03:00
|
|
|
import { Action, Dispatch } from 'redux';
|
2018-12-22 17:20:04 +03:00
|
|
|
import * as themes from 'redux-devtools-themes';
|
2020-09-10 17:10:53 +03:00
|
|
|
import { Base16Theme } from 'redux-devtools-themes';
|
|
|
|
import { ActionCreators, LiftedAction, LiftedState } from 'redux-devtools';
|
2018-12-22 17:20:04 +03:00
|
|
|
import { Toolbar, Divider } from 'devui/lib/Toolbar';
|
|
|
|
import Slider from 'devui/lib/Slider';
|
|
|
|
import Button from 'devui/lib/Button';
|
|
|
|
import SegmentedControl from 'devui/lib/SegmentedControl';
|
|
|
|
|
|
|
|
import reducer from './reducers';
|
|
|
|
import SliderButton from './SliderButton';
|
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
2018-12-22 17:20:04 +03:00
|
|
|
const { reset, jumpToState } = ActionCreators;
|
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
interface ExternalProps<S, A extends Action<unknown>> {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
|
|
dispatch: Dispatch<LiftedAction<S, A, {}>>;
|
|
|
|
preserveScrollTop: boolean;
|
|
|
|
select: (state: S) => unknown;
|
|
|
|
theme: keyof typeof themes | Base16Theme;
|
|
|
|
keyboardEnabled: boolean;
|
|
|
|
hideResetButton?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface DefaultProps {
|
|
|
|
select: (state: unknown) => unknown;
|
|
|
|
theme: keyof typeof themes;
|
|
|
|
preserveScrollTop: boolean;
|
|
|
|
keyboardEnabled: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface SliderMonitorProps<S, A extends Action<unknown>> // eslint-disable-next-line @typescript-eslint/ban-types
|
|
|
|
extends LiftedState<S, A, {}> {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
|
|
dispatch: Dispatch<LiftedAction<S, A, {}>>;
|
|
|
|
preserveScrollTop: boolean;
|
|
|
|
select: (state: S) => unknown;
|
|
|
|
theme: keyof typeof themes | Base16Theme;
|
|
|
|
keyboardEnabled: boolean;
|
|
|
|
hideResetButton?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
timer: number | undefined;
|
|
|
|
replaySpeed: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
class SliderMonitor<S, A extends Action<unknown>> extends (PureComponent ||
|
|
|
|
Component)<SliderMonitorProps<S, A>, State> {
|
2018-12-22 17:20:04 +03:00
|
|
|
static update = reducer;
|
|
|
|
|
|
|
|
static propTypes = {
|
|
|
|
dispatch: PropTypes.func,
|
|
|
|
computedStates: PropTypes.array,
|
|
|
|
stagedActionIds: PropTypes.array,
|
|
|
|
actionsById: PropTypes.object,
|
|
|
|
currentStateIndex: PropTypes.number,
|
|
|
|
monitorState: PropTypes.shape({
|
2020-08-08 23:26:39 +03:00
|
|
|
initialScrollTop: PropTypes.number,
|
2018-12-22 17:20:04 +03:00
|
|
|
}),
|
|
|
|
preserveScrollTop: PropTypes.bool,
|
2020-09-10 17:10:53 +03:00
|
|
|
// stagedActions: PropTypes.array,
|
2018-12-22 17:20:04 +03:00
|
|
|
select: PropTypes.func.isRequired,
|
|
|
|
hideResetButton: PropTypes.bool,
|
2019-01-10 21:51:14 +03:00
|
|
|
theme: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
2020-08-08 23:26:39 +03:00
|
|
|
keyboardEnabled: PropTypes.bool,
|
2018-12-22 17:20:04 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
static defaultProps = {
|
2020-09-10 17:10:53 +03:00
|
|
|
select: (state: unknown) => state,
|
2018-12-22 17:20:04 +03:00
|
|
|
theme: 'nicinabox',
|
|
|
|
preserveScrollTop: true,
|
2020-08-08 23:26:39 +03:00
|
|
|
keyboardEnabled: true,
|
2018-12-22 17:20:04 +03:00
|
|
|
};
|
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
state: State = {
|
2018-12-22 17:20:04 +03:00
|
|
|
timer: undefined,
|
2020-08-08 23:26:39 +03:00
|
|
|
replaySpeed: '1x',
|
2018-12-22 17:20:04 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
window.addEventListener('keydown', this.handleKeyPress);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
window.removeEventListener('keydown', this.handleKeyPress);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
setUpTheme = (): Base16Theme => {
|
2018-12-22 17:20:04 +03:00
|
|
|
let theme;
|
|
|
|
if (typeof this.props.theme === 'string') {
|
|
|
|
if (typeof themes[this.props.theme] !== 'undefined') {
|
|
|
|
theme = themes[this.props.theme];
|
|
|
|
} else {
|
|
|
|
theme = themes.nicinabox;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
theme = this.props.theme;
|
|
|
|
}
|
|
|
|
|
|
|
|
return theme;
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
|
|
|
handleReset = () => {
|
|
|
|
this.pauseReplay();
|
|
|
|
this.props.dispatch(reset());
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
handleKeyPress = (event: KeyboardEvent) => {
|
2018-12-22 17:20:04 +03:00
|
|
|
if (!this.props.keyboardEnabled) {
|
|
|
|
return null;
|
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
if (event.ctrlKey && event.keyCode === 74) {
|
|
|
|
// ctrl+j
|
2018-12-22 17:20:04 +03:00
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
if (this.state.timer) {
|
|
|
|
return this.pauseReplay();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.replaySpeed === 'Live') {
|
|
|
|
this.startRealtimeReplay();
|
|
|
|
} else {
|
|
|
|
this.startReplay();
|
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
} else if (event.ctrlKey && event.keyCode === 219) {
|
|
|
|
// ctrl+[
|
2018-12-22 17:20:04 +03:00
|
|
|
event.preventDefault();
|
|
|
|
this.stepLeft();
|
2019-01-10 21:51:14 +03:00
|
|
|
} else if (event.ctrlKey && event.keyCode === 221) {
|
|
|
|
// ctrl+]
|
2018-12-22 17:20:04 +03:00
|
|
|
event.preventDefault();
|
|
|
|
this.stepRight();
|
|
|
|
}
|
|
|
|
return null;
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
handleSliderChange = (value: number) => {
|
2018-12-22 17:20:04 +03:00
|
|
|
if (this.state.timer) {
|
|
|
|
this.pauseReplay();
|
|
|
|
}
|
|
|
|
|
|
|
|
this.props.dispatch(jumpToState(value));
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
|
|
|
startReplay = () => {
|
|
|
|
const { computedStates, currentStateIndex, dispatch } = this.props;
|
|
|
|
|
|
|
|
if (computedStates.length < 2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const speed = this.state.replaySpeed === '1x' ? 500 : 200;
|
|
|
|
|
|
|
|
let stateIndex;
|
|
|
|
if (currentStateIndex === computedStates.length - 1) {
|
|
|
|
dispatch(jumpToState(0));
|
|
|
|
stateIndex = 0;
|
|
|
|
} else if (currentStateIndex === computedStates.length - 2) {
|
|
|
|
dispatch(jumpToState(currentStateIndex + 1));
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
stateIndex = currentStateIndex + 1;
|
|
|
|
dispatch(jumpToState(currentStateIndex + 1));
|
|
|
|
}
|
|
|
|
|
|
|
|
let counter = stateIndex;
|
2020-09-10 17:10:53 +03:00
|
|
|
const timer = window.setInterval(() => {
|
2018-12-22 17:20:04 +03:00
|
|
|
if (counter + 1 <= computedStates.length - 1) {
|
|
|
|
dispatch(jumpToState(counter + 1));
|
|
|
|
}
|
|
|
|
counter += 1;
|
|
|
|
|
|
|
|
if (counter >= computedStates.length - 1) {
|
|
|
|
clearInterval(this.state.timer);
|
|
|
|
this.setState({
|
2020-08-08 23:26:39 +03:00
|
|
|
timer: undefined,
|
2018-12-22 17:20:04 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}, speed);
|
|
|
|
|
|
|
|
this.setState({ timer });
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
|
|
|
startRealtimeReplay = () => {
|
|
|
|
if (this.props.computedStates.length < 2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.props.currentStateIndex === this.props.computedStates.length - 1) {
|
|
|
|
this.props.dispatch(jumpToState(0));
|
|
|
|
|
|
|
|
this.loop(0);
|
|
|
|
} else {
|
|
|
|
this.loop(this.props.currentStateIndex);
|
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
loop = (index: number) => {
|
2018-12-22 17:20:04 +03:00
|
|
|
let currentTimestamp = Date.now();
|
|
|
|
let timestampDiff = this.getLatestTimestampDiff(index);
|
|
|
|
|
|
|
|
const aLoop = () => {
|
|
|
|
const replayDiff = Date.now() - currentTimestamp;
|
|
|
|
if (replayDiff >= timestampDiff) {
|
|
|
|
this.props.dispatch(jumpToState(this.props.currentStateIndex + 1));
|
|
|
|
|
2019-01-10 21:51:14 +03:00
|
|
|
if (
|
|
|
|
this.props.currentStateIndex >=
|
|
|
|
this.props.computedStates.length - 1
|
|
|
|
) {
|
2018-12-22 17:20:04 +03:00
|
|
|
this.pauseReplay();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-01-10 21:51:14 +03:00
|
|
|
timestampDiff = this.getLatestTimestampDiff(
|
|
|
|
this.props.currentStateIndex
|
|
|
|
);
|
2018-12-22 17:20:04 +03:00
|
|
|
currentTimestamp = Date.now();
|
|
|
|
|
|
|
|
this.setState({
|
2020-08-08 23:26:39 +03:00
|
|
|
timer: requestAnimationFrame(aLoop),
|
2018-12-22 17:20:04 +03:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.setState({
|
2020-08-08 23:26:39 +03:00
|
|
|
timer: requestAnimationFrame(aLoop),
|
2018-12-22 17:20:04 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if (index !== this.props.computedStates.length - 1) {
|
|
|
|
this.setState({
|
2020-08-08 23:26:39 +03:00
|
|
|
timer: requestAnimationFrame(aLoop),
|
2018-12-22 17:20:04 +03:00
|
|
|
});
|
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
getLatestTimestampDiff = (index: number) =>
|
2019-01-10 21:51:14 +03:00
|
|
|
this.getTimestampOfStateIndex(index + 1) -
|
|
|
|
this.getTimestampOfStateIndex(index);
|
2018-12-22 17:20:04 +03:00
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
getTimestampOfStateIndex = (stateIndex: number) => {
|
2018-12-22 17:20:04 +03:00
|
|
|
const id = this.props.stagedActionIds[stateIndex];
|
|
|
|
return this.props.actionsById[id].timestamp;
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
pauseReplay = (cb?: () => void) => {
|
2018-12-22 17:20:04 +03:00
|
|
|
if (this.state.timer) {
|
|
|
|
cancelAnimationFrame(this.state.timer);
|
|
|
|
clearInterval(this.state.timer);
|
2019-01-10 21:51:14 +03:00
|
|
|
this.setState(
|
|
|
|
{
|
2020-08-08 23:26:39 +03:00
|
|
|
timer: undefined,
|
2019-01-10 21:51:14 +03:00
|
|
|
},
|
|
|
|
() => {
|
|
|
|
if (typeof cb === 'function') {
|
|
|
|
cb();
|
|
|
|
}
|
2018-12-22 17:20:04 +03:00
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
);
|
2018-12-22 17:20:04 +03:00
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
|
|
|
stepLeft = () => {
|
|
|
|
this.pauseReplay();
|
|
|
|
|
|
|
|
if (this.props.currentStateIndex !== 0) {
|
|
|
|
this.props.dispatch(jumpToState(this.props.currentStateIndex - 1));
|
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
|
|
|
stepRight = () => {
|
|
|
|
this.pauseReplay();
|
|
|
|
|
|
|
|
if (this.props.currentStateIndex !== this.props.computedStates.length - 1) {
|
|
|
|
this.props.dispatch(jumpToState(this.props.currentStateIndex + 1));
|
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
2020-09-10 17:10:53 +03:00
|
|
|
changeReplaySpeed = (replaySpeed: string) => {
|
2018-12-22 17:20:04 +03:00
|
|
|
this.setState({ replaySpeed });
|
|
|
|
|
|
|
|
if (this.state.timer) {
|
|
|
|
this.pauseReplay(() => {
|
|
|
|
if (replaySpeed === 'Live') {
|
|
|
|
this.startRealtimeReplay();
|
|
|
|
} else {
|
|
|
|
this.startReplay();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2019-01-10 21:51:14 +03:00
|
|
|
};
|
2018-12-22 17:20:04 +03:00
|
|
|
|
|
|
|
render() {
|
|
|
|
const {
|
2019-01-10 21:51:14 +03:00
|
|
|
currentStateIndex,
|
|
|
|
computedStates,
|
|
|
|
actionsById,
|
|
|
|
stagedActionIds,
|
2020-08-08 23:26:39 +03:00
|
|
|
hideResetButton,
|
2018-12-22 17:20:04 +03:00
|
|
|
} = this.props;
|
|
|
|
const { replaySpeed } = this.state;
|
|
|
|
const theme = this.setUpTheme();
|
|
|
|
|
|
|
|
const max = computedStates.length - 1;
|
|
|
|
const actionId = stagedActionIds[currentStateIndex];
|
|
|
|
let actionType = actionsById[actionId].action.type;
|
|
|
|
if (actionType === undefined) actionType = '<UNDEFINED>';
|
|
|
|
else if (actionType === null) actionType = '<NULL>';
|
2020-09-10 17:10:53 +03:00
|
|
|
else actionType = (actionType as string).toString() || '<EMPTY>';
|
2018-12-22 17:20:04 +03:00
|
|
|
|
2019-01-10 21:51:14 +03:00
|
|
|
const onPlayClick =
|
|
|
|
replaySpeed === 'Live' ? this.startRealtimeReplay : this.startReplay;
|
|
|
|
const playPause = this.state.timer ? (
|
|
|
|
<SliderButton theme={theme} type="pause" onClick={this.pauseReplay} />
|
|
|
|
) : (
|
|
|
|
<SliderButton
|
|
|
|
theme={theme}
|
|
|
|
type="play"
|
|
|
|
disabled={max <= 0}
|
|
|
|
onClick={onPlayClick}
|
|
|
|
/>
|
|
|
|
);
|
2018-12-22 17:20:04 +03:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Toolbar noBorder compact fullHeight theme={theme}>
|
|
|
|
{playPause}
|
|
|
|
<Slider
|
2020-09-10 17:10:53 +03:00
|
|
|
label={actionType as string}
|
2018-12-22 17:20:04 +03:00
|
|
|
sublabel={`(${actionId})`}
|
|
|
|
min={0}
|
|
|
|
max={max}
|
|
|
|
value={currentStateIndex}
|
|
|
|
onChange={this.handleSliderChange}
|
|
|
|
theme={theme}
|
|
|
|
/>
|
|
|
|
<SliderButton
|
|
|
|
theme={theme}
|
2019-01-10 20:23:33 +03:00
|
|
|
type="stepLeft"
|
2018-12-22 17:20:04 +03:00
|
|
|
disabled={currentStateIndex <= 0}
|
|
|
|
onClick={this.stepLeft}
|
|
|
|
/>
|
|
|
|
<SliderButton
|
|
|
|
theme={theme}
|
2019-01-10 20:23:33 +03:00
|
|
|
type="stepRight"
|
2018-12-22 17:20:04 +03:00
|
|
|
disabled={currentStateIndex === max}
|
|
|
|
onClick={this.stepRight}
|
|
|
|
/>
|
|
|
|
<Divider theme={theme} />
|
|
|
|
<SegmentedControl
|
|
|
|
theme={theme}
|
|
|
|
values={['Live', '1x', '2x']}
|
|
|
|
selected={replaySpeed}
|
|
|
|
onClick={this.changeReplaySpeed}
|
|
|
|
/>
|
|
|
|
{!hideResetButton && [
|
2019-01-10 20:23:33 +03:00
|
|
|
<Divider key="divider" theme={theme} />,
|
2019-01-10 21:51:14 +03:00
|
|
|
<Button key="reset" theme={theme} onClick={this.handleReset}>
|
|
|
|
Reset
|
2020-08-08 23:26:39 +03:00
|
|
|
</Button>,
|
2018-12-22 17:20:04 +03:00
|
|
|
]}
|
|
|
|
</Toolbar>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2020-09-10 17:10:53 +03:00
|
|
|
|
|
|
|
export default (SliderMonitor as unknown) as React.ComponentType<
|
|
|
|
ExternalProps<unknown, Action<unknown>>
|
|
|
|
> & {
|
|
|
|
update(
|
|
|
|
monitorProps: ExternalProps<unknown, Action<unknown>>,
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
|
|
state: {} | undefined,
|
|
|
|
action: Action<unknown>
|
|
|
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
|
|
): {};
|
|
|
|
defaultProps: DefaultProps;
|
|
|
|
};
|