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
+8
View File
@@ -3,6 +3,14 @@
This file is the source of truth for release notes. This file is the source of truth for release notes.
The newest entry must match the version in `WandEnhancer/Properties/AssemblyInfo.cs`. The newest entry must match the version in `WandEnhancer/Properties/AssemblyInfo.cs`.
## [1.0.8.1] - 2026-05-15
### Fixes
- Fixed a syntax error in the Disable Updates patch that prevented Wand from launching. #70
- Fixed an issue where the Remote Web Panel WebSocket connection wouldn't automatically reconnect when turning returning to the app or turning on the screen.
- Reduced battery consumption and device heating on mobile device by optimizing heavy UI blur effects and eliminating unnecessary React re-renders in the Remote Web Panel. #67
## [1.0.8.0] - 2026-05-06 ## [1.0.8.0] - 2026-05-06
### Features ### Features
+24 -2
View File
@@ -35,15 +35,37 @@ WandEnhancer includes a built-in **Remote Web Panel** allowing you to control ap
3. Scan the displayed **QR code** with your phone's camera. 3. Scan the displayed **QR code** with your phone's camera.
### Troubleshooting & Remote Access: ### Troubleshooting & Remote Access:
- **Page isn't loading?** First, ensure both your PC and phone are connected to the **exact same Wi-Fi network**. Next, make sure **Network Discovery** is turned on in your Windows network settings. If it still doesn't work, Windows Firewall might be blocking the connection—you may need to manually allow inbound traffic on TCP port `3223`. - **Page isn't loading?** First, ensure both your PC and phone are connected to the **same local network**. Some routers and guest Wi-Fi networks enable client isolation/AP isolation, which blocks devices on the same SSID from reaching each other. If it still does not load, check Windows Firewall and allow inbound traffic on TCP port `3223` for your local network. If Windows marked your connection as **Public**, switching it to **Private** can also help.
- **Using mobile data or a different network?** If you want to use the panel over mobile data (LTE/5G) or from an entirely different network, you can use [Tailscale](https://tailscale.com/) or similar VPN tools. - **Using mobile data or a different network?** If you want to use the panel over mobile data (LTE/5G) or from an entirely different network, you can use [Tailscale](https://tailscale.com/) or similar VPN tools.
## 👀 How to use? ## 👀 How to use?
1. Go to the [Releases](https://github.com/k1tbyte/Wand-Enhancer/releases) page. 1. Go to the [Releases](https://github.com/k1tbyte/Wand-Enhancer/releases) page.
2. Download the latest source or binary. 2. Download the latest binary release.
3. Run the enhancer to apply local client modifications. 3. Run the enhancer to apply local client modifications.
> Source archives are intended for developers who want to build the project locally. They are not prebuilt binaries.
## 🛠️ How to build from source
Building from source on Windows requires a local development environment.
### Requirements
- `CMake`
- `Node.js` and `pnpm`
- `Visual Studio 2022` or `Build Tools for Visual Studio 2022` with `MSBuild`
- Visual Studio `Desktop development with C++` workload
- .NET Framework 4.8 desktop build tools / targeting pack
### Build steps
1. Clone this repository.
2. Install the requirements above and make sure `cmake`, `pnpm`, and `MSBuild` are available.
3. Run `build.cmd` from Command Prompt or PowerShell.
The build script installs the web panel dependencies, builds the frontend, compiles the native helper with CMake, restores NuGet packages, and builds the WPF solution.
--- ---
## ❓ Q&A ## ❓ Q&A
+1 -1
View File
@@ -85,7 +85,7 @@ namespace WandEnhancer.Core
SearchHints = new[] { "ACTION_CHECK_FOR_UPDATE" }, SearchHints = new[] { "ACTION_CHECK_FOR_UPDATE" },
Target = new Regex(@"registerHandler\(""ACTION_CHECK_FOR_UPDATE"".*?\)\)\)\)", Target = new Regex(@"registerHandler\(""ACTION_CHECK_FOR_UPDATE"".*?\)\)\)\)",
RegexOptions.Singleline), RegexOptions.Singleline),
Patch = "registerHandler(\"ACTION_CHECK_FOR_UPDATE\",(e=>expectUpdateFeedUrl(e,(e=>null)))" Patch = "registerHandler(\"ACTION_CHECK_FOR_UPDATE\",(e=>expectUpdateFeedUrl(e,(e=>null))))"
} }
} }
}, },
+2 -2
View File
@@ -51,5 +51,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.8.0")] [assembly: AssemblyVersion("1.0.8.1")]
[assembly: AssemblyFileVersion("1.0.8.0")] [assembly: AssemblyFileVersion("1.0.8.1")]
+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 { buildPinnedGroup, filterGroups, groupCheatsByCategory } from '@/features/remote-panel/category';
import { CategorySection } from '@/features/remote-panel/components/CategorySection'; 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 { handleProtocolMessage } from '@/features/remote-panel/message-handler';
import { getPinnedStorageKey, loadPinnedTargets, savePinnedTargets } from '@/features/remote-panel/pinned-storage'; 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 { 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 { ECheatType } from '@/features/remote-panel/protocol';
import { PanelSocketClient } from '@/features/remote-panel/socket-client'; import { PanelSocketClient } from '@/features/remote-panel/socket-client';
import { createInitialPanelState, EConnectionStatus, panelReducer } from '@/features/remote-panel/state'; import { createInitialPanelState, EConnectionStatus, panelReducer } from '@/features/remote-panel/state';
@@ -35,19 +35,27 @@ export const App = () => {
const [presets, setPresets] = useState<RemotePreset[]>([]); const [presets, setPresets] = useState<RemotePreset[]>([]);
const lastScrollRef = useRef(0); const lastScrollRef = useRef(0);
const clientRef = useRef<PanelSocketClient | null>(null); 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(() => { useEffect(() => {
setPinnedGameIds(loadPinnedGameIds()); setPinnedGameIds(loadPinnedGameIds());
return () => { return () => {
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
}
clientRef.current?.disconnect(); clientRef.current?.disconnect();
clientRef.current = null; clientRef.current = null;
}; };
}, []); }, []);
useEffect(() => { useEffect(() => {
trainerMetaRef.current = state.trainerMeta; stateRef.current = state;
}, [state.trainerMeta]); handleConnectRef.current = handleConnect;
pinnedStorageKeyRef.current = pinnedStorageKey;
});
const activeTrainer = state.trainerMeta?.trainer ?? null; const activeTrainer = state.trainerMeta?.trainer ?? null;
const libraryGames = useMemo( 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 { function handleConnect(): void {
clientRef.current?.disconnect(); clientRef.current?.disconnect();
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
const wsUrl = state.wsUrl.trim(); const wsUrl = state.wsUrl.trim();
if (!wsUrl) { if (!wsUrl) {
@@ -95,8 +117,17 @@ export const App = () => {
const nextClient = new PanelSocketClient(wsUrl, { const nextClient = new PanelSocketClient(wsUrl, {
onConnecting: () => dispatch({ type: 'connecting' }), onConnecting: () => dispatch({ type: 'connecting' }),
onOpen: () => dispatch({ type: 'connected' }), onOpen: () => dispatch({ type: 'connected' }),
onMessage: (message) => handleProtocolMessage(dispatch, message, trainerMetaRef.current), onMessage: (message) => handleProtocolMessage(dispatch, message, stateRef.current.trainerMeta),
onClose: () => dispatch({ type: 'error', message: 'The WebSocket connection closed.' }), 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 }), onError: (message) => dispatch({ type: 'error', message }),
}); });
@@ -105,30 +136,36 @@ export const App = () => {
} }
function handleDisconnect(): void { function handleDisconnect(): void {
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
clientRef.current?.disconnect(); clientRef.current?.disconnect();
clientRef.current = null; clientRef.current = null;
dispatch({ type: 'disconnected' }); 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); const normalizedValue = normalizeOutgoingValue(cheat, nextValue);
dispatch({ type: 'setPending', target: cheat.target, pending: true }); dispatch({ type: 'setPending', target: cheat.target, pending: true });
dispatch({ type: 'valueChanged', target: cheat.target, value: normalizedValue }); 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 }); dispatch({ type: 'setPending', target: cheat.target, pending: false });
return; 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) { if (!sent) {
dispatch({ type: 'setPending', target: cheat.target, pending: false }); dispatch({ type: 'setPending', target: cheat.target, pending: false });
dispatch({ type: 'error', message: 'The bridge socket is not open.' }); dispatch({ type: 'error', message: 'The bridge socket is not open.' });
} }
} }, []);
function handleToggleCheatPin(cheat: CheatSchema): void { const handleToggleCheatPin = useCallback((cheat: CheatSchema): void => {
const next = { ...state.pinnedTargets }; const { pinnedTargets } = stateRef.current;
const next = { ...pinnedTargets };
if (next[cheat.target]) { if (next[cheat.target]) {
delete next[cheat.target]; delete next[cheat.target];
} else { } else {
@@ -136,8 +173,8 @@ export const App = () => {
} }
dispatch({ type: 'togglePinnedTarget', target: cheat.target }); dispatch({ type: 'togglePinnedTarget', target: cheat.target });
savePinnedTargets(pinnedStorageKey, next); savePinnedTargets(pinnedStorageKeyRef.current, next);
} }, []);
function handleToggleGamePin(game: LibraryGame): void { function handleToggleGamePin(game: LibraryGame): void {
const next = togglePinnedGame(game, pinnedGameIds); const next = togglePinnedGame(game, pinnedGameIds);
@@ -248,11 +285,11 @@ export const App = () => {
<main className="min-h-svh bg-[#050608] text-(--deck-fg)"> <main className="min-h-svh bg-[#050608] text-(--deck-fg)">
<div className="flex min-h-svh w-full p-0"> <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)]"> <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="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"> <div className="relative z-10 flex h-full flex-col">
<TopBar status={state.connectionStatus} currentGame={currentGame} runningTrainer={activeTrainer} onOpenSettings={() => setLeftOpen(true)} /> <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 ? ( {!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)} /> <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 ? ( ) : !activeTrainer ? (
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useState } from 'react'; import { memo, useEffect, useMemo, useState } from 'react';
import { Icon } from '@/components/ui/icon'; import { Icon } from '@/components/ui/icon';
@@ -20,7 +20,7 @@ type CategorySectionProps = {
onTogglePin: (cheat: CheatSchema) => void; onTogglePin: (cheat: CheatSchema) => void;
}; };
export const CategorySection = ({ const CategorySectionBase = ({
group, group,
values, values,
pendingTargets, pendingTargets,
@@ -36,6 +36,15 @@ export const CategorySection = ({
const toggleCount = useMemo(() => getToggleCount(group.cheats), [group.cheats]); const toggleCount = useMemo(() => getToggleCount(group.cheats), [group.cheats]);
const handleToggle = () => setOpen((current) => !current); 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(() => { useEffect(() => {
if (forceOpen) { if (forceOpen) {
setOpen(true); setOpen(true);
@@ -45,17 +54,17 @@ export const CategorySection = ({
return ( 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"> <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}> <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)"> <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-[15px]" /> <CategoryIcon category={group.id} className="size-3.75" />
</span> </span>
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold">{group.label}</span> <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 className="mt-0.5 block font-mono text-[10.5px] text-(--deck-fg-4)">{formatSummary(group.cheats.length, enabledCount, toggleCount)}</span>
</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" /> <Icon className={cn('size-4 text-(--deck-fg-3) transition-transform', open ? 'rotate-0' : '-rotate-90')} name="chevron-down" />
</button> </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) => ( {group.cheats.map((cheat, index) => (
<CheatTile <CheatTile
key={cheat.uuid} key={cheat.uuid}
@@ -65,8 +74,8 @@ export const CategorySection = ({
pinned={Boolean(pinnedTargets[cheat.target])} pinned={Boolean(pinnedTargets[cheat.target])}
disabled={disabled} disabled={disabled}
first={index === 0} first={index === 0}
onChange={(nextValue) => onCheatChange(cheat, nextValue)} onChange={cheatHandlers[index].onChange}
onTogglePin={() => onTogglePin(cheat)} onTogglePin={cheatHandlers[index].onTogglePin}
/> />
))} ))}
</div> </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 { function getEnabledToggleCount(cheats: CheatSchema[], values: Record<string, unknown>): number {
return cheats.filter((cheat) => cheat.type === ECheatType.Toggle && Boolean(values[cheat.target])).length; 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 { Icon } from '@/components/ui/icon';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -23,7 +23,7 @@ const SWIPE_TRIGGER = 56;
const SWIPE_DEAD_ZONE = 8; const SWIPE_DEAD_ZONE = 8;
const SWIPE_ANIMATION_MS = 220; 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 [offset, setOffset] = useState(0);
const [animating, setAnimating] = useState(false); const [animating, setAnimating] = useState(false);
const [armed, setArmed] = 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 }) => { const PinReveal = ({ pinned, armed }: { pinned: boolean; armed: boolean }) => {
return ( return (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center justify-center px-4"> <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'; import { Icon, type IconName } from '@/components/ui/icon';
@@ -18,9 +18,9 @@ type LibraryDrawerProps = {
onQueryChange: (query: string) => void; onQueryChange: (query: string) => void;
}; };
export const LibraryDrawer = ({ games, query, canLaunch, onClose, onPin, onPlay, onStop, onQueryChange }: LibraryDrawerProps) => { const LibraryDrawerBase = ({ games, query, canLaunch, onClose, onPin, onPlay, onStop, onQueryChange }: LibraryDrawerProps) => {
const filteredGames = filterLibraryGames(games, query); const filteredGames = useMemo(() => filterLibraryGames(games, query), [games, query]);
const sections = getLibrarySections(filteredGames); const sections = useMemo(() => getLibrarySections(filteredGames), [filteredGames]);
return ( return (
<div className="flex h-full flex-col"> <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 = { type GameSectionProps = {
title: string; title: string;
count?: number; count?: number;
+10
View File
@@ -166,6 +166,16 @@
-webkit-backdrop-filter: none; -webkit-backdrop-filter: none;
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 { .remote-glass-control {