mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-28 14:22:49 -05:00
866 lines
27 KiB
TypeScript
866 lines
27 KiB
TypeScript
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { listen } from '@tauri-apps/api/event';
|
|
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;
|
|
}
|
|
|
|
// Interface for installation instructions
|
|
interface InstructionInfo {
|
|
type: string;
|
|
command: string;
|
|
game_title: string;
|
|
dlc_count?: number;
|
|
}
|
|
|
|
// Interface for DLC information
|
|
interface DlcInfo {
|
|
appid: string;
|
|
name: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
function App() {
|
|
const [games, setGames] = useState<Game[]>([]);
|
|
const [filter, setFilter] = useState("all");
|
|
const [searchQuery, setSearchQuery] = useState(""); // Added search query state
|
|
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);
|
|
};
|
|
|
|
// Move the 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;
|
|
});
|
|
|
|
// Add 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
|
|
}));
|
|
}
|
|
});
|
|
|
|
// In parallel, 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
|
|
// You could implement this on the backend if needed with something like:
|
|
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;
|
|
});
|
|
|
|
// Note: 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
|
|
if (isInitialLoad) {
|
|
return (
|
|
<InitialLoadingScreen
|
|
message={scanProgress.message}
|
|
progress={scanProgress.progress}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<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}
|
|
/>
|
|
)}
|
|
</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>
|
|
);
|
|
}
|
|
|
|
export default App; |