react-native-earl-gamepad 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -5
- package/dist/GamepadBridge.d.ts +14 -2
- package/dist/GamepadBridge.js +78 -1
- package/dist/GamepadDebug.js +158 -11
- package/dist/types.d.ts +24 -9
- package/dist/useGamepad.d.ts +6 -2
- package/dist/useGamepad.js +67 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
# react-native-earl-gamepad
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
3
8
|
WebView-based gamepad bridge for React Native. Polls `navigator.getGamepads()` in a hidden WebView and surfaces buttons, sticks, d-pad, and connection events to JS.
|
|
4
9
|
|
|
5
10
|
- Components: `GamepadBridge`, `useGamepad`, and `GamepadDebug`.
|
|
6
11
|
- Deadzone handling (default `0.15`) with auto-clear on disconnect.
|
|
7
12
|
- Typed events for buttons, axes, d-pad, and status.
|
|
8
13
|
|
|
14
|
+
### Why this?
|
|
15
|
+
|
|
16
|
+
Native gamepad support in React Native can be flaky or hard to maintain. Instead of relying on old native modules, it uses a hidden WebView to bridge the HTML5 Gamepad API (navigator.getGamepads()) directly to React Native. This ensures much better compatibility across iOS and Android since it relies on the web standard.
|
|
17
|
+
|
|
18
|
+
### Controller Compatibility
|
|
19
|
+
|
|
20
|
+
- Tested with: PS4, and generic Bluetooth controllers. Supports standard mapping.
|
|
21
|
+
|
|
9
22
|
## Requirements
|
|
10
23
|
|
|
11
24
|
- React Native `>=0.72`
|
|
@@ -119,7 +132,7 @@ export function HUD() {
|
|
|
119
132
|
|
|
120
133
|
### Visual debugger
|
|
121
134
|
|
|
122
|
-
Drop-in component to see a controller diagram that lights up buttons, shows stick offsets, and lists state.
|
|
135
|
+
Drop-in component to see a controller diagram that lights up buttons, shows stick offsets, and lists state. Shows live metadata (name/vendor/product, mapping, axes/buttons count, vibration support) and includes vibration test buttons plus a loader prompt when no pad is connected.
|
|
123
136
|
|
|
124
137
|
```tsx
|
|
125
138
|
import { GamepadDebug } from "react-native-earl-gamepad";
|
|
@@ -129,9 +142,9 @@ export function DebugScreen() {
|
|
|
129
142
|
}
|
|
130
143
|
```
|
|
131
144
|
|
|
132
|
-

|
|
134
146
|

|
|
147
|
+

|
|
135
148
|
|
|
136
149
|
## API
|
|
137
150
|
|
|
@@ -160,6 +173,9 @@ Return shape:
|
|
|
160
173
|
- `buttonValues: Partial<Record<GamepadButtonName, number>>` — last analog value per button (useful for LT/RT triggers).
|
|
161
174
|
- `isPressed(key: GamepadButtonName): boolean` — helper to check a single button.
|
|
162
175
|
- `bridge: JSX.Element | null` — render once to enable polling.
|
|
176
|
+
- `info: GamepadInfo` — metadata for the first controller (id, vendor/product if exposed, mapping, counts, vibration support, timestamp, index).
|
|
177
|
+
- `vibrate(duration?: number, strength?: number): void` — fire a short rumble when `vibrationActuator` is available.
|
|
178
|
+
- `stopVibration(): void` — stop an in-flight vibration when supported.
|
|
163
179
|
|
|
164
180
|
### `GamepadDebug`
|
|
165
181
|
|
|
@@ -197,13 +213,13 @@ npm install
|
|
|
197
213
|
npm run build
|
|
198
214
|
```
|
|
199
215
|
|
|
216
|
+
Build outputs to `dist/` with type declarations.
|
|
217
|
+
|
|
200
218
|
## Troubleshooting
|
|
201
219
|
|
|
202
220
|
- **[Invariant Violation: Tried to register two views with the same name RNCWebView]**: Check your `package.json` for multiple instances of `react-native-webview` and uninstall any duplicates.
|
|
203
221
|
When you install `react-native-earl-gamepad`, `react-native-webview` is already included, so you should not install it separately. or you can check it by running `npm ls react-native-webview`.
|
|
204
222
|
|
|
205
|
-
Build outputs to `dist/` with type declarations.
|
|
206
|
-
|
|
207
223
|
## License
|
|
208
224
|
|
|
209
225
|
MIT
|
package/dist/GamepadBridge.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { StyleProp, ViewStyle } from "react-native";
|
|
2
|
-
import type { AxisEvent, ButtonEvent, DpadEvent, StatusEvent } from "./types";
|
|
2
|
+
import type { AxisEvent, ButtonEvent, DpadEvent, InfoEvent, StatusEvent } from "./types";
|
|
3
|
+
type VibrationRequest = {
|
|
4
|
+
type: "once";
|
|
5
|
+
duration: number;
|
|
6
|
+
strong: number;
|
|
7
|
+
weak: number;
|
|
8
|
+
nonce: number;
|
|
9
|
+
} | {
|
|
10
|
+
type: "stop";
|
|
11
|
+
nonce: number;
|
|
12
|
+
};
|
|
3
13
|
type Props = {
|
|
4
14
|
enabled?: boolean;
|
|
5
15
|
axisThreshold?: number;
|
|
@@ -7,7 +17,9 @@ type Props = {
|
|
|
7
17
|
onButton?: (event: ButtonEvent) => void;
|
|
8
18
|
onAxis?: (event: AxisEvent) => void;
|
|
9
19
|
onStatus?: (event: StatusEvent) => void;
|
|
20
|
+
onInfo?: (event: InfoEvent) => void;
|
|
21
|
+
vibrationRequest?: VibrationRequest;
|
|
10
22
|
style?: StyleProp<ViewStyle>;
|
|
11
23
|
};
|
|
12
|
-
export default function GamepadBridge({ enabled, axisThreshold, onDpad, onButton, onAxis, onStatus, style, }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
24
|
+
export default function GamepadBridge({ enabled, axisThreshold, onDpad, onButton, onAxis, onStatus, onInfo, vibrationRequest, style, }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
13
25
|
export {};
|
package/dist/GamepadBridge.js
CHANGED
|
@@ -20,10 +20,69 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
20
20
|
const axisNames = ['leftX','leftY','rightX','rightY'];
|
|
21
21
|
let prevButtons = [];
|
|
22
22
|
let prevAxes = [];
|
|
23
|
+
let prevInfoJson = '';
|
|
24
|
+
|
|
25
|
+
function parseVendorProduct(id){
|
|
26
|
+
const vendorMatch = /Vendor:\s?([0-9a-fA-F]+)/i.exec(id || '') || /VID_([0-9a-fA-F]+)/i.exec(id || '');
|
|
27
|
+
const productMatch = /Product:\s?([0-9a-fA-F]+)/i.exec(id || '') || /PID_([0-9a-fA-F]+)/i.exec(id || '');
|
|
28
|
+
return {
|
|
29
|
+
vendor: vendorMatch ? vendorMatch[1] : null,
|
|
30
|
+
product: productMatch ? productMatch[1] : null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sendInfo(gp){
|
|
35
|
+
const connected = !!gp;
|
|
36
|
+
const { vendor, product } = parseVendorProduct(gp?.id || '');
|
|
37
|
+
const info = {
|
|
38
|
+
type: 'info',
|
|
39
|
+
connected,
|
|
40
|
+
index: gp?.index ?? null,
|
|
41
|
+
id: gp?.id ?? null,
|
|
42
|
+
mapping: gp?.mapping ?? null,
|
|
43
|
+
timestamp: gp?.timestamp ?? null,
|
|
44
|
+
canVibrate: !!gp?.vibrationActuator,
|
|
45
|
+
vendor,
|
|
46
|
+
product,
|
|
47
|
+
axes: gp?.axes?.length ?? 0,
|
|
48
|
+
buttons: gp?.buttons?.length ?? 0,
|
|
49
|
+
};
|
|
50
|
+
const nextJson = JSON.stringify(info);
|
|
51
|
+
if (nextJson !== prevInfoJson) {
|
|
52
|
+
prevInfoJson = nextJson;
|
|
53
|
+
send(info);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function vibrateOnce(duration, strong, weak){
|
|
58
|
+
const pad = (navigator.getGamepads && navigator.getGamepads()[0]) || null;
|
|
59
|
+
if (!pad || !pad.vibrationActuator || !pad.vibrationActuator.playEffect) return;
|
|
60
|
+
try {
|
|
61
|
+
pad.vibrationActuator.playEffect('dual-rumble', {
|
|
62
|
+
duration: duration || 500,
|
|
63
|
+
strongMagnitude: strong ?? 1,
|
|
64
|
+
weakMagnitude: weak ?? 1,
|
|
65
|
+
}).catch(() => {});
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function stopVibration(){
|
|
72
|
+
const pad = (navigator.getGamepads && navigator.getGamepads()[0]) || null;
|
|
73
|
+
if (!pad || !pad.vibrationActuator) return;
|
|
74
|
+
if (typeof pad.vibrationActuator.reset === 'function') {
|
|
75
|
+
try { pad.vibrationActuator.reset(); } catch (err) {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
window.__earlVibrateOnce = vibrateOnce;
|
|
80
|
+
window.__earlStopVibration = stopVibration;
|
|
23
81
|
|
|
24
82
|
function poll(){
|
|
25
83
|
const pads = (navigator.getGamepads && navigator.getGamepads()) || [];
|
|
26
84
|
const gp = pads[0];
|
|
85
|
+
sendInfo(gp);
|
|
27
86
|
if (gp) {
|
|
28
87
|
// Buttons
|
|
29
88
|
gp.buttons?.forEach((btn, index) => {
|
|
@@ -52,6 +111,8 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
52
111
|
prevAxes[index] = value;
|
|
53
112
|
});
|
|
54
113
|
} else {
|
|
114
|
+
// If no pad, send a single disconnected info payload
|
|
115
|
+
sendInfo(null);
|
|
55
116
|
if (prevButtons.length) {
|
|
56
117
|
prevButtons.forEach((btn, index) => {
|
|
57
118
|
if (btn?.pressed) {
|
|
@@ -99,7 +160,7 @@ const buildBridgeHtml = (axisThreshold) => `
|
|
|
99
160
|
})();
|
|
100
161
|
</script>
|
|
101
162
|
</body></html>`;
|
|
102
|
-
function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton, onAxis, onStatus, style, }) {
|
|
163
|
+
function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton, onAxis, onStatus, onInfo, vibrationRequest, style, }) {
|
|
103
164
|
const webviewRef = (0, react_1.useRef)(null);
|
|
104
165
|
const html = (0, react_1.useMemo)(() => buildBridgeHtml(axisThreshold), [axisThreshold]);
|
|
105
166
|
const focusBridge = (0, react_1.useCallback)(() => {
|
|
@@ -113,6 +174,19 @@ function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton,
|
|
|
113
174
|
if (enabled)
|
|
114
175
|
focusBridge();
|
|
115
176
|
}, [enabled, focusBridge]);
|
|
177
|
+
(0, react_1.useEffect)(() => {
|
|
178
|
+
if (!enabled || !vibrationRequest)
|
|
179
|
+
return;
|
|
180
|
+
const node = webviewRef.current;
|
|
181
|
+
if (!node)
|
|
182
|
+
return;
|
|
183
|
+
if (vibrationRequest.type === "once") {
|
|
184
|
+
node.injectJavaScript(`window.__earlVibrateOnce(${Math.max(vibrationRequest.duration, 0)}, ${Math.max(Math.min(vibrationRequest.strong, 1), 0)}, ${Math.max(Math.min(vibrationRequest.weak, 1), 0)}); true;`);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
node.injectJavaScript(`window.__earlStopVibration(); true;`);
|
|
188
|
+
}
|
|
189
|
+
}, [enabled, vibrationRequest]);
|
|
116
190
|
if (!enabled)
|
|
117
191
|
return null;
|
|
118
192
|
const handleMessage = (event) => {
|
|
@@ -130,6 +204,9 @@ function GamepadBridge({ enabled = true, axisThreshold = 0.15, onDpad, onButton,
|
|
|
130
204
|
else if (data.type === "status") {
|
|
131
205
|
onStatus === null || onStatus === void 0 ? void 0 : onStatus(data);
|
|
132
206
|
}
|
|
207
|
+
else if (data.type === "info") {
|
|
208
|
+
onInfo === null || onInfo === void 0 ? void 0 : onInfo(data);
|
|
209
|
+
}
|
|
133
210
|
}
|
|
134
211
|
catch {
|
|
135
212
|
// ignore malformed messages
|
package/dist/GamepadDebug.js
CHANGED
|
@@ -15,6 +15,11 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
15
15
|
const v = value(name);
|
|
16
16
|
return Math.max(v, pressed(name) ? 1 : 0);
|
|
17
17
|
};
|
|
18
|
+
const { width, height } = (0, react_native_1.useWindowDimensions)();
|
|
19
|
+
const isPortrait = height >= width;
|
|
20
|
+
const containerScaleStyle = isPortrait
|
|
21
|
+
? { transform: [{ scale: 0.8 }] }
|
|
22
|
+
: undefined;
|
|
18
23
|
const mix = (a, b, t) => Math.round(a + (b - a) * t);
|
|
19
24
|
const stickColor = (mag) => {
|
|
20
25
|
const clamped = Math.min(1, Math.max(0, mag));
|
|
@@ -30,7 +35,7 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
30
35
|
};
|
|
31
36
|
const leftMag = Math.min(1, Math.hypot(axis("leftX"), axis("leftY")));
|
|
32
37
|
const rightMag = Math.min(1, Math.hypot(axis("rightX"), axis("rightY")));
|
|
33
|
-
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.psWrapper, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.psContainer, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.psShoulderRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
38
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.psWrapper, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.psContainer, containerScaleStyle], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.psShoulderRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
34
39
|
styles.psShoulder,
|
|
35
40
|
styles.psShoulderLeft,
|
|
36
41
|
level("lb") > 0 && styles.psShoulderActive,
|
|
@@ -155,7 +160,8 @@ function ControllerVisual({ pressed, axis, value }) {
|
|
|
155
160
|
] }) }) })] })] }) }));
|
|
156
161
|
}
|
|
157
162
|
function GamepadDebug({ enabled = true, axisThreshold = 0.15, }) {
|
|
158
|
-
|
|
163
|
+
var _a, _b, _c, _d;
|
|
164
|
+
const { bridge, pressedButtons, axes, buttonValues, info, vibrate, stopVibration, } = (0, useGamepad_1.default)({
|
|
159
165
|
enabled,
|
|
160
166
|
axisThreshold,
|
|
161
167
|
});
|
|
@@ -163,7 +169,45 @@ function GamepadDebug({ enabled = true, axisThreshold = 0.15, }) {
|
|
|
163
169
|
const pressed = (key) => pressedButtons.has(key);
|
|
164
170
|
const axisValue = (key) => { var _a; return (_a = axes[key]) !== null && _a !== void 0 ? _a : 0; };
|
|
165
171
|
const buttonValue = (key) => { var _a; return (_a = buttonValues[key]) !== null && _a !== void 0 ? _a : 0; };
|
|
166
|
-
|
|
172
|
+
const isConnected = info.connected;
|
|
173
|
+
const controllerLabel = (_a = info.id) !== null && _a !== void 0 ? _a : "No controller detected";
|
|
174
|
+
const vendorLabel = (_b = info.vendor) !== null && _b !== void 0 ? _b : "—";
|
|
175
|
+
const productLabel = (_c = info.product) !== null && _c !== void 0 ? _c : "—";
|
|
176
|
+
const mappingLabel = (_d = info.mapping) !== null && _d !== void 0 ? _d : "unknown";
|
|
177
|
+
const timestampLabel = info.timestamp != null ? Math.round(info.timestamp).toString() : "—";
|
|
178
|
+
const infoItems = [
|
|
179
|
+
{ label: "Name", value: controllerLabel },
|
|
180
|
+
{ label: "Vendor", value: vendorLabel },
|
|
181
|
+
{ label: "Product", value: productLabel },
|
|
182
|
+
{ label: "Mapping", value: mappingLabel },
|
|
183
|
+
{ label: "Index", value: info.index != null ? `${info.index}` : "—" },
|
|
184
|
+
{ label: "Axes", value: `${info.axes || 0}` },
|
|
185
|
+
{ label: "Buttons", value: `${info.buttons || 0}` },
|
|
186
|
+
{ label: "Vibration", value: info.canVibrate ? "Yes" : "No" },
|
|
187
|
+
{ label: "Timestamp", value: timestampLabel },
|
|
188
|
+
];
|
|
189
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.container, children: (0, jsx_runtime_1.jsxs)(react_native_1.ScrollView, { contentContainerStyle: styles.scrollContent, showsVerticalScrollIndicator: false, children: [bridge, (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.body, children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.card, styles.controllerCard], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.statusRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.cardTitle, children: "Controller" }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: [
|
|
190
|
+
styles.statusPill,
|
|
191
|
+
isConnected
|
|
192
|
+
? styles.statusPillConnected
|
|
193
|
+
: styles.statusPillWaiting,
|
|
194
|
+
], children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.statusPillText, children: isConnected ? "Connected" : "Waiting" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.helperText, children: "Connect your gamepad and press a button to test." }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.infoGrid, children: infoItems.map(({ label, value }) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.infoItem, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.infoLabel, children: label }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.infoValue, children: value })] }, label))) }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.controller, children: [!isConnected && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.loaderOverlay, children: [(0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { color: "#2563eb", size: "small" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.loaderText, children: "Connect your gamepad and press a button to test..." })] })), (0, jsx_runtime_1.jsx)(ControllerVisual, { pressed: pressed, axis: axisValue, value: buttonValue })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.testsRow, children: [(0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: [
|
|
195
|
+
styles.button,
|
|
196
|
+
!info.canVibrate && styles.buttonDisabled,
|
|
197
|
+
], onPress: () => info.canVibrate && vibrate(900, 1), disabled: !info.canVibrate, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
|
|
198
|
+
styles.buttonText,
|
|
199
|
+
!info.canVibrate &&
|
|
200
|
+
styles.buttonTextDisabled,
|
|
201
|
+
], children: "Vibration, 1 sec" }) }), (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { style: [
|
|
202
|
+
styles.button,
|
|
203
|
+
!info.canVibrate && styles.buttonDisabled,
|
|
204
|
+
], onPress: () => info.canVibrate && stopVibration(), disabled: !info.canVibrate, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [
|
|
205
|
+
styles.buttonText,
|
|
206
|
+
!info.canVibrate &&
|
|
207
|
+
styles.buttonTextDisabled,
|
|
208
|
+
], children: "Stop vibration" }) })] }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.helperTextSmall, children: info.canVibrate
|
|
209
|
+
? "Uses vibrationActuator when available."
|
|
210
|
+
: "Vibration not reported by this controller." })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.card, styles.stateCard], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.cardTitle, children: "State" }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sectionTitle, children: "Pressed" }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.badgeRow, children: pressedList.length === 0 ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.muted, children: "None" })) : (pressedList.map((name) => ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.badge, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.badgeText, children: name }) }, name)))) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.sectionTitle, children: "Axes" }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.axesGrid, children: trackedAxes.map((axisName) => ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.axisItem, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.axisLabel, children: axisName }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.axisValue, children: format(axes[axisName]) })] }, axisName))) })] })] })] }) }));
|
|
167
211
|
}
|
|
168
212
|
const styles = react_native_1.StyleSheet.create({
|
|
169
213
|
container: {
|
|
@@ -279,6 +323,65 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
279
323
|
fontWeight: "700",
|
|
280
324
|
fontSize: 16,
|
|
281
325
|
},
|
|
326
|
+
statusRow: {
|
|
327
|
+
flexDirection: "row",
|
|
328
|
+
alignItems: "center",
|
|
329
|
+
justifyContent: "space-between",
|
|
330
|
+
gap: 8,
|
|
331
|
+
},
|
|
332
|
+
statusPill: {
|
|
333
|
+
paddingHorizontal: 10,
|
|
334
|
+
paddingVertical: 4,
|
|
335
|
+
borderRadius: 999,
|
|
336
|
+
borderWidth: 1,
|
|
337
|
+
},
|
|
338
|
+
statusPillConnected: {
|
|
339
|
+
backgroundColor: "#dcfce7",
|
|
340
|
+
borderColor: "#16a34a",
|
|
341
|
+
},
|
|
342
|
+
statusPillWaiting: {
|
|
343
|
+
backgroundColor: "#fee2e2",
|
|
344
|
+
borderColor: "#f97316",
|
|
345
|
+
},
|
|
346
|
+
statusPillText: {
|
|
347
|
+
fontSize: 12,
|
|
348
|
+
fontWeight: "700",
|
|
349
|
+
color: "#0f172a",
|
|
350
|
+
},
|
|
351
|
+
helperText: {
|
|
352
|
+
color: "#475569",
|
|
353
|
+
fontSize: 12,
|
|
354
|
+
},
|
|
355
|
+
helperTextSmall: {
|
|
356
|
+
color: "#94a3b8",
|
|
357
|
+
fontSize: 11,
|
|
358
|
+
},
|
|
359
|
+
infoGrid: {
|
|
360
|
+
flexDirection: "row",
|
|
361
|
+
flexWrap: "wrap",
|
|
362
|
+
gap: 8,
|
|
363
|
+
},
|
|
364
|
+
infoItem: {
|
|
365
|
+
flexGrow: 1,
|
|
366
|
+
flexBasis: "45%",
|
|
367
|
+
minWidth: 140,
|
|
368
|
+
backgroundColor: "#f8fafc",
|
|
369
|
+
borderWidth: 1,
|
|
370
|
+
borderColor: "#e2e8f0",
|
|
371
|
+
borderRadius: 8,
|
|
372
|
+
padding: 8,
|
|
373
|
+
gap: 2,
|
|
374
|
+
},
|
|
375
|
+
infoLabel: {
|
|
376
|
+
color: "#475569",
|
|
377
|
+
fontSize: 11,
|
|
378
|
+
fontWeight: "700",
|
|
379
|
+
},
|
|
380
|
+
infoValue: {
|
|
381
|
+
color: "#0f172a",
|
|
382
|
+
fontSize: 13,
|
|
383
|
+
fontWeight: "600",
|
|
384
|
+
},
|
|
282
385
|
controller: {
|
|
283
386
|
backgroundColor: "#f8fafc",
|
|
284
387
|
borderWidth: 1,
|
|
@@ -286,6 +389,50 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
286
389
|
padding: 12,
|
|
287
390
|
alignItems: "center",
|
|
288
391
|
justifyContent: "center",
|
|
392
|
+
position: "relative",
|
|
393
|
+
overflow: "hidden",
|
|
394
|
+
},
|
|
395
|
+
loaderOverlay: {
|
|
396
|
+
position: "absolute",
|
|
397
|
+
top: 0,
|
|
398
|
+
left: 0,
|
|
399
|
+
right: 0,
|
|
400
|
+
bottom: 0,
|
|
401
|
+
backgroundColor: "rgba(255,255,255,0.92)",
|
|
402
|
+
alignItems: "center",
|
|
403
|
+
justifyContent: "center",
|
|
404
|
+
padding: 12,
|
|
405
|
+
gap: 6,
|
|
406
|
+
zIndex: 50,
|
|
407
|
+
},
|
|
408
|
+
loaderText: {
|
|
409
|
+
color: "#0f172a",
|
|
410
|
+
fontSize: 12,
|
|
411
|
+
textAlign: "center",
|
|
412
|
+
},
|
|
413
|
+
testsRow: {
|
|
414
|
+
flexDirection: "row",
|
|
415
|
+
flexWrap: "wrap",
|
|
416
|
+
gap: 8,
|
|
417
|
+
},
|
|
418
|
+
button: {
|
|
419
|
+
backgroundColor: "#2563eb",
|
|
420
|
+
paddingVertical: 10,
|
|
421
|
+
paddingHorizontal: 12,
|
|
422
|
+
borderRadius: 8,
|
|
423
|
+
alignItems: "center",
|
|
424
|
+
justifyContent: "center",
|
|
425
|
+
},
|
|
426
|
+
buttonDisabled: {
|
|
427
|
+
backgroundColor: "#cbd5e1",
|
|
428
|
+
},
|
|
429
|
+
buttonText: {
|
|
430
|
+
color: "#f8fafc",
|
|
431
|
+
fontWeight: "700",
|
|
432
|
+
fontSize: 13,
|
|
433
|
+
},
|
|
434
|
+
buttonTextDisabled: {
|
|
435
|
+
color: "#475569",
|
|
289
436
|
},
|
|
290
437
|
axesGrid: {
|
|
291
438
|
flexDirection: "row",
|
|
@@ -349,7 +496,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
349
496
|
width: 395,
|
|
350
497
|
height: 160,
|
|
351
498
|
backgroundColor: "#e0e0e0",
|
|
352
|
-
marginLeft: 27
|
|
499
|
+
marginLeft: 50, // 27
|
|
353
500
|
borderRadius: 25,
|
|
354
501
|
position: "absolute",
|
|
355
502
|
zIndex: 0,
|
|
@@ -359,7 +506,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
359
506
|
width: 150,
|
|
360
507
|
height: 80,
|
|
361
508
|
backgroundColor: "#333",
|
|
362
|
-
marginLeft:
|
|
509
|
+
marginLeft: 170,
|
|
363
510
|
borderRadius: 7,
|
|
364
511
|
position: "absolute",
|
|
365
512
|
zIndex: 10,
|
|
@@ -375,7 +522,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
375
522
|
height: 25,
|
|
376
523
|
position: "absolute",
|
|
377
524
|
backgroundColor: "#95a5a6",
|
|
378
|
-
marginLeft:
|
|
525
|
+
marginLeft: 148,
|
|
379
526
|
marginTop: 85,
|
|
380
527
|
borderRadius: 5,
|
|
381
528
|
zIndex: 10,
|
|
@@ -390,7 +537,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
390
537
|
height: 25,
|
|
391
538
|
position: "absolute",
|
|
392
539
|
backgroundColor: "#95a5a6",
|
|
393
|
-
marginLeft:
|
|
540
|
+
marginLeft: 327,
|
|
394
541
|
marginTop: 85,
|
|
395
542
|
borderRadius: 5,
|
|
396
543
|
zIndex: 10,
|
|
@@ -405,7 +552,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
405
552
|
height: 260,
|
|
406
553
|
backgroundColor: "#e0e0e0",
|
|
407
554
|
position: "absolute",
|
|
408
|
-
left:
|
|
555
|
+
left: 23,
|
|
409
556
|
top: 95,
|
|
410
557
|
transform: [{ rotate: "11deg" }],
|
|
411
558
|
borderTopLeftRadius: 30,
|
|
@@ -426,7 +573,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
426
573
|
height: 260,
|
|
427
574
|
backgroundColor: "#e0e0e0",
|
|
428
575
|
position: "absolute",
|
|
429
|
-
left:
|
|
576
|
+
left: 353,
|
|
430
577
|
top: 95,
|
|
431
578
|
transform: [{ rotate: "-11deg" }],
|
|
432
579
|
borderTopLeftRadius: 30,
|
|
@@ -448,7 +595,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
448
595
|
height: 112,
|
|
449
596
|
borderRadius: 56,
|
|
450
597
|
marginTop: 5,
|
|
451
|
-
marginLeft:
|
|
598
|
+
marginLeft: 0,
|
|
452
599
|
borderWidth: 1,
|
|
453
600
|
borderColor: "#b0b0b0",
|
|
454
601
|
position: "relative",
|
|
@@ -514,7 +661,7 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
514
661
|
width: 112,
|
|
515
662
|
height: 112,
|
|
516
663
|
borderRadius: 56,
|
|
517
|
-
marginTop:
|
|
664
|
+
marginTop: 4,
|
|
518
665
|
borderWidth: 1,
|
|
519
666
|
borderColor: "#b0b0b0",
|
|
520
667
|
position: "relative",
|
package/dist/types.d.ts
CHANGED
|
@@ -1,26 +1,41 @@
|
|
|
1
|
-
export type DpadKey =
|
|
2
|
-
export type GamepadButtonName =
|
|
3
|
-
export type StickAxisName =
|
|
1
|
+
export type DpadKey = "up" | "down" | "left" | "right";
|
|
2
|
+
export type GamepadButtonName = "a" | "b" | "x" | "y" | "lb" | "rb" | "lt" | "rt" | "back" | "start" | "ls" | "rs" | "dpadUp" | "dpadDown" | "dpadLeft" | "dpadRight" | "home" | `button-${number}`;
|
|
3
|
+
export type StickAxisName = "leftX" | "leftY" | "rightX" | "rightY" | `axis-${number}`;
|
|
4
4
|
export type DpadEvent = {
|
|
5
|
-
type:
|
|
5
|
+
type: "dpad";
|
|
6
6
|
key: DpadKey;
|
|
7
7
|
pressed: boolean;
|
|
8
8
|
};
|
|
9
9
|
export type ButtonEvent = {
|
|
10
|
-
type:
|
|
10
|
+
type: "button";
|
|
11
11
|
button: GamepadButtonName;
|
|
12
12
|
index: number;
|
|
13
13
|
pressed: boolean;
|
|
14
14
|
value: number;
|
|
15
15
|
};
|
|
16
16
|
export type AxisEvent = {
|
|
17
|
-
type:
|
|
17
|
+
type: "axis";
|
|
18
18
|
axis: StickAxisName;
|
|
19
19
|
index: number;
|
|
20
20
|
value: number;
|
|
21
21
|
};
|
|
22
|
+
export type GamepadInfo = {
|
|
23
|
+
connected: boolean;
|
|
24
|
+
index: number | null;
|
|
25
|
+
id: string | null;
|
|
26
|
+
mapping: string | null;
|
|
27
|
+
timestamp: number | null;
|
|
28
|
+
canVibrate: boolean;
|
|
29
|
+
vendor: string | null;
|
|
30
|
+
product: string | null;
|
|
31
|
+
axes: number;
|
|
32
|
+
buttons: number;
|
|
33
|
+
};
|
|
34
|
+
export type InfoEvent = GamepadInfo & {
|
|
35
|
+
type: "info";
|
|
36
|
+
};
|
|
22
37
|
export type StatusEvent = {
|
|
23
|
-
type:
|
|
24
|
-
state:
|
|
38
|
+
type: "status";
|
|
39
|
+
state: "connected" | "disconnected";
|
|
25
40
|
};
|
|
26
|
-
export type GamepadMessage = DpadEvent | ButtonEvent | AxisEvent | StatusEvent;
|
|
41
|
+
export type GamepadMessage = DpadEvent | ButtonEvent | AxisEvent | StatusEvent | InfoEvent;
|
package/dist/useGamepad.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type JSX } from "react";
|
|
2
|
-
import type { AxisEvent, ButtonEvent, DpadEvent, GamepadButtonName, StickAxisName, StatusEvent } from "./types";
|
|
2
|
+
import type { AxisEvent, ButtonEvent, DpadEvent, GamepadButtonName, GamepadInfo, StickAxisName, InfoEvent, StatusEvent } from "./types";
|
|
3
3
|
type Options = {
|
|
4
4
|
enabled?: boolean;
|
|
5
5
|
axisThreshold?: number;
|
|
@@ -7,6 +7,7 @@ type Options = {
|
|
|
7
7
|
onAxis?: (event: AxisEvent) => void;
|
|
8
8
|
onDpad?: (event: DpadEvent) => void;
|
|
9
9
|
onStatus?: (event: StatusEvent) => void;
|
|
10
|
+
onInfo?: (event: InfoEvent) => void;
|
|
10
11
|
};
|
|
11
12
|
type Return = {
|
|
12
13
|
pressedButtons: Set<GamepadButtonName>;
|
|
@@ -14,6 +15,9 @@ type Return = {
|
|
|
14
15
|
buttonValues: Partial<Record<GamepadButtonName, number>>;
|
|
15
16
|
isPressed: (key: GamepadButtonName) => boolean;
|
|
16
17
|
bridge: JSX.Element | null;
|
|
18
|
+
info: GamepadInfo;
|
|
19
|
+
vibrate: (duration?: number, strength?: number) => void;
|
|
20
|
+
stopVibration: () => void;
|
|
17
21
|
};
|
|
18
|
-
export default function useGamepad({ enabled, axisThreshold, onButton, onAxis, onDpad, onStatus, }?: Options): Return;
|
|
22
|
+
export default function useGamepad({ enabled, axisThreshold, onButton, onAxis, onDpad, onStatus, onInfo, }?: Options): Return;
|
|
19
23
|
export {};
|
package/dist/useGamepad.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.default = useGamepad;
|
|
|
7
7
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
8
|
const react_1 = require("react");
|
|
9
9
|
const GamepadBridge_1 = __importDefault(require("./GamepadBridge"));
|
|
10
|
-
function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, onDpad, onStatus, } = {}) {
|
|
10
|
+
function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, onDpad, onStatus, onInfo, } = {}) {
|
|
11
11
|
const [pressedButtons, setPressedButtons] = (0, react_1.useState)(new Set());
|
|
12
12
|
const [buttonValues, setButtonValues] = (0, react_1.useState)({});
|
|
13
13
|
const pressedRef = (0, react_1.useRef)(new Set());
|
|
@@ -17,6 +17,20 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
17
17
|
rightX: 0,
|
|
18
18
|
rightY: 0,
|
|
19
19
|
});
|
|
20
|
+
const [info, setInfo] = (0, react_1.useState)({
|
|
21
|
+
connected: false,
|
|
22
|
+
index: null,
|
|
23
|
+
id: null,
|
|
24
|
+
mapping: null,
|
|
25
|
+
timestamp: null,
|
|
26
|
+
canVibrate: false,
|
|
27
|
+
vendor: null,
|
|
28
|
+
product: null,
|
|
29
|
+
axes: 0,
|
|
30
|
+
buttons: 0,
|
|
31
|
+
});
|
|
32
|
+
const [vibrationRequest, setVibrationRequest] = (0, react_1.useState)(null);
|
|
33
|
+
const vibrationNonce = (0, react_1.useRef)(0);
|
|
20
34
|
(0, react_1.useEffect)(() => {
|
|
21
35
|
pressedRef.current = pressedButtons;
|
|
22
36
|
}, [pressedButtons]);
|
|
@@ -43,6 +57,34 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
43
57
|
const handleDpad = (0, react_1.useCallback)((event) => {
|
|
44
58
|
onDpad === null || onDpad === void 0 ? void 0 : onDpad(event);
|
|
45
59
|
}, [onDpad]);
|
|
60
|
+
const handleInfo = (0, react_1.useCallback)((event) => {
|
|
61
|
+
setInfo(event);
|
|
62
|
+
onInfo === null || onInfo === void 0 ? void 0 : onInfo(event);
|
|
63
|
+
}, [onInfo]);
|
|
64
|
+
const handleStatus = (0, react_1.useCallback)((event) => {
|
|
65
|
+
if (event.state === "disconnected") {
|
|
66
|
+
setInfo((prev) => ({
|
|
67
|
+
...prev,
|
|
68
|
+
connected: false,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
onStatus === null || onStatus === void 0 ? void 0 : onStatus(event);
|
|
72
|
+
}, [onStatus]);
|
|
73
|
+
const vibrate = (0, react_1.useCallback)((duration = 800, strength = 1) => {
|
|
74
|
+
vibrationNonce.current += 1;
|
|
75
|
+
const safeStrength = Math.max(0, Math.min(strength, 1));
|
|
76
|
+
setVibrationRequest({
|
|
77
|
+
type: "once",
|
|
78
|
+
duration,
|
|
79
|
+
strong: safeStrength,
|
|
80
|
+
weak: safeStrength,
|
|
81
|
+
nonce: vibrationNonce.current,
|
|
82
|
+
});
|
|
83
|
+
}, []);
|
|
84
|
+
const stopVibration = (0, react_1.useCallback)(() => {
|
|
85
|
+
vibrationNonce.current += 1;
|
|
86
|
+
setVibrationRequest({ type: "stop", nonce: vibrationNonce.current });
|
|
87
|
+
}, []);
|
|
46
88
|
(0, react_1.useEffect)(() => {
|
|
47
89
|
if (!enabled && pressedRef.current.size) {
|
|
48
90
|
pressedRef.current = new Set();
|
|
@@ -51,9 +93,31 @@ function useGamepad({ enabled = true, axisThreshold = 0.15, onButton, onAxis, on
|
|
|
51
93
|
if (!enabled) {
|
|
52
94
|
setAxes({ leftX: 0, leftY: 0, rightX: 0, rightY: 0 });
|
|
53
95
|
setButtonValues({});
|
|
96
|
+
setInfo((prev) => ({
|
|
97
|
+
...prev,
|
|
98
|
+
connected: false,
|
|
99
|
+
}));
|
|
54
100
|
}
|
|
55
101
|
}, [enabled]);
|
|
56
|
-
const bridge = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsx)(GamepadBridge_1.default, { enabled: enabled, axisThreshold: axisThreshold, onDpad: handleDpad, onButton: handleButton, onAxis: handleAxis, onStatus:
|
|
102
|
+
const bridge = (0, react_1.useMemo)(() => ((0, jsx_runtime_1.jsx)(GamepadBridge_1.default, { enabled: enabled, axisThreshold: axisThreshold, onDpad: handleDpad, onButton: handleButton, onAxis: handleAxis, onStatus: handleStatus, onInfo: handleInfo, vibrationRequest: vibrationRequest !== null && vibrationRequest !== void 0 ? vibrationRequest : undefined })), [
|
|
103
|
+
enabled,
|
|
104
|
+
axisThreshold,
|
|
105
|
+
handleAxis,
|
|
106
|
+
handleButton,
|
|
107
|
+
handleDpad,
|
|
108
|
+
handleInfo,
|
|
109
|
+
handleStatus,
|
|
110
|
+
vibrationRequest,
|
|
111
|
+
]);
|
|
57
112
|
const isPressed = (0, react_1.useCallback)((key) => pressedRef.current.has(key), []);
|
|
58
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
pressedButtons,
|
|
115
|
+
axes,
|
|
116
|
+
buttonValues,
|
|
117
|
+
isPressed,
|
|
118
|
+
bridge,
|
|
119
|
+
info,
|
|
120
|
+
vibrate,
|
|
121
|
+
stopVibration,
|
|
122
|
+
};
|
|
59
123
|
}
|