mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-24 20:32:51 -05:00
feat: replace legacy Python CLI with GUI app
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -10,11 +10,13 @@ lerna-debug.log*
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
docs
|
||||||
*.local
|
*.local
|
||||||
*.lock
|
*.lock
|
||||||
.env
|
.env
|
||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
scripts/prepare-release.js
|
scripts/prepare-release.js
|
||||||
|
scripts/update-server.js
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -4,6 +4,17 @@ CreamLinux is a GUI application for Linux that simplifies the management of DLC
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Beta Status
|
||||||
|
|
||||||
|
⚠️ **IMPORTANT**: CreamLinux is currently in BETA. This means:
|
||||||
|
|
||||||
|
- Some features may be incomplete or subject to change
|
||||||
|
- You might encounter bugs or unexpected behavior
|
||||||
|
- The application is under active development
|
||||||
|
- Your feedback and bug reports are invaluable
|
||||||
|
|
||||||
|
While the core functionality is working, please be aware that this is an early release. Im continuously working to improve stability, add features, and enhance the user experience. Please report any issues you encounter on [GitHub Issues page](https://github.com/Novattz/creamlinux-installer/issues).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Auto-discovery**: Automatically finds Steam games installed on your system
|
- **Auto-discovery**: Automatically finds Steam games installed on your system
|
||||||
@@ -16,7 +27,7 @@ CreamLinux is a GUI application for Linux that simplifies the management of DLC
|
|||||||
|
|
||||||
### AppImage (Recommended)
|
### AppImage (Recommended)
|
||||||
|
|
||||||
1. Download the latest `CreamLinux.AppImage` from the [Releases](https://github.com/novattz/creamlinux/releases) page
|
1. Download the latest `CreamLinux.AppImage` from the [Releases](https://github.com/Novattz/creamlinux-installer/releases) page
|
||||||
2. Make it executable:
|
2. Make it executable:
|
||||||
```bash
|
```bash
|
||||||
chmod +x CreamLinux.AppImage
|
chmod +x CreamLinux.AppImage
|
||||||
@@ -91,28 +102,12 @@ update-desktop-database ~/.local/share/applications
|
|||||||
|
|
||||||
- **Game doesn't load**: Make sure the launch options are correctly set in Steam
|
- **Game doesn't load**: Make sure the launch options are correctly set in Steam
|
||||||
- **DLCs not showing up**: Try refreshing the game list and reinstalling
|
- **DLCs not showing up**: Try refreshing the game list and reinstalling
|
||||||
- **Cannot find Steam**: Ensure Steam is installed and you've launched it at least once
|
- **Cannot find Steam**: Ensure Steam is installed and you've launched it at least once (Flatpak is not supported yet)
|
||||||
|
|
||||||
### Debug Logs
|
### Debug Logs
|
||||||
|
|
||||||
Logs are stored at: `~/.cache/creamlinux/creamlinux.log`
|
Logs are stored at: `~/.cache/creamlinux/creamlinux.log`
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
||||||
|
|
||||||
### Development Setup
|
|
||||||
|
|
||||||
1. Clone this repository
|
|
||||||
2. Install dependencies:
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
3. Start the development server:
|
|
||||||
```bash
|
|
||||||
npm run tauri dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) file for details.
|
||||||
|
|||||||
19
latest.json
19
latest.json
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.1.13",
|
|
||||||
"notes": "Release version 0.1.13",
|
|
||||||
"pub_date": "2025-05-18T23:25:37.931Z",
|
|
||||||
"platforms": {
|
|
||||||
"linux-x86_64": {
|
|
||||||
"url": "https://github.com/novattz/rust-gui-dev/releases/download/v0.1.13/Creamlinux_0.1.13_amd64.AppImage",
|
|
||||||
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUeFJ4alZDbGRMTEIxT2JMYS9QTDdSZUtIdWV1cGtvYlc5NnJrTUE1TndkdVAyS2xBbzJwbUQ1SVp1WWVtNWgwb01pU0ovcG1DOFVaQU03MjJZZzRyZUo5UThiUTJ3NEFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzQ3NjEwNzM3CWZpbGU6Q3JlYW1saW51eF8wLjEuMTNfYW1kNjQuQXBwSW1hZ2UKY2lHVlVObXBqcW4xTEFEL3EwQTdFSi8zVnF5cHJGcHFKQmx5VEZ3TXZmaUVMa0N2R2lmUVREa1gzZzJmL2dXNEluMDQxUFdpUGsya3hkUXkrbHV2QlE9PQo="
|
|
||||||
},
|
|
||||||
"linux-deb": {
|
|
||||||
"url": "https://github.com/novattz/rust-gui-dev/releases/download/v0.1.13/Creamlinux_0.1.13_amd64.deb",
|
|
||||||
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUeFJ4alZDbGRMTEhGaXlpcExROGo5eXdPekE5NzhPNjFMbXFTUHVTRWtxRHFZTmI2T2xqMHNsM1JOZk5VOFNTYnhGTmhZUjBkaFoyVU1KYVRwa1R5dGF4NEp2ZnV4SGdFPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzQ3NjEwNzM3CWZpbGU6Q3JlYW1saW51eF8wLjEuMTNfYW1kNjQuZGViCmFnMzI1Q2gyamlMOTZPV1ZNQ2xXZzI5ekpOV3ZWQWI2OC9hQ1JpWUU4dTd3ZUNhdlRZWFZ6WVZ6RythNGt5YXl6UThLVll1K3doWDhubTFJS3NabEFnPT0K"
|
|
||||||
},
|
|
||||||
"linux-rpm": {
|
|
||||||
"url": "https://github.com/novattz/rust-gui-dev/releases/download/v0.1.13/Creamlinux-0.1.13-1.x86_64.rpm",
|
|
||||||
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUeFJ4alZDbGRMTE9tNHJiQ1V6bFdINEYxTyt6UFN3OEpLR1NOZWJQZU9KekRyM3V0RmN5Wk1rRTVpejFtTXhlN25BR243UEpHVHRObkxZOEt3SXRINjhrQzdoNWZ5QkFVPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzQ3NjEwNzM3CWZpbGU6Q3JlYW1saW51eC0wLjEuMTMtMS54ODZfNjQucnBtClFscVNjYjNhTUk1T2NCZ3huZE00V3dRc2V0NDFXOUNjbElWMllUNHRLSnkwYmpGaFN4WGxWVWRxdVRxTDljdWd2dGs4Q2R5ZjFaeDVCV1hqTTl0YUJnPT0K"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "creamlinux",
|
"name": "creamlinux",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.13",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"author": "Tickbase",
|
||||||
|
"repository": "https://github.com/Novattz/creamlinux-installer",
|
||||||
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.13"
|
version = "1.0.0"
|
||||||
description = "DLC Manager for Steam games on Linux"
|
description = "DLC Manager for Steam games on Linux"
|
||||||
authors = ["tickbase"]
|
authors = ["tickbase"]
|
||||||
license = ""
|
license = "MIT"
|
||||||
repository = ""
|
repository = "https://github.com/Novattz/creamlinux-installer"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.77.2"
|
rust-version = "1.77.2"
|
||||||
|
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ async fn fetch_game_dlcs(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Cache in memory for this session (but not on disk)
|
// Cache in memory for this session
|
||||||
let state = app_handle.state::<AppState>();
|
let state = app_handle.state::<AppState>();
|
||||||
let mut cache = state.dlc_cache.lock();
|
let mut cache = state.dlc_cache.lock();
|
||||||
cache.insert(
|
cache.insert(
|
||||||
@@ -323,7 +323,7 @@ async fn stream_game_dlcs(game_id: String, app_handle: tauri::AppHandle) -> Resu
|
|||||||
game_id
|
game_id
|
||||||
);
|
);
|
||||||
|
|
||||||
// Convert to DLCInfoWithState for in-memory caching only
|
// Convert to DLCInfoWithState for in-memory caching
|
||||||
let dlcs_with_state = dlcs
|
let dlcs_with_state = dlcs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|dlc| DlcInfoWithState {
|
.map(|dlc| DlcInfoWithState {
|
||||||
@@ -333,7 +333,7 @@ async fn stream_game_dlcs(game_id: String, app_handle: tauri::AppHandle) -> Resu
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Update in-memory cache without storing to disk
|
// Update in-memory
|
||||||
let state = app_handle.state::<AppState>();
|
let state = app_handle.state::<AppState>();
|
||||||
let mut dlc_cache = state.dlc_cache.lock();
|
let mut dlc_cache = state.dlc_cache.lock();
|
||||||
dlc_cache.insert(
|
dlc_cache.insert(
|
||||||
|
|||||||
@@ -558,7 +558,6 @@ pub async fn find_installed_games(steamapps_paths: &[PathBuf]) -> Vec<GameInfo>
|
|||||||
|
|
||||||
// Every 10 files, yield to allow progress updates
|
// Every 10 files, yield to allow progress updates
|
||||||
if manifest_idx % 10 == 0 {
|
if manifest_idx % 10 == 0 {
|
||||||
// We would update progress here in a full implementation
|
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,22 +11,21 @@
|
|||||||
"targets": "all",
|
"targets": "all",
|
||||||
"category": "Utility",
|
"category": "Utility",
|
||||||
"createUpdaterArtifacts": true,
|
"createUpdaterArtifacts": true,
|
||||||
"icon": [
|
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"]
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/128x128@2x.png",
|
|
||||||
"icons/icon.png"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"productName": "Creamlinux",
|
"productName": "Creamlinux",
|
||||||
"mainBinaryName": "creamlinux",
|
"mainBinaryName": "creamlinux",
|
||||||
"version": "0.1.13",
|
"version": "1.0.0",
|
||||||
"identifier": "com.creamlinux.dev",
|
"identifier": "com.creamlinux.dev",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"updater": {
|
"updater": {
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJDNEI1NzBBRDUxODQ3RjEKUldUeFJ4alZDbGRMTE5Vc241NG5yL080UklnaW1iUGdUWElPRXloRGtKZ3M2SWkzK0RGSDh3Q2kK",
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJDNEI1NzBBRDUxODQ3RjEKUldUeFJ4alZDbGRMTE5Vc241NG5yL080UklnaW1iUGdUWElPRXloRGtKZ3M2SWkzK0RGSDh3Q2kK",
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
"https://github.com/Novattz/rust-gui-dev/releases/latest/download/latest.json"
|
"https://github.com/Novattz/creamlinux-installer/releases/latest/download/latest.json"
|
||||||
]
|
],
|
||||||
|
"windows": {
|
||||||
|
"installMode": "passive"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useAppContext } from '@/contexts/useAppContext'
|
import { useAppContext } from '@/contexts/useAppContext'
|
||||||
import { UpdateChecker } from '@/components/updater'
|
import { UpdateNotifier } from '@/components/updater'
|
||||||
import { useAppLogic } from '@/hooks'
|
import { useAppLogic } from '@/hooks'
|
||||||
import './styles/main.scss'
|
import './styles/main.scss'
|
||||||
|
|
||||||
@@ -105,7 +105,9 @@ function App() {
|
|||||||
onClose={handleDlcDialogClose}
|
onClose={handleDlcDialogClose}
|
||||||
onConfirm={handleDlcConfirm}
|
onConfirm={handleDlcConfirm}
|
||||||
/>
|
/>
|
||||||
<UpdateChecker />
|
|
||||||
|
{/* Simple update notifier that uses toast - no UI component */}
|
||||||
|
<UpdateNotifier />
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 362 KiB After Width: | Height: | Size: 1.3 MiB |
@@ -35,7 +35,7 @@ const ActionButton: FC<ActionButtonProps> = ({
|
|||||||
return isInstalled ? `Uninstall ${product}` : `Install ${product}`
|
return isInstalled ? `Uninstall ${product}` : `Install ${product}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map to our button variant
|
// Map to button variant
|
||||||
const getButtonVariant = (): ButtonVariant => {
|
const getButtonVariant = (): ButtonVariant => {
|
||||||
// For uninstall actions, use danger variant
|
// For uninstall actions, use danger variant
|
||||||
if (isInstalled) return 'danger'
|
if (isInstalled) return 'danger'
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ export interface DialogHeaderProps {
|
|||||||
children: ReactNode
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
|
hideCloseButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header component for dialogs
|
* Header component for dialogs
|
||||||
* Contains the title and optional close button
|
* Contains the title and optional close button
|
||||||
*/
|
*/
|
||||||
const DialogHeader = ({ children, className = '', onClose }: DialogHeaderProps) => {
|
const DialogHeader = ({ children, className = '', onClose, hideCloseButton = false }: DialogHeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<div className={`dialog-header ${className}`}>
|
<div className={`dialog-header ${className}`}>
|
||||||
{children}
|
{children}
|
||||||
{onClose && (
|
{onClose && !hideCloseButton && (
|
||||||
<button className="dialog-close-button" onClick={onClose} aria-label="Close dialog">
|
<button className="dialog-close-button" onClick={onClose} aria-label="Close dialog">
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const DlcSelectionDialog = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog visible={visible} onClose={onClose} size="large" preventBackdropClose={isLoading}>
|
<Dialog visible={visible} onClose={onClose} size="large" preventBackdropClose={isLoading}>
|
||||||
<DialogHeader onClose={onClose}>
|
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
||||||
<h3>{dialogTitle}</h3>
|
<h3>{dialogTitle}</h3>
|
||||||
<div className="dlc-game-info">
|
<div className="dlc-game-info">
|
||||||
<span className="game-title">{gameTitle}</span>
|
<span className="game-title">{gameTitle}</span>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ const ProgressDialog = ({
|
|||||||
size="medium"
|
size="medium"
|
||||||
preventBackdropClose={!isCloseButtonEnabled}
|
preventBackdropClose={!isCloseButtonEnabled}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const AnimatedBackground = () => {
|
|||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color palette matching our theme
|
// Color palette matching theme
|
||||||
const colors = [
|
const colors = [
|
||||||
'rgba(74, 118, 196, 0.5)', // primary blue
|
'rgba(74, 118, 196, 0.5)', // primary blue
|
||||||
'rgba(155, 125, 255, 0.5)', // purple
|
'rgba(155, 125, 255, 0.5)', // purple
|
||||||
|
|||||||
@@ -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
|
|
||||||
14
src/components/updater/UpdateNotifier.tsx
Normal file
14
src/components/updater/UpdateNotifier.tsx
Normal 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
|
||||||
@@ -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'
|
||||||
@@ -100,7 +100,7 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
const handleDlcConfirm = (selectedDlcs: DlcInfo[]) => {
|
const handleDlcConfirm = (selectedDlcs: DlcInfo[]) => {
|
||||||
const { gameId, isEditMode } = dlcDialog
|
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 }))
|
const dlcsCopy = selectedDlcs.map((dlc) => ({ ...dlc }))
|
||||||
|
|
||||||
// Log detailed info before closing dialog
|
// Log detailed info before closing dialog
|
||||||
|
|||||||
@@ -25,7 +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
|
const [forceReload, setForceReload] = useState(false)
|
||||||
|
|
||||||
// DLC selection dialog state
|
// DLC selection dialog state
|
||||||
const [dlcDialog, setDlcDialog] = useState<DlcDialogState>({
|
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 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
|
||||||
@@ -173,17 +173,17 @@ export function useDlcManager() {
|
|||||||
visible: true,
|
visible: true,
|
||||||
gameId,
|
gameId,
|
||||||
gameTitle: game.title,
|
gameTitle: game.title,
|
||||||
dlcs: [], // Always start with empty DLCs to force a fresh load
|
dlcs: [],
|
||||||
enabledDlcs: [],
|
enabledDlcs: [],
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isEditMode: true, // This is an edit operation
|
isEditMode: true,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
progressMessage: 'Reading DLC configuration...',
|
progressMessage: 'Reading DLC configuration...',
|
||||||
timeLeft: '',
|
timeLeft: '',
|
||||||
error: null,
|
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...')
|
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', {
|
||||||
@@ -197,7 +197,7 @@ export function useDlcManager() {
|
|||||||
// Log the fresh DLC config
|
// 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
|
// Create a completely new array to avoid reference issues
|
||||||
const freshDlcs = allDlcs.map((dlc) => ({ ...dlc }))
|
const freshDlcs = allDlcs.map((dlc) => ({ ...dlc }))
|
||||||
|
|
||||||
setDlcDialog((prev) => ({
|
setDlcDialog((prev) => ({
|
||||||
@@ -256,7 +256,7 @@ export function useDlcManager() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MODIFIED: Handle DLC selection dialog close
|
// 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) {
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export function useGameActions() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (isEditMode) {
|
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 }))
|
const dlcsCopy = selectedDlcs.map((dlc) => ({ ...dlc }))
|
||||||
|
|
||||||
// Show progress dialog for editing
|
// Show progress dialog for editing
|
||||||
@@ -201,7 +201,7 @@ export function useGameActions() {
|
|||||||
selectedDlcs,
|
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) {
|
} catch (error) {
|
||||||
console.error('Error processing DLC selection:', error)
|
console.error('Error processing DLC selection:', error)
|
||||||
|
|||||||
43
src/hooks/useUpdateChecker.ts
Normal file
43
src/hooks/useUpdateChecker.ts
Normal 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
|
||||||
Reference in New Issue
Block a user