mirror of
https://github.com/MichaelScottsman/LiquidAss-Frame.git
synced 2026-10-29 04:02:11 +01:00
694 lines
42 KiB
JavaScript
694 lines
42 KiB
JavaScript
// Glass Shell runtime module "cc" (package C3b): Control Center in the main window (CC-M) and the
|
|
// status pill patch (CC3). Concept: docs/phase2/concepts/control-center.md (CC) §4; PLAN §1.2-§1.8.
|
|
// Contracts: react.md (P2: patch.targets.statusPill, ui.modal, ui.menu, find, actions), runtime.md
|
|
// (P1), reporter.md (P6: data-lgs-plate*), interaction.md (P3: data-lgs-tip). Behind flag wp.c3b.
|
|
// The DOM class names are the contract with theme/31-cc.css part B.
|
|
//
|
|
// What it does while installed:
|
|
// - patches the bar's status pill (P2 target statusPill): activating it opens CC-M while Steam's main
|
|
// window is the page the dashboard shows, else Steam's own Quick Access (CC-C, unchanged); the pill
|
|
// again closes CC-M; the menu button (≡) always opens Steam's Quick Access (CC3);
|
|
// - CC-M is a Steam modal in the main window (ui.modal: the route stays mounted, CC §4.2): three tiles
|
|
// (Now, Controls, Room) and a close circle, each a glass plate (data-lgs-plate, PLAN §1.6);
|
|
// Steam's page layers fade out under it (data-lgs-cc-fade, opacity only), C1a's glass mode goes
|
|
// to windowless through shell.glassMode('*') when the shell runs;
|
|
// - every control calls the handler Steam's own control calls, found by reading Steam's modules
|
|
// (CC §4.10): the toggles are Steam's Quick Access toggles' own value and onChange (extracted
|
|
// from the element Steam's component returns), the sliders ARE Steam's slider components, Room
|
|
// View / Recenter / Playspace are the bar's SteamVR dashboard actions (the bar's own hook),
|
|
// Return to Game is the bar tab's DashboardTabClicked, Power is SteamUIStore.OpenPowerMenu;
|
|
// - More Controls renders Steam's own Quick Access tab panels (Steam's tab list hook) inside CC;
|
|
// the notification list renders Steam's Notifications panel in place in the Now tile;
|
|
// - every side effect goes through act(): run only when P2's actions mode is 'live' (flag
|
|
// actionsLive, a trusted event) and the spy is off; otherwise logged (CC.log, api.log()).
|
|
// A control whose Steam handler is not found is not drawn (its function stays on the bar and in
|
|
// Steam's Quick Access, CC-C). install() fails closed if the pill cannot be patched.
|
|
|
|
/* eslint-disable no-var */
|
|
__LGS_RT.define({
|
|
name: 'cc',
|
|
deps: ['react'],
|
|
flag: 'wp.c3b',
|
|
install(rt) { return ccInstall(rt); },
|
|
remove() { return ccRemove(); },
|
|
});
|
|
|
|
var CC = null;
|
|
|
|
// Steam strings (PLAN §1.15): tokens first; English only on an English UI
|
|
var CC_STR = {
|
|
notifications: ['#QuickAccess_Tab_Notifications_Title', 'Notifications'],
|
|
more: ['#QuickAccess_Tab_Settings_Title', 'Quick Settings'],
|
|
perf: ['#QuickAccess_Tab_Perf_Title', 'Performance'],
|
|
battery: ['#VRDashboardBar_BatteryStatus', 'Battery'],
|
|
qam: ['#VRDashboard_QuickAccessMenu', 'Quick Access Menu'],
|
|
settings: ['#MainTabsSettings', 'Settings'],
|
|
power: ['#Power', 'Power'],
|
|
close: ['#Button_Close', 'Close'],
|
|
back: ['#Button_Back', 'Back'],
|
|
wifi: ['#QuickAccess_Tab_Settings_Section_Shortcuts_Wifi', 'Wi-Fi'],
|
|
bt: ['#QuickAccess_Tab_Bluetooth_ToggleLabel', 'Bluetooth'],
|
|
air: ['#QuickAccess_Tab_Settings_Section_Shortcuts_AirplaneMode', 'Airplane Mode'],
|
|
motion: ['#QuickAccess_Tab_Perf_VRMotionSmoothing', 'Motion Smoothing'],
|
|
off: ['#Settings_Off', 'Off'],
|
|
on: ['#Settings_On', 'On'],
|
|
resume: ['#AppDetails_ResumeGame', 'Resume'],
|
|
notPlaying: [null, 'Not Playing'],
|
|
noNotif: ['#QuickAccess_Tab_Notifications_None', 'No new notifications'],
|
|
};
|
|
var CC_ROOMVIEW_ID = 432800007; // SteamVR "Toggle Room View" (CC §4.10, SY)
|
|
var CC_PLAYSPACE_ID = 432800001; // SteamVR "Playspace Menu"
|
|
|
|
function ccStr(R, key) {
|
|
const t = CC_STR[key]; if (!t) return '';
|
|
let s = null;
|
|
try { s = t[0] ? R.ui.text(t[0], t[1]) : (String(R.ui.lang()).startsWith('en') ? t[1] : null); } catch (_) { s = null; }
|
|
return s || '';
|
|
}
|
|
|
|
// ------------------------------------------------------------------ finders (CC §4.10, read-only)
|
|
function ccFinders() {
|
|
const fn = (v) => typeof v === 'function';
|
|
return {
|
|
qamTabs: [['vrLocation', 'quick-access-menu', '#QuickAccess_Tab_Help_Title'],
|
|
(ex, u) => u.pick(ex, (v, s) => fn(v) && s.includes('vrLocation') && s.includes('useMemo') && s.includes('panel:'))],
|
|
wifi: [['SetWifiEnabled', '#QuickAccess_Tab_Settings_Section_Shortcuts_Wifi'],
|
|
(ex, u) => u.pick(ex, (v, s) => fn(v) && s.includes('SetWifiEnabled') && s.length < 1200)],
|
|
bt: [['"system_bluetooth_enabled"', '#QuickAccess_Tab_Bluetooth_ToggleLabel'],
|
|
(ex, u) => u.pick(ex, (v, s) => fn(v) && s.includes('system_bluetooth_enabled') && !s.includes('rPairedDevices'))],
|
|
vr: [['motion_smoothing_value', 'env_brightness_construct_theater', 'SetDisplayBrightness'], (ex, u) => {
|
|
const motion = u.pick(ex, (v, s) => fn(v) && s.includes('motion_smoothing:'));
|
|
const bright = u.pick(ex, (v, s) => fn(v) && s.includes('SetDisplayBrightness'));
|
|
const env = u.pick(ex, (v, s) => fn(v) && s.includes('env_brightness_construct_theater:'));
|
|
return motion || bright || env ? { motion, bright, env } : null;
|
|
}],
|
|
audio: [['#Audio_OutputVolume', '#Audio_InputVolume'],
|
|
(ex, u) => u.pick(ex, (v, s) => fn(v) && s.includes('#Audio_OutputVolume') && s.includes('aria-label') && s.length < 400)],
|
|
sys: [['AirplaneModeEnabled', 'SetAirplaneMode'], (ex, u) => {
|
|
const hook = u.pick(ex, (v, s) => fn(v) && s.includes('AirplaneModeEnabled') && s.length < 200);
|
|
const store = u.pick(ex, (v) => v && fn(v.Get) && (() => { try { return fn(v.Get().SetAirplaneMode); } catch (_) { return false; } })());
|
|
return hook && store ? { hook, store } : null;
|
|
}],
|
|
bar: [['fnInvokeAction', 'VRDashboardBarSmallButton', 'mutualCapabilities'], (ex, u) => {
|
|
const actions = u.pick(ex, (v, s) => fn(v) && s.includes('mutualCapabilities') && s.includes('defined_actions') && s.length < 2000);
|
|
const invoke = u.pick(ex, (v, s) => fn(v) && s.includes('fnInvokeAction') && s.includes('bVisuallyActive'));
|
|
const tabs = u.pick(ex, (v, s) => fn(v) && s.includes('rgTabsToShow') && s.includes('vr_steam_tab_id'));
|
|
return actions && invoke ? { actions, invoke, tabs } : null;
|
|
}],
|
|
vrMsg: [['VRGamepadUIMessages', 'm_SteamVR_ClientMethods'], (ex) => {
|
|
for (const k of Object.keys(ex)) {
|
|
let v; try { v = ex[k]; } catch (_) { continue; }
|
|
if (v && v.SteamVR && typeof v.SteamVR.DashboardTabClicked === 'function') return v;
|
|
}
|
|
return null;
|
|
}],
|
|
icons: [['AirplaneMode:()=>', 'Recenter:()=>', 'Playspace:()=>'],
|
|
(ex) => (ex && ex.WiFi && ex.AirplaneMode && ex.Settings ? ex : null)],
|
|
};
|
|
}
|
|
|
|
// ------------------------------------------------------------------ helpers
|
|
function ccNavNode(R, el) {
|
|
for (let f = el ? R.fiber.of(el) : null, i = 0; f && i < 12; i++, f = f.return) {
|
|
const n = f.memoizedProps && f.memoizedProps.node;
|
|
if (n && typeof n.BTakeFocus === 'function') return n;
|
|
}
|
|
return null;
|
|
}
|
|
function ccFocusEl(R, el) { const n = ccNavNode(R, el); if (!n) return false; try { n.BTakeFocus(3); return true; } catch (_) { return false; } }
|
|
|
|
// The callable behind a React element type (function, memo, forwardRef); null for classes and hosts.
|
|
function ccCallable(t) {
|
|
if (typeof t === 'function') return t.prototype && t.prototype.isReactComponent ? null : t;
|
|
if (t && t.$$typeof) {
|
|
const s = String(t.$$typeof);
|
|
if (s.includes('memo')) return ccCallable(t.type);
|
|
if (s.includes('forward_ref') && typeof t.render === 'function') return (p) => t.render(p, null);
|
|
}
|
|
return null;
|
|
}
|
|
// The first element with a boolean `checked` and an `onChange` in Steam's returned tree. The search
|
|
// never calls a component until the plain tree is exhausted, then calls the FIRST component in tree
|
|
// order (its hooks become ours, in a fixed order). Depth-limited.
|
|
function ccFindToggle(el, depth) {
|
|
if (!el || depth > 4) return null;
|
|
const q = [el]; let comp = null;
|
|
while (q.length) {
|
|
const e = q.shift();
|
|
if (!e || typeof e !== 'object') continue;
|
|
if (Array.isArray(e)) { for (const x of e) q.push(x); continue; }
|
|
const p = e.props; if (!p) continue;
|
|
if (typeof p.onChange === 'function' && typeof p.checked === 'boolean') return e;
|
|
if (!comp && typeof e.type !== 'string' && ccCallable(e.type)) comp = e;
|
|
if (p.children) q.push(p.children);
|
|
}
|
|
if (!comp) return null;
|
|
return ccFindToggle(ccCallable(comp.type)(comp.props), depth + 1);
|
|
}
|
|
|
|
// ------------------------------------------------------------------ install / remove
|
|
function ccInstall(rt) {
|
|
const R = rt.react || rt.use('react');
|
|
R.ready(); // fail closed (P2 rule 2)
|
|
const found = R.find(ccFinders());
|
|
const M = found.mods;
|
|
CC = { rt, R, M, where: found.where, counts: found.counts, inst: null, opening: false, closing: false, spy: false,
|
|
log: [], fade: [], attrEls: [], glassHook: null, pillHandle: null, routeAtOpen: null, timers: [], lastErr: null, debug: {} };
|
|
const S = CC;
|
|
const { jsx, jsxs, React } = R;
|
|
const c = R.c;
|
|
const e = React.createElement;
|
|
|
|
// ---------------------------------------------------------------- side effects (logged in test mode)
|
|
function act(name, fn, ev) {
|
|
let mode = 'test', reasons = [];
|
|
try { if (!S.spy) { const m = R.actions.mode(ev); mode = m.mode; reasons = m.reasons || []; } else reasons = ['spy']; } catch (_) { reasons = ['unreadable']; }
|
|
const rec = { t: Date.now(), name, mode: mode === 'live' ? 'executed' : 'logged', reasons };
|
|
S.log.push(rec); if (S.log.length > 60) S.log.shift();
|
|
if (mode !== 'live') return rec;
|
|
try { fn(); } catch (err) { rec.error = String(err && err.message || err); rt.warn('cc: ' + name + ' failed', rec.error); }
|
|
return rec;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- open / close
|
|
function mainEntry() { try { return rt.windows.main(); } catch (_) { return null; } }
|
|
function eligible() {
|
|
const m = mainEntry();
|
|
if (!m) return false;
|
|
try { if (!m.visible()) return false; } catch (_) { return false; }
|
|
return true;
|
|
}
|
|
function setAttrAll(on) {
|
|
if (!on) { for (const h of S.attrEls) { try { h.removeAttribute('data-lgs-cc'); } catch (_) { /* gone */ } } S.attrEls = []; return; }
|
|
try {
|
|
rt.windows.each((w) => {
|
|
if (w.kind !== 'main' && w.kind !== 'bar') return;
|
|
w.html.setAttribute('data-lgs-cc', 'open'); S.attrEls.push(w.html);
|
|
});
|
|
} catch (_) { /* window gone */ }
|
|
}
|
|
function basicRoot() {
|
|
const m = mainEntry(); if (!m) return null;
|
|
try { return m.doc.querySelector(rt.sel('%{BasicUiRoot}')); } catch (_) { return null; }
|
|
}
|
|
// Fade Steam's page layers: every sibling of each ancestor of our root, up to BasicUiRoot (opacity
|
|
// only; nothing removed, moved or resized; CC §4.2).
|
|
function fadeOn(rootEl) {
|
|
fadeOff();
|
|
const top = basicRoot();
|
|
if (!rootEl || !top || !top.contains(rootEl)) return;
|
|
top.setAttribute('data-lgs-cc', 'open'); S.attrEls.push(top);
|
|
for (let n = rootEl; n && n !== top; n = n.parentElement) {
|
|
const p = n.parentElement; if (!p) break;
|
|
for (const sib of p.children) {
|
|
if (sib === n || sib.hasAttribute('data-lgs-cc-fade')) continue;
|
|
sib.setAttribute('data-lgs-cc-fade', '');
|
|
S.fade.push(sib);
|
|
}
|
|
}
|
|
}
|
|
function fadeOff() {
|
|
for (const n of S.fade) { try { n.removeAttribute('data-lgs-cc-fade'); } catch (_) { /* gone */ } }
|
|
S.fade = [];
|
|
}
|
|
function glassOn() {
|
|
try {
|
|
if (!S.glassHook && rt.has('shell')) {
|
|
const sh = rt.use('shell');
|
|
S.glassHook = sh.glassMode('*', () => (S.inst || S.opening ? 'windowless' : null));
|
|
sh.refreshGlass();
|
|
}
|
|
} catch (err) { rt.warn('cc: glassMode', String(err && err.message || err)); }
|
|
}
|
|
function glassOff() {
|
|
const h = S.glassHook; S.glassHook = null;
|
|
try { if (h) h.remove(); if (rt.has('shell')) rt.use('shell').refreshGlass(); } catch (_) { /* shell gone */ }
|
|
}
|
|
function clearTimers() { for (const off of S.timers) { try { off(); } catch (_) { /* done */ } } S.timers = []; }
|
|
|
|
async function open(ev, source) {
|
|
if (S.inst || S.opening) { close('toggle'); return { opened: false, closed: true }; }
|
|
if (!eligible()) return { opened: false, reason: 'main window not shown' };
|
|
S.opening = true; S.closing = false;
|
|
S.routeAtOpen = safeRoute();
|
|
setAttrAll(true);
|
|
glassOn();
|
|
try {
|
|
S.inst = await R.ui.modal(jsx(CCView, { source: source || 'api' }));
|
|
} catch (err) {
|
|
S.opening = false; setAttrAll(false); glassOff();
|
|
S.lastErr = String(err && err.message || err);
|
|
rt.warn('cc: open failed', S.lastErr);
|
|
return { opened: false, error: S.lastErr };
|
|
}
|
|
S.opening = false;
|
|
// close when the main window navigates or hides (CC §4.1)
|
|
clearTimers();
|
|
S.timers.push(rt.setInterval(() => {
|
|
if (!S.inst) return;
|
|
const r = safeRoute();
|
|
if (r && S.routeAtOpen && r !== S.routeAtOpen) close('route');
|
|
}, 300));
|
|
return { opened: true };
|
|
}
|
|
function safeRoute() { try { return R.nav.route(); } catch (_) { return null; } }
|
|
function close(reason) {
|
|
if (!S.inst && !S.opening) return false;
|
|
if (S.closing) return true;
|
|
S.closing = true;
|
|
S.closeReason = reason || 'close';
|
|
if (S.setClosing) { try { S.setClosing(true); } catch (_) { /* unmounted */ } }
|
|
const inst = S.inst;
|
|
fadeRestore();
|
|
const done = () => {
|
|
clearTimers();
|
|
try { if (inst) inst.Close(); } catch (_) { /* already closed */ }
|
|
S.inst = null; S.closing = false; S.setClosing = null; S.setPage = null;
|
|
setAttrAll(false); glassOff();
|
|
};
|
|
const reduce = (() => { try { return mainEntry().win.matchMedia('(prefers-reduced-motion: reduce)').matches; } catch (_) { return false; } })();
|
|
if (reason === 'teardown') { done(); fadeOff(); return true; }
|
|
// the tiles dematerialize (mat-out, 350 ms); the page layers finish their fade back (441 ms)
|
|
rt.setTimeout(done, reduce ? 180 : 350);
|
|
rt.setTimeout(() => { if (!S.inst && !S.opening) fadeOff(); }, reduce ? 200 : 460);
|
|
return true;
|
|
}
|
|
// the page layers start coming back while the tiles dematerialize (CC §4.2: from 40 % of the close)
|
|
function fadeRestore() { for (const n of S.fade) { try { n.setAttribute('data-lgs-cc-fade', 'out'); } catch (_) { /* gone */ } } }
|
|
|
|
// ---------------------------------------------------------------- icons (Steam's own where found)
|
|
const IC = M.icons || {};
|
|
function svg(d, extra) {
|
|
return jsx('svg', { className: 'lgs-cc-ic', viewBox: '0 0 24 24', 'aria-hidden': 'true', children: jsx('path', Object.assign({ d, fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }, extra || {})) });
|
|
}
|
|
const GLYPH = {
|
|
close: () => svg('M6 6l12 12M18 6L6 18'),
|
|
back: () => svg('M15 5l-7 7 7 7'),
|
|
chev: () => svg('M9 5l7 7-7 7'),
|
|
gear: () => (IC.Settings ? jsx(IC.Settings, {}) : svg('M12 8a4 4 0 100 8 4 4 0 000-8zM4 12h2M18 12h2M12 4v2M12 18v2')),
|
|
power: () => (IC.Power ? jsx(IC.Power, {}) : svg('M12 3v9M6.3 6.3a8 8 0 1011.4 0')),
|
|
motion: () => svg('M3 12c2.5-5 5.5-5 8 0s5.5 5 8 0M3 17c2.5-5 5.5-5 8 0'),
|
|
recenter: () => (IC.Recenter ? jsx(IC.Recenter, {}) : svg('M12 3v4M12 17v4M3 12h4M17 12h4M12 9a3 3 0 100 6 3 3 0 000-6z')),
|
|
playspace: () => (IC.Playspace ? jsx(IC.Playspace, {}) : svg('M4 8l8-4 8 4-8 4-8-4zM4 8v8l8 4 8-4V8')),
|
|
battery: () => (IC.BatterySimple ? jsx(IC.BatterySimple, {}) : svg('M3 8h15v8H3zM20 11v2')),
|
|
bell: () => (IC.Notifications ? jsx(IC.Notifications, {}) : svg('M6 16V11a6 6 0 1112 0v5l2 2H4zM10 20h4')),
|
|
perf: () => (IC.QAMPerformance ? jsx(IC.QAMPerformance, {}) : svg('M4 18l5-6 4 3 7-9')),
|
|
play: () => svg('M8 5l11 7-11 7z', { fill: 'currentColor', stroke: 'none' }),
|
|
sliders: () => svg('M5 7h14M5 17h14M9 4v6M15 14v6'),
|
|
};
|
|
|
|
// ---------------------------------------------------------------- small controls
|
|
// One focusable control: an 80 px hit box (PLAN §1.3) that draws its visible glass inside.
|
|
function Ctl(props) {
|
|
const { k, cls, label, tip, onAct, children, extra } = props;
|
|
return jsx(c.Focusable, Object.assign({
|
|
className: 'lgs-cc-hit ' + (cls || ''),
|
|
noFocusRing: true,
|
|
'data-lgs-cc-k': k,
|
|
'aria-label': label || undefined,
|
|
'data-lgs-tip': tip ? '' : undefined,
|
|
onActivate: onAct,
|
|
children,
|
|
}, extra || {}));
|
|
}
|
|
const ref = {};
|
|
const focusK = (k) => { const el = ref.root && ref.root.querySelector('[data-lgs-cc-k="' + k + '"]'); return el ? ccFocusEl(R, el) : false; };
|
|
// explicit neighbours (CC §4.8): a handler returns true when it moved focus itself
|
|
const NB = {
|
|
power: { right: 'wifi' },
|
|
wifi: { left: 'power' },
|
|
battery: { right: 'perf' },
|
|
perf: { left: 'battery', right: 'segoff' },
|
|
ret: { right: 'more' },
|
|
notif: { right: 'more' },
|
|
more: { left: ['ret', 'notif'], right: 'segoff' },
|
|
segoff: { left: 'perf' },
|
|
motion: { right: 'recenter' },
|
|
recenter: { left: 'motion' },
|
|
};
|
|
function nbProps(k) {
|
|
const nb = NB[k]; if (!nb) return {};
|
|
const go = (t) => () => { const list = Array.isArray(t) ? t : [t]; for (const x of list) if (focusK(x)) return true; return false; };
|
|
const o = {};
|
|
if (nb.right) o.onMoveRight = go(nb.right);
|
|
if (nb.left) o.onMoveLeft = go(nb.left);
|
|
return o;
|
|
}
|
|
|
|
// A round toggle bound to the value and onChange of Steam's own Quick Access toggle.
|
|
function SteamToggle({ k, resolve, glyph, labelKey, tone }) {
|
|
let el = null;
|
|
try { el = resolve(); } catch (err) { S.debug['tog_' + k] = String(err && err.message || err); el = null; }
|
|
if (!el) return null;
|
|
const p = el.props;
|
|
const on = !!p.checked, dis = !!p.disabled;
|
|
const label = (typeof p.label === 'string' && p.label) || ccStr(R, labelKey);
|
|
return jsx(Ctl, {
|
|
k, cls: 'lgs-cc-tog' + (on ? ' is-on' : '') + (dis ? ' is-disabled' : ''), label, tip: true,
|
|
extra: Object.assign({ 'aria-pressed': on ? 'true' : 'false', 'aria-disabled': dis ? 'true' : undefined, 'data-tone': tone || 'blue', autoFocus: k === 'wifi' }, nbProps(k)),
|
|
onAct: (ev) => { if (!dis) act(k, () => p.onChange(!on), ev); },
|
|
children: jsx('div', { className: 'lgs-cc-tog-face', children: p.icon || glyph() }),
|
|
});
|
|
}
|
|
function Guard({ name, children }) { return jsx(R.ui.ErrorBoundary, { name: 'cc.' + name, fallback: null, children }); }
|
|
|
|
// ---------------------------------------------------------------- tiles
|
|
function useClock() {
|
|
const [now, setNow] = React.useState(() => new Date());
|
|
React.useEffect(() => { const id = setInterval(() => setNow(new Date()), 10000); return () => clearInterval(id); }, []);
|
|
return now;
|
|
}
|
|
function locale() { try { return R.nav.win().navigator.language || 'en-US'; } catch (_) { return 'en-US'; } }
|
|
|
|
function NowTile({ page, setPage }) {
|
|
const now = useClock();
|
|
const loc = locale();
|
|
let date = '', time = '';
|
|
try { date = now.toLocaleDateString(loc, { weekday: 'short', month: 'short', day: 'numeric' }); } catch (_) { date = now.toDateString(); }
|
|
try { time = now.toLocaleTimeString(loc, { hour: 'numeric', minute: '2-digit' }).replace(/\s?[AaPp]\.?[Mm]\.?$/, ''); } catch (_) { time = ''; }
|
|
if (page === 'notif') return jsx(NotifPage, { setPage });
|
|
return jsxs('div', { className: 'lgs-cc-now-main', children: [
|
|
jsxs(c.Focusable, { className: 'lgs-cc-corners', 'flow-children': 'row', children: [
|
|
jsx(Ctl, { k: 'settings', cls: 'lgs-cc-circle is-tl', label: ccStr(R, 'settings'), tip: true, extra: {},
|
|
onAct: (ev) => { const r = (R.Routes.Settings && typeof R.Routes.Settings.Root === 'function') ? R.Routes.Settings.Root() : '/settings'; close('settings'); R.actions.navigate(r, {}, ev); },
|
|
children: jsx('div', { className: 'lgs-cc-circle-face', children: GLYPH.gear() }) }),
|
|
jsx(Ctl, { k: 'power', cls: 'lgs-cc-circle is-tr', label: ccStr(R, 'power'), tip: true, extra: Object.assign({}, nbProps('power')),
|
|
onAct: (ev) => {
|
|
const doc = (() => { try { return R.nav.win().document.documentElement; } catch (_) { return null; } })();
|
|
close('power');
|
|
act('power', () => rt.W.SteamUIStore.OpenPowerMenu(doc), ev);
|
|
},
|
|
children: jsx('div', { className: 'lgs-cc-circle-face', children: GLYPH.power() }) }),
|
|
] }),
|
|
jsx('div', { className: 'lgs-cc-date', children: date }),
|
|
jsx('div', { className: 'lgs-cc-clock', children: time }),
|
|
jsx(Ctl, { k: 'battery', cls: 'lgs-cc-cap is-battery', label: ccStr(R, 'battery'), extra: Object.assign({}, nbProps('battery')),
|
|
onAct: () => { setPage('more', 'battery'); },
|
|
children: jsxs('div', { className: 'lgs-cc-cap-face', children: [jsx('span', { className: 'lgs-cc-cap-ic', children: GLYPH.battery() }),
|
|
jsx('span', { className: 'lgs-cc-cap-label', children: ccStr(R, 'battery') }), jsx('span', { className: 'lgs-cc-chev', children: GLYPH.chev() })] }) }),
|
|
jsx(Guard, { name: 'nowplaying', children: jsx(NowPlaying, {}) }),
|
|
jsx(Ctl, { k: 'notif', cls: 'lgs-cc-cap is-notif', label: ccStr(R, 'notifications'), extra: Object.assign({}, nbProps('notif')),
|
|
onAct: () => setPage('notif'),
|
|
children: jsxs('div', { className: 'lgs-cc-cap-face', children: [jsx('span', { className: 'lgs-cc-cap-ic', children: GLYPH.bell() }),
|
|
jsx('span', { className: 'lgs-cc-cap-label', children: ccStr(R, 'notifications') }), jsx('span', { className: 'lgs-cc-chev', children: GLYPH.chev() })] }) }),
|
|
] });
|
|
}
|
|
|
|
function NowPlaying() {
|
|
let info = null;
|
|
if (M.bar && M.bar.tabs) {
|
|
const t = M.bar.tabs() || {};
|
|
const proto = t.tabsProto || {};
|
|
const tabs = (t.rgTabsToShow || []).filter((x) => x && x.tab_id != null && x.tab_id !== proto.vr_steam_tab_id && x.tab_id !== proto.deprecated_vr_settings_tab_id);
|
|
S.debug.tabKeys = tabs[0] ? Object.keys(tabs[0]).slice(0, 20) : [];
|
|
const tab = tabs[0];
|
|
if (tab) info = { id: tab.tab_id, name: tab.display_name || tab.label || tab.name || tab.title || '' };
|
|
}
|
|
if (!info) {
|
|
return jsxs('div', { className: 'lgs-cc-platter lgs-cc-np is-idle', children: [jsx('div', { className: 'lgs-cc-np-disc' }),
|
|
jsx('div', { className: 'lgs-cc-np-text', children: jsx('div', { className: 'lgs-cc-np-sub', children: ccStr(R, 'notPlaying') }) })] });
|
|
}
|
|
return jsxs('div', { className: 'lgs-cc-platter lgs-cc-np', children: [
|
|
jsxs('div', { className: 'lgs-cc-np-head', children: [jsx('div', { className: 'lgs-cc-np-disc', children: GLYPH.play() }),
|
|
jsx('div', { className: 'lgs-cc-np-text', children: jsx('div', { className: 'lgs-cc-np-title', children: info.name }) })] }),
|
|
jsx(Ctl, { k: 'ret', cls: 'lgs-cc-cap is-return', label: ccStr(R, 'resume') || info.name, extra: Object.assign({}, nbProps('ret')),
|
|
onAct: (ev) => { close('return'); act('return', () => M.vrMsg.SteamVR.DashboardTabClicked({ tab_id: info.id }).catch(() => {}), ev); },
|
|
children: jsxs('div', { className: 'lgs-cc-cap-face is-center', children: [jsx('span', { className: 'lgs-cc-cap-ic', children: GLYPH.play() }),
|
|
jsx('span', { className: 'lgs-cc-cap-label', children: ccStr(R, 'resume') || info.name })] }) }),
|
|
] });
|
|
}
|
|
|
|
function NotifPage({ setPage }) {
|
|
const tabs = useQamTabs();
|
|
const t = tabs.find((x) => x.name === 'notifications');
|
|
React.useEffect(() => { rt.setTimeout(() => focusK('nback'), 60); }, []);
|
|
return jsxs(c.Focusable, { className: 'lgs-cc-list', 'flow-children': 'column', children: [
|
|
jsxs('div', { className: 'lgs-cc-head', children: [
|
|
jsx(Ctl, { k: 'nback', cls: 'lgs-cc-circle is-tl', label: ccStr(R, 'back'), extra: {}, onAct: () => setPage('main', null, 'notif'),
|
|
children: jsx('div', { className: 'lgs-cc-circle-face', children: GLYPH.back() }) }),
|
|
jsx('div', { className: 'lgs-cc-head-title', children: ccStr(R, 'notifications') }),
|
|
] }),
|
|
jsx(c.ScrollPanel || 'div', { className: 'lgs-cc-scroll', children: t ? jsx(Guard, { name: 'notif', children: t.panel }) : jsx('div', { className: 'lgs-cc-empty', children: ccStr(R, 'noNotif') }) }),
|
|
] });
|
|
}
|
|
|
|
// Steam's own Quick Access tab list (CC12): [{key, tab, strTitle, panel, name}] for the QAM location.
|
|
// `name` is ours: matched by Steam's own localized title strings (the tab enum is minified).
|
|
const QAM_TITLES = { notifications: '#QuickAccess_Tab_Notifications_Title', settings: '#QuickAccess_Tab_Settings_Title',
|
|
perf: '#QuickAccess_Tab_Perf_Title', battery: '#VRDashboardBar_BatteryStatus', help: '#QuickAccess_Tab_Help_Title' };
|
|
function useQamTabs() {
|
|
let list = [];
|
|
if (M.qamTabs) { try { list = M.qamTabs('quick-access-menu') || []; } catch (err) { S.debug.qamTabs = String(err && err.message || err); list = []; } }
|
|
const titles = {};
|
|
for (const n in QAM_TITLES) { try { const t = R.ui.loc(QAM_TITLES[n]); if (t) titles[t] = n; } catch (_) { /* no token */ } }
|
|
return list.filter((t) => t && t.panel && t.strTitle).map((t) => Object.assign({}, t, { name: titles[t.strTitle] || String(t.key) }));
|
|
}
|
|
|
|
function ControlsTile({ page, setPage, moreKey }) {
|
|
if (page === 'more') return jsx(MorePage, { setPage, moreKey });
|
|
const toggles = [];
|
|
if (M.wifi) toggles.push(jsx(Guard, { name: 'wifi', children: jsx(SteamToggle, { k: 'wifi', labelKey: 'wifi', glyph: GLYPH.sliders, resolve: () => ccFindToggle(M.wifi({}), 0) }) }, 'wifi'));
|
|
if (M.bt) toggles.push(jsx(Guard, { name: 'bt', children: jsx(SteamToggle, { k: 'bt', labelKey: 'bt', glyph: GLYPH.sliders, resolve: () => ccFindToggle(M.bt({ label: ccStr(R, 'bt') }), 0) }) }, 'bt'));
|
|
if (M.sys) toggles.push(jsx(Guard, { name: 'air', children: jsx(SteamToggle, { k: 'air', labelKey: 'air', tone: 'orange', glyph: GLYPH.sliders, resolve: () => {
|
|
const v = !!M.sys.hook();
|
|
return { props: { checked: v, onChange: (x) => M.sys.store.Get().SetAirplaneMode(x), label: ccStr(R, 'air'), icon: IC.AirplaneMode ? jsx(IC.AirplaneMode, {}) : null } };
|
|
} }) }, 'air'));
|
|
if (M.vr && M.vr.motion) toggles.push(jsx(Guard, { name: 'motion', children: jsx(SteamToggle, { k: 'motion', labelKey: 'motion', glyph: GLYPH.motion, resolve: () => ccFindToggle(M.vr.motion({}), 0) }) }, 'motion'));
|
|
const rows = [];
|
|
rows.push(jsx(Ctl, { k: 'perf', cls: 'lgs-cc-row', label: ccStr(R, 'perf'), extra: Object.assign({}, nbProps('perf')), onAct: () => setPage('more', 'perf'),
|
|
children: jsxs('div', { className: 'lgs-cc-row-face', children: [jsx('span', { className: 'lgs-cc-row-ic', children: GLYPH.perf() }), jsx('span', { className: 'lgs-cc-row-label', children: ccStr(R, 'perf') }), jsx('span', { className: 'lgs-cc-chev', children: GLYPH.chev() })] }) }, 'perf'));
|
|
rows.push(jsx(Ctl, { k: 'more', cls: 'lgs-cc-row', label: ccStr(R, 'more'), extra: Object.assign({}, nbProps('more')), onAct: () => setPage('more', S.lastMore || 'settings'),
|
|
children: jsxs('div', { className: 'lgs-cc-row-face', children: [jsx('span', { className: 'lgs-cc-row-ic', children: GLYPH.sliders() }), jsx('span', { className: 'lgs-cc-row-label', children: ccStr(R, 'more') }), jsx('span', { className: 'lgs-cc-chev', children: GLYPH.chev() })] }) }, 'more'));
|
|
const sliders = [];
|
|
if (M.audio) {
|
|
sliders.push(jsx('div', { className: 'lgs-cc-slider', 'data-lgs-cc-s': 'vol', children: jsx(Guard, { name: 'vol', children: jsx(M.audio, { direction: 1 }) }) }, 'vol'));
|
|
sliders.push(jsx('div', { className: 'lgs-cc-slider', 'data-lgs-cc-s': 'mic', children: jsx(Guard, { name: 'mic', children: jsx(M.audio, { direction: 0 }) }) }, 'mic'));
|
|
}
|
|
return jsxs('div', { className: 'lgs-cc-ctl-main', children: [
|
|
jsx(c.Focusable, { className: 'lgs-cc-toggles', 'flow-children': 'row', children: toggles }),
|
|
jsx('div', { className: 'lgs-cc-platter lgs-cc-rows', children: rows }),
|
|
jsx('div', { className: 'lgs-cc-sliders', children: sliders }),
|
|
] });
|
|
}
|
|
|
|
function MorePage({ setPage, moreKey }) {
|
|
const tabs = useQamTabs();
|
|
const cur = tabs.find((x) => x.name === moreKey) || tabs.find((x) => x.name === 'settings') || tabs[0];
|
|
React.useEffect(() => { rt.setTimeout(() => { const el = ref.root && ref.root.querySelector('.lgs-cc-side-row.is-selected'); if (el) ccFocusEl(R, el); }, 60); }, []);
|
|
const select = (t) => { S.lastMore = t.name; setPage('more', t.name); };
|
|
const enterPane = () => {
|
|
const pane = ref.root && ref.root.querySelector('.lgs-cc-pane');
|
|
if (!pane) return false;
|
|
for (const el of pane.querySelectorAll('.Focusable, .gpfocusable, [tabindex], button')) {
|
|
if (el.closest('[class*="Slider"]')) continue;
|
|
if (ccFocusEl(R, el)) return true;
|
|
}
|
|
return false;
|
|
};
|
|
return jsxs('div', { className: 'lgs-cc-more', children: [
|
|
jsx(c.Focusable, { className: 'lgs-cc-side', 'flow-children': 'column', children: tabs.map((t) => jsx(Ctl, {
|
|
k: 'side-' + t.name, cls: 'lgs-cc-side-row' + (cur && t.name === cur.name ? ' is-selected' : ''), label: t.strTitle || '',
|
|
extra: { 'aria-selected': cur && t.name === cur.name ? 'true' : 'false', onMoveRight: enterPane, onGamepadFocus: () => { if (!cur || t.name !== cur.name) select(t); } },
|
|
onAct: () => select(t),
|
|
children: jsxs('div', { className: 'lgs-cc-side-face', children: [jsx('span', { className: 'lgs-cc-side-ic', 'data-k': t.name, children: t.tab }), jsx('span', { className: 'lgs-cc-side-label', children: t.strTitle || '' })] }),
|
|
}, 'side-' + t.name)) }),
|
|
jsxs('div', { className: 'lgs-cc-pane-wrap', children: [
|
|
jsxs('div', { className: 'lgs-cc-head is-pane', children: [
|
|
jsx(Ctl, { k: 'mback', cls: 'lgs-cc-circle', label: ccStr(R, 'back'), extra: {}, onAct: () => setPage('main', null, 'more'),
|
|
children: jsx('div', { className: 'lgs-cc-circle-face', children: GLYPH.back() }) }),
|
|
jsx('div', { className: 'lgs-cc-head-title', children: cur ? cur.strTitle || '' : '' }),
|
|
] }),
|
|
jsx(c.Focusable, { className: 'lgs-cc-pane', 'flow-children': 'column', key: 'pane-' + (cur ? cur.name : 'none'),
|
|
children: jsx(c.ScrollPanel || 'div', { className: 'lgs-cc-scroll', children: cur ? jsx(Guard, { name: 'pane', children: cur.panel }) : null }) }),
|
|
] }),
|
|
] });
|
|
}
|
|
|
|
// Room tile: the bar's own SteamVR dashboard actions (CC §4.10, CC6)
|
|
function useBarActions() {
|
|
if (!M.bar) return null;
|
|
const cfg = M.bar.actions() || {};
|
|
const defs = cfg.defined_actions || [];
|
|
const byId = (id) => defs.find((a) => a && a.action_id === id) || null;
|
|
const items = cfg.bar_menu_items || [];
|
|
const buttons = cfg.bar_buttons || [];
|
|
// The bar's own structure, not ids (they differ between SteamVR builds): Room View is the bar's
|
|
// toggle button (invocation 2, not a menu); the Playspace menu is the bar button whose menu rows
|
|
// are SteamVR actions (not the hamburger menu).
|
|
const hamburger = new Set(buttons.filter((b) => b && b.is_main_hamburger_menu).map((b) => b.action_id));
|
|
const parents = [];
|
|
for (const m of items) if (m && m.parent_menu_action_id != null && !hamburger.has(m.parent_menu_action_id) && !parents.includes(m.parent_menu_action_id)) parents.push(m.parent_menu_action_id);
|
|
const psId = parents.find((id) => buttons.some((b) => b && b.action_id === id)) || parents[0] || CC_PLAYSPACE_ID;
|
|
const menu = items.filter((m) => m && m.parent_menu_action_id === psId).map((m) => byId(m.action_id)).filter((a) => a && a.enabled !== false);
|
|
const toggles = buttons.map((b) => b && byId(b.action_id)).filter((a) => a && a.invocation === 2 && !parents.includes(a.action_id));
|
|
const roomView = byId(CC_ROOMVIEW_ID) || toggles[0] || null;
|
|
S.debug.actions = defs.slice(0, 16).map((a) => [a.action_id, a.display_name, a.invocation]);
|
|
S.debug.bar = { buttons: buttons.slice(0, 12).map((b) => [b.type, b.action_id, !!b.is_main_hamburger_menu]), psId, roomView: roomView && roomView.action_id };
|
|
return { roomView, playspace: byId(psId), menu };
|
|
}
|
|
function RoomSeg({ action }) {
|
|
const { bVisuallyActive: on, fnInvokeAction } = M.bar.invoke(action) || {};
|
|
const set = (v) => (ev) => { if (!!on !== v) act('roomview', () => fnInvokeAction(v), ev); };
|
|
return jsxs('div', { className: 'lgs-cc-room-head', children: [
|
|
jsx('div', { className: 'lgs-cc-title', children: action.display_name || '' }),
|
|
jsxs(c.Focusable, { className: 'lgs-cc-seg', 'flow-children': 'row', 'data-on': on ? '1' : '0', children: [
|
|
jsx('div', { className: 'lgs-cc-seg-pill', 'aria-hidden': 'true' }),
|
|
jsx(Ctl, { k: 'segoff', cls: 'lgs-cc-seg-item' + (!on ? ' is-selected' : ''), label: ccStr(R, 'off'), extra: Object.assign({ 'aria-pressed': !on ? 'true' : 'false' }, nbProps('segoff')), onAct: set(false),
|
|
children: jsx('span', { className: 'lgs-cc-seg-label', children: ccStr(R, 'off') }) }),
|
|
jsx(Ctl, { k: 'segon', cls: 'lgs-cc-seg-item' + (on ? ' is-selected' : ''), label: ccStr(R, 'on'), extra: { 'aria-pressed': on ? 'true' : 'false' }, onAct: set(true),
|
|
children: jsx('span', { className: 'lgs-cc-seg-label', children: ccStr(R, 'on') }) }),
|
|
] }),
|
|
] });
|
|
}
|
|
function ActionCircle({ k, action, glyph, cls }) {
|
|
const { fnInvokeAction } = M.bar.invoke(action) || {};
|
|
return jsx(Ctl, { k, cls: 'lgs-cc-circle ' + cls, label: action.display_name || '', tip: true, extra: Object.assign({}, nbProps(k)),
|
|
onAct: (ev) => act(k, () => fnInvokeAction(), ev), children: jsx('div', { className: 'lgs-cc-circle-face', children: glyph() }) });
|
|
}
|
|
// Playspace circle: Steam's context menu in the main window with the Playspace menu's own rows
|
|
function PlayspaceMenu({ items, title }) {
|
|
const fns = items.map((a) => M.bar.invoke(a)); // hooks: one per item, fixed list (keyed by ids)
|
|
return jsx(Ctl, { k: 'playspace', cls: 'lgs-cc-circle is-tr', label: title, tip: true, extra: {},
|
|
onAct: (ev) => {
|
|
const anchor = ev && ev.currentTarget;
|
|
R.ui.menu(items.map((a, i) => ({ label: a.display_name || '', onSelected: () => act('playspace:' + a.action_id, () => fns[i].fnInvokeAction(), ev) })), anchor, { label: title });
|
|
},
|
|
children: jsx('div', { className: 'lgs-cc-circle-face', children: GLYPH.playspace() }) });
|
|
}
|
|
function RoomTile() {
|
|
const A = useBarActions();
|
|
if (!A) return null;
|
|
const recenter = A.menu.find((a) => /recent/i.test(a.display_name || ''));
|
|
const bright = [];
|
|
if (M.vr && M.vr.env) bright.push(jsx('div', { className: 'lgs-cc-slider', 'data-lgs-cc-s': 'env', children: jsx(Guard, { name: 'env', children: jsx(M.vr.env, { iconOnly: true }) }) }, 'env'));
|
|
if (M.vr && M.vr.bright) bright.push(jsx('div', { className: 'lgs-cc-slider', 'data-lgs-cc-s': 'bright', children: jsx(Guard, { name: 'bright', children: jsx(M.vr.bright, { iconOnly: true }) }) }, 'bright'));
|
|
return jsxs('div', { className: 'lgs-cc-room-main', children: [
|
|
jsxs(c.Focusable, { className: 'lgs-cc-corners', 'flow-children': 'row', children: [
|
|
recenter ? jsx(Guard, { name: 'recenter', children: jsx(ActionCircle, { k: 'recenter', action: recenter, glyph: GLYPH.recenter, cls: 'is-tl' }) }, 'rc') : jsx('div', { className: 'lgs-cc-hit-spacer' }, 'rc'),
|
|
A.menu.length ? jsx(Guard, { name: 'playspace', children: jsx(PlayspaceMenu, { items: A.menu, title: (A.playspace && A.playspace.display_name) || '' }, A.menu.map((a) => a.action_id).join('-')) }, 'ps') : null,
|
|
] }),
|
|
A.roomView ? jsx(Guard, { name: 'roomview', children: jsx(RoomSeg, { action: A.roomView }) }) : jsx('div', { className: 'lgs-cc-room-head' }),
|
|
jsx('div', { className: 'lgs-cc-sliders', children: bright }),
|
|
] });
|
|
}
|
|
|
|
// ---------------------------------------------------------------- the view
|
|
function CCView(props) {
|
|
const [page, setPageS] = React.useState('main');
|
|
const [moreKey, setMoreKey] = React.useState(null);
|
|
const [closing, setClosing] = React.useState(false);
|
|
const rootRef = React.useRef(null);
|
|
S.setClosing = setClosing;
|
|
const setPage = S.setPage = (p, key, from) => {
|
|
setPageS(p); setMoreKey(key || null);
|
|
if (p === 'main' && from) rt.setTimeout(() => focusK(from === 'notif' ? 'notif' : 'more'), 60);
|
|
};
|
|
React.useEffect(() => {
|
|
ref.root = rootRef.current;
|
|
fadeOn(rootRef.current);
|
|
const mine = rootRef.current;
|
|
return () => {
|
|
if (ref.root === mine) ref.root = null;
|
|
// Steam closed the modal itself (another modal, a window reset): give everything back
|
|
if (S.inst && !S.closing) { clearTimers(); S.inst = null; S.setClosing = null; setAttrAll(false); fadeOff(); glassOff(); }
|
|
};
|
|
}, []);
|
|
const onCancel = (ev) => {
|
|
if (ev && ev.stopPropagation) ev.stopPropagation();
|
|
if (page === 'more') {
|
|
const pane = rootRef.current && rootRef.current.querySelector('.lgs-cc-pane');
|
|
if (pane && pane.querySelector('.gpfocus')) { const el = rootRef.current.querySelector('.lgs-cc-side-row.is-selected'); if (el && ccFocusEl(R, el)) return true; }
|
|
setPage('main', null, 'more'); return true;
|
|
}
|
|
if (page === 'notif') { setPage('main', null, 'notif'); return true; }
|
|
close('cancel'); return true;
|
|
};
|
|
const onButtonDown = (ev) => {
|
|
const b = ev && ev.detail && (ev.detail.button != null ? ev.detail.button : ev.detail);
|
|
if (b !== 5 && b !== 6 || page !== 'main') return;
|
|
const order = ['now', 'ctl', 'room'];
|
|
const firstOf = { now: ['settings', 'battery'], ctl: ['wifi', 'bt', 'air', 'motion', 'perf'], room: ['recenter', 'playspace', 'segoff'] };
|
|
const el = rootRef.current && rootRef.current.querySelector('.gpfocus');
|
|
const tile = el && el.closest('[data-lgs-cc-tile]');
|
|
const i = order.indexOf(tile ? tile.getAttribute('data-lgs-cc-tile') : 'ctl') + (b === 6 ? 1 : -1);
|
|
if (i < 0 || i >= order.length) return;
|
|
for (const k of firstOf[order[i]]) if (focusK(k)) return;
|
|
};
|
|
const wide = page === 'more';
|
|
const tile = (name, plateId, cls, child) => jsx(c.Focusable, {
|
|
className: 'lgs-cc-tile ' + cls + (closing ? ' lgs-mat-out' : ' lgs-mat'), 'flow-children': 'column',
|
|
'data-lgs-cc-tile': name, 'data-lgs-plate': 'panel', 'data-lgs-plate-id': plateId,
|
|
children: child,
|
|
});
|
|
return jsxs(c.Focusable, {
|
|
className: 'lgs-cc' + (closing ? ' is-closing' : ''), 'data-page': page, 'flow-children': 'column', noFocusRing: true,
|
|
onCancel, onCancelActionDescription: page === 'main' ? ccStr(R, 'close') : ccStr(R, 'back'), onButtonDown,
|
|
ref: rootRef,
|
|
children: [
|
|
jsx('div', { className: 'lgs-cc-backdrop', onClick: () => close('backdrop') }, 'bd'),
|
|
jsxs(c.Focusable, { className: 'lgs-cc-tiles', 'flow-children': 'row', children: [
|
|
tile('now', 'cc-now', 'lgs-cc-now', jsx(Guard, { name: 'now', children: jsx(NowTile, { page, setPage }) })),
|
|
tile('ctl', 'cc-controls', 'lgs-cc-ctl' + (wide ? ' is-wide' : ''), jsx(Guard, { name: 'controls', children: jsx(ControlsTile, { page, setPage, moreKey }) })),
|
|
wide ? null : tile('room', 'cc-room', 'lgs-cc-room', jsx(Guard, { name: 'room', children: jsx(RoomTile, {}) })),
|
|
] }, 'tiles'),
|
|
jsx(Ctl, { k: 'close', cls: 'lgs-cc-close' + (closing ? ' lgs-mat-out' : ' lgs-mat'), label: ccStr(R, 'close'), tip: true,
|
|
extra: { 'data-lgs-plate': 'liquid', 'data-lgs-plate-id': 'cc-close', 'data-lgs-plate-r': 'capsule',
|
|
onMoveUp: () => { const t = rootRef.current && [...rootRef.current.querySelectorAll('.lgs-cc-ctl .lgs-cc-slider .Focusable, .lgs-cc-ctl .lgs-cc-slider .Panel')].pop(); return t ? ccFocusEl(R, t.closest('.Panel') || t) : false; } },
|
|
onAct: () => close('x'), children: jsx('div', { className: 'lgs-cc-circle-face', children: GLYPH.close() }) }, 'x'),
|
|
],
|
|
});
|
|
}
|
|
|
|
// ---------------------------------------------------------------- pill patch (CC3)
|
|
function pillOut(out) {
|
|
if (!out || !out.props || typeof out.props.children !== 'function') return out;
|
|
const kids = out.props.children;
|
|
return React.cloneElement(out, {
|
|
children: (a, b) => {
|
|
const el = kids(a, b);
|
|
if (!el || !el.props || typeof el.props.onActivate !== 'function') return el;
|
|
const steamOpen = el.props.onActivate;
|
|
return React.cloneElement(el, {
|
|
onActivate: (ev) => { if (S.inst || S.opening) { close('pill'); return; } if (!eligible()) { steamOpen(ev); return; } open(ev, 'pill'); },
|
|
onMenuButton: (ev) => { if (S.inst) close('menu'); steamOpen(ev); },
|
|
onMenuActionDescription: ccStr(R, 'qam') || undefined,
|
|
});
|
|
},
|
|
});
|
|
}
|
|
S.pillHandle = R.patch.byProps('c3b.pill', R.patch.targets.statusPill, (orig) => function (props, r) {
|
|
const out = orig.call(this, props, r);
|
|
try { return pillOut(out); } catch (err) { S.lastErr = String(err && err.message || err); return out; }
|
|
});
|
|
try { R.patch.rerender(S.pillHandle); } catch (_) { /* shows at its next render */ }
|
|
|
|
// the dashboard hiding closes CC-M
|
|
rt.windows.onHide((w) => { if (w.kind === 'main' && (S.inst || S.opening)) close('hidden'); });
|
|
|
|
return {
|
|
open: (src) => open(null, src || 'api'),
|
|
close: (reason) => close(reason || 'api'),
|
|
isOpen: () => !!(S.inst || S.opening),
|
|
page: (p, key) => { if (S.setPage) { S.setPage(p, key || null); return true; } return false; },
|
|
spy: (on) => { S.spy = !!on; return S.spy; },
|
|
log: () => S.log.slice(),
|
|
status: () => ({ open: !!S.inst, opening: S.opening, closing: S.closing, where: S.where, counts: S.counts,
|
|
found: Object.keys(ccFinders()).filter((k) => !!M[k]), pill: S.pillHandle ? { count: S.pillHandle.count, live: S.pillHandle.live } : null,
|
|
fade: S.fade.length, glassHook: !!S.glassHook, lastErr: S.lastErr, debug: S.debug, log: S.log.slice(-8) }),
|
|
};
|
|
}
|
|
|
|
function ccRemove() {
|
|
const S = CC; CC = null;
|
|
if (!S) return { patchedLeft: 0 };
|
|
try { if (S.inst || S.opening) { S.closing = false; } } catch (_) { /* */ }
|
|
try { if (S.inst) S.inst.Close(); } catch (_) { /* closed */ }
|
|
for (const n of S.fade || []) { try { n.removeAttribute('data-lgs-cc-fade'); } catch (_) { /* gone */ } }
|
|
for (const h of S.attrEls || []) { try { h.removeAttribute('data-lgs-cc'); } catch (_) { /* gone */ } }
|
|
try { if (S.glassHook) S.glassHook.remove(); } catch (_) { /* shell gone */ }
|
|
for (const off of S.timers || []) { try { off(); } catch (_) { /* done */ } }
|
|
let left = 0;
|
|
try { if (S.pillHandle) { S.pillHandle.remove(); try { S.R.patch.rerender(S.pillHandle); } catch (_) { /* */ } } } catch (_) { left = 1; }
|
|
return { patchedLeft: left };
|
|
}
|