Files
beat-pocket/extension/content/detector.ts
T
Zebratic 02a90e839c Initial scaffold: Beat Pocket MV3 extension
- manifest.json with MV3 config, host permissions for beat-battle.net
- content/detector.ts: route + overlay observer, emits bbp:phase events
- content/layout.ts: defensive mobile-first layout reflow
- content/chat-drawer.ts: bottom-sheet drawer, dispatches synthetic events
  through site's own React-managed textarea + button (preserves auth,
  rate-limit, mute, profanity guards)
- content/util/selectors.ts: text- and role-based fallback chain
- content/util/ws-proxy.ts: lightweight WebSocket instrumentation, no
  payload capture
- content/inject.css: mobile overrides using site's existing --bb-* vars
- background.js: MV3 service worker stub
- package.json + tsconfig.json + esbuild.config.mjs: build pipeline
- icons/: 16/32/48/128 placeholder PNGs (chili-red disc)
- docs/PLAN.md: full V1 architecture, risks, scope
- README.md: scope, dev workflow (load unpacked), tech
2026-07-11 02:24:37 +00:00

90 lines
2.7 KiB
TypeScript

/**
* detector.ts — route + overlay observer.
*
* - Watches the document for SPA route changes (Next.js pushState)
* - Watches for .bb-room-overlay appearing (in-game state)
* - Emits `bbp:phase` on the document with `phase` ∈ "home" | "lobby-list" | "lobby" | "in-game" | "login"
*/
import { isHome, isLogin, isInGame, isLobbyList } from './util/selectors';
export type Phase = 'home' | 'lobby-list' | 'login' | 'in-game' | 'unknown';
let lastPhase: Phase = 'unknown';
function detectPhase(): Phase {
if (isLogin()) return 'login';
if (isInGame()) return 'in-game';
if (isLobbyList()) return 'lobby-list';
if (isHome()) return 'home';
return 'unknown';
}
export function currentPhase(): Phase {
return lastPhase;
}
function emit(newPhase: Phase): void {
if (newPhase === lastPhase) return;
lastPhase = newPhase;
document.dispatchEvent(new CustomEvent('bbp:phase', { detail: { phase: newPhase } }));
}
/** Patch history.pushState / replaceState to detect SPA route changes. */
function patchHistory(): void {
const origPush = history.pushState.bind(history);
const origReplace = history.replaceState.bind(history);
history.pushState = function (...args: Parameters<typeof history.pushState>) {
const r = origPush(...args);
queueMicrotask(() => emit(detectPhase()));
return r;
};
history.replaceState = function (...args: Parameters<typeof history.replaceState>) {
const r = origReplace(...args);
queueMicrotask(() => emit(detectPhase()));
return r;
};
window.addEventListener('popstate', () => emit(detectPhase()));
}
/** Observe DOM for overlay appearing / disappearing. */
function observeOverlay(): void {
const obs = new MutationObserver(() => emit(detectPhase()));
obs.observe(document.documentElement, { childList: true, subtree: true });
}
/**
* Wait until any of the candidate selectors exist, then resolve.
*/
export function waitFor(selector: string | string[], timeoutMs = 8000): Promise<Element | null> {
const sels = Array.isArray(selector) ? selector : [selector];
return new Promise((resolve) => {
for (const s of sels) {
const el = document.querySelector(s);
if (el) return resolve(el);
}
const obs = new MutationObserver(() => {
for (const s of sels) {
const el = document.querySelector(s);
if (el) {
obs.disconnect();
return resolve(el);
}
}
});
obs.observe(document.documentElement, { childList: true, subtree: true });
setTimeout(() => {
obs.disconnect();
resolve(null);
}, timeoutMs);
});
}
export function startDetector(): Phase {
patchHistory();
observeOverlay();
// Run once now
emit(detectPhase());
return lastPhase;
}