Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UI] Clickable/Tappable Button and Sticks #26

Merged
merged 17 commits into from
Aug 11, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ Example [video](https://youtu.be/viv-B_A-A2o) of recording and running a **macro
For more videos, check out this [playlist](https://www.youtube.com/playlist?list=PLfC95bU1D4gpJEM3SYfzaI2e5vD0q7v0z).

# Status
One keyboard layout or gaming controller layout is supported to map input to the control sticks and the buttons on a Nintendo Switch controller.
One keyboard layout, gaming controller layout, using you mouse, or touchscreen is supported to map input to the control sticks and the buttons on a Nintendo Switch controller.
I've mainly tested this with Animal Crossing and Mixer - FTL low latency streaming.
This is very much a work in progress right now but you can indeed play your Switch remotely using a keyboard/controller.
This is very much a work in progress right now but you can indeed play your Switch remotely using a keyboard/controller/mouse/touchscreen.

# Macros
You can record and run **macros**!
Expand All @@ -47,10 +47,9 @@ The host (person setting this up) needs:
The client (your friend) needs:
* A web browser to open the client and send commands
* You can use the already [hosted client][client] but you may have to enable mixed content for that site in your browser's settings if the server your friend is hosting does not have SSL (a link that starts with https).
* A keyboard or gaming controller
* A keyboard or **gaming controller** is recommended or just use your mouse/touchscreen for simple stuff

# Plans
* Make controller buttons clickable/tappable.
* Support custom key bindings.
* Improve macro support: exporting/importing.
* Default layout options for common controllers.
Expand Down
7 changes: 4 additions & 3 deletions website-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
},
"scripts": {
"start": "react-scripts start",
"start-prod": "serve --single build",
"start-prod": "serve --single build",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "react-scripts test --env='./src/test/custom-test-env.js'",
"eject": "react-scripts eject",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"lint-fix": "eslint . --fix --ext .js,.jsx,.ts,.tsx"
Expand All @@ -51,6 +51,7 @@
"@types/react-router-dom": "^5.1.5",
"@typescript-eslint/eslint-plugin": "^3.0.0",
"@typescript-eslint/parser": "^3.0.0",
"eslint-plugin-react": "^7.20.0"
"eslint-plugin-react": "^7.20.0",
"fake-indexeddb": "^3.1.2"
}
}
17 changes: 11 additions & 6 deletions website-client/src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import React from 'react'
import ControllerButton from '../Controller/ControllerButton'
import classes from './Button.module.css'

const Button: React.FunctionComponent<any> = (props: any) => {
let classList = classes.Button
if (props.button.pressed) classList += " " + classes.Pressed
return (
<div className={classList}>
<h1>{props.button.symbol}</h1>
</div>
)
if (props.button.pressed) {
classList += " " + classes.Pressed
}
return <ControllerButton
name={props.button.name}
sendCommand={props.button.sendCommand}
controllerState={props.button.controllerState}
className={classList}>
<h1>{props.button.symbol}</h1>
</ControllerButton>
}

export default Button
18 changes: 0 additions & 18 deletions website-client/src/components/Controller/Controller.module.css
Original file line number Diff line number Diff line change
@@ -1,22 +1,4 @@
/* TODO Use lower-case for the names. */

.Left {
display: flex;
align-items: center;
}

.Middle {
/* Some of the settings were used before the video was in between the controls. */
/* display: flex; */
/* flex-direction: column; */
/* flex-grow: 1; */
min-width: 23rem;
}

.Right {

}

.LargeButton {
background-color: #111;
margin: 1rem;
Expand Down
147 changes: 107 additions & 40 deletions website-client/src/components/Controller/Controller.tsx
Original file line number Diff line number Diff line change
@@ -1,133 +1,200 @@
import { createStyles, withStyles } from '@material-ui/core'
import Grid from '@material-ui/core/Grid'
import React from 'react'
import { SendCommand } from '../../key-binding/KeyBinding'
import Diamond from '../Diamond/Diamond'
import VideoStream from '../VideoStream'
import cssClasses from './Controller.module.css'
import ControllerButton from './ControllerButton'
import { ControllerState } from './ControllerState'
import Joystick from './Joystick/Joystick'

const styles = () => createStyles({
controller: {
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
},

middle: {
// Some of the settings were used before the video was in between the controls.
// display: 'flex',
// flexDirection: 'column',
// flexGrow: 1,
// minWidth: '23rem',
},
})

class Controller extends React.Component<any> {
render(): React.ReactNode {
const { classes } = this.props
const controllerState: ControllerState | undefined = this.props.controllerState

class Controller extends React.Component<{
controllerState: ControllerState,
sendCommand: SendCommand,
videoStreamProps: any,
classes: any,
}> {
render(): React.ReactNode {
const { classes, sendCommand } = this.props
const controllerState: ControllerState = this.props.controllerState
return (
<div className={classes.controller}>
<div>
<Grid container className={classes.controller}>
<Grid item xs={6} sm={4} md={3} lg={2}>
<div>
<div
<ControllerButton name='l' sendCommand={sendCommand}
controllerState={controllerState}
className={
cssClasses.LargeButton +
(controllerState?.l?.isPressed ? " " + cssClasses.Pressed : "")
}
>
<p>L</p>
</div>
</ControllerButton>
<div className={cssClasses.Row}>
<div
<ControllerButton name='zl' sendCommand={sendCommand}
controllerState={controllerState}
className={
cssClasses.SmallButton +
(controllerState?.zl?.isPressed ? " " + cssClasses.Pressed : "")
}
>
<p>ZL</p>
</div>
<div
</ControllerButton>
<ControllerButton name='minus' sendCommand={sendCommand}
controllerState={controllerState}
className={
cssClasses.Symbol +
(controllerState?.minus?.isPressed ? " " + cssClasses.PressedAlt : "")
}
>
{/* Slightly wider than a typical minus. */}
<p>–</p>
</div>
</ControllerButton>
</div>
</div>
<Joystick
<Joystick name='l'
sendCommand={sendCommand}
controllerState={controllerState}
x={controllerState?.leftStick?.horizontalValue || 0}
y={controllerState?.leftStick?.verticalValue || 0}
pressed={controllerState?.leftStick?.isPressed}
/>
<Diamond
controllerState={controllerState}
buttons={[
{ symbol: "▶", pressed: controllerState?.arrowRight?.isPressed },
{ symbol: "▼", pressed: controllerState?.arrowDown?.isPressed },
{ symbol: "▲", pressed: controllerState?.arrowUp?.isPressed },
{ symbol: "◀", pressed: controllerState?.arrowLeft?.isPressed },
{
symbol: "▶", name: 'right',
sendCommand,
controllerState,
pressed: controllerState?.arrowRight?.isPressed
},
{
symbol: "▼", name: 'down',
sendCommand,
controllerState,
pressed: controllerState?.arrowDown?.isPressed
},
{
symbol: "▲", name: 'up',
sendCommand,
controllerState,
pressed: controllerState?.arrowUp?.isPressed
},
{
symbol: "◀", name: 'left',
sendCommand,
controllerState,
pressed: controllerState?.arrowLeft?.isPressed
},
]}
/>
<div
<ControllerButton name='capture' sendCommand={sendCommand}
controllerState={controllerState}
className={
cssClasses.Symbol + " " + cssClasses.capture +
(controllerState?.capture?.isPressed ? " " + cssClasses.PressedAlt : "")
}
>
<p>■</p>
</div>
</div>
<div className={cssClasses.Middle}>
</ControllerButton>
</Grid>
<Grid item className={classes.middle} sm={2} md={4} >
<VideoStream {...this.props.videoStreamProps} />
</div>
<div>
</Grid>
<Grid item xs={6} sm={4} md={3} lg={2}>
<div>
<div
<ControllerButton name='r' sendCommand={sendCommand}
controllerState={controllerState}
className={
cssClasses.LargeButton +
(controllerState?.r?.isPressed ? " " + cssClasses.Pressed : "")
}
>
<p>R</p>
</div>
</ControllerButton>
<div className={cssClasses.Row}>
<div
<ControllerButton name='plus' sendCommand={sendCommand}
controllerState={controllerState}
className={
cssClasses.Symbol +
(controllerState?.plus?.isPressed ? " " + cssClasses.PressedAlt : "")
}
>
<p>+</p>
</div>
<div
</ControllerButton>
<ControllerButton name='zr' sendCommand={sendCommand}
controllerState={controllerState}
className={
cssClasses.SmallButton +
(controllerState?.zr?.isPressed ? " " + cssClasses.Pressed : "")
}
>
<p>ZR</p>
</div>
</ControllerButton>
</div>
</div>
<Diamond
buttons={[
{ symbol: "a", pressed: controllerState?.a?.isPressed },
{ symbol: "b", pressed: controllerState?.b?.isPressed },
{ symbol: "x", pressed: controllerState?.x?.isPressed },
{ symbol: "y", pressed: controllerState?.y?.isPressed },
{
symbol: "a", name: 'a',
sendCommand,
controllerState,
pressed: controllerState?.a?.isPressed
},
{
symbol: "b", name: 'b',
sendCommand,
controllerState,
pressed: controllerState?.b?.isPressed
},
{
symbol: "x", name: 'x',
sendCommand,
controllerState,
pressed: controllerState?.x?.isPressed
},
{
symbol: "y", name: 'y',
sendCommand,
controllerState,
pressed: controllerState?.y?.isPressed
},
]}
/>
<Joystick
<Joystick name='r'
sendCommand={sendCommand}
controllerState={controllerState}
x={controllerState?.rightStick?.horizontalValue || 0}
y={controllerState?.rightStick?.verticalValue || 0}
pressed={controllerState?.rightStick?.isPressed}
/>
<div
<ControllerButton name='home' sendCommand={sendCommand}
controllerState={controllerState}
className={
cssClasses.Symbol + " " + cssClasses.home +
(controllerState?.home?.isPressed ? " " + cssClasses.PressedAlt : "")
}
>
<p>●</p>
</div>
</div>
</div>
</ControllerButton>
</Grid>
</Grid>
)
}
}
Expand Down
52 changes: 52 additions & 0 deletions website-client/src/components/Controller/ControllerButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createStyles, withStyles } from '@material-ui/core'
import React from 'react'
import { SendCommand } from '../../key-binding/KeyBinding'
import { ControllerState } from './ControllerState'

const styles = () => createStyles({
})

class ControllerButton extends React.Component<{
name: string,
sendCommand: SendCommand,
controllerState: ControllerState,
classes: any,
className: string,
}> {
constructor(props: any) {
super(props)

this.onSelect = this.onSelect.bind(this)
this.onUnselect = this.onUnselect.bind(this)
}

private onSelect(e: React.MouseEvent<HTMLDivElement, MouseEvent> | React.TouchEvent<HTMLDivElement>) {
const { name, sendCommand, controllerState } = this.props
const singleCommand = `${name} d`
const updatedGivenState = true
sendCommand(singleCommand, controllerState, updatedGivenState)
e.preventDefault()

}
private onUnselect(e: React.MouseEvent<HTMLDivElement, MouseEvent> | React.TouchEvent<HTMLDivElement>) {
const { name, sendCommand, controllerState } = this.props
const singleCommand = `${name} u`
const updatedGivenState = true
sendCommand(singleCommand, controllerState, updatedGivenState)
e.preventDefault()
}

render(): React.ReactNode {
return <div
className={this.props.className}
onMouseDown={this.onSelect}
onTouchStart={this.onSelect}
onMouseUp={this.onUnselect}
onTouchEnd={this.onUnselect}
>
{this.props.children}
</div>
}
}

export default withStyles(styles)(ControllerButton)
Loading