import { useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { CategorySection } from '@/features/remote-panel/components/CategorySection'; import { ConnectionPanel } from '@/features/remote-panel/components/ConnectionPanel'; import { DeckHeader } from '@/features/remote-panel/components/DeckHeader'; import { EmptyDeck } from '@/features/remote-panel/components/EmptyDeck'; import { TrainerOverview } from '@/features/remote-panel/components/TrainerOverview'; import { buildPinnedGroup, filterGroups, groupCheatsByCategory } from '@/features/remote-panel/category'; import { handleProtocolMessage } from '@/features/remote-panel/message-handler'; import { normalizeOutgoingValue, type CheatSchema, type TrainerMetaPayload } from '@/features/remote-panel/protocol'; import { getPinnedStorageKey, loadPinnedTargets, savePinnedTargets, } from '@/features/remote-panel/pinned-storage'; import { PanelSocketClient } from '@/features/remote-panel/socket-client'; import { createInitialPanelState, panelReducer } from '@/features/remote-panel/state'; import { Icon } from '@/components/ui/icon'; import { Input } from '@/components/ui/input'; export function App() { const [state, dispatch] = useReducer(panelReducer, createInitialPanelState()); const [searchQuery, setSearchQuery] = useState(''); const clientRef = useRef(null); const trainerMetaRef = useRef(state.trainerMeta); useEffect(() => { return () => { clientRef.current?.disconnect(); clientRef.current = null; }; }, []); useEffect(() => { trainerMetaRef.current = state.trainerMeta; }, [state.trainerMeta]); const groups = useMemo(() => groupCheatsByCategory(state.trainerMeta), [state.trainerMeta]); const pinnedGroup = useMemo( () => buildPinnedGroup(state.trainerMeta, state.pinnedTargets), [state.trainerMeta, state.pinnedTargets], ); const filteredGroups = useMemo(() => filterGroups(groups, searchQuery), [groups, searchQuery]); const filteredPinnedGroup = useMemo( () => (pinnedGroup ? filterGroups([pinnedGroup], searchQuery)[0] ?? null : null), [pinnedGroup, searchQuery], ); const pinnedStorageKey = useMemo( () => getPinnedStorageKey(state.trainerMeta?.trainer ?? null), [state.trainerMeta?.trainer], ); const activeTrainer = state.trainerMeta?.trainer ?? null; const cheatCount = state.trainerMeta?.schema.cheats.length ?? 0; const controlsDisabled = Boolean(activeTrainer?.trainerLoading || activeTrainer?.isTimeLimitExpired); useEffect(() => { dispatch({ type: 'setPinnedTargets', pinned: loadPinnedTargets(pinnedStorageKey) }); }, [pinnedStorageKey]); function connect(): void { clientRef.current?.disconnect(); const wsUrl = state.wsUrl.trim(); if (!wsUrl) { dispatch({ type: 'error', message: 'Enter a WebSocket URL first.' }); return; } const nextClient = new PanelSocketClient(wsUrl, { onConnecting: () => dispatch({ type: 'connecting' }), onOpen: () => dispatch({ type: 'connected' }), onMessage: (message) => handleProtocolMessage(dispatch, message, trainerMetaRef.current), onClose: () => dispatch({ type: 'error', message: 'The WebSocket connection closed.' }), onError: (message) => dispatch({ type: 'error', message }), }); clientRef.current = nextClient; nextClient.connect(); } function handleCheatChange(cheat: CheatSchema, nextValue: unknown): void { const normalizedValue = normalizeOutgoingValue(cheat, nextValue); dispatch({ type: 'setPending', target: cheat.target, pending: true }); dispatch({ type: 'valueChanged', target: cheat.target, value: normalizedValue }); if (state.connectionStatus !== 'connected' || !state.trainerMeta || !clientRef.current) { dispatch({ type: 'setPending', target: cheat.target, pending: false }); return; } const sent = clientRef.current.setValue(state.trainerMeta.trainer.trainerId, cheat.target, normalizedValue, cheat.uuid); if (!sent) { dispatch({ type: 'setPending', target: cheat.target, pending: false }); dispatch({ type: 'error', message: 'The bridge socket is not open.' }); } } function handleTogglePin(cheat: CheatSchema): void { const next = { ...state.pinnedTargets }; if (next[cheat.target]) { delete next[cheat.target]; } else { next[cheat.target] = true; } dispatch({ type: 'togglePinnedTarget', target: cheat.target }); savePinnedTargets(pinnedStorageKey, next); } async function loadDebugSession(): Promise { if (!import.meta.env.DEV) { return; } clientRef.current?.disconnect(); const debugSession = await import('@/features/remote-panel/debug-session'); debugSession.loadDebugSession(dispatch); } useEffect(() => { if (import.meta.env.DEV) { void import('@/features/remote-panel/debug-session').then((debugSession) => { if (debugSession.isDebugSessionRequested()) { debugSession.loadDebugSession(dispatch); return; } if (state.wsUrl.trim()) { connect(); } }); return; } if (!state.wsUrl.trim()) { return; } connect(); }, []); return (
{activeTrainer ? ( <>
setSearchQuery((event.target as HTMLInputElement).value)} placeholder="Search cheats, categories, targets..." className="h-9 pl-8 pr-8 text-sm" /> {searchQuery ? ( ) : null}
{filteredPinnedGroup ? ( ) : null} {filteredGroups.map((group) => ( ))} {searchQuery && filteredGroups.length === 0 && !filteredPinnedGroup ? (

No cheats match "{searchQuery}".

) : null}
) : ( )}
); }