02a90e839c
- 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
90 lines
2.7 KiB
TypeScript
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;
|
|
}
|