diff --git a/src/components/dialogs/ConflictDialog.tsx b/src/components/dialogs/ConflictDialog.tsx index 20e9d1b..ef2a0a1 100644 --- a/src/components/dialogs/ConflictDialog.tsx +++ b/src/components/dialogs/ConflictDialog.tsx @@ -7,66 +7,95 @@ import { DialogActions, } from '@/components/dialogs' import { Button } from '@/components/buttons' -import { Icon, warning } from '@/components/icons' +import { Icon, warning, info } from '@/components/icons' + +export interface Conflict { + gameId: string + gameTitle: string + type: 'cream-to-proton' | 'smoke-to-native' +} export interface ConflictDialogProps { visible: boolean - gameTitle: string - conflictType: 'cream-to-proton' | 'smoke-to-native' - onConfirm: () => void + conflicts: Conflict[] + onResolve: (gameId: string, conflictType: 'cream-to-proton' | 'smoke-to-native') => void + onClose: () => void } /** * Conflict Dialog component - * Shows when incompatible unlocker files are detected after platform switch + * Shows all conflicts at once with individual resolve buttons */ const ConflictDialog: React.FC = ({ visible, - gameTitle, - conflictType, - onConfirm, + conflicts, + onResolve, + onClose, }) => { - const getConflictMessage = () => { - if (conflictType === 'cream-to-proton') { - return { - title: 'CreamLinux unlocker detected, but game is set to Proton', - bodyPrefix: 'It looks like you previously installed CreamLinux while ', - bodySuffix: ' was running natively. Steam is now configured to run it with Proton, so CreamLinux files will be removed automatically.', - } + // Check if any CreamLinux conflicts exist + const hasCreamConflicts = conflicts.some((c) => c.type === 'cream-to-proton') + + const getConflictDescription = (type: 'cream-to-proton' | 'smoke-to-native') => { + if (type === 'cream-to-proton') { + return 'Will remove existing unlocker files and restore the game to a clean state.' } else { - return { - title: 'SmokeAPI unlocker detected, but game is set to Native', - bodyPrefix: 'It looks like you previously installed SmokeAPI while ', - bodySuffix: ' was running with Proton. Steam is now configured to run it natively, so SmokeAPI files will be removed automatically.', - } + return 'Will remove existing unlocker files and restore the game to a clean state.' } } - const message = getConflictMessage() - return (
-

{message.title}

+

Unlocker conflicts detected

-

- {message.bodyPrefix} - {gameTitle} - {message.bodySuffix} +

+ Some games have conflicting unlocker states that need attention.

+ +
+ {conflicts.map((conflict) => ( +
+
+
+ +
+
+

{conflict.gameTitle}

+

{getConflictDescription(conflict.type)}

+
+
+ +
+ ))} +
+ {hasCreamConflicts && ( +
+ + + Remember to remove sh ./cream.sh %command% from Steam launch options + after resolving CreamLinux conflicts. + +
+ )} -
diff --git a/src/components/dialogs/index.ts b/src/components/dialogs/index.ts index 4667c73..761695e 100644 --- a/src/components/dialogs/index.ts +++ b/src/components/dialogs/index.ts @@ -9,7 +9,6 @@ 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 { default as DisclaimerDialog } from './DisclaimerDialog' // Export types @@ -20,5 +19,4 @@ 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 +export type { ConflictDialogProps, Conflict } from './ConflictDialog' \ No newline at end of file diff --git a/src/hooks/useConflictDetection.ts b/src/hooks/useConflictDetection.ts index fb2fe9b..8694246 100644 --- a/src/hooks/useConflictDetection.ts +++ b/src/hooks/useConflictDetection.ts @@ -9,7 +9,6 @@ export interface Conflict { export interface ConflictResolution { gameId: string - removeFiles: boolean conflictType: 'cream-to-proton' | 'smoke-to-native' } @@ -19,10 +18,9 @@ export interface ConflictResolution { */ 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 [showDialog, setShowDialog] = useState(false) const [resolvedConflicts, setResolvedConflicts] = useState>(new Set()) + const [hasShownThisSession, setHasShownThisSession] = useState(false) // Detect conflicts whenever games change useEffect(() => { @@ -55,69 +53,50 @@ export function useConflictDetection(games: Game[]) { setConflicts(detectedConflicts) - // Show the first conflict if we have any and not currently processing - if (detectedConflicts.length > 0 && !currentConflict && !isProcessing) { - setCurrentConflict(detectedConflicts[0]) + // Show dialog only if: + // 1. We have conflicts + // 2. Dialog isn't already visible + // 3. We haven't shown it this session + if (detectedConflicts.length > 0 && !showDialog && !hasShownThisSession) { + setShowDialog(true) + setHasShownThisSession(true) } - }, [games, currentConflict, isProcessing, resolvedConflicts]) + }, [games, resolvedConflicts, showDialog, hasShownThisSession]) - // Handle conflict resolution - const resolveConflict = useCallback((): ConflictResolution | null => { - if (!currentConflict || isProcessing) return null + // Handle resolving a single conflict + const resolveConflict = useCallback( + (gameId: string, conflictType: 'cream-to-proton' | 'smoke-to-native'): ConflictResolution => { + // Mark this game as resolved + setResolvedConflicts((prev) => new Set(prev).add(gameId)) - setIsProcessing(true) + // Remove from conflicts list + setConflicts((prev) => prev.filter((c) => c.gameId !== gameId)) - const resolution: ConflictResolution = { - gameId: currentConflict.gameId, - removeFiles: true, // Always remove files - conflictType: currentConflict.type, + return { + gameId, + conflictType, + } + }, + [] + ) + + // Auto-close dialog when all conflicts are resolved + useEffect(() => { + if (conflicts.length === 0 && showDialog) { + setShowDialog(false) } + }, [conflicts.length, showDialog]) - // 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]) + // Handle dialog close + const closeDialog = useCallback(() => { + setShowDialog(false) + }, []) return { - currentConflict, - showReminder, + conflicts, + showDialog, resolveConflict, - closeReminder, + closeDialog, hasConflicts: conflicts.length > 0, } } \ No newline at end of file diff --git a/src/styles/components/dialogs/_conflict_dialog.scss b/src/styles/components/dialogs/_conflict_dialog.scss index f4be843..b73fd8c 100644 --- a/src/styles/components/dialogs/_conflict_dialog.scss +++ b/src/styles/components/dialogs/_conflict_dialog.scss @@ -25,64 +25,119 @@ } .conflict-dialog-body { - p { - margin-bottom: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; + + .conflict-intro { + margin: 0; color: var(--text-secondary); line-height: 1.5; + } - &:last-of-type { - margin-bottom: 0; + .conflict-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + } + + .conflict-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + transition: all 0.2s ease; + + &:hover { + background: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.15); } + } - strong { + .conflict-info { + display: flex; + align-items: flex-start; + gap: 0.75rem; + flex: 1; + min-width: 0; // Enable text truncation + } + + .conflict-icon { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: rgba(255, 193, 7, 0.1); + border-radius: 8px; + + svg { + color: var(--warning); + } + } + + .conflict-details { + flex: 1; + min-width: 0; + + h4 { + margin: 0 0 0.25rem 0; + font-size: 0.95rem; + font-weight: var(--semibold); color: var(--text-primary); - font-weight: var(--bold); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } + + p { + margin: 0; + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.4; + } + } + + .conflict-resolve-btn { + flex-shrink: 0; + min-width: 100px; } } -/* - Reminder Dialog Styles - Used for Steam launch option reminders -*/ - -.reminder-dialog-header { +.conflict-reminder { display: flex; align-items: center; - gap: 0.75rem; - - h3 { - margin: 0; - flex: 1; - font-size: 1.1rem; - color: var(--text-primary); - } + gap: 0.5rem; + padding: 0.75rem 1rem; + background: rgba(33, 150, 243, 0.1); + border: 1px solid rgba(33, 150, 243, 0.2); + border-radius: 6px; + font-size: 0.85rem; + color: var(--text-secondary); + line-height: 1.4; + margin-bottom: 1rem; svg { color: var(--info); flex-shrink: 0; } -} -.reminder-dialog-body { - p { - margin-bottom: 1rem; - color: var(--text-secondary); - line-height: 1.5; + span { + flex: 1; } - .reminder-steps { - margin: 1rem 0 0 1.5rem; - padding: 0; - color: var(--text-secondary); - line-height: 1.6; - - li { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } + code { + padding: 0.125rem 0.375rem; + background: rgba(0, 0, 0, 0.3); + border-radius: 3px; + font-family: 'Courier New', monospace; + font-size: 0.8rem; + color: var(--text-primary); + white-space: nowrap; } }