feat: replace legacy Python CLI with GUI app

This commit is contained in:
Tickbase
2025-05-19 03:41:04 +02:00
parent e55f91a66d
commit 1e2cb52f6f
22 changed files with 118 additions and 222 deletions

View File

@@ -1,5 +1,5 @@
import { useAppContext } from '@/contexts/useAppContext'
import { UpdateChecker } from '@/components/updater'
import { UpdateNotifier } from '@/components/updater'
import { useAppLogic } from '@/hooks'
import './styles/main.scss'
@@ -105,10 +105,12 @@ function App() {
onClose={handleDlcDialogClose}
onConfirm={handleDlcConfirm}
/>
<UpdateChecker />
{/* Simple update notifier that uses toast - no UI component */}
<UpdateNotifier />
</div>
</ErrorBoundary>
)
}
export default App
export default App

Binary file not shown.

Before

Width:  |  Height:  |  Size: 362 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -35,7 +35,7 @@ const ActionButton: FC<ActionButtonProps> = ({
return isInstalled ? `Uninstall ${product}` : `Install ${product}`
}
// Map to our button variant
// Map to button variant
const getButtonVariant = (): ButtonVariant => {
// For uninstall actions, use danger variant
if (isInstalled) return 'danger'

View File

@@ -4,17 +4,18 @@ export interface DialogHeaderProps {
children: ReactNode
className?: string
onClose?: () => void
hideCloseButton?: boolean;
}
/**
* Header component for dialogs
* Contains the title and optional close button
*/
const DialogHeader = ({ children, className = '', onClose }: DialogHeaderProps) => {
const DialogHeader = ({ children, className = '', onClose, hideCloseButton = false }: DialogHeaderProps) => {
return (
<div className={`dialog-header ${className}`}>
{children}
{onClose && (
{onClose && !hideCloseButton && (
<button className="dialog-close-button" onClick={onClose} aria-label="Close dialog">
×
</button>

View File

@@ -141,7 +141,7 @@ const DlcSelectionDialog = ({
return (
<Dialog visible={visible} onClose={onClose} size="large" preventBackdropClose={isLoading}>
<DialogHeader onClose={onClose}>
<DialogHeader onClose={onClose} hideCloseButton={true}>
<h3>{dialogTitle}</h3>
<div className="dlc-game-info">
<span className="game-title">{gameTitle}</span>

View File

@@ -123,7 +123,7 @@ const ProgressDialog = ({
size="medium"
preventBackdropClose={!isCloseButtonEnabled}
>
<DialogHeader>
<DialogHeader onClose={onClose} hideCloseButton={true}>
<h3>{title}</h3>
</DialogHeader>

View File

@@ -36,7 +36,7 @@ const AnimatedBackground = () => {
color: string
}
// Color palette matching our theme
// Color palette matching theme
const colors = [
'rgba(74, 118, 196, 0.5)', // primary blue
'rgba(155, 125, 255, 0.5)', // purple

View File

@@ -1,147 +0,0 @@
import { useState, useEffect } from 'react'
import { check, type Update, type DownloadEvent } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process'
import { Button } from '@/components/buttons'
/**
* React component that checks for updates and provides
* UI for downloading and installing them
*/
const UpdateChecker = () => {
const [updateAvailable, setUpdateAvailable] = useState(false)
const [updateInfo, setUpdateInfo] = useState<Update | null>(null)
const [isChecking, setIsChecking] = useState(false)
const [isDownloading, setIsDownloading] = useState(false)
const [downloadProgress, setDownloadProgress] = useState(0)
const [error, setError] = useState<string | null>(null)
// Check for updates on component mount
useEffect(() => {
checkForUpdates()
}, [])
const checkForUpdates = async () => {
try {
setIsChecking(true)
setError(null)
// Check for updates
const update = await check()
if (update) {
console.log(`Update available: ${update.version}`)
setUpdateAvailable(true)
setUpdateInfo(update)
} else {
console.log('No updates available')
setUpdateAvailable(false)
}
} catch (err) {
console.error('Failed to check for updates:', err)
setError(`Failed to check for updates: ${err instanceof Error ? err.message : String(err)}`)
} finally {
setIsChecking(false)
}
}
const downloadAndInstallUpdate = async () => {
if (!updateInfo) return
try {
setIsDownloading(true)
setError(null)
let downloaded = 0
let contentLength = 0
// Download and install update
await updateInfo.downloadAndInstall((event: DownloadEvent) => {
switch (event.event) {
case 'Started':
// Started event includes contentLength
if ('contentLength' in event.data && typeof event.data.contentLength === 'number') {
contentLength = event.data.contentLength
console.log(`Started downloading ${contentLength} bytes`)
}
break
case 'Progress':
// Progress event includes chunkLength
if ('chunkLength' in event.data && typeof event.data.chunkLength === 'number' && contentLength > 0) {
downloaded += event.data.chunkLength
const progress = (downloaded / contentLength) * 100
setDownloadProgress(progress)
console.log(`Downloaded ${downloaded} from ${contentLength}`)
}
break
case 'Finished':
console.log('Download finished')
break
}
})
console.log('Update installed, relaunching application')
await relaunch()
} catch (err) {
console.error('Failed to download and install update:', err)
setError(`Failed to download and install update: ${err instanceof Error ? err.message : String(err)}`)
setIsDownloading(false)
}
}
if (isChecking) {
return <div className="update-checker">Checking for updates...</div>
}
if (error) {
return (
<div className="update-checker error">
<p>{error}</p>
<Button variant="primary" onClick={checkForUpdates}>Try Again</Button>
</div>
)
}
if (!updateAvailable || !updateInfo) {
return null // Don't show anything if there's no update
}
return (
<div className="update-checker">
<div className="update-info">
<h3>Update Available</h3>
<p>Version {updateInfo.version} is available to download.</p>
{updateInfo.body && <p className="update-notes">{updateInfo.body}</p>}
</div>
{isDownloading ? (
<div className="update-progress">
<div className="progress-bar-container">
<div
className="progress-bar"
style={{ width: `${downloadProgress}%` }}
/>
</div>
<p>Downloading: {Math.round(downloadProgress)}%</p>
</div>
) : (
<div className="update-actions">
<Button
variant="primary"
onClick={downloadAndInstallUpdate}
disabled={isDownloading}
>
Download & Install
</Button>
<Button
variant="secondary"
onClick={() => setUpdateAvailable(false)}
>
Later
</Button>
</div>
)}
</div>
)
}
export default UpdateChecker

View File

@@ -0,0 +1,14 @@
import { useUpdateChecker } from '@/hooks/useUpdateChecker'
/**
* Simple component that uses the update checker hook
* Can be dropped in anywhere in the app
*/
const UpdateNotifier = () => {
useUpdateChecker()
// This component doesn't render anything
return null
}
export default UpdateNotifier

View File

@@ -1 +1,5 @@
export { default as UpdateChecker } from './UpdateChecker'
// Update checker implementation
export { default as useUpdateChecker } from '@/hooks/useUpdateChecker'
// Simple component for using the checker
export { default as UpdateNotifier } from './UpdateNotifier'

View File

@@ -100,7 +100,7 @@ export const AppProvider = ({ children }: AppProviderProps) => {
const handleDlcConfirm = (selectedDlcs: DlcInfo[]) => {
const { gameId, isEditMode } = dlcDialog
// MODIFIED: Create a deep copy to ensure we don't have reference issues
// Create a deep copy to ensure we don't have reference issues
const dlcsCopy = selectedDlcs.map((dlc) => ({ ...dlc }))
// Log detailed info before closing dialog

View File

@@ -25,7 +25,7 @@ export function useDlcManager() {
const [isFetchingDlcs, setIsFetchingDlcs] = useState(false)
const dlcFetchController = useRef<AbortController | null>(null)
const activeDlcFetchId = useRef<string | null>(null)
const [forceReload, setForceReload] = useState(false) // Add this state to force reloads
const [forceReload, setForceReload] = useState(false)
// DLC selection dialog state
const [dlcDialog, setDlcDialog] = useState<DlcDialogState>({
@@ -156,7 +156,7 @@ export function useDlcManager() {
}
}
// MODIFIED: Handle game edit (show DLC management dialog) with proper reloading
// Handle game edit (show DLC management dialog) with proper reloading
const handleGameEdit = async (gameId: string, games: Game[]) => {
const game = games.find((g) => g.id === gameId)
if (!game || !game.cream_installed) return
@@ -173,17 +173,17 @@ export function useDlcManager() {
visible: true,
gameId,
gameTitle: game.title,
dlcs: [], // Always start with empty DLCs to force a fresh load
dlcs: [],
enabledDlcs: [],
isLoading: true,
isEditMode: true, // This is an edit operation
isEditMode: true,
progress: 0,
progressMessage: 'Reading DLC configuration...',
timeLeft: '',
error: null,
})
// MODIFIED: Always get a fresh copy from the config file
// Always get a fresh copy from the config file
console.log('Loading DLC configuration from disk...')
try {
const allDlcs = await invoke<DlcInfo[]>('get_all_dlcs_command', {
@@ -197,7 +197,7 @@ export function useDlcManager() {
// Log the fresh DLC config
console.log('Loaded existing DLC configuration:', allDlcs)
// IMPORTANT: Create a completely new array to avoid reference issues
// Create a completely new array to avoid reference issues
const freshDlcs = allDlcs.map((dlc) => ({ ...dlc }))
setDlcDialog((prev) => ({
@@ -256,7 +256,7 @@ export function useDlcManager() {
}
}
// MODIFIED: Handle DLC selection dialog close
// Handle DLC selection dialog close
const handleDlcDialogClose = () => {
// Cancel any in-progress DLC fetching
if (isFetchingDlcs && activeDlcFetchId.current) {

View File

@@ -150,7 +150,7 @@ export function useGameActions() {
try {
if (isEditMode) {
// MODIFIED: Create a deep copy to ensure we don't have reference issues
// Create a deep copy to ensure we don't have reference issues
const dlcsCopy = selectedDlcs.map((dlc) => ({ ...dlc }))
// Show progress dialog for editing
@@ -201,7 +201,7 @@ export function useGameActions() {
selectedDlcs,
})
// Note: The progress dialog will be updated through the installation-progress event listener
// The progress dialog will be updated through the installation-progress event listener
}
} catch (error) {
console.error('Error processing DLC selection:', error)

View File

@@ -0,0 +1,43 @@
import { useEffect } from 'react'
import { check } from '@tauri-apps/plugin-updater'
import { useToasts } from '@/hooks'
/**
* Hook that silently checks for updates and shows a toast notification if an update is available
*/
export function useUpdateChecker() {
const { success, error } = useToasts()
useEffect(() => {
// Check for updates on component mount
const checkForUpdates = async () => {
try {
// Check for updates
const update = await check()
// If update is available, show a toast notification
if (update) {
console.log(`Update available: ${update.version}`)
success(`Update v${update.version} available! Check GitHub for details.`, {
duration: 8000 // Show for 8 seconds
})
}
} catch (err) {
// Log error but don't show to user
console.error('Update check failed:', err)
}
}
// Small delay to avoid interfering with app startup
const timer = setTimeout(() => {
checkForUpdates()
}, 3000)
return () => clearTimeout(timer)
}, [success, error])
// This hook doesn't return anything
return null
}
export default useUpdateChecker