Revert "Add Steam Input support for Big Picture Mode"
This reverts commit 8994b9b2d3.
This commit is contained in:
@@ -9,14 +9,12 @@ const PerformanceMonitor = require('./performance-monitor');
|
|||||||
const GPUFallback = require('./gpu-fallback');
|
const GPUFallback = require('./gpu-fallback');
|
||||||
const GPUConfig = require('./gpu-config');
|
const GPUConfig = require('./gpu-config');
|
||||||
const PluginManager = require('./plugin-manager');
|
const PluginManager = require('./plugin-manager');
|
||||||
const SteamInputManager = require('./steam-input-manager');
|
|
||||||
|
|
||||||
// Initialize performance monitoring and GPU management
|
// Initialize performance monitoring and GPU management
|
||||||
const perfMonitor = new PerformanceMonitor();
|
const perfMonitor = new PerformanceMonitor();
|
||||||
const gpuFallback = new GPUFallback();
|
const gpuFallback = new GPUFallback();
|
||||||
const gpuConfig = new GPUConfig();
|
const gpuConfig = new GPUConfig();
|
||||||
const pluginManager = new PluginManager();
|
const pluginManager = new PluginManager();
|
||||||
const steamInputManager = new SteamInputManager();
|
|
||||||
|
|
||||||
// Try to enable WebAuthn/platform authenticator features early.
|
// Try to enable WebAuthn/platform authenticator features early.
|
||||||
// This helps Chromium expose platform authenticators (Touch ID / built-in) where supported.
|
// This helps Chromium expose platform authenticators (Touch ID / built-in) where supported.
|
||||||
@@ -265,19 +263,6 @@ ipcMain.on('exit-bigpicture', () => {
|
|||||||
exitBigPictureMode();
|
exitBigPictureMode();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Steam Input bridge
|
|
||||||
ipcMain.handle('steam-input-start', (event) => {
|
|
||||||
return steamInputManager.subscribe(event.sender);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.on('steam-input-stop', (event) => {
|
|
||||||
steamInputManager.unsubscribe(event.sender);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('steam-input-status', () => {
|
|
||||||
return steamInputManager.getStatus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// IPC handler for sending mouse input events to webviews (used by Big Picture Mode)
|
// IPC handler for sending mouse input events to webviews (used by Big Picture Mode)
|
||||||
ipcMain.handle('webview-send-input-event', async (event, { webContentsId, inputEvent }) => {
|
ipcMain.handle('webview-send-input-event', async (event, { webContentsId, inputEvent }) => {
|
||||||
try {
|
try {
|
||||||
@@ -691,14 +676,6 @@ app.on('window-all-closed', () => {
|
|||||||
if (process.platform !== 'darwin') app.quit();
|
if (process.platform !== 'darwin') app.quit();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
try {
|
|
||||||
steamInputManager?.dispose?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] dispose failed:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ipcMain handlers
|
// ipcMain handlers
|
||||||
|
|
||||||
// --- Auto-Update IPC handlers ---
|
// --- Auto-Update IPC handlers ---
|
||||||
|
|||||||
Generated
+5
-42
@@ -1,22 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "nebula",
|
"name": "nebula",
|
||||||
"version": "1.3.2",
|
"version": "1.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "nebula",
|
"name": "nebula",
|
||||||
"version": "1.3.2",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.1.6",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2"
|
||||||
"steamworks.js": "^0.4.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^10.1.0",
|
|
||||||
"electron": "^39.2.7",
|
"electron": "^39.2.7",
|
||||||
"electron-builder": "^23.0.0",
|
"electron-builder": "^23.0.0",
|
||||||
"electron-nightly": "^39.0.0-nightly.20250811"
|
"electron-nightly": "^39.0.0-nightly.20250811"
|
||||||
@@ -120,13 +118,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@epic-web/invariant": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@malept/cross-spawn-promise": {
|
"node_modules/@malept/cross-spawn-promise": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz",
|
||||||
@@ -322,6 +313,7 @@
|
|||||||
"version": "22.16.3",
|
"version": "22.16.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.3.tgz",
|
||||||
"integrity": "sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==",
|
"integrity": "sha512-sr4Xz74KOUeYadexo1r8imhRtlVXcs+j3XK3TcoiYk7B1t3YRVJgtaD3cwX73NYb71pmVuMLNRhJ9XKdoDB74g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
@@ -1105,24 +1097,6 @@
|
|||||||
"buffer": "^5.1.0"
|
"buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cross-env": {
|
|
||||||
"version": "10.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
|
|
||||||
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@epic-web/invariant": "^1.0.0",
|
|
||||||
"cross-spawn": "^7.0.6"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"cross-env": "dist/bin/cross-env.js",
|
|
||||||
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -3105,18 +3079,6 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/steamworks.js": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/steamworks.js/-/steamworks.js-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-O5TTRs7ucCRql4IA/kYUIQYeghTsXqf3rAm81sC22RDId264LQYqQjuaMEUSqL60I5LdULiGu0W2/A+ZDcKBKA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||||
@@ -3292,6 +3254,7 @@
|
|||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
|
|||||||
+5
-15
@@ -5,26 +5,22 @@
|
|||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"start:steam-deck": "NEBULA_PROFILE=steam-deck electron .",
|
"start:dev": "electron . --no-sandbox --disable-gpu",
|
||||||
"start:big-picture": "NEBULA_PROFILE=big-picture electron .",
|
"start:linux": "electron . --no-sandbox",
|
||||||
"dist": "electron-builder",
|
"dist": "electron-builder",
|
||||||
"dist:steam-deck": "set NEBULA_PROFILE=steam-deck && electron-builder",
|
|
||||||
"dist:big-picture": "set NEBULA_PROFILE=big-picture && electron-builder",
|
|
||||||
"run": "electron ."
|
"run": "electron ."
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "A lightweight, privacy-focused browser with controller-friendly Big Picture Mode for gaming and Steam Deck",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "^3.1.6",
|
"dompurify": "^3.1.6",
|
||||||
"electron-updater": "^6.6.2",
|
"electron-updater": "^6.6.2",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2"
|
||||||
"steamworks.js": "^0.4.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^10.1.0",
|
|
||||||
"electron": "^39.2.7",
|
"electron": "^39.2.7",
|
||||||
"electron-builder": "^23.0.0",
|
"electron-builder": "^23.0.0",
|
||||||
"electron-nightly": "^39.0.0-nightly.20250811"
|
"electron-nightly": "^39.0.0-nightly.20250811"
|
||||||
@@ -46,13 +42,7 @@
|
|||||||
"icon": "assets/images/Logos/Nebula-Favicon.ico"
|
"icon": "assets/images/Logos/Nebula-Favicon.ico"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": [
|
"icon": "assets/images/Logos/Nebula-Favicon.png"
|
||||||
"AppImage",
|
|
||||||
"tar.gz"
|
|
||||||
],
|
|
||||||
"icon": "assets/images/Logos/Nebula-Favicon.png",
|
|
||||||
"category": "Utility",
|
|
||||||
"maintainer": "NebulaBrowser Contributors"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-12
@@ -160,18 +160,6 @@ contextBridge.exposeInMainWorld('bigPictureAPI', {
|
|||||||
ipcRenderer.invoke('webview-send-input-event', { webContentsId, inputEvent })
|
ipcRenderer.invoke('webview-send-input-event', { webContentsId, inputEvent })
|
||||||
});
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('steamInputAPI', {
|
|
||||||
start: () => ipcRenderer.invoke('steam-input-start'),
|
|
||||||
stop: () => ipcRenderer.send('steam-input-stop'),
|
|
||||||
getStatus: () => ipcRenderer.invoke('steam-input-status'),
|
|
||||||
onState: (handler) => {
|
|
||||||
if (typeof handler !== 'function') return () => {};
|
|
||||||
const wrapped = (_event, payload) => handler(payload);
|
|
||||||
ipcRenderer.on('steam-input-state', wrapped);
|
|
||||||
return () => ipcRenderer.removeListener('steam-input-state', wrapped);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Relay context-menu commands from main to active renderer context (open new tabs etc.)
|
// Relay context-menu commands from main to active renderer context (open new tabs etc.)
|
||||||
ipcRenderer.on('context-menu-command', (event, payload) => {
|
ipcRenderer.on('context-menu-command', (event, payload) => {
|
||||||
window.dispatchEvent(new CustomEvent('nebula-context-command', { detail: payload }));
|
window.dispatchEvent(new CustomEvent('nebula-context-command', { detail: payload }));
|
||||||
|
|||||||
+125
-192
@@ -144,13 +144,8 @@ const state = {
|
|||||||
// Gamepad
|
// Gamepad
|
||||||
gamepadConnected: false,
|
gamepadConnected: false,
|
||||||
gamepadIndex: null,
|
gamepadIndex: null,
|
||||||
lastInput: {},
|
lastInput: { x: 0, y: 0 },
|
||||||
inputRepeatTimer: null,
|
inputRepeatTimer: null,
|
||||||
legacyGamepadEnabled: false,
|
|
||||||
useSteamInput: false,
|
|
||||||
steamInputStatus: null,
|
|
||||||
steamInputUnsubscribe: null,
|
|
||||||
steamInputCleanupBound: false,
|
|
||||||
|
|
||||||
// Virtual cursor for webview
|
// Virtual cursor for webview
|
||||||
cursorEnabled: false,
|
cursorEnabled: false,
|
||||||
@@ -653,63 +648,12 @@ function goForward() {
|
|||||||
// GAMEPAD SUPPORT
|
// GAMEPAD SUPPORT
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
function processDirectionalInput(direction, isPressed) {
|
function initGamepadSupport() {
|
||||||
const now = Date.now();
|
|
||||||
if (isPressed && !state.lastInput[direction]) {
|
|
||||||
navigateFocus(direction);
|
|
||||||
state.lastInput[direction] = now;
|
|
||||||
} else if (!isPressed) {
|
|
||||||
state.lastInput[direction] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function processButtonInput(key, pressed, handler) {
|
|
||||||
if (pressed && !state.lastInput[key]) {
|
|
||||||
handler();
|
|
||||||
state.lastInput[key] = true;
|
|
||||||
} else if (!pressed) {
|
|
||||||
state.lastInput[key] = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCursorSpeedToggle() {
|
|
||||||
state.cursorSpeed = state.cursorSpeed === 15 ? 8 : (state.cursorSpeed === 8 ? 25 : 15);
|
|
||||||
showToast(`Cursor speed: ${state.cursorSpeed === 8 ? 'Slow' : state.cursorSpeed === 15 ? 'Normal' : 'Fast'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initGamepadSupport() {
|
|
||||||
if (window.steamInputAPI) {
|
|
||||||
try {
|
|
||||||
const status = await window.steamInputAPI.start();
|
|
||||||
state.steamInputStatus = status;
|
|
||||||
if (status?.enabled) {
|
|
||||||
state.useSteamInput = true;
|
|
||||||
if (!state.steamInputCleanupBound) {
|
|
||||||
window.addEventListener('beforeunload', cleanupSteamInput);
|
|
||||||
state.steamInputCleanupBound = true;
|
|
||||||
}
|
|
||||||
state.steamInputUnsubscribe = window.steamInputAPI.onState(handleSteamInputState);
|
|
||||||
console.log('[BigPicture] Steam Input enabled via steamworks.js');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('[BigPicture] Steam Input unavailable, falling back:', status?.reason);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[BigPicture] Failed to initialize Steam Input:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initLegacyGamepadSupport();
|
|
||||||
}
|
|
||||||
|
|
||||||
function initLegacyGamepadSupport() {
|
|
||||||
if (state.legacyGamepadEnabled) return;
|
|
||||||
if (!navigator.getGamepads) {
|
if (!navigator.getGamepads) {
|
||||||
console.warn('[BigPicture] Gamepad API not available in this environment');
|
console.warn('[BigPicture] Gamepad API not available in this environment');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.legacyGamepadEnabled = true;
|
|
||||||
|
|
||||||
// Note: On Linux (and some controllers like handheld integrated gamepads),
|
// Note: On Linux (and some controllers like handheld integrated gamepads),
|
||||||
// the `gamepadconnected` event may not fire until the first button press,
|
// the `gamepadconnected` event may not fire until the first button press,
|
||||||
// or at all. We rely on continuous polling for robustness.
|
// or at all. We rely on continuous polling for robustness.
|
||||||
@@ -787,10 +731,6 @@ function refreshActiveGamepad(isInitial = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pollGamepad() {
|
function pollGamepad() {
|
||||||
if (!state.legacyGamepadEnabled || state.useSteamInput) {
|
|
||||||
requestAnimationFrame(pollGamepad);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { active } = refreshActiveGamepad(false);
|
const { active } = refreshActiveGamepad(false);
|
||||||
if (active) {
|
if (active) {
|
||||||
handleGamepadInput(active);
|
handleGamepadInput(active);
|
||||||
@@ -800,11 +740,9 @@ function pollGamepad() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleGamepadInput(gamepad) {
|
function handleGamepadInput(gamepad) {
|
||||||
if (state.useSteamInput) return;
|
|
||||||
|
|
||||||
// D-pad and left stick for navigation
|
// D-pad and left stick for navigation
|
||||||
const leftX = gamepad.axes[0] || 0;
|
const leftX = gamepad.axes[0];
|
||||||
const leftY = gamepad.axes[1] || 0;
|
const leftY = gamepad.axes[1];
|
||||||
|
|
||||||
// D-pad buttons (indices may vary by controller)
|
// D-pad buttons (indices may vary by controller)
|
||||||
const dpadUp = gamepad.buttons[12]?.pressed;
|
const dpadUp = gamepad.buttons[12]?.pressed;
|
||||||
@@ -818,49 +756,122 @@ function handleGamepadInput(gamepad) {
|
|||||||
const stickLeft = leftX < -CONFIG.STICK_DEADZONE;
|
const stickLeft = leftX < -CONFIG.STICK_DEADZONE;
|
||||||
const stickRight = leftX > CONFIG.STICK_DEADZONE;
|
const stickRight = leftX > CONFIG.STICK_DEADZONE;
|
||||||
|
|
||||||
|
// When cursor is enabled (viewing a webpage), only D-Pad navigates sidebar
|
||||||
|
// Left stick is ignored for UI navigation in webview mode
|
||||||
const inWebviewMode = state.cursorEnabled && state.currentWebview;
|
const inWebviewMode = state.cursorEnabled && state.currentWebview;
|
||||||
|
|
||||||
|
// Combine inputs - but only use D-Pad when in webview mode
|
||||||
const up = inWebviewMode ? dpadUp : (dpadUp || stickUp);
|
const up = inWebviewMode ? dpadUp : (dpadUp || stickUp);
|
||||||
const down = inWebviewMode ? dpadDown : (dpadDown || stickDown);
|
const down = inWebviewMode ? dpadDown : (dpadDown || stickDown);
|
||||||
const left = inWebviewMode ? dpadLeft : (dpadLeft || stickLeft);
|
const left = inWebviewMode ? dpadLeft : (dpadLeft || stickLeft);
|
||||||
const right = inWebviewMode ? dpadRight : (dpadRight || stickRight);
|
const right = inWebviewMode ? dpadRight : (dpadRight || stickRight);
|
||||||
|
|
||||||
processDirectionalInput('up', up);
|
// Navigation with repeat prevention
|
||||||
processDirectionalInput('down', down);
|
const now = Date.now();
|
||||||
processDirectionalInput('left', left);
|
|
||||||
processDirectionalInput('right', right);
|
|
||||||
|
|
||||||
processButtonInput('a', gamepad.buttons[0]?.pressed, activateFocused);
|
if (up && !state.lastInput.up) {
|
||||||
processButtonInput('b', gamepad.buttons[1]?.pressed, () => goBack());
|
navigateFocus('up');
|
||||||
processButtonInput('x', gamepad.buttons[2]?.pressed, () => {
|
state.lastInput.up = now;
|
||||||
if (state.oskVisible) backspaceOSK();
|
} else if (!up) {
|
||||||
});
|
state.lastInput.up = 0;
|
||||||
processButtonInput('y', gamepad.buttons[3]?.pressed, () => {
|
}
|
||||||
|
|
||||||
|
if (down && !state.lastInput.down) {
|
||||||
|
navigateFocus('down');
|
||||||
|
state.lastInput.down = now;
|
||||||
|
} else if (!down) {
|
||||||
|
state.lastInput.down = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left && !state.lastInput.left) {
|
||||||
|
navigateFocus('left');
|
||||||
|
state.lastInput.left = now;
|
||||||
|
} else if (!left) {
|
||||||
|
state.lastInput.left = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right && !state.lastInput.right) {
|
||||||
|
navigateFocus('right');
|
||||||
|
state.lastInput.right = now;
|
||||||
|
} else if (!right) {
|
||||||
|
state.lastInput.right = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A button (usually index 0) - Always select/activate focused menu item
|
||||||
|
if (gamepad.buttons[0]?.pressed && !state.lastInput.a) {
|
||||||
|
activateFocused();
|
||||||
|
state.lastInput.a = true;
|
||||||
|
} else if (!gamepad.buttons[0]?.pressed) {
|
||||||
|
state.lastInput.a = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// B button (usually index 1) - Back/Close OSK
|
||||||
|
if (gamepad.buttons[1]?.pressed && !state.lastInput.b) {
|
||||||
|
goBack();
|
||||||
|
state.lastInput.b = true;
|
||||||
|
} else if (!gamepad.buttons[1]?.pressed) {
|
||||||
|
state.lastInput.b = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// X button (usually index 2) - Backspace when OSK is open
|
||||||
|
if (gamepad.buttons[2]?.pressed && !state.lastInput.x) {
|
||||||
|
if (state.oskVisible) {
|
||||||
|
backspaceOSK();
|
||||||
|
}
|
||||||
|
state.lastInput.x = true;
|
||||||
|
} else if (!gamepad.buttons[2]?.pressed) {
|
||||||
|
state.lastInput.x = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Y button (usually index 3) - Space when OSK open, otherwise open search
|
||||||
|
if (gamepad.buttons[3]?.pressed && !state.lastInput.y) {
|
||||||
if (state.oskVisible) {
|
if (state.oskVisible) {
|
||||||
appendToOSK(' ');
|
appendToOSK(' ');
|
||||||
} else {
|
} else {
|
||||||
openOSK('search');
|
openOSK('search');
|
||||||
}
|
}
|
||||||
});
|
state.lastInput.y = true;
|
||||||
processButtonInput('lb', gamepad.buttons[4]?.pressed, () => {
|
} else if (!gamepad.buttons[3]?.pressed) {
|
||||||
|
state.lastInput.y = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LB button (usually index 4) - Go back in webview / clear OSK
|
||||||
|
if (gamepad.buttons[4]?.pressed && !state.lastInput.lb) {
|
||||||
if (state.oskVisible) {
|
if (state.oskVisible) {
|
||||||
clearOSK();
|
clearOSK();
|
||||||
} else if (state.currentSection === 'browse' && state.currentWebview) {
|
} else if (state.currentSection === 'browse' && state.currentWebview) {
|
||||||
goBack();
|
goBack();
|
||||||
}
|
}
|
||||||
});
|
state.lastInput.lb = true;
|
||||||
processButtonInput('rb', gamepad.buttons[5]?.pressed, () => {
|
} else if (!gamepad.buttons[4]?.pressed) {
|
||||||
|
state.lastInput.lb = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RB button (usually index 5) - Go forward in webview / submit OSK
|
||||||
|
if (gamepad.buttons[5]?.pressed && !state.lastInput.rb) {
|
||||||
if (state.oskVisible) {
|
if (state.oskVisible) {
|
||||||
submitOSK();
|
submitOSK();
|
||||||
} else if (state.currentSection === 'browse' && state.currentWebview) {
|
} else if (state.currentSection === 'browse' && state.currentWebview) {
|
||||||
goForward();
|
goForward();
|
||||||
}
|
}
|
||||||
});
|
state.lastInput.rb = true;
|
||||||
processButtonInput('select', gamepad.buttons[8]?.pressed, () => {
|
} else if (!gamepad.buttons[5]?.pressed) {
|
||||||
|
state.lastInput.rb = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back/Select button (usually index 8) - Toggle sidebar when in webview
|
||||||
|
if (gamepad.buttons[8]?.pressed && !state.lastInput.select) {
|
||||||
if (state.currentSection === 'browse' && state.currentWebview) {
|
if (state.currentSection === 'browse' && state.currentWebview) {
|
||||||
toggleSidebar();
|
toggleSidebar();
|
||||||
}
|
}
|
||||||
});
|
state.lastInput.select = true;
|
||||||
processButtonInput('start', gamepad.buttons[9]?.pressed, () => {
|
} else if (!gamepad.buttons[8]?.pressed) {
|
||||||
|
state.lastInput.select = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start button (usually index 9) - Menu / Toggle sidebar when viewing webpage
|
||||||
|
if (gamepad.buttons[9]?.pressed && !state.lastInput.start) {
|
||||||
|
// If viewing a webpage, toggle sidebar instead of going to settings
|
||||||
if (state.currentSection === 'browse' && state.currentWebview) {
|
if (state.currentSection === 'browse' && state.currentWebview) {
|
||||||
toggleSidebar();
|
toggleSidebar();
|
||||||
} else if (state.currentSection !== 'settings') {
|
} else if (state.currentSection !== 'settings') {
|
||||||
@@ -868,138 +879,60 @@ function handleGamepadInput(gamepad) {
|
|||||||
} else {
|
} else {
|
||||||
switchSection('home');
|
switchSection('home');
|
||||||
}
|
}
|
||||||
});
|
state.lastInput.start = true;
|
||||||
|
} else if (!gamepad.buttons[9]?.pressed) {
|
||||||
|
state.lastInput.start = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtual cursor handling when webview is active
|
||||||
if (state.cursorEnabled && state.currentWebview) {
|
if (state.cursorEnabled && state.currentWebview) {
|
||||||
|
// Right stick for cursor movement
|
||||||
const rightX = gamepad.axes[2] || 0;
|
const rightX = gamepad.axes[2] || 0;
|
||||||
const rightY = gamepad.axes[3] || 0;
|
const rightY = gamepad.axes[3] || 0;
|
||||||
|
|
||||||
|
// Apply deadzone
|
||||||
const deadzone = 0.15;
|
const deadzone = 0.15;
|
||||||
const moveX = Math.abs(rightX) > deadzone ? rightX : 0;
|
const moveX = Math.abs(rightX) > deadzone ? rightX : 0;
|
||||||
const moveY = Math.abs(rightY) > deadzone ? rightY : 0;
|
const moveY = Math.abs(rightY) > deadzone ? rightY : 0;
|
||||||
|
|
||||||
if (moveX !== 0 || moveY !== 0) {
|
if (moveX !== 0 || moveY !== 0) {
|
||||||
moveCursor(moveX * state.cursorSpeed, moveY * state.cursorSpeed);
|
moveCursor(moveX * state.cursorSpeed, moveY * state.cursorSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Left stick for scrolling in webview mode
|
||||||
const scrollDeadzone = 0.25;
|
const scrollDeadzone = 0.25;
|
||||||
const scrollX = Math.abs(leftX) > scrollDeadzone ? leftX : 0;
|
const scrollX = Math.abs(leftX) > scrollDeadzone ? leftX : 0;
|
||||||
const scrollY = Math.abs(leftY) > scrollDeadzone ? leftY : 0;
|
const scrollY = Math.abs(leftY) > scrollDeadzone ? leftY : 0;
|
||||||
|
|
||||||
if (scrollX !== 0 || scrollY !== 0) {
|
if (scrollX !== 0 || scrollY !== 0) {
|
||||||
scrollWebview(scrollY * 20, scrollX * 20);
|
scrollWebview(scrollY * 20, scrollX * 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
processButtonInput('rt', gamepad.buttons[7]?.pressed, () => virtualClick());
|
// Right trigger (index 7) - Left click
|
||||||
processButtonInput('lt', gamepad.buttons[6]?.pressed, () => virtualClick(true));
|
if (gamepad.buttons[7]?.pressed && !state.lastInput.rt) {
|
||||||
processButtonInput('rs', gamepad.buttons[11]?.pressed, handleCursorSpeedToggle);
|
virtualClick();
|
||||||
}
|
state.lastInput.rt = true;
|
||||||
}
|
} else if (!gamepad.buttons[7]?.pressed) {
|
||||||
|
state.lastInput.rt = false;
|
||||||
function handleSteamInputState(payload) {
|
|
||||||
if (!payload || !payload.connected || !payload.controller) {
|
|
||||||
state.useSteamInput = false;
|
|
||||||
if (!state.legacyGamepadEnabled) {
|
|
||||||
initLegacyGamepadSupport();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.gamepadConnected = true;
|
// Left trigger (index 6) - Right click
|
||||||
state.useSteamInput = true;
|
if (gamepad.buttons[6]?.pressed && !state.lastInput.lt) {
|
||||||
const controller = payload.controller;
|
virtualClick(true);
|
||||||
const nav = controller.nav || {};
|
state.lastInput.lt = true;
|
||||||
const buttons = controller.buttons || {};
|
} else if (!gamepad.buttons[6]?.pressed) {
|
||||||
const analog = controller.analog || {};
|
state.lastInput.lt = false;
|
||||||
const triggers = analog.triggers || { left: 0, right: 0 };
|
|
||||||
|
|
||||||
processDirectionalInput('up', !!nav.up);
|
|
||||||
processDirectionalInput('down', !!nav.down);
|
|
||||||
processDirectionalInput('left', !!nav.left);
|
|
||||||
processDirectionalInput('right', !!nav.right);
|
|
||||||
|
|
||||||
processButtonInput('a', !!buttons.confirm, activateFocused);
|
|
||||||
processButtonInput('b', !!buttons.back, () => goBack());
|
|
||||||
processButtonInput('x', !!buttons.oskBackspace, () => {
|
|
||||||
if (state.oskVisible) backspaceOSK();
|
|
||||||
});
|
|
||||||
processButtonInput('y', !!buttons.oskSpace, () => {
|
|
||||||
if (state.oskVisible) {
|
|
||||||
appendToOSK(' ');
|
|
||||||
} else {
|
|
||||||
openOSK('search');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const shoulderLeftPressed = !!buttons.shoulderLeft || triggers.left > 0.6;
|
|
||||||
const shoulderRightPressed = !!buttons.shoulderRight || triggers.right > 0.6;
|
|
||||||
|
|
||||||
processButtonInput('lb', shoulderLeftPressed, () => {
|
|
||||||
if (state.oskVisible) {
|
|
||||||
clearOSK();
|
|
||||||
} else if (state.currentSection === 'browse' && state.currentWebview) {
|
|
||||||
goBack();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
processButtonInput('rb', shoulderRightPressed, () => {
|
|
||||||
if (state.oskVisible) {
|
|
||||||
submitOSK();
|
|
||||||
} else if (state.currentSection === 'browse' && state.currentWebview) {
|
|
||||||
goForward();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
processButtonInput('select', !!buttons.toggleSidebar, () => {
|
|
||||||
if (state.currentSection === 'browse' && state.currentWebview) {
|
|
||||||
toggleSidebar();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
processButtonInput('start', !!buttons.menu, () => {
|
|
||||||
if (state.currentSection === 'browse' && state.currentWebview) {
|
|
||||||
toggleSidebar();
|
|
||||||
} else if (state.currentSection !== 'settings') {
|
|
||||||
switchSection('settings');
|
|
||||||
} else {
|
|
||||||
switchSection('home');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
processButtonInput('osk', !!buttons.showOsk, () => {
|
|
||||||
if (state.currentWebview) {
|
|
||||||
openOSKForWebview();
|
|
||||||
} else {
|
|
||||||
openOSK('search');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (state.cursorEnabled && state.currentWebview) {
|
|
||||||
const cursorVec = analog.cursor || { x: 0, y: 0 };
|
|
||||||
const scrollVec = analog.scroll || { x: 0, y: 0 };
|
|
||||||
if (Math.abs(cursorVec.x) > 0.05 || Math.abs(cursorVec.y) > 0.05) {
|
|
||||||
moveCursor(cursorVec.x * state.cursorSpeed, cursorVec.y * state.cursorSpeed);
|
|
||||||
}
|
|
||||||
if (Math.abs(scrollVec.x) > 0.05 || Math.abs(scrollVec.y) > 0.05) {
|
|
||||||
scrollWebview(scrollVec.y * 40, scrollVec.x * 40);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const primaryPressed = !!buttons.cursorPrimary || triggers.right > 0.6;
|
// Right stick click (index 11) - Toggle cursor speed
|
||||||
const secondaryPressed = !!buttons.cursorSecondary || triggers.left > 0.6;
|
if (gamepad.buttons[11]?.pressed && !state.lastInput.rs) {
|
||||||
processButtonInput('rt', primaryPressed, () => virtualClick());
|
state.cursorSpeed = state.cursorSpeed === 15 ? 8 : (state.cursorSpeed === 8 ? 25 : 15);
|
||||||
processButtonInput('lt', secondaryPressed, () => virtualClick(true));
|
showToast(`Cursor speed: ${state.cursorSpeed === 8 ? 'Slow' : state.cursorSpeed === 15 ? 'Normal' : 'Fast'}`);
|
||||||
processButtonInput('rs', !!buttons.cursorSpeed, handleCursorSpeedToggle);
|
state.lastInput.rs = true;
|
||||||
}
|
} else if (!gamepad.buttons[11]?.pressed) {
|
||||||
}
|
state.lastInput.rs = false;
|
||||||
|
|
||||||
function cleanupSteamInput() {
|
|
||||||
if (state.steamInputUnsubscribe) {
|
|
||||||
try { state.steamInputUnsubscribe(); } catch {}
|
|
||||||
state.steamInputUnsubscribe = null;
|
|
||||||
}
|
|
||||||
if (window.steamInputAPI) {
|
|
||||||
try { window.steamInputAPI.stop(); } catch (err) {
|
|
||||||
console.warn('[BigPicture] Failed to stop Steam Input bridge:', err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.useSteamInput = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -1,310 +0,0 @@
|
|||||||
const ACTION_SET_NAME = 'nebula_bigpicture';
|
|
||||||
const DIGITAL_ACTIONS = {
|
|
||||||
up: 'bp_nav_up',
|
|
||||||
down: 'bp_nav_down',
|
|
||||||
left: 'bp_nav_left',
|
|
||||||
right: 'bp_nav_right',
|
|
||||||
confirm: 'bp_confirm',
|
|
||||||
back: 'bp_back',
|
|
||||||
oskBackspace: 'bp_osk_backspace',
|
|
||||||
oskSpace: 'bp_open_search',
|
|
||||||
shoulderLeft: 'bp_shoulder_left',
|
|
||||||
shoulderRight: 'bp_shoulder_right',
|
|
||||||
toggleSidebar: 'bp_toggle_sidebar',
|
|
||||||
menu: 'bp_menu',
|
|
||||||
select: 'bp_select',
|
|
||||||
cursorPrimary: 'bp_cursor_primary',
|
|
||||||
cursorSecondary: 'bp_cursor_secondary',
|
|
||||||
cursorSpeed: 'bp_cursor_speed',
|
|
||||||
showOsk: 'bp_show_osk'
|
|
||||||
};
|
|
||||||
const ANALOG_ACTIONS = {
|
|
||||||
nav: 'bp_nav',
|
|
||||||
cursor: 'bp_cursor_vector',
|
|
||||||
scroll: 'bp_scroll_vector',
|
|
||||||
triggerLeft: 'bp_trigger_left',
|
|
||||||
triggerRight: 'bp_trigger_right'
|
|
||||||
};
|
|
||||||
|
|
||||||
class SteamInputManager {
|
|
||||||
constructor() {
|
|
||||||
this.sdk = null;
|
|
||||||
this.client = null;
|
|
||||||
this.input = null;
|
|
||||||
this.available = false;
|
|
||||||
this.handles = {
|
|
||||||
actionSet: 0n,
|
|
||||||
digital: {},
|
|
||||||
analog: {}
|
|
||||||
};
|
|
||||||
this.handlesReady = false;
|
|
||||||
this.handlesReason = 'uninitialized';
|
|
||||||
this.subscribers = new Map();
|
|
||||||
this.cachedState = { connected: false, timestamp: Date.now() };
|
|
||||||
this.pollInterval = null;
|
|
||||||
this.lastCursorSpeedToggle = 0;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (process.env.NEBULA_DISABLE_STEAMWORKS) {
|
|
||||||
this.handlesReason = 'disabled-via-env';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let steamworks;
|
|
||||||
try {
|
|
||||||
// Lazy require so environments without the redistributable don't crash startup.
|
|
||||||
// eslint-disable-next-line global-require
|
|
||||||
steamworks = require('steamworks.js');
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] steamworks.js unavailable:', err.message);
|
|
||||||
this.handlesReason = 'module-missing';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sdk = steamworks;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const appId = this.resolveAppId();
|
|
||||||
this.client = steamworks.init(appId);
|
|
||||||
this.input = this.client?.input;
|
|
||||||
if (!this.input) throw new Error('Steam Input interface missing');
|
|
||||||
this.input.init();
|
|
||||||
if (typeof steamworks.electronEnableSteamOverlay === 'function') {
|
|
||||||
steamworks.electronEnableSteamOverlay();
|
|
||||||
}
|
|
||||||
this.available = true;
|
|
||||||
this.bootstrapHandles();
|
|
||||||
this.startPolling();
|
|
||||||
console.log('[SteamInput] Steamworks initialized', {
|
|
||||||
appId: appId || 'steam_appid.txt',
|
|
||||||
handlesReady: this.handlesReady
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Failed to initialize Steamworks:', err.message);
|
|
||||||
this.client = null;
|
|
||||||
this.input = null;
|
|
||||||
this.available = false;
|
|
||||||
this.handlesReason = 'init-failed';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveAppId() {
|
|
||||||
const envId = Number(process.env.STEAM_APP_ID || process.env.STEAMWORKS_APPID || process.env.STEAM_APPID);
|
|
||||||
if (Number.isFinite(envId) && envId > 0) {
|
|
||||||
return envId;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrapHandles() {
|
|
||||||
if (!this.input) return;
|
|
||||||
try {
|
|
||||||
const actionSet = this.input.getActionSet?.(ACTION_SET_NAME) || 0n;
|
|
||||||
const digital = {};
|
|
||||||
for (const [key, actionName] of Object.entries(DIGITAL_ACTIONS)) {
|
|
||||||
digital[key] = this.input.getDigitalAction?.(actionName) || 0n;
|
|
||||||
}
|
|
||||||
const analog = {};
|
|
||||||
for (const [key, actionName] of Object.entries(ANALOG_ACTIONS)) {
|
|
||||||
analog[key] = this.input.getAnalogAction?.(actionName) || 0n;
|
|
||||||
}
|
|
||||||
this.handles = { actionSet, digital, analog };
|
|
||||||
|
|
||||||
const directionalDigitalReady = Boolean(digital.up && digital.down && digital.left && digital.right);
|
|
||||||
const confirmReady = Boolean(digital.confirm);
|
|
||||||
const backReady = Boolean(digital.back);
|
|
||||||
const analogNavReady = Boolean(analog.nav);
|
|
||||||
this.handlesReady = Boolean(actionSet && (directionalDigitalReady || analogNavReady) && confirmReady && backReady);
|
|
||||||
this.handlesReason = this.handlesReady ? 'ok' : 'handles-missing';
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Failed to read action handles:', err.message);
|
|
||||||
this.handlesReady = false;
|
|
||||||
this.handlesReason = 'handles-error';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startPolling() {
|
|
||||||
if (!this.available || !this.input || this.pollInterval) return;
|
|
||||||
this.pollInterval = setInterval(() => this.tick(), 16);
|
|
||||||
if (this.pollInterval && typeof this.pollInterval.unref === 'function') {
|
|
||||||
this.pollInterval.unref();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tick() {
|
|
||||||
if (!this.available || !this.input) return;
|
|
||||||
try {
|
|
||||||
this.sdk?.runCallbacks?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] runCallbacks failed:', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
let payload = { connected: false, reason: this.handlesReason, timestamp: Date.now() };
|
|
||||||
|
|
||||||
try {
|
|
||||||
const controllers = this.input.getControllers?.() || [];
|
|
||||||
const controller = controllers.find(Boolean);
|
|
||||||
if (controller && this.handlesReady) {
|
|
||||||
this.activateActionSet(controller);
|
|
||||||
payload = {
|
|
||||||
connected: true,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
controller: this.buildControllerState(controller),
|
|
||||||
reason: 'ok'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Failed to query controller state:', err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cachedState = payload;
|
|
||||||
if (!this.subscribers.size) return;
|
|
||||||
|
|
||||||
for (const [id, wc] of this.subscribers.entries()) {
|
|
||||||
if (wc.isDestroyed?.()) {
|
|
||||||
this.subscribers.delete(id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
wc.send('steam-input-state', payload);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Failed to send state to renderer:', err.message);
|
|
||||||
this.subscribers.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
activateActionSet(controller) {
|
|
||||||
if (!this.handles.actionSet) return;
|
|
||||||
try {
|
|
||||||
controller.activateActionSet?.(this.handles.actionSet);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Failed to activate action set:', err.message);
|
|
||||||
this.handlesReady = false;
|
|
||||||
this.handlesReason = 'activate-failed';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildControllerState(controller) {
|
|
||||||
const navVector = this.readAnalog(controller, this.handles.analog.nav);
|
|
||||||
const cursorVector = this.readAnalog(controller, this.handles.analog.cursor);
|
|
||||||
const scrollVector = this.readAnalog(controller, this.handles.analog.scroll);
|
|
||||||
const triggerLeft = this.readAnalog(controller, this.handles.analog.triggerLeft);
|
|
||||||
const triggerRight = this.readAnalog(controller, this.handles.analog.triggerRight);
|
|
||||||
|
|
||||||
const nav = {
|
|
||||||
up: this.readDigital(controller, this.handles.digital.up) || navVector.y < -0.5,
|
|
||||||
down: this.readDigital(controller, this.handles.digital.down) || navVector.y > 0.5,
|
|
||||||
left: this.readDigital(controller, this.handles.digital.left) || navVector.x < -0.5,
|
|
||||||
right: this.readDigital(controller, this.handles.digital.right) || navVector.x > 0.5
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttons = {
|
|
||||||
confirm: this.readDigital(controller, this.handles.digital.confirm),
|
|
||||||
back: this.readDigital(controller, this.handles.digital.back),
|
|
||||||
oskBackspace: this.readDigital(controller, this.handles.digital.oskBackspace),
|
|
||||||
oskSpace: this.readDigital(controller, this.handles.digital.oskSpace),
|
|
||||||
shoulderLeft: this.readDigital(controller, this.handles.digital.shoulderLeft),
|
|
||||||
shoulderRight: this.readDigital(controller, this.handles.digital.shoulderRight),
|
|
||||||
toggleSidebar: this.readDigital(controller, this.handles.digital.toggleSidebar) || this.readDigital(controller, this.handles.digital.select),
|
|
||||||
menu: this.readDigital(controller, this.handles.digital.menu),
|
|
||||||
cursorPrimary: this.readDigital(controller, this.handles.digital.cursorPrimary),
|
|
||||||
cursorSecondary: this.readDigital(controller, this.handles.digital.cursorSecondary),
|
|
||||||
cursorSpeed: this.readDigital(controller, this.handles.digital.cursorSpeed),
|
|
||||||
showOsk: this.readDigital(controller, this.handles.digital.showOsk)
|
|
||||||
};
|
|
||||||
|
|
||||||
const analog = {
|
|
||||||
nav: navVector,
|
|
||||||
cursor: cursorVector,
|
|
||||||
scroll: scrollVector,
|
|
||||||
triggers: {
|
|
||||||
left: Math.max(Math.abs(triggerLeft.x), Math.abs(triggerLeft.y)),
|
|
||||||
right: Math.max(Math.abs(triggerRight.x), Math.abs(triggerRight.y))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
handle: controller.getHandle?.() || 0n,
|
|
||||||
type: controller.getType?.() || 'Unknown',
|
|
||||||
nav,
|
|
||||||
buttons,
|
|
||||||
analog
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
readDigital(controller, handle) {
|
|
||||||
if (!handle || typeof controller.isDigitalActionPressed !== 'function') return false;
|
|
||||||
try {
|
|
||||||
return controller.isDigitalActionPressed(handle);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Failed to read digital action:', err.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
readAnalog(controller, handle) {
|
|
||||||
if (!handle || typeof controller.getAnalogActionVector !== 'function') {
|
|
||||||
return { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const vec = controller.getAnalogActionVector(handle) || { x: 0, y: 0 };
|
|
||||||
return {
|
|
||||||
x: Number.isFinite(vec.x) ? vec.x : 0,
|
|
||||||
y: Number.isFinite(vec.y) ? vec.y : 0
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Failed to read analog action:', err.message);
|
|
||||||
return { x: 0, y: 0 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(webContents) {
|
|
||||||
if (!webContents) return this.getStatus();
|
|
||||||
const id = webContents.id;
|
|
||||||
this.subscribers.set(id, webContents);
|
|
||||||
webContents.once('destroyed', () => {
|
|
||||||
this.subscribers.delete(id);
|
|
||||||
});
|
|
||||||
if (this.cachedState) {
|
|
||||||
try {
|
|
||||||
webContents.send('steam-input-state', this.cachedState);
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Failed to push cached state:', err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.getStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsubscribe(webContents) {
|
|
||||||
if (!webContents) return;
|
|
||||||
this.subscribers.delete(webContents.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
getStatus() {
|
|
||||||
const steamDeck = Boolean(this.client?.utils?.isSteamRunningOnSteamDeck?.());
|
|
||||||
return {
|
|
||||||
enabled: this.available && this.handlesReady,
|
|
||||||
available: this.available,
|
|
||||||
handlesReady: this.handlesReady,
|
|
||||||
reason: this.handlesReason,
|
|
||||||
steamDeck
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose() {
|
|
||||||
if (this.pollInterval) {
|
|
||||||
clearInterval(this.pollInterval);
|
|
||||||
this.pollInterval = null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
this.input?.shutdown?.();
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[SteamInput] Shutdown failed:', err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = SteamInputManager;
|
|
||||||
Reference in New Issue
Block a user