diff --git a/src/App.tsx b/src/App.tsx
index ca09e08..e643923 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -25,7 +25,7 @@ import {
} from '@/components/dialogs'
// Game components
-import { GameList } from '@/components/games'
+import { GameList, EpicGameList } from '@/components/games'
/**
* Main application component
@@ -71,11 +71,25 @@ function App() {
handleSelectCreamLinux,
handleSelectSmokeAPI,
closeUnlockerDialog,
+ epicGames,
+ epicLoading,
+ epicInstallingId,
+ loadEpicGames,
+ handleEpicInstall,
+ handleEpicUninstallScream,
+ handleEpicUninstallKoaloader,
+ handleEpicSettings,
} = useAppContext()
// Conflict detection
- const { conflicts, showDialog, resolveConflict, closeDialog } =
- useConflictDetection(games)
+ const { conflicts, showDialog, resolveConflict, closeDialog } = useConflictDetection(games)
+
+ const handleSetFilter = async (f: string) => {
+ setFilter(f)
+ if (f === 'epic' && epicGames.length === 0 && !epicLoading) {
+ await loadEpicGames()
+ }
+ }
// Handle conflict resolution
const handleConflictResolve = async (
@@ -126,13 +140,22 @@ function App() {
{/* Sidebar for filtering */}
- {/* Show error or game list */}
- {error ? (
+ {filter === 'epic' ? (
+
+ ) : error ? (
Error Loading Games
{error}
diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx
index 342cf95..6cb97be 100644
--- a/src/contexts/AppContext.tsx
+++ b/src/contexts/AppContext.tsx
@@ -1,5 +1,5 @@
import { createContext } from 'react'
-import { Game, DlcInfo } from '@/types'
+import { Game, DlcInfo, EpicGame } from '@/types'
import { ActionType } from '@/components/buttons/ActionButton'
import { DlcDialogState } from '@/hooks/useDlcManager'
@@ -49,6 +49,16 @@ export interface AppContextType {
handleDlcDialogClose: () => void
handleUpdateDlcs: (gameId: string) => Promise
+ // Epic Games
+ epicGames: EpicGame[]
+ epicLoading: boolean
+ epicInstallingId: string | null
+ loadEpicGames: () => Promise
+ handleEpicInstall: (game: EpicGame) => void
+ handleEpicUninstallScream: (game: EpicGame) => void
+ handleEpicUninstallKoaloader: (game: EpicGame) => void
+ handleEpicSettings: (game: EpicGame) => void
+
// Game actions
progressDialog: ProgressDialogState
handleGameAction: (gameId: string, action: ActionType) => Promise
diff --git a/src/contexts/AppProvider.tsx b/src/contexts/AppProvider.tsx
index a7ecb4a..73c5235 100644
--- a/src/contexts/AppProvider.tsx
+++ b/src/contexts/AppProvider.tsx
@@ -1,11 +1,12 @@
import { ReactNode, useState, useEffect } from 'react'
import { AppContext, AppContextType } from './AppContext'
import { useGames, useDlcManager, useGameActions, useToasts } from '@/hooks'
-import { DlcInfo, Config } from '@/types'
+import { DlcInfo, Config, EpicGame } from '@/types'
import { ActionType } from '@/components/buttons/ActionButton'
import { ToastContainer } from '@/components/notifications'
-import { SmokeAPISettingsDialog, OptInDialog, RatingDialog, SmokeAPIVotesDialog } from '@/components/dialogs'
+import { SmokeAPISettingsDialog, OptInDialog, RatingDialog, SmokeAPIVotesDialog, EpicUnlockerSelectionDialog, ScreamAPISettingsDialog } from '@/components/dialogs'
import { invoke } from '@tauri-apps/api/core'
+import { listen } from '@tauri-apps/api/event'
// Context provider component
interface AppProviderProps {
@@ -43,6 +44,20 @@ export const AppProvider = ({ children }: AppProviderProps) => {
// Settings dialog state
const [settingsDialog, setSettingsDialog] = useState({ visible: false })
+ const [epicGames, setEpicGames] = useState([])
+ const [epicLoading, setEpicLoading] = useState(false)
+ const [epicInstallingId, setEpicInstallingId] = useState(null)
+
+ const [epicUnlockerDialog, setEpicUnlockerDialog] = useState<{
+ visible: boolean
+ game: EpicGame | null
+ }>({ visible: false, game: null })
+
+ const [screamSettingsDialog, setScreamSettingsDialog] = useState<{
+ visible: boolean
+ game: EpicGame | null
+ }>({ visible: false, game: null })
+
// SmokeAPI settings dialog state
const [smokeAPISettingsDialog, setSmokeAPISettingsDialog] = useState<{
visible: boolean
@@ -95,6 +110,91 @@ export const AppProvider = ({ children }: AppProviderProps) => {
.catch((err) => console.error('Failed to load config for reporting check:', err))
}, [])
+ useEffect(() => {
+ let unlisten: (() => void) | undefined
+ listen('epic-game-updated', (event) => {
+ const updated = event.payload
+ const prev = epicGames.find((g) => g.app_name === updated.app_name)
+
+ setEpicGames((games) =>
+ games.map((g) => (g.app_name === updated.app_name ? updated : g))
+ )
+ setEpicInstallingId(null)
+
+ // Determine what changed and show appropriate toast
+ if (prev) {
+ const installedScream = !prev.scream_installed && updated.scream_installed
+ const uninstalledScream = prev.scream_installed && !updated.scream_installed
+ const installedKoa = !prev.koaloader_installed && updated.koaloader_installed
+ const uninstalledKoa = prev.koaloader_installed && !updated.koaloader_installed
+
+ if (installedScream) {
+ success(`ScreamAPI installed for ${updated.title}`)
+ } else if (uninstalledScream) {
+ info(`ScreamAPI removed from ${updated.title}`)
+ } else if (installedKoa) {
+ success(`Koaloader installed for ${updated.title}`)
+ } else if (uninstalledKoa) {
+ info(`Koaloader removed from ${updated.title}`)
+ }
+
+ if (updated.proxy_fallback_used) {
+ warning(
+ 'No compatible proxy import found - installed using version.dll as a fallback. ' +
+ 'If the game has issues, try the direct ScreamAPI method instead.'
+ )
+ }
+ }
+ }).then((fn) => { unlisten = fn })
+ return () => { unlisten?.() }
+ }, [epicGames, success, info, warning])
+
+ const loadEpicGames = async () => {
+ setEpicLoading(true)
+ try {
+ const games = await invoke('scan_epic_games')
+ setEpicGames(games)
+ } catch (e) {
+ showError(`Failed to scan Epic games: ${e}`)
+ } finally {
+ setEpicLoading(false)
+ }
+ }
+
+ const runEpicAction = async (game: EpicGame, action: string) => {
+ setEpicInstallingId(game.app_name)
+ try {
+ await invoke('process_epic_action', { epicAction: { game, action } })
+ // state updated via epic-game-updated event listener
+ } catch (e) {
+ showError(`Action failed: ${e}`)
+ setEpicInstallingId(null)
+ }
+ }
+
+ const handleEpicInstall = (game: EpicGame) => {
+ setEpicUnlockerDialog({ visible: true, game })
+ }
+
+ const handleEpicUninstallScream = (game: EpicGame) => runEpicAction(game, 'uninstall_scream')
+ const handleEpicUninstallKoaloader = (game: EpicGame) => runEpicAction(game, 'uninstall_koaloader')
+
+ const handleEpicSettings = (game: EpicGame) => {
+ setScreamSettingsDialog({ visible: true, game })
+ }
+
+ const handleSelectScreamAPI = () => {
+ const game = epicUnlockerDialog.game
+ setEpicUnlockerDialog({ visible: false, game: null })
+ if (game) runEpicAction(game, 'install_scream')
+ }
+
+ const handleSelectKoaloader = () => {
+ const game = epicUnlockerDialog.game
+ setEpicUnlockerDialog({ visible: false, game: null })
+ if (game) runEpicAction(game, 'install_koaloader')
+ }
+
// Settings handlers
const handleSettingsOpen = () => {
setSettingsDialog({ visible: true })
@@ -366,6 +466,16 @@ export const AppProvider = ({ children }: AppProviderProps) => {
handleDlcDialogClose: closeDlcDialog,
handleUpdateDlcs: (gameId: string) => handleUpdateDlcs(gameId),
+ // Epic games
+ epicGames,
+ epicLoading,
+ epicInstallingId,
+ loadEpicGames,
+ handleEpicInstall,
+ handleEpicUninstallScream,
+ handleEpicUninstallKoaloader,
+ handleEpicSettings,
+
// Game actions
progressDialog,
handleGameAction,
@@ -458,6 +568,23 @@ export const AppProvider = ({ children }: AppProviderProps) => {
gameTitle={smokeAPISettingsDialog.gameTitle}
/>
+ {/* Epic Unlocker Selection Dialog */}
+ setEpicUnlockerDialog({ visible: false, game: null })}
+ onSelectScreamAPI={handleSelectScreamAPI}
+ onSelectKoaloader={handleSelectKoaloader}
+ />
+
+ {/* ScreamAPI Settings Dialog */}
+ setScreamSettingsDialog({ visible: false, game: null })}
+ gamePath={screamSettingsDialog.game?.install_path ?? ''}
+ gameTitle={screamSettingsDialog.game?.title ?? ''}
+ />
+
{/* SmokeAPI Votes Dialog */}