diff --git a/public/luatools.js b/public/luatools.js index eaa20ef..834f2c9 100644 --- a/public/luatools.js +++ b/public/luatools.js @@ -13,7 +13,7 @@ .active-focus { outline: 3px solid #66c0f4 !important; outline-offset: 2px !important; - box-shadow: 0 0 0 4px rgba(102, 192, 244, 0.3), + box-shadow: 0 0 0 4px rgba(11, 19, 24, 0.3), 0 0 12px rgba(102, 192, 244, 0.5) !important; position: relative !important; z-index: 9999 !important; @@ -321,7 +321,7 @@ break; case CONFIG.buttonMap.B: - // B button disabled - users should use modal buttons + // B button disabled - users should use modal buttons console.log("[Gamepad] B button pressed - ignoring"); break; @@ -1608,6 +1608,8 @@ const btn = document.createElement("a"); btn.id = id; btn.href = "#"; + btn.className = "Focusable"; + btn.tabIndex = 0; const btnColors = getThemeColors(); btn.style.cssText = `display:flex;align-items:center;justify-content:center;width:40px;height:40px;background:rgba(${btnColors.rgbString},0.1);border:1px solid ${btnColors.borderRgba};border-radius:10px;color:${btnColors.accent};font-size:18px;text-decoration:none;transition:all 0.3s ease;cursor:pointer;`; btn.innerHTML = ''; @@ -1654,6 +1656,8 @@ const btn = document.createElement("a"); btn.id = id; btn.href = "#"; + btn.className = "Focusable"; + btn.tabIndex = 0; const btnColors = getThemeColors(); btn.style.cssText = `display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;flex:1;background:rgba(${btnColors.rgbString},0.06);border:1px solid ${btnColors.borderRgba};border-radius:12px;color:${btnColors.text};font-size:11px;font-weight:500;text-decoration:none;transition:all 0.2s ease;cursor:pointer;text-align:center;padding:14px 6px;min-width:0;`; const iconHtml = iconClass @@ -1836,7 +1840,7 @@ try { const match = window.location.href.match( - /https:\/\/store\.steampowered\.com\/app\/(\d+)/, + /https:\/\/store\.steampowered\.com\/(?:gamepad\/)?app\/(\d+)/, ) || window.location.href.match( /https:\/\/steamcommunity\.com\/app\/(\d+)/, @@ -1867,7 +1871,7 @@ try { const match = window.location.href.match( - /https:\/\/store\.steampowered\.com\/app\/(\d+)/, + /https:\/\/store\.steampowered\.com\/(?:gamepad\/)?app\/(\d+)/, ) || window.location.href.match( /https:\/\/steamcommunity\.com\/app\/(\d+)/, @@ -2173,7 +2177,9 @@ // finished (just closes). The poll flips the label to "Close" on success. Class kept as // .luatools-hide-btn so the poll's existing selector still finds it. const hideBtn = document.createElement("a"); - hideBtn.className = "luatools-btn luatools-hide-btn"; + hideBtn.className = "luatools-btn luatools-hide-btn Focusable"; + hideBtn.tabIndex = 0; + hideBtn.setAttribute("data-lt-close", "1"); hideBtn.style.cssText = "display:flex;align-items:center;justify-content:center;text-align:center;"; hideBtn.innerHTML = `${lt("Cancel")}`; @@ -2357,7 +2363,9 @@ const okBtn = document.createElement("a"); okBtn.href = "#"; - okBtn.className = "luatools-btn primary"; + okBtn.className = "luatools-btn primary Focusable"; + okBtn.tabIndex = 0; + okBtn.setAttribute("data-lt-close", "1"); okBtn.style.cssText = "min-width:140px;display:flex;align-items:center;justify-content:center;text-align:center;"; okBtn.innerHTML = `${lt("Close")}`; @@ -2447,7 +2455,9 @@ const cancelBtn = document.createElement("a"); cancelBtn.href = "#"; - cancelBtn.className = "luatools-btn"; + cancelBtn.className = "luatools-btn Focusable"; + cancelBtn.tabIndex = 0; + cancelBtn.setAttribute("data-lt-close", "1"); cancelBtn.style.cssText = "flex:1;display:flex;align-items:center;justify-content:center;text-align:center;"; cancelBtn.innerHTML = `${lt("Cancel")}`; @@ -2460,7 +2470,8 @@ }; const confirmBtn = document.createElement("a"); confirmBtn.href = "#"; - confirmBtn.className = "luatools-btn primary"; + confirmBtn.className = "luatools-btn primary Focusable"; + confirmBtn.tabIndex = 0; confirmBtn.style.cssText = "flex:1;display:flex;align-items:center;justify-content:center;text-align:center;"; confirmBtn.innerHTML = `${lt("Confirm")}`; @@ -2538,6 +2549,45 @@ .luatools-pill.green { background: rgba(92, 184, 92, 0.15); color: #5cb85c; border: 1px solid rgba(92, 184, 92, 0.3); } .luatools-pill.yellow { background: rgba(255, 193, 7, 0.15); color: #ffc107; border: 1px solid rgba(255, 193, 7, 0.3); } .luatools-pill.orange { background: rgba(255, 136, 0, 0.15); color: #ff8800; border: 1px solid rgba(255, 136, 0, 0.3); } + + /* ── Big Picture / new-store redesign button stack ── + Sits as the first child of #FeatureTarget_interest-buttons (etc.), i.e. + directly ABOVE the native "Add to your wishlist" button. Buttons reuse + Valve's own btnv6_blue_hoverfade skin so they read as first-party UI, + plus the Focusable class so Steam's spatial gamepad nav picks them up. */ + .luatools-bpm-stack { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 10px; + } + .luatools-bpm-stack .luatools-button, + .luatools-bpm-stack .luatools-restart-button { + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: 100% !important; + min-height: 38px !important; + box-sizing: border-box !important; + position: relative !important; + cursor: pointer; + } + .luatools-bpm-stack .luatools-button span, + .luatools-bpm-stack .luatools-restart-button span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .luatools-bpm-stack .luatools-pills-container { + top: -22px !important; + } + /* Keyboard focus ring fallback (Steam draws its own ring for .Focusable) */ + .luatools-bpm-stack button:focus-visible, + .luatools-header-button:focus-visible, + .luatools-btn:focus-visible { + outline: 2px solid #66c0f4 !important; + outline-offset: 1px !important; + } `; document.head.appendChild(style); // This is now separate from the main style block } @@ -2574,6 +2624,388 @@ } } +function ltGetStoreAppId() { + try { + const m = + window.location.href.match( + /https:\/\/store\.steampowered\.com\/(?:gamepad\/)?app\/(\d+)/, + ) || + window.location.href.match( + /https:\/\/steamcommunity\.com\/app\/(\d+)/, + ); + return m ? parseInt(m[1], 10) : NaN; + } catch (_) { + return NaN; + } + } + + function ltIsNewDesignActionContainer(el) { + return ( + !!el && + (el.id === "gamepadPurchaseOptions" || + el.id === "FeatureTarget_interest-buttons" || + el.id === "FeatureTarget_purchase-options") + ); + } + + function ltPreferredActionContainer() { + return ( + document.getElementById("FeatureTarget_interest-buttons") || + document.getElementById("FeatureTarget_purchase-options") || + document.getElementById("gamepadPurchaseOptions") + ); + } + + // The redesigned pages can render SEVERAL of the stable-id containers (e.g. the cart + // box builds before the wishlist row). Buttons belong in exactly ONE place — above the + // wishlist — so if an earlier invocation left a stack in a non-preferred container, + // move it over here (moving preserves listeners/state) instead of building a duplicate. + function ltAdoptOrphanBpmStack(container) { + const ids = [ + "gamepadPurchaseOptions", + "FeatureTarget_purchase-options", + "FeatureTarget_interest-buttons", + ]; + for (let i = 0; i < ids.length; i++) { + const c = document.getElementById(ids[i]); + if (!c || c === container) continue; + const s = c.querySelector(":scope > .luatools-bpm-stack"); + if (s) { + try { + container.insertBefore(s, container.firstChild); + } catch (_) {} + return s; + } + } + return null; + } + + function ltFindBpmStack() { + const preferred = ltPreferredActionContainer(); + if (preferred) { + const s = preferred.querySelector(":scope > .luatools-bpm-stack"); + if (s) return s; + } + return document.querySelector(".luatools-bpm-stack"); + } + + function ltInsertNewDesignAddButton(stackEl) { + if (!stackEl || stackEl.querySelector(".luatools-button")) return; + const b = document.createElement("button"); + b.type = "button"; + b.tabIndex = 0; + // Valve's own storefront button skin + Focusable => looks AND navigates natively. + // Clicks are handled by the existing delegated .luatools-button document handler, + // exactly like on desktop pages — no per-instance listener needed. + b.className = + "btnv6_blue_hoverfade btn_medium luatools-button Focusable"; + const label = lt("Add via LuaTools"); + b.title = label; + b.setAttribute("data-tooltip-text", label); + const sp = document.createElement("span"); + sp.textContent = label; + b.appendChild(sp); + stackEl.appendChild(b); + window.__LuaToolsButtonInserted = true; + backendLog("LuaTools BPM Add button inserted"); + } + + // Register injected controls with Valve's gamepad focus-nav tree. The redesigned store + // pages run Steam's GPNav system (FocusNavController + InstrumentFocusElements): merely + // having the Focusable class is NOT enough — elements are enumerated into the nav tree at + // render time, so anything injected later is invisible to the d-pad until re-instrumented. + function ltRegisterGamepadFocus(el) { + if (!el) return; + try { + if (typeof window.ForceUpdateFocusElements === "function") { + window.ForceUpdateFocusElements(el); + } else if (typeof window.InstrumentFocusElements === "function") { + window.InstrumentFocusElements(el); + } + } catch (_) {} + // GPNav can finish booting after we insert (g_bGamepadNavReady flips late) — re-run once + // it's up so buttons added during startup still land in the tree. + try { + if ( + typeof window.RunWhenGamepadNavReady === "function" && + !window.__ltNavReadyHooked + ) { + window.__ltNavReadyHooked = true; + window.RunWhenGamepadNavReady(function () { + try { + const s = document.querySelector(".luatools-bpm-stack"); + if (s && typeof window.InstrumentFocusElements === "function") { + window.InstrumentFocusElements(s); + } + } catch (_) {} + }); + } + } catch (_) {} + } + + function ensureNewDesignStoreButtons(container) { + try { + ensureStyles(); + + // Canonical anchor: whichever stable container we treat as "the" button home right now + // (the wishlist row once it exists). If we were handed a different one, manage the + // canonical container instead so buttons never end up in two places. + const preferred = ltPreferredActionContainer(); + if (preferred && preferred !== container) { + ensureNewDesignStoreButtons(preferred); + return; + } + + // Stack = first child of the container → directly above the wishlist button. + let stack = container.querySelector(":scope > .luatools-bpm-stack"); + if (!stack) { + stack = + ltAdoptOrphanBpmStack(container) || + document.createElement("div"); + stack.className = "luatools-bpm-stack"; + container.insertBefore(stack, container.firstChild); + } else if (container.firstChild !== stack) { + container.insertBefore(stack, container.firstChild); + } + + // Restart Steam — always offered. + if (!stack.querySelector(".luatools-restart-button")) { + const rb = document.createElement("button"); + rb.type = "button"; + rb.tabIndex = 0; + rb.className = + "btnv6_blue_hoverfade btn_medium luatools-restart-button Focusable"; + const rtext = lt("Restart Steam"); + rb.title = rtext; + rb.setAttribute("data-tooltip-text", rtext); + const rsp = document.createElement("span"); + rsp.textContent = rtext; + rb.appendChild(rsp); + rb.addEventListener("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + askRestartConfirmation(); + }); + stack.appendChild(rb); + window.__LuaToolsRestartInserted = true; + backendLog("LuaTools BPM Restart button inserted"); + } + + // Add via LuaTools — only while the game isn't managed yet (same presence gate + // and stuck-check self-heal as the desktop path). + if ( + !stack.querySelector(".luatools-button") && + window.__LuaToolsGameAdded !== true && + !window.__LuaToolsButtonInserted + ) { + const appid = ltGetStoreAppId(); + if ( + !isNaN(appid) && + typeof Millennium !== "undefined" && + typeof Millennium.callServerMethod === "function" + ) { + if ( + window.__LuaToolsPresenceCheckInFlight && + window.__LuaToolsPresenceCheckAppId === appid && + Date.now() - (window.__LuaToolsPresenceCheckAt || 0) >= 8000 + ) { + // A previous check never settled — stop treating it as in-flight. + window.__LuaToolsPresenceCheckInFlight = false; + } + if ( + !window.__LuaToolsPresenceCheckInFlight || + window.__LuaToolsPresenceCheckAppId !== appid + ) { + window.__LuaToolsPresenceCheckInFlight = true; + window.__LuaToolsPresenceCheckAt = Date.now(); + window.__LuaToolsPresenceCheckAppId = appid; + window.__LuaToolsCurrentAppId = appid; + Millennium.callServerMethod("luatools", "HasLuaToolsForApp", { + appid, + }) + .then(function (payload) { + return typeof payload === "string" + ? JSON.parse(payload) + : payload; + }) + .then(function (payload) { + window.__LuaToolsPresenceCheckInFlight = false; + if (payload && payload.success && payload.exists === true) { + window.__LuaToolsGameAdded = true; + backendLog( + "LuaTools already present for this app; BPM Add button withheld", + ); + return; + } + ltInsertNewDesignAddButton(ltFindBpmStack()); + }) + .catch(function () { + window.__LuaToolsPresenceCheckInFlight = false; + ltInsertNewDesignAddButton(ltFindBpmStack()); + }); + } + } else { + // No bridge/appid → still show the button; the delegated click path no-ops safely. + ltInsertNewDesignAddButton(stack); + } + } + + // Game became managed while we were open (added from the download popup) → drop it. + if (window.__LuaToolsGameAdded === true) { + const ab = stack.querySelector(".luatools-button"); + if (ab && ab.parentElement) ab.parentElement.removeChild(ab); + } + + // Hand our buttons to Steam's gamepad focus-nav tree so the d-pad can reach them. + ltRegisterGamepadFocus(stack); + + // Pinning observer: React re-renders can remove/reorder our stack; re-assert placement + // and children (debounced so a burst of mutations collapses into one pass). One observer + // per container (WeakMap) so migrating between containers never orphans the old watch. + try { + if (!window.__LuaToolsBpmObservers) + window.__LuaToolsBpmObservers = + typeof WeakMap !== "undefined" ? new WeakMap() : null; + const have = + window.__LuaToolsBpmObservers && + window.__LuaToolsBpmObservers.get(container); + if (!have) { + const obs = new MutationObserver(function () { + clearTimeout(window.__LuaToolsBpmObsTimer); + window.__LuaToolsBpmObsTimer = setTimeout(function () { + try { + ensureNewDesignStoreButtons(container); + } catch (_) {} + }, 250); + }); + obs.observe(container, { childList: true, subtree: false }); + if (window.__LuaToolsBpmObservers) + window.__LuaToolsBpmObservers.set(container, obs); + } + } catch (_) {} + } catch (e) { + backendLog("LuaTools: ensureNewDesignStoreButtons error: " + e); + } + } + + // ── Controller/keyboard overlay plumbing ──────────────────────────────────── + // B/Esc close the TOPMOST open LuaTools overlay by clicking its marked dismiss control + // ([data-lt-close]; Cancel/Close/Dismiss/OK all carry it). Returns true when something closed. + window.__LuaToolsCloseTopOverlay = function () { + try { + var selectors = [ + ".luatools-overlay", + ".luatools-settings-overlay", + ".luatools-alert-overlay", + ".luatools-confirm-overlay", + ".luatools-loadedapps-overlay", + ]; + var top = null; + for (var i = 0; i < selectors.length; i++) { + var found = document.querySelectorAll(selectors[i]); + for (var j = 0; j < found.length; j++) { + var el = found[j]; + var r = el.getBoundingClientRect(); + if (r.width > 0 && r.height > 0) top = el; // later selector list order ≈ z-order + } + } + if (!top) return false; + var closer = + top.querySelector("[data-lt-close]") || + top.querySelector(".luatools-hide-btn"); + if (closer && closer.click) { + closer.click(); + return true; + } + // Last resort: just remove the overlay shell (settings popup has no data-lt-close mark + // on purpose — closing it is done via its own X/settings buttons below). + top.remove(); + return true; + } catch (_) { + return false; + } + }; + + // Keyboard fallbacks for environments where Steam's gamepad layer isn't translating input: + // Esc/Backspace closes our overlays (unless typing in a field), Enter/Space activates the + // focused non-native control inside them (source rows etc.). Registered on WINDOW in the + // CAPTURE phase — window capture fires before every document-level listener, including + // Valve's own page scripts that treat a bare Escape as "navigate back". Without winning + // that race, closing the confirm dialog via keyboard/B also bounced the browser view back. + window.addEventListener( + "keydown", + function (e) { + try { + if (e.key !== "Escape" && e.key !== "Backspace") return; + var t = e.target; + if ( + t && + (t.tagName === "INPUT" || + t.tagName === "TEXTAREA" || + t.tagName === "SELECT" || + t.isContentEditable) + ) + return; + if ( + !document.querySelector( + ".luatools-overlay, .luatools-settings-overlay, .luatools-alert-overlay, .luatools-confirm-overlay, .luatools-loadedapps-overlay", + ) + ) + return; + if (window.__LuaToolsCloseTopOverlay && window.__LuaToolsCloseTopOverlay()) { + e.preventDefault(); + e.stopPropagation(); + } + } catch (_) {} + }, + true, + ); + + window.addEventListener( + "keydown", + function (e) { + try { + if (e.key !== "Enter" && e.key !== " ") return; + var t = e.target; + if (!t || !t.closest) return; + if ( + t.tagName === "INPUT" || + t.tagName === "TEXTAREA" || + t.tagName === "SELECT" || + t.isContentEditable + ) + return; + var ov = t.closest( + ".luatools-overlay, .luatools-settings-overlay, .luatools-alert-overlay, .luatools-confirm-overlay, .luatools-loadedapps-overlay", + ); + if (!ov) return; + var tabindex = t.getAttribute && t.getAttribute("tabindex"); + if (tabindex !== null && tabindex !== "-1") { + e.preventDefault(); + t.click(); + } + } catch (_) {} + }, + true, + ); + + // Watchdog: redesigned pages build the cart box (#gamepadPurchaseOptions) BEFORE the + // wishlist row (#FeatureTarget_interest-buttons), so our first insertion lands in the + // cart area and the page-wide observer stops re-firing once an Add button exists + // (its needGameBtn check sees one already). This cheap poll re-homes the stack into + // whichever container is currently canonical — i.e. above the wishlist as soon as it + // renders — and rebuilds everything if React wiped us outright. + setInterval(function () { + try { + const curStack = document.querySelector(".luatools-bpm-stack"); + const pref = ltPreferredActionContainer(); + if (!pref) return; + if (!curStack || curStack.parentElement !== pref) { + ensureNewDesignStoreButtons(pref); + } + } catch (_) {} + }, 1500); + // Function to add the LuaTools button // Add throttle to prevent excessive executions let lastButtonCheckTime = 0; @@ -2653,6 +3085,7 @@ headerContainer.appendChild(headerBtn); window.__LuaToolsHeaderInserted = true; + ltRegisterGamepadFocus(headerBtn); backendLog("Inserted store header button"); } @@ -2662,20 +3095,53 @@ // Look for the appropriate container based on mode let targetContainer; if (isBigPicture) { - // In Big Picture mode, use the queue button's parent as reference + // In Big Picture mode, try multiple selectors for the button container on store app pages + // #queueBtnFollow is the "Add to Library" / "Follow" button on game pages const queueBtn = document.querySelector("#queueBtnFollow"); - targetContainer = queueBtn ? queueBtn.parentElement : null; + if (queueBtn && queueBtn.parentElement) { + targetContainer = queueBtn.parentElement; + } else { + // Fallback: Big Picture mode store app pages may have different structure. + // Stable new-redesign ids FIRST (wishlist row wins so we land above the wishlist + // button even when several anchors exist), fuzzy class fallbacks last. + targetContainer = + document.querySelector("#FeatureTarget_interest-buttons") || // Primary: Wishlist/Follow/Ignore container + document.querySelector("#FeatureTarget_purchase-options") || // Parent of gamepadPurchaseOptions + document.querySelector("#gamepadPurchaseOptions") || // Add to Cart container + document.querySelector("#queueBtnFollow")?.parentElement || // Legacy Fallback + document.querySelector(".gamepage_buttons") || + document.querySelector(".apphub_ActionButtons") || + document.querySelector("[class*='ActionButtons']") || + document.querySelector("[class*='btn_group']") || + document.querySelector(".store_page_actions") || + document.querySelector(".page_actions_ctn") || + document.querySelector(".block.action_buttons") || + document.querySelector(".glance_tags_ctn"); + } } else { - // In normal mode, use the SteamDB buttons container + // In normal mode, prefer the classic SteamDB/other-sites row; on redesigned app pages + // (no legacy anchors left) fall through to the stable new-design containers so desktop + // users get the same above-the-wishlist placement instead of nothing. targetContainer = document.querySelector(".steamdb-buttons") || document.querySelector("[data-steamdb-buttons]") || - document.querySelector(".apphub_OtherSiteInfo"); + document.querySelector(".apphub_OtherSiteInfo") || + document.querySelector("#FeatureTarget_interest-buttons") || + document.querySelector("#FeatureTarget_purchase-options") || + document.querySelector("#gamepadPurchaseOptions"); } if (targetContainer) { const steamdbContainer = targetContainer; + // New-design containers are React-managed blocks (hashed classes, no siblings): + // the append-lookalike-link flow below would misplace ugly buttons there. Delegate + // to the dedicated above-the-wishlist stack builder instead. + if (ltIsNewDesignActionContainer(steamdbContainer)) { + ensureNewDesignStoreButtons(steamdbContainer); + return; + } + // Keep our buttons in a deterministic spot. This row is filled asynchronously — Steam // injects the external-site links and Community Hub AFTER page load (and the SteamDB // extension its icon), which used to leave our buttons wherever they first landed, @@ -2717,19 +3183,32 @@ !window.__LuaToolsRestartInserted ) { ensureStyles(); - // In Big Picture mode, use queue button as reference; otherwise use first link in container - const referenceBtn = isBigPicture - ? document.querySelector("#queueBtnFollow") - : steamdbContainer.querySelector("a"); + // In Big Picture mode, try multiple selectors for reference button + let referenceBtn; + if (isBigPicture) { + referenceBtn = document.querySelector("#gamepadPurchaseOptions button") || + document.querySelector("#FeatureTarget_interest-buttons button") || + document.querySelector("#FeatureTarget_purchase-options button") || + document.querySelector("#queueBtnFollow") || + document.querySelector(".gamepage_buttons a") || + document.querySelector(".apphub_ActionButtons a") || + document.querySelector("[class*='ActionButtons'] a") || + document.queryselector("[class*='btn_group'] a") || + document.querySelector(".store_page_actions a") || + document.querySelector(".page_actions_ctn a") || + document.querySelector(".block.action_buttons a"); + } else { + referenceBtn = steamdbContainer.querySelector("a"); + } // Use same custom button for both modes const restartBtn = document.createElement("a"); if (referenceBtn && referenceBtn.className) { restartBtn.className = - referenceBtn.className + " luatools-restart-button"; + referenceBtn.className + " luatools-restart-button Focusable"; } else { restartBtn.className = - "btnv6_blue_hoverfade btn_medium luatools-restart-button"; + "btnv6_blue_hoverfade btn_medium luatools-restart-button Focusable"; } restartBtn.href = "#"; const restartText = lt("Restart Steam"); @@ -2779,10 +3258,23 @@ // Check if button already exists to avoid duplicates if (!existingBtn && !window.__LuaToolsButtonInserted) { // Create the LuaTools button modeled after existing SteamDB/PCGW buttons - // In Big Picture mode, use queue button as reference; otherwise use first link in container - let referenceBtn = isBigPicture - ? document.querySelector("#queueBtnFollow") - : steamdbContainer.querySelector("a"); + // In Big Picture mode, try multiple selectors for reference button + let referenceBtn; + if (isBigPicture) { + referenceBtn = document.querySelector("#gamepadPurchaseOptions button") || + document.querySelector("#FeatureTarget_interest-buttons button") || + document.querySelector("#FeatureTarget_purchase-options button") || + document.querySelector("#queueBtnFollow") || + document.querySelector(".gamepage_buttons a") || + document.querySelector(".apphub_ActionButtons a") || + document.querySelector("[class*='ActionButtons'] a") || + document.querySelector("[class*='btn_group'] a") || + document.querySelector(".store_page_actions a") || + document.querySelector(".page_actions_ctn a") || + document.querySelector(".block.action_buttons a"); + } else { + referenceBtn = steamdbContainer.querySelector("a"); + } // Use same custom button for both modes const luatoolsButton = document.createElement("a"); @@ -2790,10 +3282,10 @@ // Copy classes from an existing button to match look-and-feel, but set our own label if (referenceBtn && referenceBtn.className) { luatoolsButton.className = - referenceBtn.className + " luatools-button"; + referenceBtn.className + " luatools-button Focusable"; } else { luatoolsButton.className = - "btnv6_blue_hoverfade btn_medium luatools-button"; + "btnv6_blue_hoverfade btn_medium luatools-button Focusable"; } const span = document.createElement("span"); const addViaText = lt("Add via LuaTools"); @@ -2824,7 +3316,7 @@ try { const match = window.location.href.match( - /https:\/\/store\.steampowered\.com\/app\/(\d+)/, + /https:\/\/store\.steampowered\.com\/(?:gamepad\/)?app\/(\d+)/, ) || window.location.href.match( /https:\/\/steamcommunity\.com\/app\/(\d+)/, @@ -3071,6 +3563,9 @@ } catch (e) { /* ignore */ } + + // Register the legacy-row buttons with Steam's gamepad focus-nav (old-layout BPM pages). + ltRegisterGamepadFocus(steamdbContainer); } else { if (!logState.missingOnce) { backendLog("LuaTools: steamdbContainer not found on this page"); @@ -3271,7 +3766,7 @@ try { const match = window.location.href.match( - /https:\/\/store\.steampowered\.com\/app\/(\d+)/, + /https:\/\/store\.steampowered\.com\/(?:gamepad\/)?app\/(\d+)/, ) || window.location.href.match( /https:\/\/steamcommunity\.com\/app\/(\d+)/, @@ -3370,8 +3865,15 @@ const observer = new MutationObserver(function () { clearTimeout(mutationTimeout); mutationTimeout = setTimeout(function () { + // Check for both normal mode and Big Picture mode containers var gameContainer = document.querySelector( - ".steamdb-buttons, [data-steamdb-buttons], .apphub_OtherSiteInfo, #queueBtnFollow", + ".steamdb-buttons, [data-steamdb-buttons], .apphub_OtherSiteInfo, " + + // Big Picture mode store app page containers (specific IDs first - stable selectors) + "#gamepadPurchaseOptions, #FeatureTarget_interest-buttons, #FeatureTarget_purchase-options, " + + // Big Picture mode fallback (class-based, less stable) + ".gamepage_buttons, .apphub_ActionButtons, [class*='ActionButtons'], " + + "[class*='btn_group'], .store_page_actions, .page_actions_ctn, " + + ".block.action_buttons, .glance_tags_ctn, #queueBtnFollow", ); var needGameBtn = !!gameContainer && @@ -3470,7 +3972,9 @@ "Left click to install, Right click for SteamDB", ); const dismissBtn = document.createElement("a"); - dismissBtn.className = "luatools-btn"; + dismissBtn.className = "luatools-btn Focusable"; + dismissBtn.tabIndex = 0; + dismissBtn.setAttribute("data-lt-close", "1"); dismissBtn.innerHTML = "" + lt("Dismiss") + ""; dismissBtn.href = "#"; dismissBtn.onclick = function (e) {