// Gamepad/controller support. // // Rather than a parallel gamepad->action mapping table, this polls // navigator.getGamepads() and dispatches synthetic keydown events onto // document with the same key/code pairs a physical keyboard would send. // static/js/shortcuts.js's existing dispatcher (scope checks, text-field/ // modal guards, library grid nav, player shortcuts) handles the rest. // // Steam Deck: Steam Input re-emits the Deck's controls as a standard // XInput-style virtual pad (both in Gaming Mode and in Desktop Mode when // launched via a non-Steam shortcut with a controller template), so this // reports mapping: 'standard' and the button layout below lines up with // the Deck's physical ABXY. If a pad reports a non-standard mapping // (e.g. raw HID with no Steam Input in between), this no-ops rather than // guessing button order. // // Plain non-module script; degrades to a no-op without the Gamepad API. (function () { 'use strict'; if (typeof navigator === 'undefined' || !navigator.getGamepads) return; var BUTTON_KEYS = { // Bottom face button (Xbox A / PS Cross "X") — play/pause on the player // screen; also activates the currently-selected library card, since // Space is already treated as an activation key there alongside Enter. 0: { key: ' ', code: 'Space' }, 1: { key: 'Escape', code: 'Escape' }, // Xbox B / PS Circle // 2 (Xbox X / PS Square) intentionally unmapped — undecided. }; var RAIL_REVEAL_BUTTON = 3; // Y — reveals the player screen's left tool rail // The player rail (#v3-player-rail) has no keyboard shortcut to reuse — it's // shown via CSS on #v3-railzone:hover or :focus-within (see v3.css). So // instead of a synthetic keydown, this directly focuses the rail's first // icon, which the existing :focus-within rule already reveals it for — // the same mechanism a Tab-key user gets for free. function revealPlayerRail() { var active = document.querySelector('.screen.active'); if (!active || active.id !== 'player') return; var icon = document.querySelector('#v3-player-rail .v3-rail-icon'); if (icon) icon.focus(); } var DPAD_BUTTONS = { 12: { key: 'ArrowUp', code: 'ArrowUp' }, 13: { key: 'ArrowDown', code: 'ArrowDown' }, 14: { key: 'ArrowLeft', code: 'ArrowLeft' }, 15: { key: 'ArrowRight', code: 'ArrowRight' }, }; var STICK_DEADZONE = 0.5; var REPEAT_DELAY_MS = 400; var REPEAT_INTERVAL_MS = 120; var polling = false; var buttonWasDown = {}; // index -> bool, for edge-detection (no repeat) var dirWasDown = {}; // 'up'/'down'/'left'/'right' -> bool var dirRepeatAt = {}; // 'up'/'down'/'left'/'right' -> timestamp of next repeat var connectedIndices = {}; // gamepad.index -> true, tracks which slots we've announced function fireKey(spec) { // Dispatch on the focused element (falling back to document when nothing // is focused), not document itself. document.activeElement is always an // ancestor-inclusive descendant of document, so this still bubbles up // through every existing document-level listener exactly as before — but // now a focused