mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2025-12-05 19:45:36 -05:00
Formatting
This commit is contained in:
@@ -47,10 +47,10 @@ function StatusIndicator({ status }) {
|
|||||||
status === 'success'
|
status === 'success'
|
||||||
? 'Check'
|
? 'Check'
|
||||||
: status === 'warning'
|
: status === 'warning'
|
||||||
? 'Warning'
|
? 'Warning'
|
||||||
: status === 'error'
|
: status === 'error'
|
||||||
? 'Close'
|
? 'Close'
|
||||||
: 'Info'
|
: 'Info'
|
||||||
|
|
||||||
return <Icon name={iconName} variant="bold" />
|
return <Icon name={iconName} variant="bold" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ function App() {
|
|||||||
filteredGames,
|
filteredGames,
|
||||||
handleRefresh,
|
handleRefresh,
|
||||||
isLoading,
|
isLoading,
|
||||||
error
|
error,
|
||||||
} = useAppLogic({ autoLoad: true })
|
} = useAppLogic({ autoLoad: true })
|
||||||
|
|
||||||
// Get action handlers from context
|
// Get action handlers from context
|
||||||
@@ -38,15 +38,12 @@ function App() {
|
|||||||
progressDialog,
|
progressDialog,
|
||||||
handleGameAction,
|
handleGameAction,
|
||||||
handleDlcConfirm,
|
handleDlcConfirm,
|
||||||
handleGameEdit
|
handleGameEdit,
|
||||||
} = useAppContext()
|
} = useAppContext()
|
||||||
|
|
||||||
// Show loading screen during initial load
|
// Show loading screen during initial load
|
||||||
if (isInitialLoad) {
|
if (isInitialLoad) {
|
||||||
return <InitialLoadingScreen
|
return <InitialLoadingScreen message={scanProgress.message} progress={scanProgress.progress} />
|
||||||
message={scanProgress.message}
|
|
||||||
progress={scanProgress.progress}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -66,13 +66,9 @@ const ActionButton: FC<ActionButtonProps> = ({
|
|||||||
disabled={disabled || isWorking}
|
disabled={disabled || isWorking}
|
||||||
fullWidth
|
fullWidth
|
||||||
className={`action-button ${className}`}
|
className={`action-button ${className}`}
|
||||||
leftIcon={isWorking ? undefined : (
|
leftIcon={
|
||||||
<Icon
|
isWorking ? undefined : <Icon name={iconInfo.name} variant={iconInfo.variant} size="md" />
|
||||||
name={iconInfo.name}
|
}
|
||||||
variant={iconInfo.variant}
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{getButtonText()}
|
{getButtonText()}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Icon, check } from '@/components/icons'
|
import { Icon, check } from '@/components/icons'
|
||||||
|
|
||||||
interface AnimatedCheckboxProps {
|
interface AnimatedCheckboxProps {
|
||||||
checked: boolean;
|
checked: boolean
|
||||||
onChange: () => void;
|
onChange: () => void
|
||||||
label?: string;
|
label?: string
|
||||||
sublabel?: string;
|
sublabel?: string
|
||||||
className?: string;
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,22 +20,10 @@ const AnimatedCheckbox = ({
|
|||||||
}: AnimatedCheckboxProps) => {
|
}: AnimatedCheckboxProps) => {
|
||||||
return (
|
return (
|
||||||
<label className={`animated-checkbox ${className}`}>
|
<label className={`animated-checkbox ${className}`}>
|
||||||
<input
|
<input type="checkbox" checked={checked} onChange={onChange} className="checkbox-original" />
|
||||||
type="checkbox"
|
|
||||||
checked={checked}
|
|
||||||
onChange={onChange}
|
|
||||||
className="checkbox-original"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={`checkbox-custom ${checked ? 'checked' : ''}`}>
|
<span className={`checkbox-custom ${checked ? 'checked' : ''}`}>
|
||||||
{checked && (
|
{checked && <Icon name={check} variant="bold" size="sm" className="checkbox-icon" />}
|
||||||
<Icon
|
|
||||||
name={check}
|
|
||||||
variant="bold"
|
|
||||||
size="sm"
|
|
||||||
className="checkbox-icon"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{(label || sublabel) && (
|
{(label || sublabel) && (
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { FC, ButtonHTMLAttributes } from 'react';
|
import { FC, ButtonHTMLAttributes } from 'react'
|
||||||
|
|
||||||
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning';
|
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning'
|
||||||
export type ButtonSize = 'small' | 'medium' | 'large';
|
export type ButtonSize = 'small' | 'medium' | 'large'
|
||||||
|
|
||||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: ButtonVariant;
|
variant?: ButtonVariant
|
||||||
size?: ButtonSize;
|
size?: ButtonSize
|
||||||
isLoading?: boolean;
|
isLoading?: boolean
|
||||||
leftIcon?: React.ReactNode;
|
leftIcon?: React.ReactNode
|
||||||
rightIcon?: React.ReactNode;
|
rightIcon?: React.ReactNode
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +32,7 @@ const Button: FC<ButtonProps> = ({
|
|||||||
small: 'btn-sm',
|
small: 'btn-sm',
|
||||||
medium: 'btn-md',
|
medium: 'btn-md',
|
||||||
large: 'btn-lg',
|
large: 'btn-lg',
|
||||||
}[size];
|
}[size]
|
||||||
|
|
||||||
// Variant class mapping
|
// Variant class mapping
|
||||||
const variantClass = {
|
const variantClass = {
|
||||||
@@ -41,7 +41,7 @@ const Button: FC<ButtonProps> = ({
|
|||||||
danger: 'btn-danger',
|
danger: 'btn-danger',
|
||||||
success: 'btn-success',
|
success: 'btn-success',
|
||||||
warning: 'btn-warning',
|
warning: 'btn-warning',
|
||||||
}[variant];
|
}[variant]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -61,7 +61,7 @@ const Button: FC<ButtonProps> = ({
|
|||||||
<span className="btn-text">{children}</span>
|
<span className="btn-text">{children}</span>
|
||||||
{rightIcon && !isLoading && <span className="btn-icon btn-icon-right">{rightIcon}</span>}
|
{rightIcon && !isLoading && <span className="btn-icon btn-icon-right">{rightIcon}</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Button;
|
export default Button
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
// Export all button components
|
// Export all button components
|
||||||
export { default as Button } from './Button';
|
export { default as Button } from './Button'
|
||||||
export { default as ActionButton } from './ActionButton';
|
export { default as ActionButton } from './ActionButton'
|
||||||
export { default as AnimatedCheckbox } from './AnimatedCheckbox';
|
export { default as AnimatedCheckbox } from './AnimatedCheckbox'
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type { ButtonVariant, ButtonSize } from './Button';
|
export type { ButtonVariant, ButtonSize } from './Button'
|
||||||
export type { ActionType } from './ActionButton';
|
export type { ActionType } from './ActionButton'
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ export type LoadingType = 'spinner' | 'dots' | 'progress'
|
|||||||
export type LoadingSize = 'small' | 'medium' | 'large'
|
export type LoadingSize = 'small' | 'medium' | 'large'
|
||||||
|
|
||||||
interface LoadingIndicatorProps {
|
interface LoadingIndicatorProps {
|
||||||
size?: LoadingSize;
|
size?: LoadingSize
|
||||||
type?: LoadingType;
|
type?: LoadingType
|
||||||
message?: string;
|
message?: string
|
||||||
progress?: number;
|
progress?: number
|
||||||
className?: string;
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,9 +53,7 @@ const LoadingIndicator = ({
|
|||||||
style={{ width: `${Math.min(Math.max(progress, 0), 100)}%` }}
|
style={{ width: `${Math.min(Math.max(progress, 0), 100)}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
{progress > 0 && (
|
{progress > 0 && <div className="progress-percentage">{Math.round(progress)}%</div>}
|
||||||
<div className="progress-percentage">{Math.round(progress)}%</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export { default as LoadingIndicator } from './LoadingIndicator';
|
export { default as LoadingIndicator } from './LoadingIndicator'
|
||||||
|
|
||||||
export type { LoadingSize, LoadingType } from './LoadingIndicator';
|
export type { LoadingSize, LoadingType } from './LoadingIndicator'
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { ReactNode, useEffect, useState } from 'react'
|
import { ReactNode, useEffect, useState } from 'react'
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
visible: boolean;
|
visible: boolean
|
||||||
onClose?: () => void;
|
onClose?: () => void
|
||||||
className?: string;
|
className?: string
|
||||||
preventBackdropClose?: boolean;
|
preventBackdropClose?: boolean
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
size?: 'small' | 'medium' | 'large';
|
size?: 'small' | 'medium' | 'large'
|
||||||
showAnimationOnUnmount?: boolean;
|
showAnimationOnUnmount?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,13 +66,8 @@ const Dialog = ({
|
|||||||
}[size]
|
}[size]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`dialog-overlay ${showContent ? 'visible' : ''}`} onClick={handleBackdropClick}>
|
||||||
className={`dialog-overlay ${showContent ? 'visible' : ''}`}
|
<div className={`dialog ${sizeClass} ${className} ${showContent ? 'dialog-visible' : ''}`}>
|
||||||
onClick={handleBackdropClick}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`dialog ${sizeClass} ${className} ${showContent ? 'dialog-visible' : ''}`}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,31 +1,23 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
export interface DialogActionsProps {
|
export interface DialogActionsProps {
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
className?: string;
|
className?: string
|
||||||
align?: 'start' | 'center' | 'end';
|
align?: 'start' | 'center' | 'end'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actions container for dialog footers
|
* Actions container for dialog footers
|
||||||
* Provides consistent spacing and alignment for action buttons
|
* Provides consistent spacing and alignment for action buttons
|
||||||
*/
|
*/
|
||||||
const DialogActions = ({
|
const DialogActions = ({ children, className = '', align = 'end' }: DialogActionsProps) => {
|
||||||
children,
|
|
||||||
className = '',
|
|
||||||
align = 'end'
|
|
||||||
}: DialogActionsProps) => {
|
|
||||||
const alignClass = {
|
const alignClass = {
|
||||||
start: 'justify-start',
|
start: 'justify-start',
|
||||||
center: 'justify-center',
|
center: 'justify-center',
|
||||||
end: 'justify-end'
|
end: 'justify-end',
|
||||||
}[align];
|
}[align]
|
||||||
|
|
||||||
return (
|
return <div className={`dialog-actions ${alignClass} ${className}`}>{children}</div>
|
||||||
<div className={`dialog-actions ${alignClass} ${className}`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DialogActions
|
export default DialogActions
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
export interface DialogBodyProps {
|
export interface DialogBodyProps {
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
className?: string;
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,11 +10,7 @@ export interface DialogBodyProps {
|
|||||||
* Contains the main content with scrolling capability
|
* Contains the main content with scrolling capability
|
||||||
*/
|
*/
|
||||||
const DialogBody = ({ children, className = '' }: DialogBodyProps) => {
|
const DialogBody = ({ children, className = '' }: DialogBodyProps) => {
|
||||||
return (
|
return <div className={`dialog-body ${className}`}>{children}</div>
|
||||||
<div className={`dialog-body ${className}`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DialogBody
|
export default DialogBody
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
export interface DialogFooterProps {
|
export interface DialogFooterProps {
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
className?: string;
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,11 +10,7 @@ export interface DialogFooterProps {
|
|||||||
* Contains action buttons and optional status information
|
* Contains action buttons and optional status information
|
||||||
*/
|
*/
|
||||||
const DialogFooter = ({ children, className = '' }: DialogFooterProps) => {
|
const DialogFooter = ({ children, className = '' }: DialogFooterProps) => {
|
||||||
return (
|
return <div className={`dialog-footer ${className}`}>{children}</div>
|
||||||
<div className={`dialog-footer ${className}`}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DialogFooter
|
export default DialogFooter
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
export interface DialogHeaderProps {
|
export interface DialogHeaderProps {
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
className?: string;
|
className?: string
|
||||||
onClose?: () => void;
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,11 +15,7 @@ const DialogHeader = ({ children, className = '', onClose }: DialogHeaderProps)
|
|||||||
<div className={`dialog-header ${className}`}>
|
<div className={`dialog-header ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
{onClose && (
|
{onClose && (
|
||||||
<button
|
<button className="dialog-close-button" onClick={onClose} aria-label="Close dialog">
|
||||||
className="dialog-close-button"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="Close dialog"
|
|
||||||
>
|
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ import { Button, AnimatedCheckbox } from '@/components/buttons'
|
|||||||
import { DlcInfo } from '@/types'
|
import { DlcInfo } from '@/types'
|
||||||
|
|
||||||
export interface DlcSelectionDialogProps {
|
export interface DlcSelectionDialogProps {
|
||||||
visible: boolean;
|
visible: boolean
|
||||||
gameTitle: string;
|
gameTitle: string
|
||||||
dlcs: DlcInfo[];
|
dlcs: DlcInfo[]
|
||||||
onClose: () => void;
|
onClose: () => void
|
||||||
onConfirm: (selectedDlcs: DlcInfo[]) => void;
|
onConfirm: (selectedDlcs: DlcInfo[]) => void
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
isEditMode?: boolean;
|
isEditMode?: boolean
|
||||||
loadingProgress?: number;
|
loadingProgress?: number
|
||||||
estimatedTimeLeft?: string;
|
estimatedTimeLeft?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,9 +118,9 @@ const DlcSelectionDialog = ({
|
|||||||
// Submit selected DLCs to parent component
|
// Submit selected DLCs to parent component
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
// Create a deep copy to prevent reference issues
|
// Create a deep copy to prevent reference issues
|
||||||
const dlcsCopy = JSON.parse(JSON.stringify(selectedDlcs));
|
const dlcsCopy = JSON.parse(JSON.stringify(selectedDlcs))
|
||||||
onConfirm(dlcsCopy);
|
onConfirm(dlcsCopy)
|
||||||
}, [onConfirm, selectedDlcs]);
|
}, [onConfirm, selectedDlcs])
|
||||||
|
|
||||||
// Count selected DLCs
|
// Count selected DLCs
|
||||||
const selectedCount = selectedDlcs.filter((dlc) => dlc.enabled).length
|
const selectedCount = selectedDlcs.filter((dlc) => dlc.enabled).length
|
||||||
@@ -140,12 +140,7 @@ const DlcSelectionDialog = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog visible={visible} onClose={onClose} size="large" preventBackdropClose={isLoading}>
|
||||||
visible={visible}
|
|
||||||
onClose={onClose}
|
|
||||||
size="large"
|
|
||||||
preventBackdropClose={isLoading}
|
|
||||||
>
|
|
||||||
<DialogHeader onClose={onClose}>
|
<DialogHeader onClose={onClose}>
|
||||||
<h3>{dialogTitle}</h3>
|
<h3>{dialogTitle}</h3>
|
||||||
<div className="dlc-game-info">
|
<div className="dlc-game-info">
|
||||||
@@ -224,11 +219,7 @@ const DlcSelectionDialog = ({
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="primary" onClick={handleConfirm} disabled={isLoading}>
|
||||||
variant="primary"
|
|
||||||
onClick={handleConfirm}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{actionButtonText}
|
{actionButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -7,20 +7,20 @@ import DialogActions from './DialogActions'
|
|||||||
import { Button } from '@/components/buttons'
|
import { Button } from '@/components/buttons'
|
||||||
|
|
||||||
export interface InstallationInstructions {
|
export interface InstallationInstructions {
|
||||||
type: string;
|
type: string
|
||||||
command: string;
|
command: string
|
||||||
game_title: string;
|
game_title: string
|
||||||
dlc_count?: number;
|
dlc_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProgressDialogProps {
|
export interface ProgressDialogProps {
|
||||||
visible: boolean;
|
visible: boolean
|
||||||
title: string;
|
title: string
|
||||||
message: string;
|
message: string
|
||||||
progress: number;
|
progress: number
|
||||||
showInstructions?: boolean;
|
showInstructions?: boolean
|
||||||
instructions?: InstallationInstructions;
|
instructions?: InstallationInstructions
|
||||||
onClose?: () => void;
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,20 +154,13 @@ const ProgressDialog = ({
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
{showInstructions && showCopyButton && (
|
{showInstructions && showCopyButton && (
|
||||||
<Button
|
<Button variant="primary" onClick={handleCopyCommand}>
|
||||||
variant="primary"
|
|
||||||
onClick={handleCopyCommand}
|
|
||||||
>
|
|
||||||
{copySuccess ? 'Copied!' : 'Copy to Clipboard'}
|
{copySuccess ? 'Copied!' : 'Copy to Clipboard'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isCloseButtonEnabled && (
|
{isCloseButtonEnabled && (
|
||||||
<Button
|
<Button variant="secondary" onClick={onClose} disabled={!isCloseButtonEnabled}>
|
||||||
variant="secondary"
|
|
||||||
onClick={onClose}
|
|
||||||
disabled={!isCloseButtonEnabled}
|
|
||||||
>
|
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
// Export all dialog components
|
// Export all dialog components
|
||||||
export { default as Dialog } from './Dialog';
|
export { default as Dialog } from './Dialog'
|
||||||
export { default as DialogHeader } from './DialogHeader';
|
export { default as DialogHeader } from './DialogHeader'
|
||||||
export { default as DialogBody } from './DialogBody';
|
export { default as DialogBody } from './DialogBody'
|
||||||
export { default as DialogFooter } from './DialogFooter';
|
export { default as DialogFooter } from './DialogFooter'
|
||||||
export { default as DialogActions } from './DialogActions';
|
export { default as DialogActions } from './DialogActions'
|
||||||
export { default as ProgressDialog } from './ProgressDialog';
|
export { default as ProgressDialog } from './ProgressDialog'
|
||||||
export { default as DlcSelectionDialog } from './DlcSelectionDialog';
|
export { default as DlcSelectionDialog } from './DlcSelectionDialog'
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type { DialogProps } from './Dialog';
|
export type { DialogProps } from './Dialog'
|
||||||
export type { DialogHeaderProps } from './DialogHeader';
|
export type { DialogHeaderProps } from './DialogHeader'
|
||||||
export type { DialogBodyProps } from './DialogBody';
|
export type { DialogBodyProps } from './DialogBody'
|
||||||
export type { DialogFooterProps } from './DialogFooter';
|
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'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo } from 'react'
|
||||||
import {GameItem, ImagePreloader} from '@/components/games'
|
import { GameItem, ImagePreloader } from '@/components/games'
|
||||||
import { ActionType } from '@/components/buttons'
|
import { ActionType } from '@/components/buttons'
|
||||||
import { Game } from '@/types'
|
import { Game } from '@/types'
|
||||||
import LoadingIndicator from '../common/LoadingIndicator'
|
import LoadingIndicator from '../common/LoadingIndicator'
|
||||||
@@ -35,11 +35,7 @@ const GameList = ({ games, isLoading, onAction, onEdit }: GameListProps) => {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="game-list">
|
<div className="game-list">
|
||||||
<LoadingIndicator
|
<LoadingIndicator type="spinner" size="large" message="Scanning for games..." />
|
||||||
type="spinner"
|
|
||||||
size="large"
|
|
||||||
message="Scanning for games..."
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// Export all game components
|
// Export all game components
|
||||||
export { default as GameList } from './GameList';
|
export { default as GameList } from './GameList'
|
||||||
export { default as GameItem } from './GameItem';
|
export { default as GameItem } from './GameItem'
|
||||||
export { default as ImagePreloader } from './ImagePreloader';
|
export { default as ImagePreloader } from './ImagePreloader'
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const getSizeValue = (size: IconSize): string => {
|
|||||||
sm: '16px',
|
sm: '16px',
|
||||||
md: '24px',
|
md: '24px',
|
||||||
lg: '32px',
|
lg: '32px',
|
||||||
xl: '48px'
|
xl: '48px',
|
||||||
}
|
}
|
||||||
|
|
||||||
return sizeMap[size] || sizeMap.md
|
return sizeMap[size] || sizeMap.md
|
||||||
@@ -54,11 +54,15 @@ const getSizeValue = (size: IconSize): string => {
|
|||||||
/**
|
/**
|
||||||
* Gets the icon component based on name and variant
|
* Gets the icon component based on name and variant
|
||||||
*/
|
*/
|
||||||
const getIconComponent = (name: string, variant: IconVariant | string): React.ComponentType<React.SVGProps<SVGSVGElement>> | null => {
|
const getIconComponent = (
|
||||||
|
name: string,
|
||||||
|
variant: IconVariant | string
|
||||||
|
): React.ComponentType<React.SVGProps<SVGSVGElement>> | null => {
|
||||||
// Normalize variant to ensure it's a valid IconVariant
|
// Normalize variant to ensure it's a valid IconVariant
|
||||||
const normalizedVariant = (variant === 'bold' || variant === 'outline' || variant === 'brand')
|
const normalizedVariant =
|
||||||
? variant as IconVariant
|
variant === 'bold' || variant === 'outline' || variant === 'brand'
|
||||||
: undefined;
|
? (variant as IconVariant)
|
||||||
|
: undefined
|
||||||
|
|
||||||
// Try to get the icon from the specified variant
|
// Try to get the icon from the specified variant
|
||||||
switch (normalizedVariant) {
|
switch (normalizedVariant) {
|
||||||
|
|||||||
@@ -109,13 +109,7 @@ const AnimatedBackground = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return <canvas ref={canvasRef} className="animated-background" aria-hidden="true" />
|
||||||
<canvas
|
|
||||||
ref={canvasRef}
|
|
||||||
className="animated-background"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AnimatedBackground
|
export default AnimatedBackground
|
||||||
@@ -2,14 +2,14 @@ import { Component, ErrorInfo, ReactNode } from 'react'
|
|||||||
import { Button } from '@/components/buttons'
|
import { Button } from '@/components/buttons'
|
||||||
|
|
||||||
interface ErrorBoundaryProps {
|
interface ErrorBoundaryProps {
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
fallback?: ReactNode;
|
fallback?: ReactNode
|
||||||
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
onError?: (error: Error, errorInfo: ErrorInfo) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ErrorBoundaryState {
|
interface ErrorBoundaryState {
|
||||||
hasError: boolean;
|
hasError: boolean
|
||||||
error: Error | null;
|
error: Error | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,11 +63,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
|||||||
<p>{this.state.error?.toString()}</p>
|
<p>{this.state.error?.toString()}</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<Button
|
<Button variant="primary" onClick={this.handleReset} className="error-retry-button">
|
||||||
variant="primary"
|
|
||||||
onClick={this.handleReset}
|
|
||||||
className="error-retry-button"
|
|
||||||
>
|
|
||||||
Try again
|
Try again
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,12 +12,7 @@ interface HeaderProps {
|
|||||||
* Application header component
|
* Application header component
|
||||||
* Contains the app title, search input, and refresh button
|
* Contains the app title, search input, and refresh button
|
||||||
*/
|
*/
|
||||||
const Header = ({
|
const Header = ({ onRefresh, refreshDisabled = false, onSearch, searchQuery }: HeaderProps) => {
|
||||||
onRefresh,
|
|
||||||
refreshDisabled = false,
|
|
||||||
onSearch,
|
|
||||||
searchQuery,
|
|
||||||
}: HeaderProps) => {
|
|
||||||
return (
|
return (
|
||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
<div className="app-title">
|
<div className="app-title">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
interface InitialLoadingScreenProps {
|
interface InitialLoadingScreenProps {
|
||||||
message: string;
|
message: string
|
||||||
progress: number;
|
progress: number
|
||||||
onComplete?: () => void;
|
onComplete?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,30 +11,30 @@ interface InitialLoadingScreenProps {
|
|||||||
*/
|
*/
|
||||||
const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps) => {
|
const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps) => {
|
||||||
const [detailedStatus, setDetailedStatus] = useState<string[]>([
|
const [detailedStatus, setDetailedStatus] = useState<string[]>([
|
||||||
"Initializing application...",
|
'Initializing application...',
|
||||||
"Setting up Steam integration...",
|
'Setting up Steam integration...',
|
||||||
"Preparing DLC management..."
|
'Preparing DLC management...',
|
||||||
]);
|
])
|
||||||
|
|
||||||
// Use a sequence of messages based on progress
|
// Use a sequence of messages based on progress
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const messages = [
|
const messages = [
|
||||||
{ threshold: 10, message: "Checking system requirements..." },
|
{ threshold: 10, message: 'Checking system requirements...' },
|
||||||
{ threshold: 30, message: "Scanning Steam libraries..." },
|
{ threshold: 30, message: 'Scanning Steam libraries...' },
|
||||||
{ threshold: 50, message: "Discovering games..." },
|
{ threshold: 50, message: 'Discovering games...' },
|
||||||
{ threshold: 70, message: "Analyzing game configurations..." },
|
{ threshold: 70, message: 'Analyzing game configurations...' },
|
||||||
{ threshold: 90, message: "Preparing user interface..." },
|
{ threshold: 90, message: 'Preparing user interface...' },
|
||||||
{ threshold: 100, message: "Ready to launch!" }
|
{ threshold: 100, message: 'Ready to launch!' },
|
||||||
];
|
]
|
||||||
|
|
||||||
// Find current status message based on progress
|
// Find current status message based on progress
|
||||||
const currentMessage = messages.find(m => progress <= m.threshold)?.message || "Loading...";
|
const currentMessage = messages.find((m) => progress <= m.threshold)?.message || 'Loading...'
|
||||||
|
|
||||||
// Add new messages to the log as progress increases
|
// Add new messages to the log as progress increases
|
||||||
if (currentMessage && !detailedStatus.includes(currentMessage)) {
|
if (currentMessage && !detailedStatus.includes(currentMessage)) {
|
||||||
setDetailedStatus(prev => [...prev, currentMessage]);
|
setDetailedStatus((prev) => [...prev, currentMessage])
|
||||||
}
|
}
|
||||||
}, [progress, detailedStatus]);
|
}, [progress, detailedStatus])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="initial-loading-screen">
|
<div className="initial-loading-screen">
|
||||||
@@ -69,7 +69,7 @@ const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps)
|
|||||||
<div className="progress-percentage">{Math.round(progress)}%</div>
|
<div className="progress-percentage">{Math.round(progress)}%</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default InitialLoadingScreen
|
export default InitialLoadingScreen
|
||||||
@@ -22,7 +22,7 @@ const Sidebar = ({ setFilter, currentFilter }: SidebarProps) => {
|
|||||||
const filters: FilterItem[] = [
|
const filters: FilterItem[] = [
|
||||||
{ id: 'all', label: 'All Games', icon: layers, variant: 'bold' },
|
{ id: 'all', label: 'All Games', icon: layers, variant: 'bold' },
|
||||||
{ id: 'native', label: 'Native', icon: linux, variant: 'brand' },
|
{ id: 'native', label: 'Native', icon: linux, variant: 'brand' },
|
||||||
{ id: 'proton', label: 'Proton Required', icon: proton, variant: 'brand' }
|
{ id: 'proton', label: 'Proton Required', icon: proton, variant: 'brand' },
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -32,19 +32,14 @@ const Sidebar = ({ setFilter, currentFilter }: SidebarProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="filter-list">
|
<ul className="filter-list">
|
||||||
{filters.map(filter => (
|
{filters.map((filter) => (
|
||||||
<li
|
<li
|
||||||
key={filter.id}
|
key={filter.id}
|
||||||
className={currentFilter === filter.id ? 'active' : ''}
|
className={currentFilter === filter.id ? 'active' : ''}
|
||||||
onClick={() => setFilter(filter.id)}
|
onClick={() => setFilter(filter.id)}
|
||||||
>
|
>
|
||||||
<div className="filter-item">
|
<div className="filter-item">
|
||||||
<Icon
|
<Icon name={filter.icon} variant={filter.variant} size="md" className="filter-icon" />
|
||||||
name={filter.icon}
|
|
||||||
variant={filter.variant}
|
|
||||||
size="md"
|
|
||||||
className="filter-icon"
|
|
||||||
/>
|
|
||||||
<span>{filter.label}</span>
|
<span>{filter.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Export all layout components
|
// Export all layout components
|
||||||
export { default as Header } from './Header';
|
export { default as Header } from './Header'
|
||||||
export { default as Sidebar } from './Sidebar';
|
export { default as Sidebar } from './Sidebar'
|
||||||
export { default as AnimatedBackground } from './AnimatedBackground';
|
export { default as AnimatedBackground } from './AnimatedBackground'
|
||||||
export { default as InitialLoadingScreen } from './InitialLoadingScreen';
|
export { default as InitialLoadingScreen } from './InitialLoadingScreen'
|
||||||
export { default as ErrorBoundary } from './ErrorBoundary';
|
export { default as ErrorBoundary } from './ErrorBoundary'
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import { ReactNode, useState, useEffect, useCallback } from 'react'
|
|||||||
import { Icon, check, info, warning, error } from '@/components/icons'
|
import { Icon, check, info, warning, error } from '@/components/icons'
|
||||||
|
|
||||||
export interface ToastProps {
|
export interface ToastProps {
|
||||||
id: string;
|
id: string
|
||||||
type: 'success' | 'error' | 'warning' | 'info';
|
type: 'success' | 'error' | 'warning' | 'info'
|
||||||
title?: string;
|
title?: string
|
||||||
message: string;
|
message: string
|
||||||
duration?: number;
|
duration?: number
|
||||||
onDismiss: (id: string) => void;
|
onDismiss: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,20 +20,20 @@ const Toast = ({
|
|||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
duration = 5000, // default 5 seconds
|
duration = 5000, // default 5 seconds
|
||||||
onDismiss
|
onDismiss,
|
||||||
}: ToastProps) => {
|
}: ToastProps) => {
|
||||||
const [visible, setVisible] = useState(false)
|
const [visible, setVisible] = useState(false)
|
||||||
const [isClosing, setIsClosing] = useState(false);
|
const [isClosing, setIsClosing] = useState(false)
|
||||||
|
|
||||||
// Use useCallback to memoize the handleDismiss function
|
// Use useCallback to memoize the handleDismiss function
|
||||||
const handleDismiss = useCallback(() => {
|
const handleDismiss = useCallback(() => {
|
||||||
setIsClosing(true);
|
setIsClosing(true)
|
||||||
// Give time for exit animation
|
// Give time for exit animation
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setVisible(false);
|
setVisible(false)
|
||||||
setTimeout(() => onDismiss(id), 50);
|
setTimeout(() => onDismiss(id), 50)
|
||||||
}, 300);
|
}, 300)
|
||||||
}, [id, onDismiss]);
|
}, [id, onDismiss])
|
||||||
|
|
||||||
// Handle animation on mount/unmount
|
// Handle animation on mount/unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,20 +60,22 @@ const Toast = ({
|
|||||||
const getIcon = (): ReactNode => {
|
const getIcon = (): ReactNode => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'success':
|
case 'success':
|
||||||
return <Icon name={check} size="md" variant='bold' />
|
return <Icon name={check} size="md" variant="bold" />
|
||||||
case 'error':
|
case 'error':
|
||||||
return <Icon name={error} size="md" variant='bold' />
|
return <Icon name={error} size="md" variant="bold" />
|
||||||
case 'warning':
|
case 'warning':
|
||||||
return <Icon name={warning} size="md" variant='bold' />
|
return <Icon name={warning} size="md" variant="bold" />
|
||||||
case 'info':
|
case 'info':
|
||||||
return <Icon name={info} size="md" variant='bold' />
|
return <Icon name={info} size="md" variant="bold" />
|
||||||
default:
|
default:
|
||||||
return <Icon name={info} size="md" variant='bold' />
|
return <Icon name={info} size="md" variant="bold" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`toast toast-${type} ${visible ? 'visible' : ''} ${isClosing ? 'closing' : ''}`}>
|
<div
|
||||||
|
className={`toast toast-${type} ${visible ? 'visible' : ''} ${isClosing ? 'closing' : ''}`}
|
||||||
|
>
|
||||||
<div className="toast-icon">{getIcon()}</div>
|
<div className="toast-icon">{getIcon()}</div>
|
||||||
<div className="toast-content">
|
<div className="toast-content">
|
||||||
{title && <h4 className="toast-title">{title}</h4>}
|
{title && <h4 className="toast-title">{title}</h4>}
|
||||||
|
|||||||
@@ -9,20 +9,16 @@ export type ToastPosition =
|
|||||||
| 'bottom-center'
|
| 'bottom-center'
|
||||||
|
|
||||||
interface ToastContainerProps {
|
interface ToastContainerProps {
|
||||||
toasts: Omit<ToastProps, 'onDismiss'>[];
|
toasts: Omit<ToastProps, 'onDismiss'>[]
|
||||||
onDismiss: (id: string) => void;
|
onDismiss: (id: string) => void
|
||||||
position?: ToastPosition;
|
position?: ToastPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Container for toast notifications
|
* Container for toast notifications
|
||||||
* Manages positioning and rendering of all toast notifications
|
* Manages positioning and rendering of all toast notifications
|
||||||
*/
|
*/
|
||||||
const ToastContainer = ({
|
const ToastContainer = ({ toasts, onDismiss, position = 'bottom-right' }: ToastContainerProps) => {
|
||||||
toasts,
|
|
||||||
onDismiss,
|
|
||||||
position = 'bottom-right',
|
|
||||||
}: ToastContainerProps) => {
|
|
||||||
if (toasts.length === 0) {
|
if (toasts.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export { default as Toast } from './Toast';
|
export { default as Toast } from './Toast'
|
||||||
export { default as ToastContainer } from './ToastContainer';
|
export { default as ToastContainer } from './ToastContainer'
|
||||||
|
|
||||||
export type { ToastProps } from './Toast';
|
export type { ToastProps } from './Toast'
|
||||||
export type { ToastPosition } from './ToastContainer';
|
export type { ToastPosition } from './ToastContainer'
|
||||||
|
|||||||
@@ -4,54 +4,58 @@ import { ActionType } from '@/components/buttons/ActionButton'
|
|||||||
|
|
||||||
// Types for context sub-components
|
// Types for context sub-components
|
||||||
export interface InstallationInstructions {
|
export interface InstallationInstructions {
|
||||||
type: string;
|
type: string
|
||||||
command: string;
|
command: string
|
||||||
game_title: string;
|
game_title: string
|
||||||
dlc_count?: number;
|
dlc_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DlcDialogState {
|
export interface DlcDialogState {
|
||||||
visible: boolean;
|
visible: boolean
|
||||||
gameId: string;
|
gameId: string
|
||||||
gameTitle: string;
|
gameTitle: string
|
||||||
dlcs: DlcInfo[];
|
dlcs: DlcInfo[]
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
isEditMode: boolean;
|
isEditMode: boolean
|
||||||
progress: number;
|
progress: number
|
||||||
timeLeft?: string;
|
timeLeft?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProgressDialogState {
|
export interface ProgressDialogState {
|
||||||
visible: boolean;
|
visible: boolean
|
||||||
title: string;
|
title: string
|
||||||
message: string;
|
message: string
|
||||||
progress: number;
|
progress: number
|
||||||
showInstructions: boolean;
|
showInstructions: boolean
|
||||||
instructions?: InstallationInstructions;
|
instructions?: InstallationInstructions
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the context type
|
// Define the context type
|
||||||
export interface AppContextType {
|
export interface AppContextType {
|
||||||
// Game state
|
// Game state
|
||||||
games: Game[];
|
games: Game[]
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
error: string | null;
|
error: string | null
|
||||||
loadGames: () => Promise<boolean>;
|
loadGames: () => Promise<boolean>
|
||||||
handleProgressDialogClose: () => void;
|
handleProgressDialogClose: () => void
|
||||||
|
|
||||||
// DLC management
|
// DLC management
|
||||||
dlcDialog: DlcDialogState;
|
dlcDialog: DlcDialogState
|
||||||
handleGameEdit: (gameId: string) => void;
|
handleGameEdit: (gameId: string) => void
|
||||||
handleDlcDialogClose: () => void;
|
handleDlcDialogClose: () => void
|
||||||
|
|
||||||
// Game actions
|
// Game actions
|
||||||
progressDialog: ProgressDialogState;
|
progressDialog: ProgressDialogState
|
||||||
handleGameAction: (gameId: string, action: ActionType) => Promise<void>;
|
handleGameAction: (gameId: string, action: ActionType) => Promise<void>
|
||||||
handleDlcConfirm: (selectedDlcs: DlcInfo[]) => void;
|
handleDlcConfirm: (selectedDlcs: DlcInfo[]) => void
|
||||||
|
|
||||||
// Toast notifications
|
// Toast notifications
|
||||||
showToast: (message: string, type: 'success' | 'error' | 'warning' | 'info', options?: Record<string, unknown>) => void;
|
showToast: (
|
||||||
|
message: string,
|
||||||
|
type: 'success' | 'error' | 'warning' | 'info',
|
||||||
|
options?: Record<string, unknown>
|
||||||
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the context with a default value
|
// Create the context with a default value
|
||||||
export const AppContext = createContext<AppContextType | undefined>(undefined);
|
export const AppContext = createContext<AppContextType | undefined>(undefined)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ToastContainer } from '@/components/notifications'
|
|||||||
|
|
||||||
// Context provider component
|
// Context provider component
|
||||||
interface AppProviderProps {
|
interface AppProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,13 +16,7 @@ interface AppProviderProps {
|
|||||||
*/
|
*/
|
||||||
export const AppProvider = ({ children }: AppProviderProps) => {
|
export const AppProvider = ({ children }: AppProviderProps) => {
|
||||||
// Use our custom hooks
|
// Use our custom hooks
|
||||||
const {
|
const { games, isLoading, error, loadGames, setGames } = useGames()
|
||||||
games,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
loadGames,
|
|
||||||
setGames,
|
|
||||||
} = useGames()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dlcDialog,
|
dlcDialog,
|
||||||
@@ -39,20 +33,13 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
handleDlcConfirm: executeDlcConfirm,
|
handleDlcConfirm: executeDlcConfirm,
|
||||||
} = useGameActions()
|
} = useGameActions()
|
||||||
|
|
||||||
const {
|
const { toasts, removeToast, success, error: showError, warning, info } = useToasts()
|
||||||
toasts,
|
|
||||||
removeToast,
|
|
||||||
success,
|
|
||||||
error: showError,
|
|
||||||
warning,
|
|
||||||
info
|
|
||||||
} = useToasts()
|
|
||||||
|
|
||||||
// Game action handler with proper error reporting
|
// Game action handler with proper error reporting
|
||||||
const handleGameAction = async (gameId: string, action: ActionType) => {
|
const handleGameAction = async (gameId: string, action: ActionType) => {
|
||||||
const game = games.find(g => g.id === gameId)
|
const game = games.find((g) => g.id === gameId)
|
||||||
if (!game) {
|
if (!game) {
|
||||||
showError("Game not found")
|
showError('Game not found')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,8 +69,8 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
|
|
||||||
// For other actions (uninstall cream, install/uninstall smoke)
|
// For other actions (uninstall cream, install/uninstall smoke)
|
||||||
// Mark game as installing
|
// Mark game as installing
|
||||||
setGames(prevGames =>
|
setGames((prevGames) =>
|
||||||
prevGames.map(g => g.id === gameId ? {...g, installing: true} : g)
|
prevGames.map((g) => (g.id === gameId ? { ...g, installing: true } : g))
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -91,16 +78,20 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
if (action.includes('install')) {
|
if (action.includes('install')) {
|
||||||
success(`Successfully installed ${action.includes('cream') ? 'CreamLinux' : 'SmokeAPI'} for ${game.title}`)
|
success(
|
||||||
|
`Successfully installed ${action.includes('cream') ? 'CreamLinux' : 'SmokeAPI'} for ${game.title}`
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
success(`Successfully uninstalled ${action.includes('cream') ? 'CreamLinux' : 'SmokeAPI'} from ${game.title}`)
|
success(
|
||||||
|
`Successfully uninstalled ${action.includes('cream') ? 'CreamLinux' : 'SmokeAPI'} from ${game.title}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showError(`Action failed: ${error}`)
|
showError(`Action failed: ${error}`)
|
||||||
} finally {
|
} finally {
|
||||||
// Reset installing state
|
// Reset installing state
|
||||||
setGames(prevGames =>
|
setGames((prevGames) =>
|
||||||
prevGames.map(g => g.id === gameId ? {...g, installing: false} : g)
|
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,45 +101,61 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
const { gameId, isEditMode } = dlcDialog
|
const { gameId, isEditMode } = dlcDialog
|
||||||
|
|
||||||
// MODIFIED: Create a deep copy to ensure we don't have reference issues
|
// MODIFIED: Create a deep copy to ensure we don't have reference issues
|
||||||
const dlcsCopy = selectedDlcs.map(dlc => ({...dlc}))
|
const dlcsCopy = selectedDlcs.map((dlc) => ({ ...dlc }))
|
||||||
|
|
||||||
// Log detailed info before closing dialog
|
// Log detailed info before closing dialog
|
||||||
console.log(`Saving ${dlcsCopy.filter(d => d.enabled).length} enabled and ${
|
console.log(
|
||||||
dlcsCopy.length - dlcsCopy.filter(d => d.enabled).length
|
`Saving ${dlcsCopy.filter((d) => d.enabled).length} enabled and ${
|
||||||
} disabled DLCs`)
|
dlcsCopy.length - dlcsCopy.filter((d) => d.enabled).length
|
||||||
|
} disabled DLCs`
|
||||||
|
)
|
||||||
|
|
||||||
// Close dialog FIRST to avoid UI state issues
|
// Close dialog FIRST to avoid UI state issues
|
||||||
closeDlcDialog()
|
closeDlcDialog()
|
||||||
|
|
||||||
// Update game state to show it's installing
|
// Update game state to show it's installing
|
||||||
setGames(prevGames =>
|
setGames((prevGames) =>
|
||||||
prevGames.map(g => g.id === gameId ? { ...g, installing: true } : g)
|
prevGames.map((g) => (g.id === gameId ? { ...g, installing: true } : g))
|
||||||
)
|
)
|
||||||
|
|
||||||
executeDlcConfirm(dlcsCopy, gameId, isEditMode, games)
|
executeDlcConfirm(dlcsCopy, gameId, isEditMode, games)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
success(isEditMode
|
success(
|
||||||
? "DLC configuration updated successfully"
|
isEditMode
|
||||||
: "CreamLinux installed with selected DLCs")
|
? 'DLC configuration updated successfully'
|
||||||
|
: 'CreamLinux installed with selected DLCs'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
showError(`DLC operation failed: ${error}`)
|
showError(`DLC operation failed: ${error}`)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// Reset installing state
|
// Reset installing state
|
||||||
setGames(prevGames =>
|
setGames((prevGames) =>
|
||||||
prevGames.map(g => g.id === gameId ? { ...g, installing: false } : g)
|
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic toast show function
|
// Generic toast show function
|
||||||
const showToast = (message: string, type: 'success' | 'error' | 'warning' | 'info', options = {}) => {
|
const showToast = (
|
||||||
|
message: string,
|
||||||
|
type: 'success' | 'error' | 'warning' | 'info',
|
||||||
|
options = {}
|
||||||
|
) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'success': success(message, options); break;
|
case 'success':
|
||||||
case 'error': showError(message, options); break;
|
success(message, options)
|
||||||
case 'warning': warning(message, options); break;
|
break
|
||||||
case 'info': info(message, options); break;
|
case 'error':
|
||||||
|
showError(message, options)
|
||||||
|
break
|
||||||
|
case 'warning':
|
||||||
|
warning(message, options)
|
||||||
|
break
|
||||||
|
case 'info':
|
||||||
|
info(message, options)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './AppContext';
|
export * from './AppContext'
|
||||||
export * from './AppProvider';
|
export * from './AppProvider'
|
||||||
export * from './useAppContext';
|
export * from './useAppContext'
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Export all hooks
|
// Export all hooks
|
||||||
export { useGames } from './useGames';
|
export { useGames } from './useGames'
|
||||||
export { useDlcManager } from './useDlcManager';
|
export { useDlcManager } from './useDlcManager'
|
||||||
export { useGameActions } from './useGameActions';
|
export { useGameActions } from './useGameActions'
|
||||||
export { useToasts } from './useToasts';
|
export { useToasts } from './useToasts'
|
||||||
export { useAppLogic } from './useAppLogic';
|
export { useAppLogic } from './useAppLogic'
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type { ToastType, Toast, ToastOptions } from './useToasts';
|
export type { ToastType, Toast, ToastOptions } from './useToasts'
|
||||||
export type { DlcDialogState } from './useDlcManager';
|
export type { DlcDialogState } from './useDlcManager'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef } from 'react'
|
|||||||
import { useAppContext } from '@/contexts/useAppContext'
|
import { useAppContext } from '@/contexts/useAppContext'
|
||||||
|
|
||||||
interface UseAppLogicOptions {
|
interface UseAppLogicOptions {
|
||||||
autoLoad?: boolean;
|
autoLoad?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,13 +13,7 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
|
|||||||
const { autoLoad = true } = options
|
const { autoLoad = true } = options
|
||||||
|
|
||||||
// Get values from app context
|
// Get values from app context
|
||||||
const {
|
const { games, loadGames, isLoading, error, showToast } = useAppContext()
|
||||||
games,
|
|
||||||
loadGames,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
showToast
|
|
||||||
} = useAppContext()
|
|
||||||
|
|
||||||
// Local state for filtering and UI
|
// Local state for filtering and UI
|
||||||
const [filter, setFilter] = useState('all')
|
const [filter, setFilter] = useState('all')
|
||||||
@@ -28,7 +22,7 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
|
|||||||
const isInitializedRef = useRef(false)
|
const isInitializedRef = useRef(false)
|
||||||
const [scanProgress, setScanProgress] = useState({
|
const [scanProgress, setScanProgress] = useState({
|
||||||
message: 'Initializing...',
|
message: 'Initializing...',
|
||||||
progress: 0
|
progress: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Filter games based on current filter and search
|
// Filter games based on current filter and search
|
||||||
@@ -41,8 +35,8 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
|
|||||||
(filter === 'proton' && !game.native)
|
(filter === 'proton' && !game.native)
|
||||||
|
|
||||||
// Then filter by search query
|
// Then filter by search query
|
||||||
const searchMatch = !searchQuery.trim() ||
|
const searchMatch =
|
||||||
game.title.toLowerCase().includes(searchQuery.toLowerCase())
|
!searchQuery.trim() || game.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
|
||||||
return platformMatch && searchMatch
|
return platformMatch && searchMatch
|
||||||
})
|
})
|
||||||
@@ -58,18 +52,18 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
|
|||||||
if (!autoLoad || !isInitialLoad || isInitializedRef.current) return
|
if (!autoLoad || !isInitialLoad || isInitializedRef.current) return
|
||||||
|
|
||||||
isInitializedRef.current = true
|
isInitializedRef.current = true
|
||||||
console.log("[APP LOGIC #2] Starting initialization")
|
console.log('[APP LOGIC #2] Starting initialization')
|
||||||
|
|
||||||
const initialLoad = async () => {
|
const initialLoad = async () => {
|
||||||
try {
|
try {
|
||||||
setScanProgress({ message: 'Scanning for games...', progress: 20 })
|
setScanProgress({ message: 'Scanning for games...', progress: 20 })
|
||||||
await new Promise(resolve => setTimeout(resolve, 800))
|
await new Promise((resolve) => setTimeout(resolve, 800))
|
||||||
|
|
||||||
setScanProgress({ message: 'Loading game information...', progress: 50 })
|
setScanProgress({ message: 'Loading game information...', progress: 50 })
|
||||||
await loadGames()
|
await loadGames()
|
||||||
|
|
||||||
setScanProgress({ message: 'Finishing up...', progress: 90 })
|
setScanProgress({ message: 'Finishing up...', progress: 90 })
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
setScanProgress({ message: 'Ready!', progress: 100 })
|
setScanProgress({ message: 'Ready!', progress: 100 })
|
||||||
setTimeout(() => setIsInitialLoad(false), 500)
|
setTimeout(() => setIsInitialLoad(false), 500)
|
||||||
@@ -104,6 +98,6 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
|
|||||||
filteredGames: filteredGames(),
|
filteredGames: filteredGames(),
|
||||||
handleRefresh,
|
handleRefresh,
|
||||||
isLoading,
|
isLoading,
|
||||||
error
|
error,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,17 +4,17 @@ import { listen } from '@tauri-apps/api/event'
|
|||||||
import { Game, DlcInfo } from '@/types'
|
import { Game, DlcInfo } from '@/types'
|
||||||
|
|
||||||
export interface DlcDialogState {
|
export interface DlcDialogState {
|
||||||
visible: boolean;
|
visible: boolean
|
||||||
gameId: string;
|
gameId: string
|
||||||
gameTitle: string;
|
gameTitle: string
|
||||||
dlcs: DlcInfo[];
|
dlcs: DlcInfo[]
|
||||||
enabledDlcs: string[];
|
enabledDlcs: string[]
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
isEditMode: boolean;
|
isEditMode: boolean
|
||||||
progress: number;
|
progress: number
|
||||||
progressMessage: string;
|
progressMessage: string
|
||||||
timeLeft: string;
|
timeLeft: string
|
||||||
error: string | null;
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,9 +60,9 @@ export function useDlcManager() {
|
|||||||
|
|
||||||
// When progress is 100%, mark loading as complete and reset fetch state
|
// When progress is 100%, mark loading as complete and reset fetch state
|
||||||
const unlistenDlcProgress = await listen<{
|
const unlistenDlcProgress = await listen<{
|
||||||
message: string;
|
message: string
|
||||||
progress: number;
|
progress: number
|
||||||
timeLeft?: string;
|
timeLeft?: string
|
||||||
}>('dlc-progress', (event) => {
|
}>('dlc-progress', (event) => {
|
||||||
const { message, progress, timeLeft } = event.payload
|
const { message, progress, timeLeft } = event.payload
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ export function useDlcManager() {
|
|||||||
console.log('Loaded existing DLC configuration:', allDlcs)
|
console.log('Loaded existing DLC configuration:', allDlcs)
|
||||||
|
|
||||||
// IMPORTANT: Create a completely new array to avoid reference issues
|
// IMPORTANT: Create a completely new array to avoid reference issues
|
||||||
const freshDlcs = allDlcs.map(dlc => ({...dlc}))
|
const freshDlcs = allDlcs.map((dlc) => ({ ...dlc }))
|
||||||
|
|
||||||
setDlcDialog((prev) => ({
|
setDlcDialog((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
@@ -26,16 +26,17 @@ export function useGameActions() {
|
|||||||
try {
|
try {
|
||||||
// Listen for progress updates from the backend
|
// Listen for progress updates from the backend
|
||||||
const unlistenProgress = await listen<{
|
const unlistenProgress = await listen<{
|
||||||
title: string;
|
title: string
|
||||||
message: string;
|
message: string
|
||||||
progress: number;
|
progress: number
|
||||||
complete: boolean;
|
complete: boolean
|
||||||
show_instructions?: boolean;
|
show_instructions?: boolean
|
||||||
instructions?: InstallationInstructions;
|
instructions?: InstallationInstructions
|
||||||
}>('installation-progress', (event) => {
|
}>('installation-progress', (event) => {
|
||||||
console.log('Received installation-progress event:', event)
|
console.log('Received installation-progress event:', event)
|
||||||
|
|
||||||
const { title, message, progress, complete, show_instructions, instructions } = event.payload
|
const { title, message, progress, complete, show_instructions, instructions } =
|
||||||
|
event.payload
|
||||||
|
|
||||||
if (complete && !show_instructions) {
|
if (complete && !show_instructions) {
|
||||||
// Hide dialog when complete if no instructions
|
// Hide dialog when complete if no instructions
|
||||||
@@ -64,7 +65,7 @@ export function useGameActions() {
|
|||||||
|
|
||||||
let cleanup: (() => void) | null = null
|
let cleanup: (() => void) | null = null
|
||||||
|
|
||||||
setupEventListeners().then(unlisten => {
|
setupEventListeners().then((unlisten) => {
|
||||||
cleanup = unlisten
|
cleanup = unlisten
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -79,156 +80,156 @@ export function useGameActions() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Unified handler for game actions (install/uninstall)
|
// Unified handler for game actions (install/uninstall)
|
||||||
const handleGameAction = useCallback(async (gameId: string, action: ActionType, games: Game[]) => {
|
const handleGameAction = useCallback(
|
||||||
try {
|
async (gameId: string, action: ActionType, games: Game[]) => {
|
||||||
// For CreamLinux installation, we should NOT call process_game_action directly
|
try {
|
||||||
// Instead, we show the DLC selection dialog first, which is handled in AppProvider
|
// For CreamLinux installation, we should NOT call process_game_action directly
|
||||||
if (action === 'install_cream') {
|
// Instead, we show the DLC selection dialog first, which is handled in AppProvider
|
||||||
return
|
if (action === 'install_cream') {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// For other actions (uninstall_cream, install_smoke, uninstall_smoke)
|
// For other actions (uninstall_cream, install_smoke, uninstall_smoke)
|
||||||
// Find game to get title
|
// Find game to get title
|
||||||
const game = games.find((g) => g.id === gameId)
|
const game = games.find((g) => g.id === gameId)
|
||||||
if (!game) return
|
if (!game) return
|
||||||
|
|
||||||
// Get title based on action
|
// Get title based on action
|
||||||
const isCream = action.includes('cream')
|
const isCream = action.includes('cream')
|
||||||
const isInstall = action.includes('install')
|
const isInstall = action.includes('install')
|
||||||
const product = isCream ? 'CreamLinux' : 'SmokeAPI'
|
const product = isCream ? 'CreamLinux' : 'SmokeAPI'
|
||||||
const operation = isInstall ? 'Installing' : 'Uninstalling'
|
const operation = isInstall ? 'Installing' : 'Uninstalling'
|
||||||
|
|
||||||
// Show progress dialog
|
// 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
|
|
||||||
await invoke('process_game_action', {
|
|
||||||
gameAction: {
|
|
||||||
game_id: gameId,
|
|
||||||
action,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
} 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,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Hide dialog after a delay
|
|
||||||
setTimeout(() => {
|
|
||||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
|
||||||
}, 3000)
|
|
||||||
|
|
||||||
// Rethrow to allow upstream handling
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Handle DLC selection confirmation
|
|
||||||
const handleDlcConfirm = useCallback(async (
|
|
||||||
selectedDlcs: DlcInfo[],
|
|
||||||
gameId: string,
|
|
||||||
isEditMode: boolean,
|
|
||||||
games: Game[]
|
|
||||||
) => {
|
|
||||||
// Find the game
|
|
||||||
const game = games.find((g) => g.id === gameId)
|
|
||||||
if (!game) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isEditMode) {
|
|
||||||
// MODIFIED: Create a deep copy to ensure we don't have reference issues
|
|
||||||
const dlcsCopy = selectedDlcs.map(dlc => ({...dlc}));
|
|
||||||
|
|
||||||
// Show progress dialog for editing
|
|
||||||
setProgressDialog({
|
setProgressDialog({
|
||||||
visible: true,
|
visible: true,
|
||||||
title: `Updating DLCs for ${game.title}`,
|
title: `${operation} ${product} for ${game.title}`,
|
||||||
message: 'Updating DLC configuration...',
|
message: isInstall ? 'Downloading required files...' : 'Removing files...',
|
||||||
progress: 30,
|
progress: isInstall ? 0 : 30,
|
||||||
showInstructions: false,
|
showInstructions: false,
|
||||||
instructions: undefined,
|
instructions: undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('Saving DLC configuration:', dlcsCopy)
|
console.log(`Invoking process_game_action for game ${gameId} with action ${action}`)
|
||||||
|
|
||||||
// Call the backend to update the DLC configuration
|
// Call the backend with the unified action
|
||||||
await invoke('update_dlc_configuration_command', {
|
await invoke('process_game_action', {
|
||||||
gamePath: game.path,
|
gameAction: {
|
||||||
dlcs: dlcsCopy,
|
game_id: gameId,
|
||||||
|
action,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error processing action ${action} for game ${gameId}:`, error)
|
||||||
|
|
||||||
// Update progress dialog for completion
|
// Show error in progress dialog
|
||||||
setProgressDialog((prev) => ({
|
setProgressDialog((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
title: `Update Complete: ${game.title}`,
|
message: `Error: ${error}`,
|
||||||
message: 'DLC configuration updated successfully!',
|
|
||||||
progress: 100,
|
progress: 100,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Hide dialog after a delay
|
// Hide dialog after a delay
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||||
}, 2000)
|
}, 3000)
|
||||||
} 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: 'Preparing to download CreamLinux...',
|
|
||||||
progress: 0,
|
|
||||||
showInstructions: false,
|
|
||||||
instructions: undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Invoke the installation with the selected DLCs
|
// Rethrow to allow upstream handling
|
||||||
await invoke('install_cream_with_dlcs_command', {
|
throw error
|
||||||
gameId,
|
|
||||||
selectedDlcs,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Note: The progress dialog will be updated through the installation-progress event listener
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
},
|
||||||
console.error('Error processing DLC selection:', error)
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
// Show error in progress dialog
|
// Handle DLC selection confirmation
|
||||||
setProgressDialog((prev) => ({
|
const handleDlcConfirm = useCallback(
|
||||||
...prev,
|
async (selectedDlcs: DlcInfo[], gameId: string, isEditMode: boolean, games: Game[]) => {
|
||||||
message: `Error: ${error}`,
|
// Find the game
|
||||||
progress: 100,
|
const game = games.find((g) => g.id === gameId)
|
||||||
}))
|
if (!game) return
|
||||||
|
|
||||||
// Hide dialog after a delay
|
try {
|
||||||
setTimeout(() => {
|
if (isEditMode) {
|
||||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
// MODIFIED: Create a deep copy to ensure we don't have reference issues
|
||||||
}, 3000)
|
const dlcsCopy = selectedDlcs.map((dlc) => ({ ...dlc }))
|
||||||
|
|
||||||
// Rethrow to allow upstream handling
|
// Show progress dialog for editing
|
||||||
throw error
|
setProgressDialog({
|
||||||
}
|
visible: true,
|
||||||
}, [])
|
title: `Updating DLCs for ${game.title}`,
|
||||||
|
message: 'Updating DLC configuration...',
|
||||||
|
progress: 30,
|
||||||
|
showInstructions: false,
|
||||||
|
instructions: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('Saving DLC configuration:', dlcsCopy)
|
||||||
|
|
||||||
|
// Call the backend to update the DLC configuration
|
||||||
|
await invoke('update_dlc_configuration_command', {
|
||||||
|
gamePath: game.path,
|
||||||
|
dlcs: dlcsCopy,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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 }))
|
||||||
|
}, 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: 'Preparing to download CreamLinux...',
|
||||||
|
progress: 0,
|
||||||
|
showInstructions: false,
|
||||||
|
instructions: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Invoke the installation with the selected DLCs
|
||||||
|
await invoke('install_cream_with_dlcs_command', {
|
||||||
|
gameId,
|
||||||
|
selectedDlcs,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Note: The progress dialog will be updated through the installation-progress event listener
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error processing DLC selection:', error)
|
||||||
|
|
||||||
|
// Show error in progress dialog
|
||||||
|
setProgressDialog((prev) => ({
|
||||||
|
...prev,
|
||||||
|
message: `Error: ${error}`,
|
||||||
|
progress: 100,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Hide dialog after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||||
|
}, 3000)
|
||||||
|
|
||||||
|
// Rethrow to allow upstream handling
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
progressDialog,
|
progressDialog,
|
||||||
setProgressDialog,
|
setProgressDialog,
|
||||||
handleCloseProgressDialog,
|
handleCloseProgressDialog,
|
||||||
handleGameAction,
|
handleGameAction,
|
||||||
handleDlcConfirm
|
handleDlcConfirm,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,17 +64,15 @@ export function useGames() {
|
|||||||
// Update only the specific game in the state
|
// Update only the specific game in the state
|
||||||
setGames((prevGames) =>
|
setGames((prevGames) =>
|
||||||
prevGames.map((game) =>
|
prevGames.map((game) =>
|
||||||
game.id === updatedGame.id
|
game.id === updatedGame.id ? { ...updatedGame, platform: 'Steam' } : game
|
||||||
? { ...updatedGame, platform: 'Steam' }
|
|
||||||
: game
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Listen for scan progress events
|
// Listen for scan progress events
|
||||||
const unlistenScanProgress = await listen<{
|
const unlistenScanProgress = await listen<{
|
||||||
message: string;
|
message: string
|
||||||
progress: number;
|
progress: number
|
||||||
}>('scan-progress', (event) => {
|
}>('scan-progress', (event) => {
|
||||||
const { message, progress } = event.payload
|
const { message, progress } = event.payload
|
||||||
|
|
||||||
@@ -102,7 +100,7 @@ export function useGames() {
|
|||||||
|
|
||||||
// Cleanup function
|
// Cleanup function
|
||||||
return () => {
|
return () => {
|
||||||
unlisteners.forEach(fn => fn())
|
unlisteners.forEach((fn) => fn())
|
||||||
}
|
}
|
||||||
}, [loadGames, isInitialLoad])
|
}, [loadGames, isInitialLoad])
|
||||||
|
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ export type ToastType = 'success' | 'error' | 'warning' | 'info'
|
|||||||
* Toast interface
|
* Toast interface
|
||||||
*/
|
*/
|
||||||
export interface Toast {
|
export interface Toast {
|
||||||
id: string;
|
id: string
|
||||||
message: string;
|
message: string
|
||||||
type: ToastType;
|
type: ToastType
|
||||||
duration?: number;
|
duration?: number
|
||||||
title?: string;
|
title?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toast options interface
|
* Toast options interface
|
||||||
*/
|
*/
|
||||||
export interface ToastOptions {
|
export interface ToastOptions {
|
||||||
title?: string;
|
title?: string
|
||||||
duration?: number;
|
duration?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,51 +36,66 @@ export function useToasts() {
|
|||||||
* Removes a toast by ID
|
* Removes a toast by ID
|
||||||
*/
|
*/
|
||||||
const removeToast = useCallback((id: string) => {
|
const removeToast = useCallback((id: string) => {
|
||||||
setToasts(currentToasts => currentToasts.filter(toast => toast.id !== id))
|
setToasts((currentToasts) => currentToasts.filter((toast) => toast.id !== id))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a new toast with the specified type and options
|
* Adds a new toast with the specified type and options
|
||||||
*/
|
*/
|
||||||
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
|
const addToast = useCallback(
|
||||||
const id = uuidv4()
|
(toast: Omit<Toast, 'id'>) => {
|
||||||
const newToast = { ...toast, id }
|
const id = uuidv4()
|
||||||
|
const newToast = { ...toast, id }
|
||||||
|
|
||||||
setToasts(currentToasts => [...currentToasts, newToast])
|
setToasts((currentToasts) => [...currentToasts, newToast])
|
||||||
|
|
||||||
// Auto-remove toast after its duration expires
|
// Auto-remove toast after its duration expires
|
||||||
if (toast.duration !== Infinity) {
|
if (toast.duration !== Infinity) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
removeToast(id)
|
removeToast(id)
|
||||||
}, toast.duration || 5000) // Default 5 seconds
|
}, toast.duration || 5000) // Default 5 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
return id
|
return id
|
||||||
}, [removeToast])
|
},
|
||||||
|
[removeToast]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shorthand method for success toasts
|
* Shorthand method for success toasts
|
||||||
*/
|
*/
|
||||||
const success = useCallback((message: string, options: ToastOptions = {}) =>
|
const success = useCallback(
|
||||||
addToast({ message, type: 'success', ...options }), [addToast])
|
(message: string, options: ToastOptions = {}) =>
|
||||||
|
addToast({ message, type: 'success', ...options }),
|
||||||
|
[addToast]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shorthand method for error toasts
|
* Shorthand method for error toasts
|
||||||
*/
|
*/
|
||||||
const error = useCallback((message: string, options: ToastOptions = {}) =>
|
const error = useCallback(
|
||||||
addToast({ message, type: 'error', ...options }), [addToast])
|
(message: string, options: ToastOptions = {}) =>
|
||||||
|
addToast({ message, type: 'error', ...options }),
|
||||||
|
[addToast]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shorthand method for warning toasts
|
* Shorthand method for warning toasts
|
||||||
*/
|
*/
|
||||||
const warning = useCallback((message: string, options: ToastOptions = {}) =>
|
const warning = useCallback(
|
||||||
addToast({ message, type: 'warning', ...options }), [addToast])
|
(message: string, options: ToastOptions = {}) =>
|
||||||
|
addToast({ message, type: 'warning', ...options }),
|
||||||
|
[addToast]
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shorthand method for info toasts
|
* Shorthand method for info toasts
|
||||||
*/
|
*/
|
||||||
const info = useCallback((message: string, options: ToastOptions = {}) =>
|
const info = useCallback(
|
||||||
addToast({ message, type: 'info', ...options }), [addToast])
|
(message: string, options: ToastOptions = {}) =>
|
||||||
|
addToast({ message, type: 'info', ...options }),
|
||||||
|
[addToast]
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toasts,
|
toasts,
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
* Game image sources from Steam's CDN
|
* Game image sources from Steam's CDN
|
||||||
*/
|
*/
|
||||||
export const SteamImageType = {
|
export const SteamImageType = {
|
||||||
HEADER: 'header', // 460x215
|
HEADER: 'header', // 460x215
|
||||||
CAPSULE: 'capsule_616x353', // 616x353
|
CAPSULE: 'capsule_616x353', // 616x353
|
||||||
LOGO: 'logo', // Game logo with transparency
|
LOGO: 'logo', // Game logo with transparency
|
||||||
LIBRARY_HERO: 'library_hero', // 1920x620
|
LIBRARY_HERO: 'library_hero', // 1920x620
|
||||||
LIBRARY_CAPSULE: 'library_600x900', // 600x900
|
LIBRARY_CAPSULE: 'library_600x900', // 600x900
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
@@ -68,11 +68,7 @@ export const findBestGameImage = async (appId: string): Promise<string | null> =
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try these image types in order of preference
|
// Try these image types in order of preference
|
||||||
const typesToTry = [
|
const typesToTry = [SteamImageType.HEADER, SteamImageType.CAPSULE, SteamImageType.LIBRARY_CAPSULE]
|
||||||
SteamImageType.HEADER,
|
|
||||||
SteamImageType.CAPSULE,
|
|
||||||
SteamImageType.LIBRARY_CAPSULE
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const type of typesToTry) {
|
for (const type of typesToTry) {
|
||||||
const url = getSteamImageUrl(appId, type)
|
const url = getSteamImageUrl(appId, type)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './ImageService';
|
export * from './ImageService'
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Satoshi';
|
font-family: 'Satoshi';
|
||||||
src: url('../assets/fonts/Satoshi.ttf') format('ttf'),
|
src:
|
||||||
|
url('../assets/fonts/Satoshi.ttf') format('ttf'),
|
||||||
url('../assets/fonts/Roboto.ttf') format('ttf'),
|
url('../assets/fonts/Roboto.ttf') format('ttf'),
|
||||||
url('../assets/fonts/WorkSans.ttf') format('ttf');
|
url('../assets/fonts/WorkSans.ttf') format('ttf');
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|||||||
@@ -22,11 +22,8 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-image: radial-gradient(
|
background-image:
|
||||||
circle at 20% 30%,
|
radial-gradient(circle at 20% 30%, rgba(var(--primary-color), 0.05) 0%, transparent 70%),
|
||||||
rgba(var(--primary-color), 0.05) 0%,
|
|
||||||
transparent 70%
|
|
||||||
),
|
|
||||||
radial-gradient(circle at 80% 70%, rgba(var(--cream-color), 0.05) 0%, transparent 70%);
|
radial-gradient(circle at 80% 70%, rgba(var(--cream-color), 0.05) 0%, transparent 70%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: var(--z-bg);
|
z-index: var(--z-bg);
|
||||||
|
|||||||
@@ -36,7 +36,9 @@
|
|||||||
border: 1px solid var(--border-soft);
|
border: 1px solid var(--border-soft);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
transition: transform 0.2s var(--easing-bounce), opacity 0.2s ease-out;
|
transition:
|
||||||
|
transform 0.2s var(--easing-bounce),
|
||||||
|
opacity 0.2s ease-out;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -45,11 +45,15 @@
|
|||||||
|
|
||||||
// Special styling for cards with different statuses
|
// Special styling for cards with different statuses
|
||||||
.game-item-card:has(.status-badge.cream) {
|
.game-item-card:has(.status-badge.cream) {
|
||||||
box-shadow: var(--shadow-standard), 0 0 15px rgba(128, 181, 255, 0.15);
|
box-shadow:
|
||||||
|
var(--shadow-standard),
|
||||||
|
0 0 15px rgba(128, 181, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.game-item-card:has(.status-badge.smoke) {
|
.game-item-card:has(.status-badge.smoke) {
|
||||||
box-shadow: var(--shadow-standard), 0 0 15px rgba(255, 239, 150, 0.15);
|
box-shadow:
|
||||||
|
var(--shadow-standard),
|
||||||
|
0 0 15px rgba(255, 239, 150, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Game item overlay
|
// Game item overlay
|
||||||
|
|||||||
@@ -37,7 +37,9 @@
|
|||||||
// Interactive icons
|
// Interactive icons
|
||||||
&.icon-clickable {
|
&.icon-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
opacity 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
|
|||||||
@@ -96,7 +96,9 @@
|
|||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 2px rgba(var(--primary-color), 0.3), inset 0 2px 5px rgba(0, 0, 0, 0.2);
|
box-shadow:
|
||||||
|
0 0 0 2px rgba(var(--primary-color), 0.3),
|
||||||
|
inset 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
& + .search-icon {
|
& + .search-icon {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* DLC information interface
|
* DLC information interface
|
||||||
*/
|
*/
|
||||||
export interface DlcInfo {
|
export interface DlcInfo {
|
||||||
appid: string;
|
appid: string
|
||||||
name: string;
|
name: string
|
||||||
enabled: boolean;
|
enabled: boolean
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,13 @@
|
|||||||
* Game information interface
|
* Game information interface
|
||||||
*/
|
*/
|
||||||
export interface Game {
|
export interface Game {
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
path: string;
|
path: string
|
||||||
platform?: string;
|
platform?: string
|
||||||
native: boolean;
|
native: boolean
|
||||||
api_files: string[];
|
api_files: string[]
|
||||||
cream_installed?: boolean;
|
cream_installed?: boolean
|
||||||
smoke_installed?: boolean;
|
smoke_installed?: boolean
|
||||||
installing?: boolean;
|
installing?: boolean
|
||||||
}
|
}
|
||||||
@@ -9,17 +9,17 @@
|
|||||||
*/
|
*/
|
||||||
export function formatTime(seconds: number): string {
|
export function formatTime(seconds: number): string {
|
||||||
if (seconds < 60) {
|
if (seconds < 60) {
|
||||||
return `${Math.round(seconds)}s`;
|
return `${Math.round(seconds)}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
const minutes = Math.floor(seconds / 60)
|
||||||
const remainingSeconds = Math.round(seconds % 60);
|
const remainingSeconds = Math.round(seconds % 60)
|
||||||
|
|
||||||
if (remainingSeconds === 0) {
|
if (remainingSeconds === 0) {
|
||||||
return `${minutes}m`;
|
return `${minutes}m`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${minutes}m ${remainingSeconds}s`;
|
return `${minutes}m ${remainingSeconds}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,10 +31,10 @@ export function formatTime(seconds: number): string {
|
|||||||
*/
|
*/
|
||||||
export function truncateString(str: string, maxLength: number, suffix: string = '...'): string {
|
export function truncateString(str: string, maxLength: number, suffix: string = '...'): string {
|
||||||
if (str.length <= maxLength) {
|
if (str.length <= maxLength) {
|
||||||
return str;
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
return str.substring(0, maxLength - suffix.length) + suffix;
|
return str.substring(0, maxLength - suffix.length) + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,17 +47,17 @@ export function debounce<T extends (...args: unknown[]) => unknown>(
|
|||||||
fn: T,
|
fn: T,
|
||||||
delay: number
|
delay: number
|
||||||
): (...args: Parameters<T>) => void {
|
): (...args: Parameters<T>) => void {
|
||||||
let timer: NodeJS.Timeout | null = null;
|
let timer: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
return function(...args: Parameters<T>) {
|
return function (...args: Parameters<T>) {
|
||||||
if (timer) {
|
if (timer) {
|
||||||
clearTimeout(timer);
|
clearTimeout(timer)
|
||||||
}
|
}
|
||||||
|
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
fn(...args);
|
fn(...args)
|
||||||
}, delay);
|
}, delay)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -70,16 +70,16 @@ export function throttle<T extends (...args: unknown[]) => unknown>(
|
|||||||
fn: T,
|
fn: T,
|
||||||
limit: number
|
limit: number
|
||||||
): (...args: Parameters<T>) => void {
|
): (...args: Parameters<T>) => void {
|
||||||
let lastCall = 0;
|
let lastCall = 0
|
||||||
|
|
||||||
return function(...args: Parameters<T>) {
|
return function (...args: Parameters<T>) {
|
||||||
const now = Date.now();
|
const now = Date.now()
|
||||||
|
|
||||||
if (now - lastCall < limit) {
|
if (now - lastCall < limit) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCall = now;
|
lastCall = now
|
||||||
return fn(...args);
|
return fn(...args)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
export * from './helpers';
|
export * from './helpers'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import svgr from "vite-plugin-svgr";
|
import svgr from 'vite-plugin-svgr'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|||||||
Reference in New Issue
Block a user