Skip to content

Commit

Permalink
[UI] Clickable/Tappable Button and Sticks (#26)
Browse files Browse the repository at this point in the history
* client/Joystick: Move both joysticks with touch.
* client: Support clicking and dragging joysticks with the mouse.
* client: Handle passing around controllerState to buttons a little better
* ui: Show when two buttons are touched at the same time.
* README: Mouse/Touchscreen are supported.
* ui: Get most buttons to show on mobile screens.
* sendComand: Allow updating state.
* client: Only keep one ControllerState.
* client: Only run command once per press.
* client: Bump to 0.6
  • Loading branch information
juharris authored Aug 11, 2020
1 parent fcd5c8b commit a9b3f76
Show file tree
Hide file tree
Showing 21 changed files with 707 additions and 226 deletions.
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
2 changes: 2 additions & 0 deletions website-client/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
node_modules/
1 change: 0 additions & 1 deletion website-client/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ COPY public/ ./public/
COPY tsconfig.json ./
COPY src/ ./src/


RUN yarn build

CMD yarn start-prod
2 changes: 1 addition & 1 deletion website-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ docker run --rm -d -p 5000:5000 --name switch-remoteplay-client switch-remotepla

#### Updating the Public Image
You will need permission to push a new image to Docker Hub.
```
```bash
docker login
docker tag switch-remoteplay-client juharris/switch-remoteplay-client:latest
docker push juharris/switch-remoteplay-client:latest
Expand Down
9 changes: 5 additions & 4 deletions website-client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "switch-rp-client",
"version": "0.5.0",
"version": "0.6.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.10.0",
Expand All @@ -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
Loading

0 comments on commit a9b3f76

Please sign in to comment.