Formatting

This commit is contained in:
Tickbase
2025-05-18 18:23:06 +02:00
parent bbbd7482c1
commit 81519e89b7
61 changed files with 714 additions and 775 deletions

View File

@@ -27,7 +27,7 @@ function App() {
filteredGames,
handleRefresh,
isLoading,
error
error,
} = useAppLogic({ autoLoad: true })
// Get action handlers from context
@@ -38,15 +38,12 @@ function App() {
progressDialog,
handleGameAction,
handleDlcConfirm,
handleGameEdit
handleGameEdit,
} = useAppContext()
// Show loading screen during initial load
if (isInitialLoad) {
return <InitialLoadingScreen
message={scanProgress.message}
progress={scanProgress.progress}
/>
return <InitialLoadingScreen message={scanProgress.message} progress={scanProgress.progress} />
}
return (

View File

@@ -66,13 +66,9 @@ const ActionButton: FC<ActionButtonProps> = ({
disabled={disabled || isWorking}
fullWidth
className={`action-button ${className}`}
leftIcon={isWorking ? undefined : (
<Icon
name={iconInfo.name}
variant={iconInfo.variant}
size="md"
/>
)}
leftIcon={
isWorking ? undefined : <Icon name={iconInfo.name} variant={iconInfo.variant} size="md" />
}
>
{getButtonText()}
</Button>

View File

@@ -1,11 +1,11 @@
import { Icon, check } from '@/components/icons'
interface AnimatedCheckboxProps {
checked: boolean;
onChange: () => void;
label?: string;
sublabel?: string;
className?: string;
checked: boolean
onChange: () => void
label?: string
sublabel?: string
className?: string
}
/**
@@ -20,22 +20,10 @@ const AnimatedCheckbox = ({
}: AnimatedCheckboxProps) => {
return (
<label className={`animated-checkbox ${className}`}>
<input
type="checkbox"
checked={checked}
onChange={onChange}
className="checkbox-original"
/>
<input type="checkbox" checked={checked} onChange={onChange} className="checkbox-original" />
<span className={`checkbox-custom ${checked ? 'checked' : ''}`}>
{checked && (
<Icon
name={check}
variant="bold"
size="sm"
className="checkbox-icon"
/>
)}
{checked && <Icon name={check} variant="bold" size="sm" className="checkbox-icon" />}
</span>
{(label || sublabel) && (

View File

@@ -1,15 +1,15 @@
import { FC, ButtonHTMLAttributes } from 'react';
import { FC, ButtonHTMLAttributes } from 'react'
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning';
export type ButtonSize = 'small' | 'medium' | 'large';
export type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success' | 'warning'
export type ButtonSize = 'small' | 'medium' | 'large'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
size?: ButtonSize;
isLoading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
fullWidth?: boolean;
variant?: ButtonVariant
size?: ButtonSize
isLoading?: boolean
leftIcon?: React.ReactNode
rightIcon?: React.ReactNode
fullWidth?: boolean
}
/**
@@ -32,7 +32,7 @@ const Button: FC<ButtonProps> = ({
small: 'btn-sm',
medium: 'btn-md',
large: 'btn-lg',
}[size];
}[size]
// Variant class mapping
const variantClass = {
@@ -41,7 +41,7 @@ const Button: FC<ButtonProps> = ({
danger: 'btn-danger',
success: 'btn-success',
warning: 'btn-warning',
}[variant];
}[variant]
return (
<button
@@ -61,7 +61,7 @@ const Button: FC<ButtonProps> = ({
<span className="btn-text">{children}</span>
{rightIcon && !isLoading && <span className="btn-icon btn-icon-right">{rightIcon}</span>}
</button>
);
};
)
}
export default Button;
export default Button

View File

@@ -1,8 +1,8 @@
// Export all button components
export { default as Button } from './Button';
export { default as ActionButton } from './ActionButton';
export { default as AnimatedCheckbox } from './AnimatedCheckbox';
export { default as Button } from './Button'
export { default as ActionButton } from './ActionButton'
export { default as AnimatedCheckbox } from './AnimatedCheckbox'
// Export types
export type { ButtonVariant, ButtonSize } from './Button';
export type { ActionType } from './ActionButton';
export type { ButtonVariant, ButtonSize } from './Button'
export type { ActionType } from './ActionButton'

View File

@@ -4,11 +4,11 @@ export type LoadingType = 'spinner' | 'dots' | 'progress'
export type LoadingSize = 'small' | 'medium' | 'large'
interface LoadingIndicatorProps {
size?: LoadingSize;
type?: LoadingType;
message?: string;
progress?: number;
className?: string;
size?: LoadingSize
type?: LoadingType
message?: string
progress?: number
className?: string
}
/**
@@ -53,9 +53,7 @@ const LoadingIndicator = ({
style={{ width: `${Math.min(Math.max(progress, 0), 100)}%` }}
></div>
</div>
{progress > 0 && (
<div className="progress-percentage">{Math.round(progress)}%</div>
)}
{progress > 0 && <div className="progress-percentage">{Math.round(progress)}%</div>}
</div>
)

View File

@@ -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'

View File

@@ -1,13 +1,13 @@
import { ReactNode, useEffect, useState } from 'react'
export interface DialogProps {
visible: boolean;
onClose?: () => void;
className?: string;
preventBackdropClose?: boolean;
children: ReactNode;
size?: 'small' | 'medium' | 'large';
showAnimationOnUnmount?: boolean;
visible: boolean
onClose?: () => void
className?: string
preventBackdropClose?: boolean
children: ReactNode
size?: 'small' | 'medium' | 'large'
showAnimationOnUnmount?: boolean
}
/**
@@ -66,13 +66,8 @@ const Dialog = ({
}[size]
return (
<div
className={`dialog-overlay ${showContent ? 'visible' : ''}`}
onClick={handleBackdropClick}
>
<div
className={`dialog ${sizeClass} ${className} ${showContent ? 'dialog-visible' : ''}`}
>
<div className={`dialog-overlay ${showContent ? 'visible' : ''}`} onClick={handleBackdropClick}>
<div className={`dialog ${sizeClass} ${className} ${showContent ? 'dialog-visible' : ''}`}>
{children}
</div>
</div>

View File

@@ -1,31 +1,23 @@
import { ReactNode } from 'react'
export interface DialogActionsProps {
children: ReactNode;
className?: string;
align?: 'start' | 'center' | 'end';
children: ReactNode
className?: string
align?: 'start' | 'center' | 'end'
}
/**
* Actions container for dialog footers
* Provides consistent spacing and alignment for action buttons
*/
const DialogActions = ({
children,
className = '',
align = 'end'
}: DialogActionsProps) => {
const DialogActions = ({ children, className = '', align = 'end' }: DialogActionsProps) => {
const alignClass = {
start: 'justify-start',
center: 'justify-center',
end: 'justify-end'
}[align];
end: 'justify-end',
}[align]
return (
<div className={`dialog-actions ${alignClass} ${className}`}>
{children}
</div>
)
return <div className={`dialog-actions ${alignClass} ${className}`}>{children}</div>
}
export default DialogActions

View File

@@ -1,8 +1,8 @@
import { ReactNode } from 'react'
export interface DialogBodyProps {
children: ReactNode;
className?: string;
children: ReactNode
className?: string
}
/**
@@ -10,11 +10,7 @@ export interface DialogBodyProps {
* Contains the main content with scrolling capability
*/
const DialogBody = ({ children, className = '' }: DialogBodyProps) => {
return (
<div className={`dialog-body ${className}`}>
{children}
</div>
)
return <div className={`dialog-body ${className}`}>{children}</div>
}
export default DialogBody

View File

@@ -1,8 +1,8 @@
import { ReactNode } from 'react'
export interface DialogFooterProps {
children: ReactNode;
className?: string;
children: ReactNode
className?: string
}
/**
@@ -10,11 +10,7 @@ export interface DialogFooterProps {
* Contains action buttons and optional status information
*/
const DialogFooter = ({ children, className = '' }: DialogFooterProps) => {
return (
<div className={`dialog-footer ${className}`}>
{children}
</div>
)
return <div className={`dialog-footer ${className}`}>{children}</div>
}
export default DialogFooter

View File

@@ -1,9 +1,9 @@
import { ReactNode } from 'react'
export interface DialogHeaderProps {
children: ReactNode;
className?: string;
onClose?: () => void;
children: ReactNode
className?: string
onClose?: () => void
}
/**
@@ -15,11 +15,7 @@ const DialogHeader = ({ children, className = '', onClose }: DialogHeaderProps)
<div className={`dialog-header ${className}`}>
{children}
{onClose && (
<button
className="dialog-close-button"
onClick={onClose}
aria-label="Close dialog"
>
<button className="dialog-close-button" onClick={onClose} aria-label="Close dialog">
×
</button>
)}

View File

@@ -8,15 +8,15 @@ import { Button, AnimatedCheckbox } from '@/components/buttons'
import { DlcInfo } from '@/types'
export interface DlcSelectionDialogProps {
visible: boolean;
gameTitle: string;
dlcs: DlcInfo[];
onClose: () => void;
onConfirm: (selectedDlcs: DlcInfo[]) => void;
isLoading: boolean;
isEditMode?: boolean;
loadingProgress?: number;
estimatedTimeLeft?: string;
visible: boolean
gameTitle: string
dlcs: DlcInfo[]
onClose: () => void
onConfirm: (selectedDlcs: DlcInfo[]) => void
isLoading: boolean
isEditMode?: boolean
loadingProgress?: number
estimatedTimeLeft?: string
}
/**
@@ -118,9 +118,9 @@ const DlcSelectionDialog = ({
// Submit selected DLCs to parent component
const handleConfirm = useCallback(() => {
// Create a deep copy to prevent reference issues
const dlcsCopy = JSON.parse(JSON.stringify(selectedDlcs));
onConfirm(dlcsCopy);
}, [onConfirm, selectedDlcs]);
const dlcsCopy = JSON.parse(JSON.stringify(selectedDlcs))
onConfirm(dlcsCopy)
}, [onConfirm, selectedDlcs])
// Count selected DLCs
const selectedCount = selectedDlcs.filter((dlc) => dlc.enabled).length
@@ -140,12 +140,7 @@ const DlcSelectionDialog = ({
}
return (
<Dialog
visible={visible}
onClose={onClose}
size="large"
preventBackdropClose={isLoading}
>
<Dialog visible={visible} onClose={onClose} size="large" preventBackdropClose={isLoading}>
<DialogHeader onClose={onClose}>
<h3>{dialogTitle}</h3>
<div className="dlc-game-info">
@@ -224,11 +219,7 @@ const DlcSelectionDialog = ({
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleConfirm}
disabled={isLoading}
>
<Button variant="primary" onClick={handleConfirm} disabled={isLoading}>
{actionButtonText}
</Button>
</DialogActions>

View File

@@ -7,20 +7,20 @@ import DialogActions from './DialogActions'
import { Button } from '@/components/buttons'
export interface InstallationInstructions {
type: string;
command: string;
game_title: string;
dlc_count?: number;
type: string
command: string
game_title: string
dlc_count?: number
}
export interface ProgressDialogProps {
visible: boolean;
title: string;
message: string;
progress: number;
showInstructions?: boolean;
instructions?: InstallationInstructions;
onClose?: () => void;
visible: boolean
title: string
message: string
progress: number
showInstructions?: boolean
instructions?: InstallationInstructions
onClose?: () => void
}
/**
@@ -154,20 +154,13 @@ const ProgressDialog = ({
<DialogFooter>
<DialogActions>
{showInstructions && showCopyButton && (
<Button
variant="primary"
onClick={handleCopyCommand}
>
<Button variant="primary" onClick={handleCopyCommand}>
{copySuccess ? 'Copied!' : 'Copy to Clipboard'}
</Button>
)}
{isCloseButtonEnabled && (
<Button
variant="secondary"
onClick={onClose}
disabled={!isCloseButtonEnabled}
>
<Button variant="secondary" onClick={onClose} disabled={!isCloseButtonEnabled}>
Close
</Button>
)}

View File

@@ -1,17 +1,17 @@
// Export all dialog components
export { default as Dialog } from './Dialog';
export { default as DialogHeader } from './DialogHeader';
export { default as DialogBody } from './DialogBody';
export { default as DialogFooter } from './DialogFooter';
export { default as DialogActions } from './DialogActions';
export { default as ProgressDialog } from './ProgressDialog';
export { default as DlcSelectionDialog } from './DlcSelectionDialog';
export { default as Dialog } from './Dialog'
export { default as DialogHeader } from './DialogHeader'
export { default as DialogBody } from './DialogBody'
export { default as DialogFooter } from './DialogFooter'
export { default as DialogActions } from './DialogActions'
export { default as ProgressDialog } from './ProgressDialog'
export { default as DlcSelectionDialog } from './DlcSelectionDialog'
// Export types
export type { DialogProps } from './Dialog';
export type { DialogHeaderProps } from './DialogHeader';
export type { DialogBodyProps } from './DialogBody';
export type { DialogFooterProps } from './DialogFooter';
export type { DialogActionsProps } from './DialogActions';
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog';
export type { DlcSelectionDialogProps } from './DlcSelectionDialog';
export type { DialogProps } from './Dialog'
export type { DialogHeaderProps } from './DialogHeader'
export type { DialogBodyProps } from './DialogBody'
export type { DialogFooterProps } from './DialogFooter'
export type { DialogActionsProps } from './DialogActions'
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useMemo } from 'react'
import {GameItem, ImagePreloader} from '@/components/games'
import { GameItem, ImagePreloader } from '@/components/games'
import { ActionType } from '@/components/buttons'
import { Game } from '@/types'
import LoadingIndicator from '../common/LoadingIndicator'
@@ -35,11 +35,7 @@ const GameList = ({ games, isLoading, onAction, onEdit }: GameListProps) => {
if (isLoading) {
return (
<div className="game-list">
<LoadingIndicator
type="spinner"
size="large"
message="Scanning for games..."
/>
<LoadingIndicator type="spinner" size="large" message="Scanning for games..." />
</div>
)
}

View File

@@ -1,4 +1,4 @@
// Export all game components
export { default as GameList } from './GameList';
export { default as GameItem } from './GameItem';
export { default as ImagePreloader } from './ImagePreloader';
export { default as GameList } from './GameList'
export { default as GameItem } from './GameItem'
export { default as ImagePreloader } from './ImagePreloader'

View File

@@ -45,7 +45,7 @@ const getSizeValue = (size: IconSize): string => {
sm: '16px',
md: '24px',
lg: '32px',
xl: '48px'
xl: '48px',
}
return sizeMap[size] || sizeMap.md
@@ -54,11 +54,15 @@ const getSizeValue = (size: IconSize): string => {
/**
* 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
const normalizedVariant = (variant === 'bold' || variant === 'outline' || variant === 'brand')
? variant as IconVariant
: undefined;
const normalizedVariant =
variant === 'bold' || variant === 'outline' || variant === 'brand'
? (variant as IconVariant)
: undefined
// Try to get the icon from the specified variant
switch (normalizedVariant) {

View File

@@ -109,13 +109,7 @@ const AnimatedBackground = () => {
}
}, [])
return (
<canvas
ref={canvasRef}
className="animated-background"
aria-hidden="true"
/>
)
return <canvas ref={canvasRef} className="animated-background" aria-hidden="true" />
}
export default AnimatedBackground

View File

@@ -2,14 +2,14 @@ import { Component, ErrorInfo, ReactNode } from 'react'
import { Button } from '@/components/buttons'
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
children: ReactNode
fallback?: ReactNode
onError?: (error: Error, errorInfo: ErrorInfo) => void
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
hasError: boolean
error: Error | null
}
/**
@@ -63,11 +63,7 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
<p>{this.state.error?.toString()}</p>
</details>
<Button
variant="primary"
onClick={this.handleReset}
className="error-retry-button"
>
<Button variant="primary" onClick={this.handleReset} className="error-retry-button">
Try again
</Button>
</div>

View File

@@ -12,12 +12,7 @@ interface HeaderProps {
* Application header component
* Contains the app title, search input, and refresh button
*/
const Header = ({
onRefresh,
refreshDisabled = false,
onSearch,
searchQuery,
}: HeaderProps) => {
const Header = ({ onRefresh, refreshDisabled = false, onSearch, searchQuery }: HeaderProps) => {
return (
<header className="app-header">
<div className="app-title">

View File

@@ -1,9 +1,9 @@
import { useEffect, useState } from 'react'
interface InitialLoadingScreenProps {
message: string;
progress: number;
onComplete?: () => void;
message: string
progress: number
onComplete?: () => void
}
/**
@@ -11,30 +11,30 @@ interface InitialLoadingScreenProps {
*/
const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps) => {
const [detailedStatus, setDetailedStatus] = useState<string[]>([
"Initializing application...",
"Setting up Steam integration...",
"Preparing DLC management..."
]);
'Initializing application...',
'Setting up Steam integration...',
'Preparing DLC management...',
])
// Use a sequence of messages based on progress
useEffect(() => {
const messages = [
{ threshold: 10, message: "Checking system requirements..." },
{ threshold: 30, message: "Scanning Steam libraries..." },
{ threshold: 50, message: "Discovering games..." },
{ threshold: 70, message: "Analyzing game configurations..." },
{ threshold: 90, message: "Preparing user interface..." },
{ threshold: 100, message: "Ready to launch!" }
];
{ threshold: 10, message: 'Checking system requirements...' },
{ threshold: 30, message: 'Scanning Steam libraries...' },
{ threshold: 50, message: 'Discovering games...' },
{ threshold: 70, message: 'Analyzing game configurations...' },
{ threshold: 90, message: 'Preparing user interface...' },
{ threshold: 100, message: 'Ready to launch!' },
]
// 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
if (currentMessage && !detailedStatus.includes(currentMessage)) {
setDetailedStatus(prev => [...prev, currentMessage]);
setDetailedStatus((prev) => [...prev, currentMessage])
}
}, [progress, detailedStatus]);
}, [progress, detailedStatus])
return (
<div className="initial-loading-screen">
@@ -69,7 +69,7 @@ const InitialLoadingScreen = ({ message, progress }: InitialLoadingScreenProps)
<div className="progress-percentage">{Math.round(progress)}%</div>
</div>
</div>
);
};
)
}
export default InitialLoadingScreen

View File

@@ -22,7 +22,7 @@ const Sidebar = ({ setFilter, currentFilter }: SidebarProps) => {
const filters: FilterItem[] = [
{ id: 'all', label: 'All Games', icon: layers, variant: 'bold' },
{ 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 (
@@ -32,19 +32,14 @@ const Sidebar = ({ setFilter, currentFilter }: SidebarProps) => {
</div>
<ul className="filter-list">
{filters.map(filter => (
{filters.map((filter) => (
<li
key={filter.id}
className={currentFilter === filter.id ? 'active' : ''}
onClick={() => setFilter(filter.id)}
>
<div className="filter-item">
<Icon
name={filter.icon}
variant={filter.variant}
size="md"
className="filter-icon"
/>
<Icon name={filter.icon} variant={filter.variant} size="md" className="filter-icon" />
<span>{filter.label}</span>
</div>
</li>

View File

@@ -1,6 +1,6 @@
// Export all layout components
export { default as Header } from './Header';
export { default as Sidebar } from './Sidebar';
export { default as AnimatedBackground } from './AnimatedBackground';
export { default as InitialLoadingScreen } from './InitialLoadingScreen';
export { default as ErrorBoundary } from './ErrorBoundary';
export { default as Header } from './Header'
export { default as Sidebar } from './Sidebar'
export { default as AnimatedBackground } from './AnimatedBackground'
export { default as InitialLoadingScreen } from './InitialLoadingScreen'
export { default as ErrorBoundary } from './ErrorBoundary'

View File

@@ -2,12 +2,12 @@ import { ReactNode, useState, useEffect, useCallback } from 'react'
import { Icon, check, info, warning, error } from '@/components/icons'
export interface ToastProps {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
title?: string;
message: string;
duration?: number;
onDismiss: (id: string) => void;
id: string
type: 'success' | 'error' | 'warning' | 'info'
title?: string
message: string
duration?: number
onDismiss: (id: string) => void
}
/**
@@ -20,20 +20,20 @@ const Toast = ({
title,
message,
duration = 5000, // default 5 seconds
onDismiss
onDismiss,
}: ToastProps) => {
const [visible, setVisible] = useState(false)
const [isClosing, setIsClosing] = useState(false);
const [isClosing, setIsClosing] = useState(false)
// Use useCallback to memoize the handleDismiss function
const handleDismiss = useCallback(() => {
setIsClosing(true);
setIsClosing(true)
// Give time for exit animation
setTimeout(() => {
setVisible(false);
setTimeout(() => onDismiss(id), 50);
}, 300);
}, [id, onDismiss]);
setVisible(false)
setTimeout(() => onDismiss(id), 50)
}, 300)
}, [id, onDismiss])
// Handle animation on mount/unmount
useEffect(() => {
@@ -60,20 +60,22 @@ const Toast = ({
const getIcon = (): ReactNode => {
switch (type) {
case 'success':
return <Icon name={check} size="md" variant='bold' />
return <Icon name={check} size="md" variant="bold" />
case 'error':
return <Icon name={error} size="md" variant='bold' />
return <Icon name={error} size="md" variant="bold" />
case 'warning':
return <Icon name={warning} size="md" variant='bold' />
return <Icon name={warning} size="md" variant="bold" />
case 'info':
return <Icon name={info} size="md" variant='bold' />
return <Icon name={info} size="md" variant="bold" />
default:
return <Icon name={info} size="md" variant='bold' />
return <Icon name={info} size="md" variant="bold" />
}
}
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-content">
{title && <h4 className="toast-title">{title}</h4>}

View File

@@ -9,20 +9,16 @@ export type ToastPosition =
| 'bottom-center'
interface ToastContainerProps {
toasts: Omit<ToastProps, 'onDismiss'>[];
onDismiss: (id: string) => void;
position?: ToastPosition;
toasts: Omit<ToastProps, 'onDismiss'>[]
onDismiss: (id: string) => void
position?: ToastPosition
}
/**
* Container for toast notifications
* Manages positioning and rendering of all toast notifications
*/
const ToastContainer = ({
toasts,
onDismiss,
position = 'bottom-right',
}: ToastContainerProps) => {
const ToastContainer = ({ toasts, onDismiss, position = 'bottom-right' }: ToastContainerProps) => {
if (toasts.length === 0) {
return null
}

View File

@@ -1,5 +1,5 @@
export { default as Toast } from './Toast';
export { default as ToastContainer } from './ToastContainer';
export { default as Toast } from './Toast'
export { default as ToastContainer } from './ToastContainer'
export type { ToastProps } from './Toast';
export type { ToastPosition } from './ToastContainer';
export type { ToastProps } from './Toast'
export type { ToastPosition } from './ToastContainer'

View File

@@ -4,54 +4,58 @@ import { ActionType } from '@/components/buttons/ActionButton'
// Types for context sub-components
export interface InstallationInstructions {
type: string;
command: string;
game_title: string;
dlc_count?: number;
type: string
command: string
game_title: string
dlc_count?: number
}
export interface DlcDialogState {
visible: boolean;
gameId: string;
gameTitle: string;
dlcs: DlcInfo[];
isLoading: boolean;
isEditMode: boolean;
progress: number;
timeLeft?: string;
visible: boolean
gameId: string
gameTitle: string
dlcs: DlcInfo[]
isLoading: boolean
isEditMode: boolean
progress: number
timeLeft?: string
}
export interface ProgressDialogState {
visible: boolean;
title: string;
message: string;
progress: number;
showInstructions: boolean;
instructions?: InstallationInstructions;
visible: boolean
title: string
message: string
progress: number
showInstructions: boolean
instructions?: InstallationInstructions
}
// Define the context type
export interface AppContextType {
// Game state
games: Game[];
isLoading: boolean;
error: string | null;
loadGames: () => Promise<boolean>;
handleProgressDialogClose: () => void;
games: Game[]
isLoading: boolean
error: string | null
loadGames: () => Promise<boolean>
handleProgressDialogClose: () => void
// DLC management
dlcDialog: DlcDialogState;
handleGameEdit: (gameId: string) => void;
handleDlcDialogClose: () => void;
dlcDialog: DlcDialogState
handleGameEdit: (gameId: string) => void
handleDlcDialogClose: () => void
// Game actions
progressDialog: ProgressDialogState;
handleGameAction: (gameId: string, action: ActionType) => Promise<void>;
handleDlcConfirm: (selectedDlcs: DlcInfo[]) => void;
progressDialog: ProgressDialogState
handleGameAction: (gameId: string, action: ActionType) => Promise<void>
handleDlcConfirm: (selectedDlcs: DlcInfo[]) => void
// 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
export const AppContext = createContext<AppContextType | undefined>(undefined);
export const AppContext = createContext<AppContextType | undefined>(undefined)

View File

@@ -7,7 +7,7 @@ import { ToastContainer } from '@/components/notifications'
// Context provider component
interface AppProviderProps {
children: ReactNode;
children: ReactNode
}
/**
@@ -16,13 +16,7 @@ interface AppProviderProps {
*/
export const AppProvider = ({ children }: AppProviderProps) => {
// Use our custom hooks
const {
games,
isLoading,
error,
loadGames,
setGames,
} = useGames()
const { games, isLoading, error, loadGames, setGames } = useGames()
const {
dlcDialog,
@@ -39,20 +33,13 @@ export const AppProvider = ({ children }: AppProviderProps) => {
handleDlcConfirm: executeDlcConfirm,
} = useGameActions()
const {
toasts,
removeToast,
success,
error: showError,
warning,
info
} = useToasts()
const { toasts, removeToast, success, error: showError, warning, info } = useToasts()
// Game action handler with proper error reporting
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) {
showError("Game not found")
showError('Game not found')
return
}
@@ -82,8 +69,8 @@ export const AppProvider = ({ children }: AppProviderProps) => {
// For other actions (uninstall cream, install/uninstall smoke)
// Mark game as installing
setGames(prevGames =>
prevGames.map(g => g.id === gameId ? {...g, installing: true} : g)
setGames((prevGames) =>
prevGames.map((g) => (g.id === gameId ? { ...g, installing: true } : g))
)
try {
@@ -91,16 +78,20 @@ export const AppProvider = ({ children }: AppProviderProps) => {
// Show success message
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 {
success(`Successfully uninstalled ${action.includes('cream') ? 'CreamLinux' : 'SmokeAPI'} from ${game.title}`)
success(
`Successfully uninstalled ${action.includes('cream') ? 'CreamLinux' : 'SmokeAPI'} from ${game.title}`
)
}
} catch (error) {
showError(`Action failed: ${error}`)
} finally {
// Reset installing state
setGames(prevGames =>
prevGames.map(g => g.id === gameId ? {...g, installing: false} : g)
setGames((prevGames) =>
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
)
}
}
@@ -110,45 +101,61 @@ export const AppProvider = ({ children }: AppProviderProps) => {
const { gameId, isEditMode } = dlcDialog
// 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
console.log(`Saving ${dlcsCopy.filter(d => d.enabled).length} enabled and ${
dlcsCopy.length - dlcsCopy.filter(d => d.enabled).length
} disabled DLCs`)
console.log(
`Saving ${dlcsCopy.filter((d) => d.enabled).length} enabled and ${
dlcsCopy.length - dlcsCopy.filter((d) => d.enabled).length
} disabled DLCs`
)
// Close dialog FIRST to avoid UI state issues
closeDlcDialog()
// Update game state to show it's installing
setGames(prevGames =>
prevGames.map(g => g.id === gameId ? { ...g, installing: true } : g)
setGames((prevGames) =>
prevGames.map((g) => (g.id === gameId ? { ...g, installing: true } : g))
)
executeDlcConfirm(dlcsCopy, gameId, isEditMode, games)
.then(() => {
success(isEditMode
? "DLC configuration updated successfully"
: "CreamLinux installed with selected DLCs")
success(
isEditMode
? 'DLC configuration updated successfully'
: 'CreamLinux installed with selected DLCs'
)
})
.catch(error => {
.catch((error) => {
showError(`DLC operation failed: ${error}`)
})
.finally(() => {
// Reset installing state
setGames(prevGames =>
prevGames.map(g => g.id === gameId ? { ...g, installing: false } : g)
setGames((prevGames) =>
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
)
})
}
// 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) {
case 'success': success(message, options); break;
case 'error': showError(message, options); break;
case 'warning': warning(message, options); break;
case 'info': info(message, options); break;
case 'success':
success(message, options)
break
case 'error':
showError(message, options)
break
case 'warning':
warning(message, options)
break
case 'info':
info(message, options)
break
}
}

View File

@@ -1,3 +1,3 @@
export * from './AppContext';
export * from './AppProvider';
export * from './useAppContext';
export * from './AppContext'
export * from './AppProvider'
export * from './useAppContext'

View File

@@ -1,10 +1,10 @@
// Export all hooks
export { useGames } from './useGames';
export { useDlcManager } from './useDlcManager';
export { useGameActions } from './useGameActions';
export { useToasts } from './useToasts';
export { useAppLogic } from './useAppLogic';
export { useGames } from './useGames'
export { useDlcManager } from './useDlcManager'
export { useGameActions } from './useGameActions'
export { useToasts } from './useToasts'
export { useAppLogic } from './useAppLogic'
// Export types
export type { ToastType, Toast, ToastOptions } from './useToasts';
export type { DlcDialogState } from './useDlcManager';
export type { ToastType, Toast, ToastOptions } from './useToasts'
export type { DlcDialogState } from './useDlcManager'

View File

@@ -2,7 +2,7 @@ import { useState, useCallback, useEffect, useRef } from 'react'
import { useAppContext } from '@/contexts/useAppContext'
interface UseAppLogicOptions {
autoLoad?: boolean;
autoLoad?: boolean
}
/**
@@ -13,13 +13,7 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
const { autoLoad = true } = options
// Get values from app context
const {
games,
loadGames,
isLoading,
error,
showToast
} = useAppContext()
const { games, loadGames, isLoading, error, showToast } = useAppContext()
// Local state for filtering and UI
const [filter, setFilter] = useState('all')
@@ -28,7 +22,7 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
const isInitializedRef = useRef(false)
const [scanProgress, setScanProgress] = useState({
message: 'Initializing...',
progress: 0
progress: 0,
})
// Filter games based on current filter and search
@@ -41,8 +35,8 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
(filter === 'proton' && !game.native)
// Then filter by search query
const searchMatch = !searchQuery.trim() ||
game.title.toLowerCase().includes(searchQuery.toLowerCase())
const searchMatch =
!searchQuery.trim() || game.title.toLowerCase().includes(searchQuery.toLowerCase())
return platformMatch && searchMatch
})
@@ -58,18 +52,18 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
if (!autoLoad || !isInitialLoad || isInitializedRef.current) return
isInitializedRef.current = true
console.log("[APP LOGIC #2] Starting initialization")
console.log('[APP LOGIC #2] Starting initialization')
const initialLoad = async () => {
try {
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 })
await loadGames()
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 })
setTimeout(() => setIsInitialLoad(false), 500)
@@ -104,6 +98,6 @@ export function useAppLogic(options: UseAppLogicOptions = {}) {
filteredGames: filteredGames(),
handleRefresh,
isLoading,
error
error,
}
}

View File

@@ -4,17 +4,17 @@ import { listen } from '@tauri-apps/api/event'
import { Game, DlcInfo } from '@/types'
export interface DlcDialogState {
visible: boolean;
gameId: string;
gameTitle: string;
dlcs: DlcInfo[];
enabledDlcs: string[];
isLoading: boolean;
isEditMode: boolean;
progress: number;
progressMessage: string;
timeLeft: string;
error: string | null;
visible: boolean
gameId: string
gameTitle: string
dlcs: DlcInfo[]
enabledDlcs: string[]
isLoading: boolean
isEditMode: boolean
progress: number
progressMessage: string
timeLeft: string
error: string | null
}
/**
@@ -60,9 +60,9 @@ export function useDlcManager() {
// When progress is 100%, mark loading as complete and reset fetch state
const unlistenDlcProgress = await listen<{
message: string;
progress: number;
timeLeft?: string;
message: string
progress: number
timeLeft?: string
}>('dlc-progress', (event) => {
const { message, progress, timeLeft } = event.payload
@@ -198,7 +198,7 @@ export function useDlcManager() {
console.log('Loaded existing DLC configuration:', allDlcs)
// IMPORTANT: Create a completely new array to avoid reference issues
const freshDlcs = allDlcs.map(dlc => ({...dlc}))
const freshDlcs = allDlcs.map((dlc) => ({ ...dlc }))
setDlcDialog((prev) => ({
...prev,

View File

@@ -26,16 +26,17 @@ export function useGameActions() {
try {
// Listen for progress updates from the backend
const unlistenProgress = await listen<{
title: string;
message: string;
progress: number;
complete: boolean;
show_instructions?: boolean;
instructions?: InstallationInstructions;
title: string
message: string
progress: number
complete: boolean
show_instructions?: boolean
instructions?: InstallationInstructions
}>('installation-progress', (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) {
// Hide dialog when complete if no instructions
@@ -64,7 +65,7 @@ export function useGameActions() {
let cleanup: (() => void) | null = null
setupEventListeners().then(unlisten => {
setupEventListeners().then((unlisten) => {
cleanup = unlisten
})
@@ -79,7 +80,8 @@ export function useGameActions() {
}, [])
// Unified handler for game actions (install/uninstall)
const handleGameAction = useCallback(async (gameId: string, action: ActionType, games: Game[]) => {
const handleGameAction = useCallback(
async (gameId: string, action: ActionType, games: Game[]) => {
try {
// For CreamLinux installation, we should NOT call process_game_action directly
// Instead, we show the DLC selection dialog first, which is handled in AppProvider
@@ -117,7 +119,6 @@ export function useGameActions() {
action,
},
})
} catch (error) {
console.error(`Error processing action ${action} for game ${gameId}:`, error)
@@ -136,15 +137,13 @@ export function useGameActions() {
// Rethrow to allow upstream handling
throw error
}
}, [])
},
[]
)
// Handle DLC selection confirmation
const handleDlcConfirm = useCallback(async (
selectedDlcs: DlcInfo[],
gameId: string,
isEditMode: boolean,
games: Game[]
) => {
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
@@ -152,7 +151,7 @@ export function useGameActions() {
try {
if (isEditMode) {
// 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 }))
// Show progress dialog for editing
setProgressDialog({
@@ -222,13 +221,15 @@ export function useGameActions() {
// Rethrow to allow upstream handling
throw error
}
}, [])
},
[]
)
return {
progressDialog,
setProgressDialog,
handleCloseProgressDialog,
handleGameAction,
handleDlcConfirm
handleDlcConfirm,
}
}

View File

@@ -64,17 +64,15 @@ export function useGames() {
// Update only the specific game in the state
setGames((prevGames) =>
prevGames.map((game) =>
game.id === updatedGame.id
? { ...updatedGame, platform: 'Steam' }
: game
game.id === updatedGame.id ? { ...updatedGame, platform: 'Steam' } : game
)
)
})
// Listen for scan progress events
const unlistenScanProgress = await listen<{
message: string;
progress: number;
message: string
progress: number
}>('scan-progress', (event) => {
const { message, progress } = event.payload
@@ -102,7 +100,7 @@ export function useGames() {
// Cleanup function
return () => {
unlisteners.forEach(fn => fn())
unlisteners.forEach((fn) => fn())
}
}, [loadGames, isInitialLoad])

View File

@@ -10,19 +10,19 @@ export type ToastType = 'success' | 'error' | 'warning' | 'info'
* Toast interface
*/
export interface Toast {
id: string;
message: string;
type: ToastType;
duration?: number;
title?: string;
id: string
message: string
type: ToastType
duration?: number
title?: string
}
/**
* Toast options interface
*/
export interface ToastOptions {
title?: string;
duration?: number;
title?: string
duration?: number
}
/**
@@ -36,17 +36,18 @@ export function useToasts() {
* Removes a toast by ID
*/
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
*/
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
const addToast = useCallback(
(toast: Omit<Toast, 'id'>) => {
const id = uuidv4()
const newToast = { ...toast, id }
setToasts(currentToasts => [...currentToasts, newToast])
setToasts((currentToasts) => [...currentToasts, newToast])
// Auto-remove toast after its duration expires
if (toast.duration !== Infinity) {
@@ -56,31 +57,45 @@ export function useToasts() {
}
return id
}, [removeToast])
},
[removeToast]
)
/**
* Shorthand method for success toasts
*/
const success = useCallback((message: string, options: ToastOptions = {}) =>
addToast({ message, type: 'success', ...options }), [addToast])
const success = useCallback(
(message: string, options: ToastOptions = {}) =>
addToast({ message, type: 'success', ...options }),
[addToast]
)
/**
* Shorthand method for error toasts
*/
const error = useCallback((message: string, options: ToastOptions = {}) =>
addToast({ message, type: 'error', ...options }), [addToast])
const error = useCallback(
(message: string, options: ToastOptions = {}) =>
addToast({ message, type: 'error', ...options }),
[addToast]
)
/**
* Shorthand method for warning toasts
*/
const warning = useCallback((message: string, options: ToastOptions = {}) =>
addToast({ message, type: 'warning', ...options }), [addToast])
const warning = useCallback(
(message: string, options: ToastOptions = {}) =>
addToast({ message, type: 'warning', ...options }),
[addToast]
)
/**
* Shorthand method for info toasts
*/
const info = useCallback((message: string, options: ToastOptions = {}) =>
addToast({ message, type: 'info', ...options }), [addToast])
const info = useCallback(
(message: string, options: ToastOptions = {}) =>
addToast({ message, type: 'info', ...options }),
[addToast]
)
return {
toasts,

View File

@@ -68,11 +68,7 @@ export const findBestGameImage = async (appId: string): Promise<string | null> =
}
// Try these image types in order of preference
const typesToTry = [
SteamImageType.HEADER,
SteamImageType.CAPSULE,
SteamImageType.LIBRARY_CAPSULE
]
const typesToTry = [SteamImageType.HEADER, SteamImageType.CAPSULE, SteamImageType.LIBRARY_CAPSULE]
for (const type of typesToTry) {
const url = getSteamImageUrl(appId, type)

View File

@@ -1 +1 @@
export * from './ImageService';
export * from './ImageService'

View File

@@ -4,7 +4,8 @@
@font-face {
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/WorkSans.ttf') format('ttf');
font-weight: 400;

View File

@@ -22,11 +22,8 @@
left: 0;
right: 0;
bottom: 0;
background-image: radial-gradient(
circle at 20% 30%,
rgba(var(--primary-color), 0.05) 0%,
transparent 70%
),
background-image:
radial-gradient(circle at 20% 30%, rgba(var(--primary-color), 0.05) 0%, transparent 70%),
radial-gradient(circle at 80% 70%, rgba(var(--cream-color), 0.05) 0%, transparent 70%);
pointer-events: none;
z-index: var(--z-bg);

View File

@@ -36,7 +36,9 @@
border: 1px solid var(--border-soft);
opacity: 0;
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;
display: flex;
flex-direction: column;

View File

@@ -45,11 +45,15 @@
// Special styling for cards with different statuses
.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) {
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

View File

@@ -37,7 +37,9 @@
// Interactive icons
&.icon-clickable {
cursor: pointer;
transition: transform 0.2s ease, opacity 0.2s ease;
transition:
transform 0.2s ease,
opacity 0.2s ease;
&:hover {
opacity: 0.8;

View File

@@ -96,7 +96,9 @@
border-color: var(--primary-color);
background-color: rgba(255, 255, 255, 0.1);
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 {
color: var(--primary-color);

View File

@@ -2,7 +2,7 @@
* DLC information interface
*/
export interface DlcInfo {
appid: string;
name: string;
enabled: boolean;
appid: string
name: string
enabled: boolean
}

View File

@@ -2,13 +2,13 @@
* Game information interface
*/
export interface Game {
id: string;
title: string;
path: string;
platform?: string;
native: boolean;
api_files: string[];
cream_installed?: boolean;
smoke_installed?: boolean;
installing?: boolean;
id: string
title: string
path: string
platform?: string
native: boolean
api_files: string[]
cream_installed?: boolean
smoke_installed?: boolean
installing?: boolean
}

View File

@@ -9,17 +9,17 @@
*/
export function formatTime(seconds: number): string {
if (seconds < 60) {
return `${Math.round(seconds)}s`;
return `${Math.round(seconds)}s`
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.round(seconds % 60);
const minutes = Math.floor(seconds / 60)
const remainingSeconds = Math.round(seconds % 60)
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 {
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,
delay: number
): (...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) {
clearTimeout(timer);
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(...args);
}, delay);
};
fn(...args)
}, delay)
}
}
/**
@@ -70,16 +70,16 @@ export function throttle<T extends (...args: unknown[]) => unknown>(
fn: T,
limit: number
): (...args: Parameters<T>) => void {
let lastCall = 0;
let lastCall = 0
return function(...args: Parameters<T>) {
const now = Date.now();
return function (...args: Parameters<T>) {
const now = Date.now()
if (now - lastCall < limit) {
return;
return
}
lastCall = now;
return fn(...args);
};
lastCall = now
return fn(...args)
}
}

View File

@@ -1 +1 @@
export * from './helpers';
export * from './helpers'

View File

@@ -1,7 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
import svgr from "vite-plugin-svgr";
import svgr from 'vite-plugin-svgr'
// https://vitejs.dev/config/
export default defineConfig({