patch: fix a syntax error in the Disable Updates, websocket connection wouldn't automatically reconnect, ui rerender optimizations

bump version to 1.0.8.1
This commit is contained in:
kitbyte
2026-05-15 14:46:04 +03:00
parent 710d014e6d
commit 1f5ba9fc95
9 changed files with 138 additions and 36 deletions
+54 -17
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useReducer, useRef, useState, type UIEvent } from 'react';
import { useCallback, useEffect, useMemo, useReducer, useRef, useState, type UIEvent } from 'react';
import { buildPinnedGroup, filterGroups, groupCheatsByCategory } from '@/features/remote-panel/category';
import { CategorySection } from '@/features/remote-panel/components/CategorySection';
@@ -16,7 +16,7 @@ import { loadPinnedGameIds, savePinnedGameIds, togglePinnedGame } from '@/featur
import { handleProtocolMessage } from '@/features/remote-panel/message-handler';
import { getPinnedStorageKey, loadPinnedTargets, savePinnedTargets } from '@/features/remote-panel/pinned-storage';
import { capturePresetValues, createPreset, getPresetStorageKey, loadPresets, savePresets, type RemotePreset } from '@/features/remote-panel/preset-storage';
import { normalizeOutgoingValue, type CheatSchema, type InstalledAppSummary, type TrainerMetaPayload } from '@/features/remote-panel/protocol';
import { normalizeOutgoingValue, type CheatSchema, type InstalledAppSummary } from '@/features/remote-panel/protocol';
import { ECheatType } from '@/features/remote-panel/protocol';
import { PanelSocketClient } from '@/features/remote-panel/socket-client';
import { createInitialPanelState, EConnectionStatus, panelReducer } from '@/features/remote-panel/state';
@@ -35,19 +35,27 @@ export const App = () => {
const [presets, setPresets] = useState<RemotePreset[]>([]);
const lastScrollRef = useRef(0);
const clientRef = useRef<PanelSocketClient | null>(null);
const trainerMetaRef = useRef<TrainerMetaPayload | null>(state.trainerMeta);
const stateRef = useRef(state);
const handleConnectRef = useRef<() => void>(() => {});
const pinnedStorageKeyRef = useRef<string | null>('');
const reconnectTimeoutRef = useRef<number | null>(null);
useEffect(() => {
setPinnedGameIds(loadPinnedGameIds());
return () => {
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
}
clientRef.current?.disconnect();
clientRef.current = null;
};
}, []);
useEffect(() => {
trainerMetaRef.current = state.trainerMeta;
}, [state.trainerMeta]);
stateRef.current = state;
handleConnectRef.current = handleConnect;
pinnedStorageKeyRef.current = pinnedStorageKey;
});
const activeTrainer = state.trainerMeta?.trainer ?? null;
const libraryGames = useMemo(
@@ -83,8 +91,22 @@ export const App = () => {
}
}, []);
useEffect(() => {
function onVisibilityChange() {
if (document.visibilityState === 'visible' && !clientRef.current?.isOpen()) {
handleConnectRef.current();
}
}
document.addEventListener('visibilitychange', onVisibilityChange);
return () => document.removeEventListener('visibilitychange', onVisibilityChange);
}, []);
function handleConnect(): void {
clientRef.current?.disconnect();
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
const wsUrl = state.wsUrl.trim();
if (!wsUrl) {
@@ -95,8 +117,17 @@ export const App = () => {
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.' }),
onMessage: (message) => handleProtocolMessage(dispatch, message, stateRef.current.trainerMeta),
onClose: () => {
dispatch({ type: 'error', message: 'The WebSocket connection closed.' });
if (document.visibilityState === 'visible') {
reconnectTimeoutRef.current = window.setTimeout(() => {
if (document.visibilityState === 'visible' && stateRef.current.wsUrl.trim()) {
handleConnectRef.current();
}
}, 2000);
}
},
onError: (message) => dispatch({ type: 'error', message }),
});
@@ -105,30 +136,36 @@ export const App = () => {
}
function handleDisconnect(): void {
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
clientRef.current?.disconnect();
clientRef.current = null;
dispatch({ type: 'disconnected' });
}
function handleCheatChange(cheat: CheatSchema, nextValue: unknown): void {
const handleCheatChange = useCallback((cheat: CheatSchema, nextValue: unknown): void => {
const { connectionStatus, trainerMeta } = stateRef.current;
const normalizedValue = normalizeOutgoingValue(cheat, nextValue);
dispatch({ type: 'setPending', target: cheat.target, pending: true });
dispatch({ type: 'valueChanged', target: cheat.target, value: normalizedValue });
if (state.connectionStatus !== EConnectionStatus.Connected || !state.trainerMeta || !clientRef.current) {
if (connectionStatus !== EConnectionStatus.Connected || !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);
const sent = clientRef.current.setValue(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 handleToggleCheatPin(cheat: CheatSchema): void {
const next = { ...state.pinnedTargets };
const handleToggleCheatPin = useCallback((cheat: CheatSchema): void => {
const { pinnedTargets } = stateRef.current;
const next = { ...pinnedTargets };
if (next[cheat.target]) {
delete next[cheat.target];
} else {
@@ -136,8 +173,8 @@ export const App = () => {
}
dispatch({ type: 'togglePinnedTarget', target: cheat.target });
savePinnedTargets(pinnedStorageKey, next);
}
savePinnedTargets(pinnedStorageKeyRef.current, next);
}, []);
function handleToggleGamePin(game: LibraryGame): void {
const next = togglePinnedGame(game, pinnedGameIds);
@@ -248,11 +285,11 @@ export const App = () => {
<main className="min-h-svh bg-[#050608] text-(--deck-fg)">
<div className="flex min-h-svh w-full p-0">
<section className="relative h-svh w-full overflow-hidden bg-(--deck-bg) shadow-[0_40px_100px_-20px_rgba(0,0,0,.7),0_0_0_1px_rgba(255,255,255,.06)]">
<div className="pointer-events-none absolute -inset-12 z-0 bg-[radial-gradient(circle_at_30%_15%,color-mix(in_oklab,var(--deck-accent)_22%,transparent),transparent_45%),radial-gradient(circle_at_80%_85%,color-mix(in_oklab,var(--deck-accent)_16%,transparent),transparent_45%),radial-gradient(circle_at_20%_80%,color-mix(in_oklab,var(--deck-accent)_8%,transparent),transparent_50%)] blur-[50px]" />
<div className="pointer-events-none absolute -inset-12 z-0 bg-[radial-gradient(circle_at_30%_15%,color-mix(in_oklab,var(--deck-accent)_22%,transparent),transparent_45%),radial-gradient(circle_at_80%_85%,color-mix(in_oklab,var(--deck-accent)_16%,transparent),transparent_45%),radial-gradient(circle_at_20%_80%,color-mix(in_oklab,var(--deck-accent)_8%,transparent),transparent_50%)]" />
<div className="pointer-events-none absolute inset-0 z-0 bg-[radial-gradient(ellipse_100%_60%_at_50%_0%,rgba(255,255,255,0.025),transparent)]" />
<div className="relative z-10 flex h-full flex-col">
<TopBar status={state.connectionStatus} currentGame={currentGame} runningTrainer={activeTrainer} onOpenSettings={() => setLeftOpen(true)} />
<div className="remote-scrollbar-hidden min-h-0 flex-1 overflow-y-auto overscroll-contain px-3.5 pb-[110px]" onScroll={handleScroll}>
<div className="remote-scrollbar-hidden min-h-0 flex-1 overflow-y-auto overscroll-contain px-3.5 pb-27.5" onScroll={handleScroll}>
{!connected ? (
<PlaceholderState icon="plug" title="Bridge offline" sub="Open Settings to point Wand at your trainer bridge over WebSocket." action="Open Settings" onAction={() => setLeftOpen(true)} />
) : !activeTrainer ? (
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
import { memo, useEffect, useMemo, useState } from 'react';
import { Icon } from '@/components/ui/icon';
@@ -20,7 +20,7 @@ type CategorySectionProps = {
onTogglePin: (cheat: CheatSchema) => void;
};
export const CategorySection = ({
const CategorySectionBase = ({
group,
values,
pendingTargets,
@@ -36,6 +36,15 @@ export const CategorySection = ({
const toggleCount = useMemo(() => getToggleCount(group.cheats), [group.cheats]);
const handleToggle = () => setOpen((current) => !current);
const cheatHandlers = useMemo(
() =>
group.cheats.map((cheat) => ({
onChange: (nextValue: unknown) => onCheatChange(cheat, nextValue),
onTogglePin: () => onTogglePin(cheat),
})),
[group.cheats, onCheatChange, onTogglePin],
);
useEffect(() => {
if (forceOpen) {
setOpen(true);
@@ -45,17 +54,17 @@ export const CategorySection = ({
return (
<section className="mb-2.5 overflow-hidden rounded-[14px] border border-white/10 bg-white/[0.035] shadow-[inset_0_1px_0_rgba(255,255,255,.05)] backdrop-blur-2xl">
<button type="button" className="flex w-full items-center gap-2.5 px-3.5 py-3 text-left text-(--deck-fg)" onClick={handleToggle}>
<span className="flex size-[30px] shrink-0 items-center justify-center rounded-[8px] border border-[color-mix(in_oklab,var(--deck-accent)_22%,transparent)] bg-white/[0.04] text-(--deck-accent)">
<CategoryIcon category={group.id} className="size-[15px]" />
<span className="flex size-7.5 shrink-0 items-center justify-center rounded-[8px] border border-[color-mix(in_oklab,var(--deck-accent)_22%,transparent)] bg-white/4 text-(--deck-accent)">
<CategoryIcon category={group.id} className="size-3.75" />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold">{group.label}</span>
<span className="mt-0.5 block font-mono text-[10.5px] text-(--deck-fg-4)">{formatSummary(group.cheats.length, enabledCount, toggleCount)}</span>
</span>
{enabledCount > 0 ? <span className="inline-flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full bg-(--deck-accent) px-1.5 text-center font-mono text-[10px] font-bold leading-none tabular-nums text-black">{enabledCount}</span> : null}
{enabledCount > 0 ? <span className="inline-flex h-4.5 min-w-4.5 shrink-0 items-center justify-center rounded-full bg-(--deck-accent) px-1.5 text-center font-mono text-[10px] font-bold leading-none tabular-nums text-black">{enabledCount}</span> : null}
<Icon className={cn('size-4 text-(--deck-fg-3) transition-transform', open ? 'rotate-0' : '-rotate-90')} name="chevron-down" />
</button>
<div className={cn('overflow-hidden transition-[max-height] duration-300', open ? 'max-h-[4000px]' : 'max-h-0')}>
<div className={cn('overflow-hidden transition-[max-height] duration-300', open ? 'max-h-1000' : 'max-h-0')}>
{group.cheats.map((cheat, index) => (
<CheatTile
key={cheat.uuid}
@@ -65,8 +74,8 @@ export const CategorySection = ({
pinned={Boolean(pinnedTargets[cheat.target])}
disabled={disabled}
first={index === 0}
onChange={(nextValue) => onCheatChange(cheat, nextValue)}
onTogglePin={() => onTogglePin(cheat)}
onChange={cheatHandlers[index].onChange}
onTogglePin={cheatHandlers[index].onTogglePin}
/>
))}
</div>
@@ -74,6 +83,18 @@ export const CategorySection = ({
);
};
export const CategorySection = memo(CategorySectionBase, (prev, next) => {
if (prev.group !== next.group) return false;
if (prev.disabled !== next.disabled) return false;
if (prev.forceOpen !== next.forceOpen) return false;
for (const cheat of next.group.cheats) {
if (prev.values[cheat.target] !== next.values[cheat.target]) return false;
if (prev.pendingTargets[cheat.target] !== next.pendingTargets[cheat.target]) return false;
if (prev.pinnedTargets[cheat.target] !== next.pinnedTargets[cheat.target]) return false;
}
return true;
});
function getEnabledToggleCount(cheats: CheatSchema[], values: Record<string, unknown>): number {
return cheats.filter((cheat) => cheat.type === ECheatType.Toggle && Boolean(values[cheat.target])).length;
}
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react';
import { memo, useEffect, useRef, useState, type PointerEvent as ReactPointerEvent } from 'react';
import { Icon } from '@/components/ui/icon';
import { cn } from '@/lib/utils';
@@ -23,7 +23,7 @@ const SWIPE_TRIGGER = 56;
const SWIPE_DEAD_ZONE = 8;
const SWIPE_ANIMATION_MS = 220;
export const CheatTile = ({ cheat, value, pending, disabled, pinned, first, onChange, onTogglePin }: CheatTileProps) => {
const CheatTileBase = ({ cheat, value, pending, disabled, pinned, first, onChange, onTogglePin }: CheatTileProps) => {
const [offset, setOffset] = useState(0);
const [animating, setAnimating] = useState(false);
const [armed, setArmed] = useState(false);
@@ -127,6 +127,8 @@ export const CheatTile = ({ cheat, value, pending, disabled, pinned, first, onCh
);
};
export const CheatTile = memo(CheatTileBase);
const PinReveal = ({ pinned, armed }: { pinned: boolean; armed: boolean }) => {
return (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center justify-center px-4">
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import { memo, useMemo, type ReactNode } from 'react';
import { Icon, type IconName } from '@/components/ui/icon';
@@ -18,9 +18,9 @@ type LibraryDrawerProps = {
onQueryChange: (query: string) => void;
};
export const LibraryDrawer = ({ games, query, canLaunch, onClose, onPin, onPlay, onStop, onQueryChange }: LibraryDrawerProps) => {
const filteredGames = filterLibraryGames(games, query);
const sections = getLibrarySections(filteredGames);
const LibraryDrawerBase = ({ games, query, canLaunch, onClose, onPin, onPlay, onStop, onQueryChange }: LibraryDrawerProps) => {
const filteredGames = useMemo(() => filterLibraryGames(games, query), [games, query]);
const sections = useMemo(() => getLibrarySections(filteredGames), [filteredGames]);
return (
<div className="flex h-full flex-col">
@@ -60,6 +60,8 @@ export const LibraryDrawer = ({ games, query, canLaunch, onClose, onPin, onPlay,
);
};
export const LibraryDrawer = memo(LibraryDrawerBase);
type GameSectionProps = {
title: string;
count?: number;
+10
View File
@@ -166,6 +166,16 @@
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
.remote-glass-header {
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
.remote-glass-control {
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}
.remote-glass-control {