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) {