diff --git a/src/App.tsx b/src/App.tsx index ab162a8..32858a1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,27 @@ import { useState } from 'react' +import { invoke } from '@tauri-apps/api/core' import { useAppContext } from '@/contexts/useAppContext' -import { useAppLogic } from '@/hooks' +import { useAppLogic, useConflictDetection } from '@/hooks' import './styles/main.scss' // Layout components -import { Header, Sidebar, InitialLoadingScreen, ErrorBoundary, UpdateScreen, AnimatedBackground } from '@/components/layout' +import { + Header, + Sidebar, + InitialLoadingScreen, + ErrorBoundary, + UpdateScreen, + AnimatedBackground, +} from '@/components/layout' // Dialog components -import { ProgressDialog, DlcSelectionDialog, SettingsDialog } from '@/components/dialogs' +import { + ProgressDialog, + DlcSelectionDialog, + SettingsDialog, + ConflictDialog, + ReminderDialog, +} from '@/components/dialogs' // Game components import { GameList } from '@/components/games' @@ -17,6 +31,7 @@ import { GameList } from '@/components/games' */ function App() { const [updateComplete, setUpdateComplete] = useState(false) + // Get application logic from hook const { filter, @@ -33,6 +48,7 @@ function App() { // Get action handlers from context const { + games, dlcDialog, handleDlcDialogClose, handleProgressDialogClose, @@ -45,8 +61,30 @@ function App() { handleSettingsOpen, handleSettingsClose, handleSmokeAPISettingsOpen, + showToast, } = useAppContext() + // Conflict detection + const { currentConflict, showReminder, resolveConflict, closeReminder } = + useConflictDetection(games) + + // Handle conflict resolution + const handleConflictResolve = async () => { + const resolution = resolveConflict() + if (!resolution) return + + // Always remove files - use the special conflict resolution command + try { + await invoke('resolve_platform_conflict', { + gameId: resolution.gameId, + conflictType: resolution.conflictType, + }) + } catch (error) { + console.error('Error resolving conflict:', error) + showToast(`Failed to resolve conflict: ${error}`, 'error') + } + } + // Show update screen first if (!updateComplete) { return setUpdateComplete(true)} /> @@ -73,7 +111,11 @@ function App() {
{/* Sidebar for filtering */} - + {/* Show error or game list */} {error ? ( @@ -123,10 +165,20 @@ function App() { /> {/* Settings Dialog */} - + + + {/* Conflict Detection Dialog */} + {currentConflict && ( + + )} + + {/* Steam Launch Options Reminder */} +
) diff --git a/src/components/dialogs/index.ts b/src/components/dialogs/index.ts index c5ec29f..5a9f955 100644 --- a/src/components/dialogs/index.ts +++ b/src/components/dialogs/index.ts @@ -8,6 +8,8 @@ export { default as ProgressDialog } from './ProgressDialog' export { default as DlcSelectionDialog } from './DlcSelectionDialog' export { default as SettingsDialog } from './SettingsDialog' export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog' +export { default as ConflictDialog } from './ConflictDialog' +export { default as ReminderDialog } from './ReminderDialog' // Export types export type { DialogProps } from './Dialog' @@ -17,3 +19,5 @@ export type { DialogFooterProps } from './DialogFooter' export type { DialogActionsProps } from './DialogActions' export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog' export type { DlcSelectionDialogProps } from './DlcSelectionDialog' +export type { ConflictDialogProps } from './ConflictDialog' +export type { ReminderDialogProps } from './ReminderDialog' \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 9d4a3da..c31ac31 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -4,7 +4,9 @@ export { useDlcManager } from './useDlcManager' export { useGameActions } from './useGameActions' export { useToasts } from './useToasts' export { useAppLogic } from './useAppLogic' +export { useConflictDetection } from './useConflictDetection' // Export types export type { ToastType, Toast, ToastOptions } from './useToasts' export type { DlcDialogState } from './useDlcManager' +export type { Conflict, ConflictResolution } from './useConflictDetection' \ No newline at end of file diff --git a/src/hooks/useConflictDetection.ts b/src/hooks/useConflictDetection.ts new file mode 100644 index 0000000..fb2fe9b --- /dev/null +++ b/src/hooks/useConflictDetection.ts @@ -0,0 +1,123 @@ +import { useState, useEffect, useCallback } from 'react' +import { Game } from '@/types' + +export interface Conflict { + gameId: string + gameTitle: string + type: 'cream-to-proton' | 'smoke-to-native' +} + +export interface ConflictResolution { + gameId: string + removeFiles: boolean + conflictType: 'cream-to-proton' | 'smoke-to-native' +} + +/** + * Hook for detecting platform conflicts + * Identifies when unlocker files exist for the wrong platform + */ +export function useConflictDetection(games: Game[]) { + const [conflicts, setConflicts] = useState([]) + const [currentConflict, setCurrentConflict] = useState(null) + const [showReminder, setShowReminder] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + const [resolvedConflicts, setResolvedConflicts] = useState>(new Set()) + + // Detect conflicts whenever games change + useEffect(() => { + const detectedConflicts: Conflict[] = [] + + games.forEach((game) => { + // Skip if we've already resolved a conflict for this game + if (resolvedConflicts.has(game.id)) { + return + } + + // Conflict 1: CreamLinux installed but game is now Proton + if (!game.native && game.cream_installed) { + detectedConflicts.push({ + gameId: game.id, + gameTitle: game.title, + type: 'cream-to-proton', + }) + } + + // Conflict 2: SmokeAPI installed but game is now Native + if (game.native && game.smoke_installed) { + detectedConflicts.push({ + gameId: game.id, + gameTitle: game.title, + type: 'smoke-to-native', + }) + } + }) + + setConflicts(detectedConflicts) + + // Show the first conflict if we have any and not currently processing + if (detectedConflicts.length > 0 && !currentConflict && !isProcessing) { + setCurrentConflict(detectedConflicts[0]) + } + }, [games, currentConflict, isProcessing, resolvedConflicts]) + + // Handle conflict resolution + const resolveConflict = useCallback((): ConflictResolution | null => { + if (!currentConflict || isProcessing) return null + + setIsProcessing(true) + + const resolution: ConflictResolution = { + gameId: currentConflict.gameId, + removeFiles: true, // Always remove files + conflictType: currentConflict.type, + } + + // Mark this game as resolved so we don't re-detect the conflict + setResolvedConflicts((prev) => new Set(prev).add(currentConflict.gameId)) + + // Remove this conflict from the list + const remainingConflicts = conflicts.filter((c) => c.gameId !== currentConflict.gameId) + setConflicts(remainingConflicts) + + // Close current conflict dialog immediately + setCurrentConflict(null) + + // Determine what to show next based on conflict type + if (resolution.conflictType === 'cream-to-proton') { + // CreamLinux removal - show reminder after delay + setTimeout(() => { + setShowReminder(true) + setIsProcessing(false) + }, 100) + } else { + // SmokeAPI removal - no reminder, just show next conflict or finish + setTimeout(() => { + if (remainingConflicts.length > 0) { + setCurrentConflict(remainingConflicts[0]) + } + setIsProcessing(false) + }, 100) + } + + return resolution + }, [currentConflict, conflicts, isProcessing]) + + // Close reminder dialog + const closeReminder = useCallback(() => { + setShowReminder(false) + + // After closing reminder, check if there are more conflicts + if (conflicts.length > 0) { + setCurrentConflict(conflicts[0]) + } + }, [conflicts]) + + return { + currentConflict, + showReminder, + resolveConflict, + closeReminder, + hasConflicts: conflicts.length > 0, + } +} \ No newline at end of file