mirror of
https://github.com/k1tbyte/Wand-Enhancer.git
synced 2026-08-28 17:01:04 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user