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'
|
||||
? 'Check'
|
||||
: status === 'warning'
|
||||
? 'Warning'
|
||||
: status === 'error'
|
||||
? 'Close'
|
||||
: 'Info'
|
||||
? 'Warning'
|
||||
: status === 'error'
|
||||
? 'Close'
|
||||
: 'Info'
|
||||
|
||||
return <Icon name={iconName} variant="bold" />
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './AppContext';
|
||||
export * from './AppProvider';
|
||||
export * from './useAppContext';
|
||||
export * from './AppContext'
|
||||
export * from './AppProvider'
|
||||
export * from './useAppContext'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,156 +80,156 @@ export function useGameActions() {
|
||||
}, [])
|
||||
|
||||
// Unified handler for game actions (install/uninstall)
|
||||
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
|
||||
if (action === 'install_cream') {
|
||||
return
|
||||
}
|
||||
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
|
||||
if (action === 'install_cream') {
|
||||
return
|
||||
}
|
||||
|
||||
// For other actions (uninstall_cream, install_smoke, uninstall_smoke)
|
||||
// Find game to get title
|
||||
const game = games.find((g) => g.id === gameId)
|
||||
if (!game) return
|
||||
// For other actions (uninstall_cream, install_smoke, uninstall_smoke)
|
||||
// Find game to get title
|
||||
const game = games.find((g) => g.id === gameId)
|
||||
if (!game) return
|
||||
|
||||
// Get title based on action
|
||||
const isCream = action.includes('cream')
|
||||
const isInstall = action.includes('install')
|
||||
const product = isCream ? 'CreamLinux' : 'SmokeAPI'
|
||||
const operation = isInstall ? 'Installing' : 'Uninstalling'
|
||||
// Get title based on action
|
||||
const isCream = action.includes('cream')
|
||||
const isInstall = action.includes('install')
|
||||
const product = isCream ? 'CreamLinux' : 'SmokeAPI'
|
||||
const operation = isInstall ? 'Installing' : 'Uninstalling'
|
||||
|
||||
// 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
|
||||
// Show progress dialog
|
||||
setProgressDialog({
|
||||
visible: true,
|
||||
title: `Updating DLCs for ${game.title}`,
|
||||
message: 'Updating DLC configuration...',
|
||||
progress: 30,
|
||||
title: `${operation} ${product} for ${game.title}`,
|
||||
message: isInstall ? 'Downloading required files...' : 'Removing files...',
|
||||
progress: isInstall ? 0 : 30,
|
||||
showInstructions: false,
|
||||
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
|
||||
await invoke('update_dlc_configuration_command', {
|
||||
gamePath: game.path,
|
||||
dlcs: dlcsCopy,
|
||||
// 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)
|
||||
|
||||
// Update progress dialog for completion
|
||||
// Show error in progress dialog
|
||||
setProgressDialog((prev) => ({
|
||||
...prev,
|
||||
title: `Update Complete: ${game.title}`,
|
||||
message: 'DLC configuration updated successfully!',
|
||||
message: `Error: ${error}`,
|
||||
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,
|
||||
})
|
||||
}, 3000)
|
||||
|
||||
// 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
|
||||
// Rethrow to allow upstream handling
|
||||
throw error
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing DLC selection:', error)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Show error in progress dialog
|
||||
setProgressDialog((prev) => ({
|
||||
...prev,
|
||||
message: `Error: ${error}`,
|
||||
progress: 100,
|
||||
}))
|
||||
// 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
|
||||
|
||||
// Hide dialog after a delay
|
||||
setTimeout(() => {
|
||||
setProgressDialog((prev) => ({ ...prev, visible: false }))
|
||||
}, 3000)
|
||||
try {
|
||||
if (isEditMode) {
|
||||
// MODIFIED: Create a deep copy to ensure we don't have reference issues
|
||||
const dlcsCopy = selectedDlcs.map((dlc) => ({ ...dlc }))
|
||||
|
||||
// Rethrow to allow upstream handling
|
||||
throw error
|
||||
}
|
||||
}, [])
|
||||
// Show progress dialog for editing
|
||||
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 {
|
||||
progressDialog,
|
||||
setProgressDialog,
|
||||
handleCloseProgressDialog,
|
||||
handleGameAction,
|
||||
handleDlcConfirm
|
||||
handleDlcConfirm,
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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,51 +36,66 @@ 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 id = uuidv4()
|
||||
const newToast = { ...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) {
|
||||
setTimeout(() => {
|
||||
removeToast(id)
|
||||
}, toast.duration || 5000) // Default 5 seconds
|
||||
}
|
||||
// Auto-remove toast after its duration expires
|
||||
if (toast.duration !== Infinity) {
|
||||
setTimeout(() => {
|
||||
removeToast(id)
|
||||
}, toast.duration || 5000) // Default 5 seconds
|
||||
}
|
||||
|
||||
return id
|
||||
}, [removeToast])
|
||||
return id
|
||||
},
|
||||
[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,
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* Game image sources from Steam's CDN
|
||||
*/
|
||||
export const SteamImageType = {
|
||||
HEADER: 'header', // 460x215
|
||||
CAPSULE: 'capsule_616x353', // 616x353
|
||||
LOGO: 'logo', // Game logo with transparency
|
||||
LIBRARY_HERO: 'library_hero', // 1920x620
|
||||
HEADER: 'header', // 460x215
|
||||
CAPSULE: 'capsule_616x353', // 616x353
|
||||
LOGO: 'logo', // Game logo with transparency
|
||||
LIBRARY_HERO: 'library_hero', // 1920x620
|
||||
LIBRARY_CAPSULE: 'library_600x900', // 600x900
|
||||
} as const
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from './ImageService';
|
||||
export * from './ImageService'
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* DLC information interface
|
||||
*/
|
||||
export interface DlcInfo {
|
||||
appid: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
appid: string
|
||||
name: string
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
export * from './helpers';
|
||||
export * from './helpers'
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user