mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-24 12:22:49 -05:00
redesign conflict dialog #92
This commit is contained in:
@@ -7,66 +7,95 @@ import {
|
|||||||
DialogActions,
|
DialogActions,
|
||||||
} from '@/components/dialogs'
|
} from '@/components/dialogs'
|
||||||
import { Button } from '@/components/buttons'
|
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 {
|
export interface ConflictDialogProps {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
gameTitle: string
|
conflicts: Conflict[]
|
||||||
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
onResolve: (gameId: string, conflictType: 'cream-to-proton' | 'smoke-to-native') => void
|
||||||
onConfirm: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conflict Dialog component
|
* 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<ConflictDialogProps> = ({
|
const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||||
visible,
|
visible,
|
||||||
gameTitle,
|
conflicts,
|
||||||
conflictType,
|
onResolve,
|
||||||
onConfirm,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const getConflictMessage = () => {
|
// Check if any CreamLinux conflicts exist
|
||||||
if (conflictType === 'cream-to-proton') {
|
const hasCreamConflicts = conflicts.some((c) => c.type === 'cream-to-proton')
|
||||||
return {
|
|
||||||
title: 'CreamLinux unlocker detected, but game is set to Proton',
|
const getConflictDescription = (type: 'cream-to-proton' | 'smoke-to-native') => {
|
||||||
bodyPrefix: 'It looks like you previously installed CreamLinux while ',
|
if (type === 'cream-to-proton') {
|
||||||
bodySuffix: ' was running natively. Steam is now configured to run it with Proton, so CreamLinux files will be removed automatically.',
|
return 'Will remove existing unlocker files and restore the game to a clean state.'
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return 'Will remove existing unlocker files and restore the game to a clean state.'
|
||||||
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.',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = getConflictMessage()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog visible={visible} size="large" preventBackdropClose={true}>
|
<Dialog visible={visible} size="large" preventBackdropClose={true}>
|
||||||
<DialogHeader hideCloseButton={true}>
|
<DialogHeader hideCloseButton={true}>
|
||||||
<div className="conflict-dialog-header">
|
<div className="conflict-dialog-header">
|
||||||
<Icon name={warning} variant="solid" size="lg" />
|
<Icon name={warning} variant="solid" size="lg" />
|
||||||
<h3>{message.title}</h3>
|
<h3>Unlocker conflicts detected</h3>
|
||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<div className="conflict-dialog-body">
|
<div className="conflict-dialog-body">
|
||||||
<p>
|
<p className="conflict-intro">
|
||||||
{message.bodyPrefix}
|
Some games have conflicting unlocker states that need attention.
|
||||||
<strong>{gameTitle}</strong>
|
|
||||||
{message.bodySuffix}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="conflict-list">
|
||||||
|
{conflicts.map((conflict) => (
|
||||||
|
<div key={conflict.gameId} className="conflict-item">
|
||||||
|
<div className="conflict-info">
|
||||||
|
<div className="conflict-icon">
|
||||||
|
<Icon name={warning} variant="solid" size="md" />
|
||||||
|
</div>
|
||||||
|
<div className="conflict-details">
|
||||||
|
<h4>{conflict.gameTitle}</h4>
|
||||||
|
<p>{getConflictDescription(conflict.type)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => onResolve(conflict.gameId, conflict.type)}
|
||||||
|
className="conflict-resolve-btn"
|
||||||
|
>
|
||||||
|
Resolve
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
{hasCreamConflicts && (
|
||||||
|
<div className="conflict-reminder">
|
||||||
|
<Icon name={info} variant="solid" size="md" />
|
||||||
|
<span>
|
||||||
|
Remember to remove <code>sh ./cream.sh %command%</code> from Steam launch options
|
||||||
|
after resolving CreamLinux conflicts.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button variant="primary" onClick={onConfirm}>
|
<Button variant="secondary" onClick={onClose}>
|
||||||
OK
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export { default as DlcSelectionDialog } from './DlcSelectionDialog'
|
|||||||
export { default as SettingsDialog } from './SettingsDialog'
|
export { default as SettingsDialog } from './SettingsDialog'
|
||||||
export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog'
|
export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog'
|
||||||
export { default as ConflictDialog } from './ConflictDialog'
|
export { default as ConflictDialog } from './ConflictDialog'
|
||||||
export { default as ReminderDialog } from './ReminderDialog'
|
|
||||||
export { default as DisclaimerDialog } from './DisclaimerDialog'
|
export { default as DisclaimerDialog } from './DisclaimerDialog'
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
@@ -20,5 +19,4 @@ export type { DialogFooterProps } from './DialogFooter'
|
|||||||
export type { DialogActionsProps } from './DialogActions'
|
export type { DialogActionsProps } from './DialogActions'
|
||||||
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
|
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
|
||||||
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
|
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
|
||||||
export type { ConflictDialogProps } from './ConflictDialog'
|
export type { ConflictDialogProps, Conflict } from './ConflictDialog'
|
||||||
export type { ReminderDialogProps } from './ReminderDialog'
|
|
||||||
@@ -9,7 +9,6 @@ export interface Conflict {
|
|||||||
|
|
||||||
export interface ConflictResolution {
|
export interface ConflictResolution {
|
||||||
gameId: string
|
gameId: string
|
||||||
removeFiles: boolean
|
|
||||||
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,10 +18,9 @@ export interface ConflictResolution {
|
|||||||
*/
|
*/
|
||||||
export function useConflictDetection(games: Game[]) {
|
export function useConflictDetection(games: Game[]) {
|
||||||
const [conflicts, setConflicts] = useState<Conflict[]>([])
|
const [conflicts, setConflicts] = useState<Conflict[]>([])
|
||||||
const [currentConflict, setCurrentConflict] = useState<Conflict | null>(null)
|
const [showDialog, setShowDialog] = useState(false)
|
||||||
const [showReminder, setShowReminder] = useState(false)
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
|
||||||
const [resolvedConflicts, setResolvedConflicts] = useState<Set<string>>(new Set())
|
const [resolvedConflicts, setResolvedConflicts] = useState<Set<string>>(new Set())
|
||||||
|
const [hasShownThisSession, setHasShownThisSession] = useState(false)
|
||||||
|
|
||||||
// Detect conflicts whenever games change
|
// Detect conflicts whenever games change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,69 +53,50 @@ export function useConflictDetection(games: Game[]) {
|
|||||||
|
|
||||||
setConflicts(detectedConflicts)
|
setConflicts(detectedConflicts)
|
||||||
|
|
||||||
// Show the first conflict if we have any and not currently processing
|
// Show dialog only if:
|
||||||
if (detectedConflicts.length > 0 && !currentConflict && !isProcessing) {
|
// 1. We have conflicts
|
||||||
setCurrentConflict(detectedConflicts[0])
|
// 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
|
// Handle resolving a single conflict
|
||||||
const resolveConflict = useCallback((): ConflictResolution | null => {
|
const resolveConflict = useCallback(
|
||||||
if (!currentConflict || isProcessing) return null
|
(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 = {
|
return {
|
||||||
gameId: currentConflict.gameId,
|
gameId,
|
||||||
removeFiles: true, // Always remove files
|
conflictType,
|
||||||
conflictType: currentConflict.type,
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
// Handle dialog close
|
||||||
setResolvedConflicts((prev) => new Set(prev).add(currentConflict.gameId))
|
const closeDialog = useCallback(() => {
|
||||||
|
setShowDialog(false)
|
||||||
// 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 {
|
return {
|
||||||
currentConflict,
|
conflicts,
|
||||||
showReminder,
|
showDialog,
|
||||||
resolveConflict,
|
resolveConflict,
|
||||||
closeReminder,
|
closeDialog,
|
||||||
hasConflicts: conflicts.length > 0,
|
hasConflicts: conflicts.length > 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,64 +25,119 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.conflict-dialog-body {
|
.conflict-dialog-body {
|
||||||
p {
|
display: flex;
|
||||||
margin-bottom: 1rem;
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.conflict-intro {
|
||||||
|
margin: 0;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-of-type {
|
.conflict-list {
|
||||||
margin-bottom: 0;
|
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);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
.conflict-reminder {
|
||||||
Reminder Dialog Styles
|
|
||||||
Used for Steam launch option reminders
|
|
||||||
*/
|
|
||||||
|
|
||||||
.reminder-dialog-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
h3 {
|
background: rgba(33, 150, 243, 0.1);
|
||||||
margin: 0;
|
border: 1px solid rgba(33, 150, 243, 0.2);
|
||||||
flex: 1;
|
border-radius: 6px;
|
||||||
font-size: 1.1rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-secondary);
|
||||||
}
|
line-height: 1.4;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--info);
|
color: var(--info);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.reminder-dialog-body {
|
span {
|
||||||
p {
|
flex: 1;
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reminder-steps {
|
code {
|
||||||
margin: 1rem 0 0 1.5rem;
|
padding: 0.125rem 0.375rem;
|
||||||
padding: 0;
|
background: rgba(0, 0, 0, 0.3);
|
||||||
color: var(--text-secondary);
|
border-radius: 3px;
|
||||||
line-height: 1.6;
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
li {
|
color: var(--text-primary);
|
||||||
margin-bottom: 0.5rem;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user