bits and bob

This commit is contained in:
Tickbase
2025-05-18 17:57:36 +02:00
parent 4b70cec6e9
commit 79fd51c5e5
20 changed files with 173 additions and 55 deletions

View File

@@ -117,8 +117,10 @@ const DlcSelectionDialog = ({
// Submit selected DLCs to parent component // Submit selected DLCs to parent component
const handleConfirm = useCallback(() => { const handleConfirm = useCallback(() => {
onConfirm(selectedDlcs) // Create a deep copy to prevent reference issues
}, [onConfirm, selectedDlcs]) const dlcsCopy = JSON.parse(JSON.stringify(selectedDlcs));
onConfirm(dlcsCopy);
}, [onConfirm, selectedDlcs]);
// Count selected DLCs // Count selected DLCs
const selectedCount = selectedDlcs.filter((dlc) => dlc.enabled).length const selectedCount = selectedDlcs.filter((dlc) => dlc.enabled).length

View File

@@ -102,7 +102,7 @@ const Icon: React.FC<IconProps> = ({
if (BRAND_ICON_NAMES.has(name)) { if (BRAND_ICON_NAMES.has(name)) {
defaultVariant = 'brand' defaultVariant = 'brand'
} else { } else {
defaultVariant = 'bold' // Default to outline for non-brand icons defaultVariant = 'bold' // Default to bold for non-brand icons
} }
} }

View File

@@ -1,6 +1,7 @@
// Bold variant icons // Bold variant icons
export { ReactComponent as Linux } from './linux.svg' export { ReactComponent as Linux } from './linux.svg'
export { ReactComponent as steam } from './steam.svg' export { ReactComponent as Steam } from './steam.svg'
export { ReactComponent as windows } from './windows.svg' export { ReactComponent as Windows } from './windows.svg'
export { ReactComponent as github } from './github.svg' export { ReactComponent as Github } from './github.svg'
export { ReactComponent as discord } from './discord.svg' export { ReactComponent as Discord } from './discord.svg'
export { ReactComponent as Proton } from './proton.svg'

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -27,11 +27,13 @@ export const search = 'Search'
export const trash = 'Trash' export const trash = 'Trash'
export const warning = 'Warning' export const warning = 'Warning'
export const wine = 'Wine' export const wine = 'Wine'
export const diamond = 'Diamond'
// Brand icons // Brand icons
export const discord = 'Discord' export const discord = 'Discord'
export const github = 'GitHub' export const github = 'GitHub'
export const linux = 'Linux' export const linux = 'Linux'
export const proton = 'Proton'
export const steam = 'Steam' export const steam = 'Steam'
export const windows = 'Windows' export const windows = 'Windows'
@@ -54,11 +56,13 @@ export const IconNames = {
Trash: trash, Trash: trash,
Warning: warning, Warning: warning,
Wine: wine, Wine: wine,
Diamond: diamond,
// Brand icons // Brand icons
Discord: discord, Discord: discord,
GitHub: github, GitHub: github,
Linux: linux, Linux: linux,
Proton: proton,
Steam: steam, Steam: steam,
Windows: windows, Windows: windows,
} as const } as const

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M9.2 8.25h5.6L12.15 3h-.3zm2.05 11.85V9.75H2.625zm1.5 0l8.625-10.35H12.75zm3.7-11.85h5.175L19.55 4.1q-.275-.5-.737-.8T17.775 3H13.85zm-14.075 0H7.55L10.15 3H6.225q-.575 0-1.037.3t-.738.8z"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@@ -15,3 +15,4 @@ export { ReactComponent as Search } from './search.svg'
export { ReactComponent as Trash } from './trash.svg' export { ReactComponent as Trash } from './trash.svg'
export { ReactComponent as Warning } from './warning.svg' export { ReactComponent as Warning } from './warning.svg'
export { ReactComponent as Wine } from './wine.svg' export { ReactComponent as Wine } from './wine.svg'
export { ReactComponent as Diamond } from './diamond.svg'

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M12 19.875q-.425 0-.825-.187t-.7-.538L2.825 10q-.225-.275-.337-.6t-.113-.675q0-.225.038-.462t.162-.438L4.45 4.1q.275-.5.738-.8T6.225 3h11.55q.575 0 1.038.3t.737.8l1.875 3.725q.125.2.163.437t.037.463q0 .35-.112.675t-.338.6l-7.65 9.15q-.3.35-.7.538t-.825.187M9.625 8h4.75l-1.5-3h-1.75zM11 16.675V10H5.45zm2 0L18.55 10H13zM16.6 8h2.65l-1.5-3H15.1zM4.75 8H7.4l1.5-3H6.25z"/></svg>

After

Width:  |  Height:  |  Size: 488 B

View File

@@ -15,3 +15,4 @@ export { ReactComponent as Search } from './search.svg'
export { ReactComponent as Trash } from './trash.svg' export { ReactComponent as Trash } from './trash.svg'
export { ReactComponent as Warning } from './warning.svg' export { ReactComponent as Warning } from './warning.svg'
export { ReactComponent as Wine } from './wine.svg' export { ReactComponent as Wine } from './wine.svg'
export { ReactComponent as Diamond } from './diamond.svg'

View File

@@ -1,5 +1,5 @@
import { Button } from '@/components/buttons' import { Button } from '@/components/buttons'
import { Icon, info, refresh, search } from '@/components/icons' import { Icon, diamond, refresh, search } from '@/components/icons'
interface HeaderProps { interface HeaderProps {
onRefresh: () => void onRefresh: () => void
@@ -21,7 +21,7 @@ const Header = ({
return ( return (
<header className="app-header"> <header className="app-header">
<div className="app-title"> <div className="app-title">
<Icon name={info} variant="bold" size="md" className="app-logo-icon" /> <Icon name={diamond} variant="bold" size="lg" className="app-logo-icon" />
<h1>CreamLinux</h1> <h1>CreamLinux</h1>
</div> </div>
<div className="header-controls"> <div className="header-controls">

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react' import { useEffect, useState } from 'react'
interface InitialLoadingScreenProps { interface InitialLoadingScreenProps {
message: string; message: string;
@@ -9,21 +9,32 @@ interface InitialLoadingScreenProps {
/** /**
* Initial loading screen displayed when the app first loads * Initial loading screen displayed when the app first loads
*/ */
const InitialLoadingScreen = ({ const InitialLoadingScreen = ({ message, progress, onComplete }: InitialLoadingScreenProps) => {
message, const [detailedStatus, setDetailedStatus] = useState<string[]>([
progress, "Initializing application...",
onComplete "Setting up Steam integration...",
}: InitialLoadingScreenProps) => { "Preparing DLC management..."
// Call onComplete when progress reaches 100% ]);
useEffect(() => {
if (progress >= 100 && onComplete) {
const timer = setTimeout(() => {
onComplete();
}, 500); // Small delay to show completion
return () => clearTimeout(timer); // 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!" }
];
// Find current status message based on progress
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]);
} }
}, [progress, onComplete]); }, [progress, detailedStatus]);
return ( return (
<div className="initial-loading-screen"> <div className="initial-loading-screen">
@@ -31,6 +42,7 @@ const InitialLoadingScreen = ({
<h1>CreamLinux</h1> <h1>CreamLinux</h1>
<div className="loading-animation"> <div className="loading-animation">
{/* Enhanced animation with SVG or more elaborate CSS animation */}
<div className="loading-circles"> <div className="loading-circles">
<div className="circle circle-1"></div> <div className="circle circle-1"></div>
<div className="circle circle-2"></div> <div className="circle circle-2"></div>
@@ -40,6 +52,16 @@ const InitialLoadingScreen = ({
<p className="loading-message">{message}</p> <p className="loading-message">{message}</p>
{/* Add a detailed status log that shows progress steps */}
<div className="loading-status-log">
{detailedStatus.slice(-4).map((status, index) => (
<div key={index} className="status-line">
<span className="status-indicator"></span>
<span className="status-text">{status}</span>
</div>
))}
</div>
<div className="progress-bar-container"> <div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${progress}%` }} /> <div className="progress-bar" style={{ width: `${progress}%` }} />
</div> </div>
@@ -47,7 +69,7 @@ const InitialLoadingScreen = ({
<div className="progress-percentage">{Math.round(progress)}%</div> <div className="progress-percentage">{Math.round(progress)}%</div>
</div> </div>
</div> </div>
) );
} };
export default InitialLoadingScreen export default InitialLoadingScreen

View File

@@ -1,4 +1,4 @@
import { Icon, layers, linux, wine } from '@/components/icons' import { Icon, layers, linux, proton } from '@/components/icons'
interface SidebarProps { interface SidebarProps {
setFilter: (filter: string) => void setFilter: (filter: string) => void
@@ -22,7 +22,7 @@ const Sidebar = ({ setFilter, currentFilter }: SidebarProps) => {
const filters: FilterItem[] = [ const filters: FilterItem[] = [
{ id: 'all', label: 'All Games', icon: layers, variant: 'bold' }, { id: 'all', label: 'All Games', icon: layers, variant: 'bold' },
{ id: 'native', label: 'Native', icon: linux, variant: 'brand' }, { id: 'native', label: 'Native', icon: linux, variant: 'brand' },
{ id: 'proton', label: 'Proton Required', icon: wine, variant: 'bold' } { id: 'proton', label: 'Proton Required', icon: proton, variant: 'brand' }
] ]
return ( return (

View File

@@ -23,13 +23,17 @@ const Toast = ({
onDismiss onDismiss
}: ToastProps) => { }: ToastProps) => {
const [visible, setVisible] = useState(false) const [visible, setVisible] = useState(false)
const [isClosing, setIsClosing] = useState(false);
// Use useCallback to memoize the handleDismiss function // Use useCallback to memoize the handleDismiss function
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {
setVisible(false) setIsClosing(true);
// Give time for exit animation // Give time for exit animation
setTimeout(() => onDismiss(id), 300) setTimeout(() => {
}, [id, onDismiss]) setVisible(false);
setTimeout(() => onDismiss(id), 50);
}, 300);
}, [id, onDismiss]);
// Handle animation on mount/unmount // Handle animation on mount/unmount
useEffect(() => { useEffect(() => {
@@ -69,7 +73,7 @@ const Toast = ({
} }
return ( return (
<div className={`toast toast-${type} ${visible ? 'visible' : ''}`}> <div className={`toast toast-${type} ${visible ? 'visible' : ''} ${isClosing ? 'closing' : ''}`}>
<div className="toast-icon">{getIcon()}</div> <div className="toast-icon">{getIcon()}</div>
<div className="toast-content"> <div className="toast-content">
{title && <h4 className="toast-title">{title}</h4>} {title && <h4 className="toast-title">{title}</h4>}

View File

@@ -107,15 +107,25 @@ export const AppProvider = ({ children }: AppProviderProps) => {
// DLC confirmation wrapper // DLC confirmation wrapper
const handleDlcConfirm = (selectedDlcs: DlcInfo[]) => { const handleDlcConfirm = (selectedDlcs: DlcInfo[]) => {
closeDlcDialog()
const { gameId, isEditMode } = dlcDialog const { gameId, isEditMode } = dlcDialog
// MODIFIED: Create a deep copy to ensure we don't have reference issues
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`)
// Close dialog FIRST to avoid UI state issues
closeDlcDialog()
// Update game state to show it's installing // Update game state to show it's installing
setGames(prevGames => setGames(prevGames =>
prevGames.map(g => g.id === gameId ? { ...g, installing: true } : g) prevGames.map(g => g.id === gameId ? { ...g, installing: true } : g)
) )
executeDlcConfirm(selectedDlcs, gameId, isEditMode, games) executeDlcConfirm(dlcsCopy, gameId, isEditMode, games)
.then(() => { .then(() => {
success(isEditMode success(isEditMode
? "DLC configuration updated successfully" ? "DLC configuration updated successfully"

View File

@@ -25,6 +25,7 @@ export function useDlcManager() {
const [isFetchingDlcs, setIsFetchingDlcs] = useState(false) const [isFetchingDlcs, setIsFetchingDlcs] = useState(false)
const dlcFetchController = useRef<AbortController | null>(null) const dlcFetchController = useRef<AbortController | null>(null)
const activeDlcFetchId = useRef<string | null>(null) const activeDlcFetchId = useRef<string | null>(null)
const [forceReload, setForceReload] = useState(false) // Add this state to force reloads
// DLC selection dialog state // DLC selection dialog state
const [dlcDialog, setDlcDialog] = useState<DlcDialogState>({ const [dlcDialog, setDlcDialog] = useState<DlcDialogState>({
@@ -155,7 +156,7 @@ export function useDlcManager() {
} }
} }
// Handle game edit (show DLC management dialog) // MODIFIED: Handle game edit (show DLC management dialog) with proper reloading
const handleGameEdit = async (gameId: string, games: Game[]) => { const handleGameEdit = async (gameId: string, games: Game[]) => {
const game = games.find((g) => g.id === gameId) const game = games.find((g) => g.id === gameId)
if (!game || !game.cream_installed) return if (!game || !game.cream_installed) return
@@ -172,7 +173,7 @@ export function useDlcManager() {
visible: true, visible: true,
gameId, gameId,
gameTitle: game.title, gameTitle: game.title,
dlcs: [], dlcs: [], // Always start with empty DLCs to force a fresh load
enabledDlcs: [], enabledDlcs: [],
isLoading: true, isLoading: true,
isEditMode: true, // This is an edit operation isEditMode: true, // This is an edit operation
@@ -182,23 +183,33 @@ export function useDlcManager() {
error: null, error: null,
}) })
// Try to read all DLCs from the configuration file first (including disabled ones) // MODIFIED: Always get a fresh copy from the config file
console.log('Loading DLC configuration from disk...')
try { try {
const allDlcs = await invoke<DlcInfo[]>('get_all_dlcs_command', { const allDlcs = await invoke<DlcInfo[]>('get_all_dlcs_command', {
gamePath: game.path, gamePath: game.path,
}).catch(() => [] as DlcInfo[]) }).catch((e) => {
console.error('Error loading DLCs:', e)
return [] as DlcInfo[]
})
if (allDlcs.length > 0) { if (allDlcs.length > 0) {
// If we have DLCs from the config file, use them // Log the fresh DLC config
console.log('Loaded existing DLC configuration:', allDlcs) console.log('Loaded existing DLC configuration:', allDlcs)
// IMPORTANT: Create a completely new array to avoid reference issues
const freshDlcs = allDlcs.map(dlc => ({...dlc}))
setDlcDialog((prev) => ({ setDlcDialog((prev) => ({
...prev, ...prev,
dlcs: allDlcs, dlcs: freshDlcs,
isLoading: false, isLoading: false,
progress: 100, progress: 100,
progressMessage: 'Loaded existing DLC configuration', progressMessage: 'Loaded existing DLC configuration',
})) }))
// Reset force reload flag
setForceReload(false)
return return
} }
} catch (error) { } catch (error) {
@@ -245,7 +256,7 @@ export function useDlcManager() {
} }
} }
// Handle DLC selection dialog close // MODIFIED: Handle DLC selection dialog close
const handleDlcDialogClose = () => { const handleDlcDialogClose = () => {
// Cancel any in-progress DLC fetching // Cancel any in-progress DLC fetching
if (isFetchingDlcs && activeDlcFetchId.current) { if (isFetchingDlcs && activeDlcFetchId.current) {
@@ -267,8 +278,15 @@ export function useDlcManager() {
dlcFetchController.current = null dlcFetchController.current = null
} }
// Close dialog // Close dialog and reset state
setDlcDialog((prev) => ({ ...prev, visible: false })) setDlcDialog((prev) => ({
...prev,
visible: false,
dlcs: [], // Clear DLCs to force a reload next time
}))
// Set flag to force reload next time
setForceReload(true)
} }
// Update DLCs being streamed with enabled state // Update DLCs being streamed with enabled state
@@ -291,5 +309,6 @@ export function useDlcManager() {
streamGameDlcs, streamGameDlcs,
handleGameEdit, handleGameEdit,
handleDlcDialogClose, handleDlcDialogClose,
forceReload,
} }
} }

View File

@@ -151,7 +151,9 @@ export function useGameActions() {
try { try {
if (isEditMode) { if (isEditMode) {
// If in edit mode, we're updating existing cream_api.ini // 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 for editing
setProgressDialog({ setProgressDialog({
visible: true, visible: true,
@@ -162,10 +164,12 @@ export function useGameActions() {
instructions: undefined, instructions: undefined,
}) })
console.log('Saving DLC configuration:', dlcsCopy)
// Call the backend to update the DLC configuration // Call the backend to update the DLC configuration
await invoke('update_dlc_configuration_command', { await invoke('update_dlc_configuration_command', {
gamePath: game.path, gamePath: game.path,
dlcs: selectedDlcs, dlcs: dlcsCopy,
}) })
// Update progress dialog for completion // Update progress dialog for completion

View File

@@ -109,6 +109,10 @@
overflow-y: auto; overflow-y: auto;
flex: 1; flex: 1;
@include custom-scrollbar; @include custom-scrollbar;
p {
margin-bottom: 1rem;
}
} }
// Dialog footer // Dialog footer

View File

@@ -33,7 +33,7 @@
// Instruction container // Instruction container
.instruction-container { .instruction-container {
margin-top: 1.5rem; margin-top: 1rem;
padding-top: 1rem; padding-top: 1rem;
border-top: 1px solid var(--border-soft); border-top: 1px solid var(--border-soft);

View File

@@ -69,6 +69,44 @@
min-height: 3rem; min-height: 3rem;
} }
.loading-status-log {
margin: 1rem 0;
text-align: left;
max-height: 100px;
overflow-y: auto;
background-color: rgba(0, 0, 0, 0.2);
border-radius: var(--radius-sm);
padding: 0.5rem;
.status-line {
margin: 0.5rem 0;
display: flex;
align-items: center;
.status-indicator {
color: var(--primary-color);
margin-right: 0.5rem;
font-size: 1.2rem;
}
.status-text {
color: var(--text-secondary);
font-size: 0.9rem;
}
&:last-child {
.status-indicator {
color: var(--success);
}
.status-text {
color: var(--text-primary);
font-weight: 600;
}
}
}
}
.progress-bar-container { .progress-bar-container {
height: 8px; height: 8px;
background-color: var(--border-soft); background-color: var(--border-soft);

View File

@@ -77,6 +77,11 @@
transform: translateY(0); transform: translateY(0);
} }
&.closing {
opacity: 0;
transform: translateY(-10px);
}
// Type-specific styling // Type-specific styling
&.toast-success { &.toast-success {
border-color: var(--success); border-color: var(--success);