import { useCallback, useEffect, useRef, useState } from 'react'; import { invoke } from '@tauri-apps/api/core'; import { listen, UnlistenFn } from '@tauri-apps/api/event'; import { DownloadProgressPayload, Game, GamesListPayload, InstallStatus, } from '../lib/types'; import { activeStatusById, isInProgress, mergeGameUpdate, normalizeGamesListPayload, } from '../lib/gameState'; interface PendingPatch { install_status?: InstallStatus; clearStatus?: boolean; } const applyPatch = (game: Game, patch: PendingPatch): Game => { let next: Game = { ...game }; if (patch.install_status !== undefined) next.install_status = patch.install_status; if (patch.clearStatus) { next.status_message = undefined; next.status_level = undefined; } return next; }; /** * Owns the games list and derives card status from backend snapshots. Returns * a fire-and-forget `markChecking` helper so action calls can immediately show * a "Checking peers…" state until the next backend snapshot arrives. */ export interface UseGamesResult { games: Game[]; setGames: React.Dispatch>; totalPeerCount: number; requestGames: () => Promise; markChecking: (id: string) => void; } export const useGames = (rescanGameDir: () => void): UseGamesResult => { const [games, setGames] = useState([]); const [totalPeerCount, setTotalPeerCount] = useState(0); const rescanRef = useRef(rescanGameDir); rescanRef.current = rescanGameDir; const markChecking = useCallback((id: string) => { setGames(prev => prev.map(item => item.id === id && !isInProgress(item.install_status) ? applyPatch(item, { install_status: InstallStatus.CheckingPeers, clearStatus: true, }) : item )); }, []); const requestGames = useCallback(async () => { try { await invoke('request_games'); } catch (err) { console.error('request_games failed:', err); } }, []); useEffect(() => { const unlisteners: UnlistenFn[] = []; let cancelled = false; const handleErrorEvent = ( id: string, message: string, { triggerRescan = false }: { triggerRescan?: boolean } = {}, ) => { setGames(prev => prev.map(item => item.id === id ? { ...item, install_status: item.installed ? InstallStatus.Installed : InstallStatus.NotInstalled, status_message: message, status_level: 'error', download_progress: undefined, } : item)); if (triggerRescan) rescanRef.current(); }; const register = async () => { try { unlisteners.push(await listen('games-list-updated', (event) => { const payload = normalizeGamesListPayload( event.payload as GamesListPayload | Game[], ); const activeStatuses = activeStatusById(payload.active_operations); setGames(prev => { const previousById = new Map(prev.map(item => [item.id, item])); return payload.games.map(game => mergeGameUpdate( game, previousById.get(game.id), activeStatuses.get(game.id), )); }); })); unlisteners.push(await listen('game-download-failed', (e) => { handleErrorEvent(e.payload as string, 'Download failed. Please try again.', { triggerRescan: true, }); })); unlisteners.push(await listen('game-download-peers-gone', (e) => { handleErrorEvent(e.payload as string, 'Failed: all peers gone.', { triggerRescan: true, }); })); unlisteners.push(await listen('game-download-progress', (e) => { const { id, ...download_progress } = e.payload as DownloadProgressPayload; setGames(prev => prev.map(item => item.id === id ? { ...item, download_progress, } : item)); })); unlisteners.push(await listen('game-no-peers', (e) => { handleErrorEvent(e.payload as string, 'No peers currently have this game.'); })); unlisteners.push(await listen('game-install-finished', () => { rescanRef.current(); })); unlisteners.push(await listen('game-install-failed', (e) => { handleErrorEvent(e.payload as string, 'Install failed. Please try again.'); })); unlisteners.push(await listen('game-uninstall-failed', (e) => { handleErrorEvent(e.payload as string, 'Uninstall failed. Please try again.'); })); unlisteners.push(await listen('game-remove-download-finished', () => { rescanRef.current(); })); unlisteners.push(await listen('game-remove-download-failed', (e) => { handleErrorEvent(e.payload as string, 'Remove failed. Please try again.', { triggerRescan: true, }); })); unlisteners.push(await listen('peer-count-updated', (e) => { setTotalPeerCount(e.payload as number); })); if (!cancelled) { await invoke('request_games').catch(err => console.error('request_games failed:', err), ); } } catch (err) { console.error('Failed to register game listeners:', err); } }; void register(); return () => { cancelled = true; unlisteners.forEach(fn => fn()); }; }, []); return { games, setGames, totalPeerCount, requestGames, markChecking, }; };