mirror of
https://github.com/lhns/steam-frame-nix.git
synced 2026-10-06 09:00:25 +02:00
- pinned-desktop (VERSION 3): drop the stop() of a predecessor global that never existed in this repo; log errors as "sfui <NAME>:" through one guard helper, like the grid patch. - steamCloseButton.enable is a mkEnableOption like the other feature switches; enable descriptions no longer end in a stray newline. - launcher-menu.nix header lists show-all and no longer claims the patches use no webpack module; hiddenApps mentions showAllApps. - Patch comments: frame-controls ring timing matches the code (half of longPressMs, at most 1 s), window-curvature names frame-controls as what moves its control into the bar, steam-close-button no longer refers to an unpublished version.
163 lines
7.3 KiB
JavaScript
163 lines
7.3 KiB
JavaScript
// pinned-desktop: in the VR dashboard's "+" menu (section
|
|
// #VRDashboard_LaunchNonSteamApp), "Desktop" (the nested Plasma session) is
|
|
// pinned above or below the scrolling program list instead of scrolling with
|
|
// it.
|
|
//
|
|
// This file is a function expression, called by the file lib/default.nix
|
|
// (mkPatch) generates: (<this file>)(find, sigs, opts), with find the finder
|
|
// library (lib/finders.js) and opts { position: "top" } or
|
|
// { position: "bottom" } from launcher-menu.nix.
|
|
//
|
|
// DOM patch, evaluated in SharedJSContext (the bar popups share its realm):
|
|
// a timer attaches a MutationObserver to every dashboard bar popup document
|
|
// (g_PopupManager); whenever the menu renders, the stock Desktop item (found
|
|
// through its React fiber: list key == strExePath "steamos-nested-desktop")
|
|
// is hidden by CSS and a pinned block is inserted into the list container:
|
|
// "bottom" appends it after the scroll region (1px separator, then a clone of
|
|
// the item), "top" inserts it right before the scroll region, i.e. below the
|
|
// menu heading (clone, then separator). The clone keeps the item's classes,
|
|
// icon and CSS :hover; clicking it clicks the hidden stock item, so Steam's
|
|
// own handler runs (nav sound + SteamClient.Apps.LaunchNonSteamApp). The
|
|
// list container is a flex column with a max-height, so the scroll region
|
|
// shrinks to make room and the menu keeps its size.
|
|
// Anchors: React props and keys (the list key, the section's `header` prop)
|
|
// and the bar popups' window names ("valve.steam.gamepadui.barpopup...");
|
|
// scripts/check-signatures.mjs checks that Steam's UI still has them
|
|
// ("launcher-menu-pinned-desktop" in lib/signatures.json).
|
|
// unpatch.js (or a new VERSION/position) calls __sfuiPinnedDesktop.stop(),
|
|
// which removes the pinned blocks, CSS, markers, observers and the timer.
|
|
((find, sigs, opts) => {
|
|
const NAME = 'launcher-menu-pinned-desktop';
|
|
const VERSION = 3;
|
|
const POS = opts.position === 'top' ? 'top' : 'bottom';
|
|
const KEY = 'steamos-nested-desktop';
|
|
const HIDDEN = 'data-sfui-desktop-hidden';
|
|
const PIN = 'sfui-pinned-desktop';
|
|
const STYLE_ID = 'sfui-pinned-desktop-style';
|
|
const CSS = `
|
|
[${HIDDEN}] { display: none !important; }
|
|
.${PIN} { flex: 0 0 auto; }
|
|
.${PIN} > .${PIN}-sep { height: 1px; background: rgba(255, 255, 255, 0.1); }
|
|
.${PIN} > [role=button]::after { display: none !important; }`;
|
|
|
|
const prev = window.__sfuiPinnedDesktop;
|
|
if (prev?.version === VERSION && prev.position === POS) { prev.scan(); return 'unchanged'; }
|
|
prev?.stop?.();
|
|
if (!window.g_PopupManager) return 'g_PopupManager missing';
|
|
|
|
const docs = new Map(); // popup document -> MutationObserver
|
|
const guard = (f) => (...a) => { try { f(...a); } catch (e) { console.error(`sfui ${NAME}:`, e); } };
|
|
// The stock Desktop item: role=button element whose fiber ancestors include
|
|
// the list entry keyed by the program's exe path.
|
|
const isDesktop = (el) => {
|
|
const f = find.findFiberUp(el, (x) => x.key != null, 12);
|
|
return !!f && (f.key === KEY || String(f.key).endsWith('/' + KEY));
|
|
};
|
|
// The list container (host div of the section component, which has a `header` prop).
|
|
const listOf = (el) => {
|
|
const f = find.findFiberUp(el, (x) => x.memoizedProps && typeof x.memoizedProps === 'object' &&
|
|
'header' in x.memoizedProps && typeof x.type === 'function', 60);
|
|
let c = f?.child;
|
|
while (c && typeof c.type !== 'string') c = c.child;
|
|
return c?.stateNode ?? null;
|
|
};
|
|
// Child of the list container that holds the (scrolling) items.
|
|
const scrollerOf = (orig, list) => {
|
|
let el = orig;
|
|
while (el && el.parentElement !== list) el = el.parentElement;
|
|
return el;
|
|
};
|
|
|
|
const buildPin = (d, orig) => {
|
|
const w = d.defaultView;
|
|
const pin = d.createElement('div');
|
|
pin.className = PIN;
|
|
const panel = orig.parentElement; // scroll panel: copy its side padding
|
|
const cs = w.getComputedStyle(panel);
|
|
pin.style.paddingLeft = cs.paddingLeft;
|
|
pin.style.paddingRight = cs.paddingRight;
|
|
if (POS === 'top') pin.style.paddingTop = cs.paddingTop;
|
|
else pin.style.paddingBottom = cs.paddingBottom;
|
|
const margin = cs.getPropertyValue('--field-negative-horizontal-margin');
|
|
if (margin) pin.style.setProperty('--field-negative-horizontal-margin', margin);
|
|
const sep = d.createElement('div');
|
|
sep.className = `${PIN}-sep`;
|
|
const item = orig.cloneNode(true);
|
|
item.removeAttribute(HIDDEN);
|
|
item.classList.remove('gpfocus', 'gpfocuswithin');
|
|
item.addEventListener('click', (e) => {
|
|
// Not to React's root listener (the clone has no fiber); the stock item
|
|
// gets its own click instead.
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
if (orig.isConnected) orig.click();
|
|
});
|
|
if (POS === 'top') pin.append(item, sep); else pin.append(sep, item);
|
|
pin.__sfuiOrig = orig;
|
|
pin.__sfuiLabel = orig.textContent;
|
|
return pin;
|
|
};
|
|
|
|
const update = (d) => {
|
|
const pins = [...d.getElementsByClassName(PIN)];
|
|
const hidden = d.querySelector(`[${HIDDEN}]`);
|
|
let orig = hidden?.isConnected && isDesktop(hidden) ? hidden : null;
|
|
if (!orig) {
|
|
for (const el of d.querySelectorAll('[role=button]'))
|
|
if (!el.closest('.' + PIN) && isDesktop(el)) { orig = el; break; }
|
|
}
|
|
for (const el of d.querySelectorAll(`[${HIDDEN}]`)) if (el !== orig) el.removeAttribute(HIDDEN);
|
|
const list = orig && listOf(orig);
|
|
const scroller = list && scrollerOf(orig, list);
|
|
const placed = (p) => POS === 'top' ? p.nextElementSibling === scroller : list.lastElementChild === p;
|
|
const ok = (p) => p.__sfuiOrig === orig && p.parentElement === list && placed(p) &&
|
|
p.__sfuiLabel === orig.textContent;
|
|
let keep = null;
|
|
for (const p of pins) if (!scroller || keep || !ok(p)) p.remove(); else keep = p;
|
|
if (!scroller) return;
|
|
if (!orig.hasAttribute(HIDDEN)) orig.setAttribute(HIDDEN, '');
|
|
if (!keep) {
|
|
const pin = buildPin(d, orig);
|
|
if (POS === 'top') list.insertBefore(pin, scroller); else list.appendChild(pin);
|
|
}
|
|
};
|
|
|
|
const attach = (d) => {
|
|
if (docs.has(d) || !d?.body) return;
|
|
if (!d.getElementById(STYLE_ID)) {
|
|
const s = d.createElement('style');
|
|
s.id = STYLE_ID; s.textContent = CSS;
|
|
(d.head ?? d.documentElement).appendChild(s);
|
|
}
|
|
const obs = new MutationObserver(guard(() => update(d)));
|
|
obs.observe(d.body, { childList: true, subtree: true, characterData: true });
|
|
docs.set(d, obs);
|
|
update(d);
|
|
};
|
|
|
|
const scan = () => {
|
|
for (const [d, obs] of docs) if (!d.defaultView || d.defaultView.closed) { obs.disconnect(); docs.delete(d); }
|
|
for (const p of g_PopupManager.GetPopups())
|
|
if (/barpopup/.test(p.m_strName ?? '')) guard(() => attach(p.window?.document))();
|
|
};
|
|
|
|
const timer = setInterval(scan, 1000);
|
|
const stop = () => {
|
|
clearInterval(timer);
|
|
for (const [d, obs] of docs) {
|
|
obs.disconnect();
|
|
try {
|
|
for (const p of [...d.getElementsByClassName(PIN)]) p.remove();
|
|
for (const el of d.querySelectorAll(`[${HIDDEN}]`)) el.removeAttribute(HIDDEN);
|
|
d.getElementById(STYLE_ID)?.remove();
|
|
} catch {}
|
|
}
|
|
docs.clear();
|
|
if (window.__sfuiPinnedDesktop === state) delete window.__sfuiPinnedDesktop;
|
|
};
|
|
const state = { version: VERSION, position: POS, scan, stop, docs };
|
|
window.__sfuiPinnedDesktop = state;
|
|
scan();
|
|
return `patched (${POS})`;
|
|
})
|