import { useState, useEffect, useRef } from 'react' import { invoke } from '@tauri-apps/api/core' import { listen } from '@tauri-apps/api/event' import { Game, DlcInfo } from '@/types' export interface DlcDialogState { visible: boolean gameId: string gameTitle: string dlcs: DlcInfo[] enabledDlcs: string[] isLoading: boolean isEditMode: boolean isUpdating: boolean progress: number progressMessage: string timeLeft: string error: string | null newDlcsCount: number } /** * Hook for managing DLC functionality * Handles fetching, filtering, and updating DLCs */ export function useDlcManager() { const [isFetchingDlcs, setIsFetchingDlcs] = useState(false) const dlcFetchController = useRef(null) const activeDlcFetchId = useRef(null) const [forceReload, setForceReload] = useState(false) // DLC selection dialog state const [dlcDialog, setDlcDialog] = useState({ visible: false, gameId: '', gameTitle: '', dlcs: [], enabledDlcs: [], isLoading: false, isEditMode: false, isUpdating: false, progress: 0, progressMessage: '', timeLeft: '', error: null, newDlcsCount: 0, }) // Set up event listeners for DLC streaming useEffect(() => { // Listen for individual DLC found events const setupDlcEventListeners = async () => { try { // This event is emitted for each DLC as it's found const unlistenDlcFound = await listen('dlc-found', (event) => { const dlc = JSON.parse(event.payload) as { appid: string; name: string } // Add the DLC to the current list with enabled=true by default setDlcDialog((prev) => ({ ...prev, dlcs: [...prev.dlcs, { ...dlc, enabled: true }], })) }) // When progress is 100%, mark loading as complete and reset fetch state const unlistenDlcProgress = await listen<{ message: string progress: number timeLeft?: string }>('dlc-progress', (event) => { const { message, progress, timeLeft } = event.payload // Update the progress indicator setDlcDialog((prev) => ({ ...prev, progress, progressMessage: message, timeLeft: timeLeft || '', })) // If progress is 100%, mark loading as complete if (progress === 100) { setTimeout(() => { setDlcDialog((prev) => ({ ...prev, isLoading: false, isUpdating: false, })) // Reset fetch state setIsFetchingDlcs(false) activeDlcFetchId.current = null }, 500) } }) // This event is emitted if there's an error const unlistenDlcError = await listen<{ error: string }>('dlc-error', (event) => { const { error } = event.payload console.error('DLC streaming error:', error) // Show error in dialog setDlcDialog((prev) => ({ ...prev, error, isLoading: false, })) }) return () => { unlistenDlcFound() unlistenDlcProgress() unlistenDlcError() } } catch (error) { console.error('Error setting up DLC event listeners:', error) return () => {} } } const cleanup = setupDlcEventListeners() return () => { cleanup.then((fn) => fn()) } }, []) // Clean up if component unmounts during a fetch useEffect(() => { return () => { // Clean up any ongoing fetch operations if (dlcFetchController.current) { dlcFetchController.current.abort() dlcFetchController.current = null } } }, []) // Function to fetch DLCs for a game with streaming updates const streamGameDlcs = async (gameId: string): Promise => { try { // Set up flag to indicate we're fetching DLCs setIsFetchingDlcs(true) activeDlcFetchId.current = gameId // Start streaming DLCs - this won't return DLCs directly // Instead, it triggers events that we'll listen for await invoke('stream_game_dlcs', { gameId }) return } catch (error) { if (error instanceof DOMException && error.name === 'AbortError') { console.log('DLC fetching was aborted') } else { console.error('Error starting DLC stream:', error) throw error } } finally { // Reset state when done or on error setIsFetchingDlcs(false) activeDlcFetchId.current = null } } // Handle game edit (show DLC management dialog) with proper reloading const handleGameEdit = async (gameId: string, games: Game[]) => { const game = games.find((g) => g.id === gameId) if (!game || !game.cream_installed) return // Check if we're already fetching DLCs for this game if (isFetchingDlcs && activeDlcFetchId.current === gameId) { console.log(`Already fetching DLCs for ${gameId}, ignoring duplicate request`) return } try { // Show dialog immediately with empty DLC list setDlcDialog({ visible: true, gameId, gameTitle: game.title, dlcs: [], enabledDlcs: [], isLoading: true, isEditMode: true, isUpdating: false, progress: 0, progressMessage: 'Reading DLC configuration...', timeLeft: '', error: null, newDlcsCount: 0, }) // Always get a fresh copy from the config file console.log('Loading DLC configuration from disk...') try { const allDlcs = await invoke('get_all_dlcs_command', { gamePath: game.path, }).catch((e) => { console.error('Error loading DLCs:', e) return [] as DlcInfo[] }) if (allDlcs.length > 0) { // Log the fresh DLC config console.log('Loaded existing DLC configuration:', allDlcs) // Create a completely new array to avoid reference issues const freshDlcs = allDlcs.map((dlc) => ({ ...dlc })) setDlcDialog((prev) => ({ ...prev, dlcs: freshDlcs, isLoading: false, progress: 100, progressMessage: 'Loaded existing DLC configuration', })) // Reset force reload flag setForceReload(false) return } } catch (error) { console.warn('Could not read existing DLC configuration, falling back to API:', error) // Continue with API loading if config reading fails } // Mark that we're fetching DLCs for this game setIsFetchingDlcs(true) activeDlcFetchId.current = gameId // Create abort controller for fetch operation dlcFetchController.current = new AbortController() // Start streaming DLCs await streamGameDlcs(gameId).catch((error) => { if (error.name !== 'AbortError') { console.error('Error streaming DLCs:', error) setDlcDialog((prev) => ({ ...prev, error: `Failed to load DLCs: ${error}`, isLoading: false, })) } }) // Try to get the enabled DLCs const enabledDlcs = await invoke('get_enabled_dlcs_command', { gamePath: game.path, }).catch(() => [] as string[]) // We'll update the enabled state of DLCs as they come in setDlcDialog((prev) => ({ ...prev, enabledDlcs, })) } catch (error) { console.error('Error preparing DLC edit:', error) setDlcDialog((prev) => ({ ...prev, error: `Failed to prepare DLC editor: ${error}`, isLoading: false, })) } } // Handle DLC selection dialog close const handleDlcDialogClose = () => { // Cancel any in-progress DLC fetching if (isFetchingDlcs && activeDlcFetchId.current) { console.log(`Aborting DLC fetch for game ${activeDlcFetchId.current}`) // This will signal to the Rust backend that we want to stop the process invoke('abort_dlc_fetch', { gameId: activeDlcFetchId.current }).catch((err) => console.error('Error aborting DLC fetch:', err) ) // Reset state activeDlcFetchId.current = null setIsFetchingDlcs(false) } // Clear controller if (dlcFetchController.current) { dlcFetchController.current.abort() dlcFetchController.current = null } // Close dialog and reset state setDlcDialog((prev) => ({ ...prev, visible: false, dlcs: [], // Clear DLCs to force a reload next time })) // Set flag to force reload next time setForceReload(true) } // Update DLCs being streamed with enabled state useEffect(() => { if (dlcDialog.enabledDlcs.length > 0) { setDlcDialog((prev) => ({ ...prev, dlcs: prev.dlcs.map((dlc) => ({ ...dlc, enabled: prev.enabledDlcs.length === 0 || prev.enabledDlcs.includes(dlc.appid), })), })) } }, [dlcDialog.dlcs, dlcDialog.enabledDlcs]) // Function to update DLC list (refetch from Steam API) const handleUpdateDlcs = async (gameId: string) => { try { // Store current app IDs to identify new DLCs later const currentAppIds = new Set(dlcDialog.dlcs.map((dlc) => dlc.appid)) // Set updating state and clear DLCs setDlcDialog((prev) => ({ ...prev, isUpdating: true, isLoading: true, progress: 0, progressMessage: 'Checking for new DLCs...', newDlcsCount: 0, dlcs: [], // Clear current DLCs to start fresh })) // Mark that we're fetching DLCs for this game setIsFetchingDlcs(true) activeDlcFetchId.current = gameId // Start streaming DLCs await streamGameDlcs(gameId) // After streaming, calculate new DLCs // This will be done when progress reaches 100% in the listener setTimeout(() => { setDlcDialog((prev) => { const actualNewCount = prev.dlcs.filter(dlc => !currentAppIds.has(dlc.appid)).length return { ...prev, newDlcsCount: actualNewCount > 0 ? actualNewCount : 0, } }) }, 1000) } catch (error) { console.error('Error updating DLCs:', error) setDlcDialog((prev) => ({ ...prev, error: `Failed to update DLCs: ${error}`, isLoading: false, isUpdating: false, })) } } return { dlcDialog, setDlcDialog, isFetchingDlcs, streamGameDlcs, handleGameEdit, handleDlcDialogClose, handleUpdateDlcs, forceReload, } }