mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-05-02 04:52:03 -04:00
Initial changes
This commit is contained in:
938
src/App.tsx
938
src/App.tsx
@@ -1,854 +1,114 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { useAppContext } from '@/contexts/useAppContext'
|
||||
import { useAppLogic } from '@/hooks'
|
||||
import './styles/main.scss'
|
||||
import GameList from './components/GameList'
|
||||
import Header from './components/Header'
|
||||
import Sidebar from './components/Sidebar'
|
||||
import ProgressDialog from './components/ProgressDialog'
|
||||
import DlcSelectionDialog from './components/DlcSelectionDialog'
|
||||
import AnimatedBackground from './components/AnimatedBackground'
|
||||
import InitialLoadingScreen from './components/InitialLoadingScreen'
|
||||
import { ActionType } from './components/ActionButton'
|
||||
|
||||
// Game interface
|
||||
interface Game {
|
||||
id: string
|
||||
title: string
|
||||
path: string
|
||||
native: boolean
|
||||
platform?: string
|
||||
api_files: string[]
|
||||
cream_installed?: boolean
|
||||
smoke_installed?: boolean
|
||||
installing?: boolean
|
||||
}
|
||||
// Layout components
|
||||
import { Header, Sidebar, InitialLoadingScreen, ErrorBoundary } from '@/components/layout'
|
||||
import AnimatedBackground from '@/components/layout/AnimatedBackground'
|
||||
|
||||
// Interface for installation instructions
|
||||
interface InstructionInfo {
|
||||
type: string
|
||||
command: string
|
||||
game_title: string
|
||||
dlc_count?: number
|
||||
}
|
||||
// Dialog components
|
||||
import { ProgressDialog, DlcSelectionDialog } from '@/components/dialogs'
|
||||
|
||||
// Interface for DLC information
|
||||
interface DlcInfo {
|
||||
appid: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
}
|
||||
// Game components
|
||||
import { GameList } from '@/components/games'
|
||||
|
||||
/**
|
||||
* Main application component
|
||||
*/
|
||||
function App() {
|
||||
const [games, setGames] = useState<Game[]>([])
|
||||
const [filter, setFilter] = useState('all')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true)
|
||||
const [scanProgress, setScanProgress] = useState({
|
||||
message: 'Initializing...',
|
||||
progress: 0,
|
||||
})
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const refreshInProgress = useRef(false)
|
||||
const [isFetchingDlcs, setIsFetchingDlcs] = useState(false)
|
||||
const dlcFetchController = useRef<AbortController | null>(null)
|
||||
const activeDlcFetchId = useRef<string | null>(null)
|
||||
|
||||
// Progress dialog state
|
||||
const [progressDialog, setProgressDialog] = useState({
|
||||
visible: false,
|
||||
title: '',
|
||||
message: '',
|
||||
progress: 0,
|
||||
showInstructions: false,
|
||||
instructions: undefined as InstructionInfo | undefined,
|
||||
})
|
||||
|
||||
// DLC selection dialog state
|
||||
const [dlcDialog, setDlcDialog] = useState({
|
||||
visible: false,
|
||||
gameId: '',
|
||||
gameTitle: '',
|
||||
dlcs: [] as DlcInfo[],
|
||||
enabledDlcs: [] as string[],
|
||||
isLoading: false,
|
||||
isEditMode: false,
|
||||
progress: 0,
|
||||
progressMessage: '',
|
||||
timeLeft: '',
|
||||
error: null as string | null,
|
||||
})
|
||||
|
||||
// Handle search query changes
|
||||
const handleSearchChange = (query: string) => {
|
||||
setSearchQuery(query)
|
||||
}
|
||||
|
||||
// LoadGames function outside of the useEffect to make it reusable
|
||||
const loadGames = useCallback(async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
console.log('Invoking scan_steam_games')
|
||||
const steamGames = await invoke<Game[]>('scan_steam_games').catch((err) => {
|
||||
console.error('Error from scan_steam_games:', err)
|
||||
throw err
|
||||
})
|
||||
|
||||
// Platform property to match the GameList component's expectation
|
||||
const gamesWithPlatform = steamGames.map((game) => ({
|
||||
...game,
|
||||
platform: 'Steam',
|
||||
}))
|
||||
|
||||
console.log(`Loaded ${gamesWithPlatform.length} games`)
|
||||
setGames(gamesWithPlatform)
|
||||
setIsInitialLoad(false) // Mark initial load as complete
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Error loading games:', error)
|
||||
setError(`Failed to load games: ${error}`)
|
||||
setIsInitialLoad(false) // Mark initial load as complete even on error
|
||||
return false
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Set up event listeners first
|
||||
const setupEventListeners = async () => {
|
||||
try {
|
||||
console.log('Setting up event listeners')
|
||||
|
||||
// Listen for progress updates from the backend
|
||||
const unlistenProgress = await listen('installation-progress', (event) => {
|
||||
console.log('Received installation-progress event:', event)
|
||||
|
||||
const { title, message, progress, complete, show_instructions, instructions } =
|
||||
event.payload as {
|
||||
title: string
|
||||
message: string
|
||||
progress: number
|
||||
complete: boolean
|
||||
show_instructions?: boolean
|
||||
instructions?: InstructionInfo
|
||||
}
|
||||
|
||||
if (complete && !show_instructions) {
|
||||
// Hide dialog when complete if no instructions
|
||||
setTimeout(() => {
|
||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||
|
||||
// Only refresh games list if dialog is closing without instructions
|
||||
if (!refreshInProgress.current) {
|
||||
refreshInProgress.current = true
|
||||
setTimeout(() => {
|
||||
loadGames().then(() => {
|
||||
refreshInProgress.current = false
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
// Update progress dialog
|
||||
setProgressDialog({
|
||||
visible: true,
|
||||
title,
|
||||
message,
|
||||
progress,
|
||||
showInstructions: show_instructions || false,
|
||||
instructions,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for scan progress events
|
||||
const unlistenScanProgress = await listen('scan-progress', (event) => {
|
||||
const { message, progress } = event.payload as {
|
||||
message: string
|
||||
progress: number
|
||||
}
|
||||
|
||||
console.log('Received scan-progress event:', message, progress)
|
||||
|
||||
// Update scan progress state
|
||||
setScanProgress({
|
||||
message,
|
||||
progress,
|
||||
})
|
||||
})
|
||||
|
||||
// Listen for individual game updates
|
||||
const unlistenGameUpdated = await listen('game-updated', (event) => {
|
||||
console.log('Received game-updated event:', event)
|
||||
|
||||
const updatedGame = event.payload as Game
|
||||
|
||||
// Update only the specific game in the state
|
||||
setGames((prevGames) =>
|
||||
prevGames.map((game) =>
|
||||
game.id === updatedGame.id ? { ...updatedGame, platform: 'Steam' } : game
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
return () => {
|
||||
unlistenProgress()
|
||||
unlistenScanProgress()
|
||||
unlistenGameUpdated()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting up event listeners:', error)
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
|
||||
// First set up event listeners, then load games
|
||||
let unlisten: (() => void) | null = null
|
||||
|
||||
setupEventListeners()
|
||||
.then((unlistenFn) => {
|
||||
unlisten = unlistenFn
|
||||
return loadGames()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to initialize:', error)
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (unlisten) {
|
||||
unlisten()
|
||||
}
|
||||
}
|
||||
}, [loadGames])
|
||||
|
||||
// Debugging for state changes
|
||||
useEffect(() => {
|
||||
// Debug state changes
|
||||
if (games.length > 0) {
|
||||
// Count native and installed games
|
||||
const nativeCount = games.filter((g) => g.native).length
|
||||
const creamInstalledCount = games.filter((g) => g.cream_installed).length
|
||||
const smokeInstalledCount = games.filter((g) => g.smoke_installed).length
|
||||
|
||||
console.log(
|
||||
`Game state updated: ${games.length} total games, ${nativeCount} native, ${creamInstalledCount} with CreamLinux, ${smokeInstalledCount} with SmokeAPI`
|
||||
)
|
||||
|
||||
// Log any games with unexpected states
|
||||
const problematicGames = games.filter((g) => {
|
||||
// Native games that have SmokeAPI installed (shouldn't happen)
|
||||
if (g.native && g.smoke_installed) return true
|
||||
|
||||
// Non-native games with CreamLinux installed (shouldn't happen)
|
||||
if (!g.native && g.cream_installed) return true
|
||||
|
||||
// Non-native games without API files but with SmokeAPI installed (shouldn't happen)
|
||||
if (!g.native && (!g.api_files || g.api_files.length === 0) && g.smoke_installed)
|
||||
return true
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (problematicGames.length > 0) {
|
||||
console.warn('Found games with unexpected states:', problematicGames)
|
||||
}
|
||||
}
|
||||
}, [games])
|
||||
|
||||
// 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 string) as { appid: string; name: string }
|
||||
|
||||
// Add the DLC to the current list with enabled=true
|
||||
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('dlc-progress', (event) => {
|
||||
const { message, progress, timeLeft } = event.payload as {
|
||||
message: string
|
||||
progress: number
|
||||
timeLeft?: string
|
||||
}
|
||||
|
||||
// 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,
|
||||
}))
|
||||
|
||||
// Reset fetch state
|
||||
setIsFetchingDlcs(false)
|
||||
activeDlcFetchId.current = null
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
// This event is emitted if there's an error
|
||||
const unlistenDlcError = await listen('dlc-error', (event) => {
|
||||
const { error } = event.payload as { error: string }
|
||||
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 unlisten = setupDlcEventListeners()
|
||||
return () => {
|
||||
unlisten.then((fn) => fn())
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Listen for scan progress events
|
||||
useEffect(() => {
|
||||
const listenToScanProgress = async () => {
|
||||
try {
|
||||
const unlistenScanProgress = await listen('scan-progress', (event) => {
|
||||
const { message, progress } = event.payload as {
|
||||
message: string
|
||||
progress: number
|
||||
}
|
||||
|
||||
// Update loading message
|
||||
setProgressDialog((prev) => ({
|
||||
...prev,
|
||||
visible: true,
|
||||
title: 'Scanning for Games',
|
||||
message,
|
||||
progress,
|
||||
showInstructions: false,
|
||||
instructions: undefined,
|
||||
}))
|
||||
|
||||
// Auto-close when complete
|
||||
if (progress >= 100) {
|
||||
setTimeout(() => {
|
||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||
}, 1500)
|
||||
}
|
||||
})
|
||||
|
||||
return unlistenScanProgress
|
||||
} catch (error) {
|
||||
console.error('Error setting up scan progress listener:', error)
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
|
||||
const unlistenPromise = listenToScanProgress()
|
||||
return () => {
|
||||
unlistenPromise.then((unlisten) => unlisten())
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleCloseProgressDialog = () => {
|
||||
// Just hide the dialog without refreshing game list
|
||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||
|
||||
// Only refresh if we need to (instructions didn't trigger update)
|
||||
if (progressDialog.showInstructions === false && !refreshInProgress.current) {
|
||||
refreshInProgress.current = true
|
||||
setTimeout(() => {
|
||||
loadGames().then(() => {
|
||||
refreshInProgress.current = false
|
||||
})
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
// Function to fetch DLCs for a game with streaming updates
|
||||
const streamGameDlcs = async (gameId: string): Promise<void> => {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Handle game edit (show DLC management dialog)
|
||||
const handleGameEdit = async (gameId: string) => {
|
||||
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: [] as string[],
|
||||
isLoading: true,
|
||||
isEditMode: true,
|
||||
progress: 0,
|
||||
progressMessage: 'Reading DLC configuration...',
|
||||
timeLeft: '',
|
||||
error: null,
|
||||
})
|
||||
|
||||
// Try to read all DLCs from the configuration file first (including disabled ones)
|
||||
try {
|
||||
const allDlcs = await invoke<DlcInfo[]>('get_all_dlcs_command', {
|
||||
gamePath: game.path,
|
||||
}).catch(() => [] as DlcInfo[])
|
||||
|
||||
if (allDlcs.length > 0) {
|
||||
// If we have DLCs from the config file, use them
|
||||
console.log('Loaded existing DLC configuration:', allDlcs)
|
||||
|
||||
setDlcDialog((prev) => ({
|
||||
...prev,
|
||||
dlcs: allDlcs,
|
||||
isLoading: false,
|
||||
progress: 100,
|
||||
progressMessage: 'Loaded existing DLC configuration',
|
||||
}))
|
||||
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<string[]>('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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Unified handler for all game actions (install/uninstall cream/smoke)
|
||||
const handleGameAction = async (gameId: string, action: ActionType) => {
|
||||
try {
|
||||
// Find game to get title
|
||||
const game = games.find((g) => g.id === gameId)
|
||||
if (!game) return
|
||||
|
||||
// If we're installing CreamLinux, show DLC selection first
|
||||
if (action === 'install_cream') {
|
||||
try {
|
||||
// Show dialog immediately with empty DLC list and loading state
|
||||
setDlcDialog({
|
||||
visible: true,
|
||||
gameId,
|
||||
gameTitle: game.title,
|
||||
dlcs: [], // Start with an empty array
|
||||
enabledDlcs: [] as string[],
|
||||
isLoading: true,
|
||||
isEditMode: false,
|
||||
progress: 0,
|
||||
progressMessage: 'Fetching DLC list...',
|
||||
timeLeft: '',
|
||||
error: null,
|
||||
})
|
||||
|
||||
// Start streaming DLCs - only once
|
||||
await streamGameDlcs(gameId).catch((error) => {
|
||||
console.error('Error streaming DLCs:', error)
|
||||
setDlcDialog((prev) => ({
|
||||
...prev,
|
||||
error: `Failed to load DLCs: ${error}`,
|
||||
isLoading: false,
|
||||
}))
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error fetching DLCs:', error)
|
||||
|
||||
// If DLC fetching fails, close dialog and show error
|
||||
setDlcDialog((prev) => ({
|
||||
...prev,
|
||||
visible: false,
|
||||
isLoading: false,
|
||||
}))
|
||||
|
||||
setProgressDialog({
|
||||
visible: true,
|
||||
title: `Error fetching DLCs for ${game.title}`,
|
||||
message: `Failed to fetch DLCs: ${error}`,
|
||||
progress: 100,
|
||||
showInstructions: false,
|
||||
instructions: undefined,
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||
}, 3000)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// For other actions, proceed directly
|
||||
// Update local state to show installation in progress
|
||||
setGames((prevGames) =>
|
||||
prevGames.map((g) => (g.id === gameId ? { ...g, installing: true } : g))
|
||||
)
|
||||
|
||||
// Get title based on action
|
||||
const isCream = action.includes('cream')
|
||||
const isInstall = action.includes('install')
|
||||
const product = isCream ? 'CreamLinux' : 'SmokeAPI'
|
||||
const operation = isInstall ? 'Installing' : 'Uninstalling'
|
||||
|
||||
// Show progress dialog
|
||||
setProgressDialog({
|
||||
visible: true,
|
||||
title: `${operation} ${product} for ${game.title}`,
|
||||
message: isInstall ? 'Downloading required files...' : 'Removing files...',
|
||||
progress: isInstall ? 0 : 30,
|
||||
showInstructions: false,
|
||||
instructions: undefined,
|
||||
})
|
||||
|
||||
console.log(`Invoking process_game_action for game ${gameId} with action ${action}`)
|
||||
|
||||
// Call the backend with the unified action
|
||||
const updatedGame = await invoke('process_game_action', {
|
||||
gameAction: {
|
||||
game_id: gameId,
|
||||
action,
|
||||
},
|
||||
}).catch((err) => {
|
||||
console.error(`Error from process_game_action:`, err)
|
||||
throw err
|
||||
})
|
||||
|
||||
console.log('Game action completed, updated game:', updatedGame)
|
||||
|
||||
// Update our local state with the result from the backend
|
||||
if (updatedGame) {
|
||||
setGames((prevGames) =>
|
||||
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing action ${action} for game ${gameId}:`, error)
|
||||
|
||||
// Show error in progress dialog
|
||||
setProgressDialog((prev) => ({
|
||||
...prev,
|
||||
message: `Error: ${error}`,
|
||||
progress: 100,
|
||||
}))
|
||||
|
||||
// Reset installing state
|
||||
setGames((prevGames) =>
|
||||
prevGames.map((game) => (game.id === gameId ? { ...game, installing: false } : game))
|
||||
)
|
||||
|
||||
// Hide dialog after a delay
|
||||
setTimeout(() => {
|
||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
setDlcDialog((prev) => ({ ...prev, visible: false }))
|
||||
}
|
||||
|
||||
// Handle DLC selection confirmation
|
||||
const handleDlcConfirm = async (selectedDlcs: DlcInfo[]) => {
|
||||
// Close the dialog first
|
||||
setDlcDialog((prev) => ({ ...prev, visible: false }))
|
||||
|
||||
const gameId = dlcDialog.gameId
|
||||
const game = games.find((g) => g.id === gameId)
|
||||
if (!game) return
|
||||
|
||||
// Update local state to show installation in progress
|
||||
setGames((prevGames) =>
|
||||
prevGames.map((g) => (g.id === gameId ? { ...g, installing: true } : g))
|
||||
)
|
||||
|
||||
try {
|
||||
if (dlcDialog.isEditMode) {
|
||||
// If in edit mode, we're updating existing cream_api.ini
|
||||
// Show progress dialog for editing
|
||||
setProgressDialog({
|
||||
visible: true,
|
||||
title: `Updating DLCs for ${game.title}`,
|
||||
message: 'Updating DLC configuration...',
|
||||
progress: 30,
|
||||
showInstructions: false,
|
||||
instructions: undefined,
|
||||
})
|
||||
|
||||
// Call the backend to update the DLC configuration
|
||||
await invoke('update_dlc_configuration_command', {
|
||||
gamePath: game.path,
|
||||
dlcs: selectedDlcs,
|
||||
})
|
||||
|
||||
// Update progress dialog for completion
|
||||
setProgressDialog((prev) => ({
|
||||
...prev,
|
||||
title: `Update Complete: ${game.title}`,
|
||||
message: 'DLC configuration updated successfully!',
|
||||
progress: 100,
|
||||
}))
|
||||
|
||||
// Hide dialog after a delay
|
||||
setTimeout(() => {
|
||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||
// Reset installing state
|
||||
setGames((prevGames) =>
|
||||
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
|
||||
)
|
||||
}, 2000)
|
||||
} else {
|
||||
// We're doing a fresh install with selected DLCs
|
||||
// Show progress dialog for installation right away
|
||||
setProgressDialog({
|
||||
visible: true,
|
||||
title: `Installing CreamLinux for ${game.title}`,
|
||||
message: 'Processing...',
|
||||
progress: 0,
|
||||
showInstructions: false,
|
||||
instructions: undefined,
|
||||
})
|
||||
|
||||
// Invoke the installation with the selected DLCs
|
||||
await invoke('install_cream_with_dlcs_command', {
|
||||
gameId,
|
||||
selectedDlcs,
|
||||
}).catch((err) => {
|
||||
console.error(`Error installing CreamLinux with selected DLCs:`, err)
|
||||
throw err
|
||||
})
|
||||
|
||||
// We don't need to manually close the dialog or update the game state
|
||||
// because the backend will emit progress events that handle this
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing DLC selection:', error)
|
||||
|
||||
// Show error in progress dialog
|
||||
setProgressDialog((prev) => ({
|
||||
...prev,
|
||||
message: `Error: ${error}`,
|
||||
progress: 100,
|
||||
}))
|
||||
|
||||
// Reset installing state
|
||||
setGames((prevGames) =>
|
||||
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
|
||||
)
|
||||
|
||||
// Hide dialog after a delay
|
||||
setTimeout(() => {
|
||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
// 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])
|
||||
|
||||
// Filter games based on sidebar filter and search query
|
||||
const filteredGames = games.filter((game) => {
|
||||
// First filter by the platform/type
|
||||
const platformMatch =
|
||||
filter === 'all' ||
|
||||
(filter === 'native' && game.native) ||
|
||||
(filter === 'proton' && !game.native)
|
||||
|
||||
// Then filter by search query (if any)
|
||||
const searchMatch =
|
||||
searchQuery.trim() === '' || game.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
// Both filters must match
|
||||
return platformMatch && searchMatch
|
||||
})
|
||||
|
||||
// Check if we should show the initial loading screen
|
||||
// Get application logic from hook
|
||||
const {
|
||||
filter,
|
||||
setFilter,
|
||||
searchQuery,
|
||||
handleSearchChange,
|
||||
isInitialLoad,
|
||||
scanProgress,
|
||||
filteredGames,
|
||||
handleRefresh,
|
||||
isLoading,
|
||||
error
|
||||
} = useAppLogic({ autoLoad: true })
|
||||
|
||||
// Get action handlers from context
|
||||
const {
|
||||
dlcDialog,
|
||||
handleDlcDialogClose,
|
||||
progressDialog,
|
||||
handleGameAction,
|
||||
handleDlcConfirm,
|
||||
handleGameEdit
|
||||
} = useAppContext()
|
||||
|
||||
// Show loading screen during initial load
|
||||
if (isInitialLoad) {
|
||||
return <InitialLoadingScreen message={scanProgress.message} progress={scanProgress.progress} />
|
||||
return <InitialLoadingScreen
|
||||
message={scanProgress.message}
|
||||
progress={scanProgress.progress}
|
||||
/>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
{/* Animated background */}
|
||||
<AnimatedBackground />
|
||||
<ErrorBoundary>
|
||||
<div className="app-container">
|
||||
{/* Animated background */}
|
||||
<AnimatedBackground />
|
||||
|
||||
<Header onRefresh={loadGames} onSearch={handleSearchChange} searchQuery={searchQuery} />
|
||||
<div className="main-content">
|
||||
<Sidebar setFilter={setFilter} currentFilter={filter} />
|
||||
{error ? (
|
||||
<div className="error-message">
|
||||
<h3>Error Loading Games</h3>
|
||||
<p>{error}</p>
|
||||
<button onClick={loadGames}>Retry</button>
|
||||
</div>
|
||||
) : (
|
||||
<GameList
|
||||
games={filteredGames}
|
||||
isLoading={isLoading}
|
||||
onAction={handleGameAction}
|
||||
onEdit={handleGameEdit}
|
||||
/>
|
||||
)}
|
||||
{/* Header with search */}
|
||||
<Header
|
||||
onRefresh={handleRefresh}
|
||||
onSearch={handleSearchChange}
|
||||
searchQuery={searchQuery}
|
||||
refreshDisabled={isLoading}
|
||||
/>
|
||||
|
||||
<div className="main-content">
|
||||
{/* Sidebar for filtering */}
|
||||
<Sidebar setFilter={setFilter} currentFilter={filter} />
|
||||
|
||||
{/* Show error or game list */}
|
||||
{error ? (
|
||||
<div className="error-message">
|
||||
<h3>Error Loading Games</h3>
|
||||
<p>{error}</p>
|
||||
<button onClick={handleRefresh}>Retry</button>
|
||||
</div>
|
||||
) : (
|
||||
<GameList
|
||||
games={filteredGames}
|
||||
isLoading={isLoading}
|
||||
onAction={handleGameAction}
|
||||
onEdit={handleGameEdit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Progress Dialog */}
|
||||
<ProgressDialog
|
||||
visible={progressDialog.visible}
|
||||
title={progressDialog.title}
|
||||
message={progressDialog.message}
|
||||
progress={progressDialog.progress}
|
||||
showInstructions={progressDialog.showInstructions}
|
||||
instructions={progressDialog.instructions}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
|
||||
{/* DLC Selection Dialog */}
|
||||
<DlcSelectionDialog
|
||||
visible={dlcDialog.visible}
|
||||
gameTitle={dlcDialog.gameTitle}
|
||||
dlcs={dlcDialog.dlcs}
|
||||
isLoading={dlcDialog.isLoading}
|
||||
isEditMode={dlcDialog.isEditMode}
|
||||
loadingProgress={dlcDialog.progress}
|
||||
estimatedTimeLeft={dlcDialog.timeLeft}
|
||||
onClose={handleDlcDialogClose}
|
||||
onConfirm={handleDlcConfirm}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Progress Dialog */}
|
||||
<ProgressDialog
|
||||
visible={progressDialog.visible}
|
||||
title={progressDialog.title}
|
||||
message={progressDialog.message}
|
||||
progress={progressDialog.progress}
|
||||
showInstructions={progressDialog.showInstructions}
|
||||
instructions={progressDialog.instructions}
|
||||
onClose={handleCloseProgressDialog}
|
||||
/>
|
||||
|
||||
{/* DLC Selection Dialog */}
|
||||
<DlcSelectionDialog
|
||||
visible={dlcDialog.visible}
|
||||
gameTitle={dlcDialog.gameTitle}
|
||||
dlcs={dlcDialog.dlcs}
|
||||
isLoading={dlcDialog.isLoading}
|
||||
isEditMode={dlcDialog.isEditMode}
|
||||
loadingProgress={dlcDialog.progress}
|
||||
estimatedTimeLeft={dlcDialog.timeLeft}
|
||||
onClose={handleDlcDialogClose}
|
||||
onConfirm={handleDlcConfirm}
|
||||
/>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
export default App
|
||||
Reference in New Issue
Block a user