mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-28 22:32:49 -05:00
Compare commits
15 Commits
v1.3.2
...
41da6731a7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41da6731a7 | ||
|
|
5f8f389687 | ||
|
|
1d8422dc65 | ||
|
|
677e3ef12d | ||
|
|
33266f3781 | ||
|
|
9703f21209 | ||
|
|
3459158d3f | ||
|
|
418b470d4a | ||
|
|
fd606cbc2e | ||
|
|
5845cf9bd8 | ||
|
|
6294b99a14 | ||
|
|
595fe53254 | ||
|
|
3801404138 | ||
|
|
919749d0ae | ||
|
|
d4ae5d74e9 |
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -142,3 +142,24 @@ jobs:
|
||||
includeUpdaterJson: true
|
||||
tauriScript: 'npm run tauri'
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
publish-release:
|
||||
name: Publish release
|
||||
needs: [create-release, build-tauri]
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Publish GitHub release (unset draft)
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const release_id = Number("${{ needs.create-release.outputs.release_id }}");
|
||||
|
||||
await github.rest.repos.updateRelease({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id,
|
||||
draft: false
|
||||
});
|
||||
|
||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,3 +1,18 @@
|
||||
## [1.3.4] - 03-01-2026
|
||||
|
||||
### Added
|
||||
- Disclaimer dialog explaining that CreamLinux Installer manages DLC IDs, not actual DLC files
|
||||
- User config stored in `~/.config/creamlinux/config.json`
|
||||
- **"Don't show again" option**: Users can permanently dismiss the disclaimer via checkbox
|
||||
|
||||
## [1.3.3] - 26-12-2025
|
||||
|
||||
### Added
|
||||
- Platform conflict detection
|
||||
- Automatic removal of incompatible unlocker files when switching between Native/Proton
|
||||
- Reminder dialog for steam launch options after creamlinux removal
|
||||
- Conflict dialog to show which game had the conflict
|
||||
|
||||
## [1.3.2] - 23-12-2025
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Tickbase
|
||||
Copyright (c) 2026 Tickbase
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CreamLinux
|
||||
|
||||
CreamLinux is a GUI application for Linux that simplifies the management of DLC in Steam games. It provides a user-friendly interface to install and configure CreamAPI (for native Linux games) and SmokeAPI (for Windows games running through Proton).
|
||||
CreamLinux is a GUI application for Linux that simplifies the management of DLC IDs in Steam games. It provides a user-friendly interface to install and configure CreamAPI (for native Linux games) and SmokeAPI (for Windows games running through Proton).
|
||||
|
||||
## Watch the demo here:
|
||||
|
||||
@@ -61,7 +61,7 @@ While the core functionality is working, please be aware that this is an early r
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Novattz/creamlinux-installer.git
|
||||
cd creamlinux
|
||||
cd creamlinux-installer
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
@@ -124,7 +124,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) f
|
||||
|
||||
## Credits
|
||||
|
||||
- [Creamlinux](https://github.com/anticitizn/creamlinux) - Native DLC support
|
||||
- [Creamlinux](https://github.com/anticitizn/creamlinux) - Native support
|
||||
- [SmokeAPI](https://github.com/acidicoala/SmokeAPI) - Proton support
|
||||
- [Tauri](https://tauri.app/) - Framework for building the desktop application
|
||||
- [React](https://reactjs.org/) - UI library
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "creamlinux",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "creamlinux",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.5.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "creamlinux",
|
||||
"private": true,
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.4",
|
||||
"type": "module",
|
||||
"author": "Tickbase",
|
||||
"repository": "https://github.com/Novattz/creamlinux-installer",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "creamlinux-installer"
|
||||
version = "1.3.2"
|
||||
version = "1.3.4"
|
||||
description = "DLC Manager for Steam games on Linux"
|
||||
authors = ["tickbase"]
|
||||
license = "MIT"
|
||||
|
||||
118
src-tauri/src/config.rs
Normal file
118
src-tauri/src/config.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use log::info;
|
||||
|
||||
// User configuration structure
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
// Whether to show the disclaimer on startup
|
||||
pub show_disclaimer: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_disclaimer: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the config directory path (~/.config/creamlinux)
|
||||
fn get_config_dir() -> Result<PathBuf, String> {
|
||||
let home = std::env::var("HOME")
|
||||
.map_err(|_| "Failed to get HOME directory".to_string())?;
|
||||
|
||||
let config_dir = PathBuf::from(home).join(".config").join("creamlinux");
|
||||
Ok(config_dir)
|
||||
}
|
||||
|
||||
// Get the config file path
|
||||
fn get_config_path() -> Result<PathBuf, String> {
|
||||
let config_dir = get_config_dir()?;
|
||||
Ok(config_dir.join("config.json"))
|
||||
}
|
||||
|
||||
// Ensure the config directory exists
|
||||
fn ensure_config_dir() -> Result<(), String> {
|
||||
let config_dir = get_config_dir()?;
|
||||
|
||||
if !config_dir.exists() {
|
||||
fs::create_dir_all(&config_dir)
|
||||
.map_err(|e| format!("Failed to create config directory: {}", e))?;
|
||||
info!("Created config directory at {:?}", config_dir);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Load configuration from disk
|
||||
pub fn load_config() -> Result<Config, String> {
|
||||
ensure_config_dir()?;
|
||||
|
||||
let config_path = get_config_path()?;
|
||||
|
||||
// If config file doesn't exist, create default config
|
||||
if !config_path.exists() {
|
||||
let default_config = Config::default();
|
||||
save_config(&default_config)?;
|
||||
info!("Created default config file at {:?}", config_path);
|
||||
return Ok(default_config);
|
||||
}
|
||||
|
||||
// Read and parse config file
|
||||
let config_str = fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("Failed to read config file: {}", e))?;
|
||||
|
||||
let config: Config = serde_json::from_str(&config_str)
|
||||
.map_err(|e| format!("Failed to parse config file: {}", e))?;
|
||||
|
||||
info!("Loaded config from {:?}", config_path);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
// Save configuration to disk
|
||||
pub fn save_config(config: &Config) -> Result<(), String> {
|
||||
ensure_config_dir()?;
|
||||
|
||||
let config_path = get_config_path()?;
|
||||
|
||||
let config_str = serde_json::to_string_pretty(config)
|
||||
.map_err(|e| format!("Failed to serialize config: {}", e))?;
|
||||
|
||||
fs::write(&config_path, config_str)
|
||||
.map_err(|e| format!("Failed to write config file: {}", e))?;
|
||||
|
||||
info!("Saved config to {:?}", config_path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Update a specific config value
|
||||
pub fn update_config<F>(updater: F) -> Result<Config, String>
|
||||
where
|
||||
F: FnOnce(&mut Config),
|
||||
{
|
||||
let mut config = load_config()?;
|
||||
updater(&mut config);
|
||||
save_config(&config)?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = Config::default();
|
||||
assert!(config.show_disclaimer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization() {
|
||||
let config = Config::default();
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
let parsed: Config = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(config.show_disclaimer, parsed.show_disclaimer);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,10 @@ mod installer;
|
||||
mod searcher;
|
||||
mod unlockers;
|
||||
mod smokeapi_config;
|
||||
mod config;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
|
||||
use dlc_manager::DlcInfoWithState;
|
||||
use installer::{Game, InstallerAction, InstallerType};
|
||||
use log::{debug, error, info, warn};
|
||||
@@ -45,6 +48,19 @@ pub struct AppState {
|
||||
fetch_cancellation: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
// Load the current configuration
|
||||
#[tauri::command]
|
||||
fn load_config() -> Result<Config, String> {
|
||||
config::load_config()
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
#[tauri::command]
|
||||
fn update_config(config_data: Config) -> Result<Config, String> {
|
||||
config::save_config(&config_data)?;
|
||||
Ok(config_data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
|
||||
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
|
||||
@@ -456,6 +472,146 @@ fn delete_smokeapi_config(game_path: String) -> Result<(), String> {
|
||||
smokeapi_config::delete_config(&game_path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn resolve_platform_conflict(
|
||||
game_id: String,
|
||||
conflict_type: String, // "cream-to-proton" or "smoke-to-native"
|
||||
state: State<'_, AppState>,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<Game, String> {
|
||||
info!(
|
||||
"Resolving platform conflict for game {}: {}",
|
||||
game_id, conflict_type
|
||||
);
|
||||
|
||||
let game = {
|
||||
let games = state.games.lock();
|
||||
games
|
||||
.get(&game_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("Game with ID {} not found", game_id))?
|
||||
};
|
||||
|
||||
let game_title = game.title.clone();
|
||||
|
||||
// Emit progress
|
||||
installer::emit_progress(
|
||||
&app_handle,
|
||||
&format!("Resolving Conflict: {}", game_title),
|
||||
"Removing conflicting files...",
|
||||
50.0,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
// Perform the appropriate removal based on conflict type
|
||||
match conflict_type.as_str() {
|
||||
"cream-to-proton" => {
|
||||
// Remove CreamLinux files (bypassing native check)
|
||||
info!("Removing CreamLinux files from Proton game: {}", game_title);
|
||||
|
||||
CreamLinux::uninstall_from_game(&game.path, &game.id)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to remove CreamLinux files: {}", e))?;
|
||||
|
||||
// Remove version from manifest
|
||||
crate::cache::remove_creamlinux_version(&game.path)?;
|
||||
}
|
||||
"smoke-to-native" => {
|
||||
// Remove SmokeAPI files (bypassing proton check)
|
||||
info!("Removing SmokeAPI files from native game: {}", game_title);
|
||||
|
||||
// For native games, we need to manually remove backup files since
|
||||
// the main DLL might already be gone
|
||||
// Look for and remove *_o.dll backup files
|
||||
use walkdir::WalkDir;
|
||||
let mut removed_files = false;
|
||||
|
||||
for entry in WalkDir::new(&game.path)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
||||
|
||||
// Remove steam_api*_o.dll backup files
|
||||
if filename.starts_with("steam_api") && filename.ends_with("_o.dll") {
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(_) => {
|
||||
info!("Removed SmokeAPI backup file: {}", path.display());
|
||||
removed_files = true;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to remove backup file {}: {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also try the normal uninstall if api_files are present
|
||||
if !game.api_files.is_empty() {
|
||||
let api_files_str = game.api_files.join(",");
|
||||
if let Err(e) = SmokeAPI::uninstall_from_game(&game.path, &api_files_str).await {
|
||||
// Don't fail if this errors - we might have already cleaned up manually above
|
||||
warn!("SmokeAPI uninstall warning: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if !removed_files {
|
||||
warn!("No SmokeAPI files found to remove for: {}", game_title);
|
||||
}
|
||||
|
||||
// Remove version from manifest
|
||||
crate::cache::remove_smokeapi_version(&game.path)?;
|
||||
}
|
||||
_ => return Err(format!("Invalid conflict type: {}", conflict_type)),
|
||||
}
|
||||
|
||||
installer::emit_progress(
|
||||
&app_handle,
|
||||
&format!("Conflict Resolved: {}", game_title),
|
||||
"Conflicting files have been removed successfully!",
|
||||
100.0,
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
// Update game state
|
||||
let updated_game = {
|
||||
let mut games_map = state.games.lock();
|
||||
let game = games_map
|
||||
.get_mut(&game_id)
|
||||
.ok_or_else(|| format!("Game with ID {} not found after conflict resolution", game_id))?;
|
||||
|
||||
match conflict_type.as_str() {
|
||||
"cream-to-proton" => {
|
||||
game.cream_installed = false;
|
||||
}
|
||||
"smoke-to-native" => {
|
||||
game.smoke_installed = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
game.installing = false;
|
||||
game.clone()
|
||||
};
|
||||
|
||||
if let Err(e) = app_handle.emit("game-updated", &updated_game) {
|
||||
warn!("Failed to emit game-updated event: {}", e);
|
||||
}
|
||||
|
||||
info!("Platform conflict resolved successfully for: {}", game_title);
|
||||
Ok(updated_game)
|
||||
}
|
||||
|
||||
fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::file::FileAppender;
|
||||
@@ -516,6 +672,9 @@ fn main() {
|
||||
read_smokeapi_config,
|
||||
write_smokeapi_config,
|
||||
delete_smokeapi_config,
|
||||
resolve_platform_conflict,
|
||||
load_config,
|
||||
update_config,
|
||||
])
|
||||
.setup(|app| {
|
||||
info!("Tauri application setup");
|
||||
|
||||
@@ -256,11 +256,7 @@ fn check_creamlinux_installed(game_path: &Path) -> bool {
|
||||
|
||||
// Check if a game has SmokeAPI installed
|
||||
fn check_smokeapi_installed(game_path: &Path, api_files: &[String]) -> bool {
|
||||
if api_files.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SmokeAPI creates backups with _o.dll suffix
|
||||
// First check the provided api_files for backup files
|
||||
for api_file in api_files {
|
||||
let api_path = game_path.join(api_file);
|
||||
let api_dir = api_path.parent().unwrap_or(game_path);
|
||||
@@ -275,6 +271,28 @@ fn check_smokeapi_installed(game_path: &Path, api_files: &[String]) -> bool {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Also scan for orphaned backup files (in case the main DLL was removed)
|
||||
// This handles the Proton->Native switch case where steam_api*.dll is gone
|
||||
// but steam_api*_o.dll backup remains
|
||||
for entry in WalkDir::new(game_path)
|
||||
.max_depth(5)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
{
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
||||
|
||||
// Look for steam_api*_o.dll backup files (SmokeAPI pattern)
|
||||
if filename.starts_with("steam_api") && filename.ends_with("_o.dll") {
|
||||
debug!("Found orphaned SmokeAPI backup file: {}", path.display());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
@@ -631,12 +649,10 @@ pub async fn find_installed_games(steamapps_paths: &[PathBuf]) -> Vec<GameInfo>
|
||||
// Check for CreamLinux installation
|
||||
let cream_installed = check_creamlinux_installed(&game_path);
|
||||
|
||||
// Check for SmokeAPI installation (only for non-native games with Steam API DLLs)
|
||||
let smoke_installed = if !is_native && !api_files.is_empty() {
|
||||
check_smokeapi_installed(&game_path, &api_files)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
// Check for SmokeAPI installation
|
||||
// For Proton games: check if api_files exist
|
||||
// For Native games: ALSO check for orphaned backup files (proton->native switch)
|
||||
let smoke_installed = check_smokeapi_installed(&game_path, &api_files);
|
||||
|
||||
// Create the game info
|
||||
let game_info = GameInfo {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
},
|
||||
"productName": "Creamlinux",
|
||||
"mainBinaryName": "creamlinux",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.4",
|
||||
"identifier": "com.creamlinux.dev",
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
|
||||
74
src/App.tsx
74
src/App.tsx
@@ -1,13 +1,28 @@
|
||||
import { useState } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useAppContext } from '@/contexts/useAppContext'
|
||||
import { useAppLogic } from '@/hooks'
|
||||
import { useAppLogic, useConflictDetection, useDisclaimer } from '@/hooks'
|
||||
import './styles/main.scss'
|
||||
|
||||
// Layout components
|
||||
import { Header, Sidebar, InitialLoadingScreen, ErrorBoundary, UpdateScreen, AnimatedBackground } from '@/components/layout'
|
||||
import {
|
||||
Header,
|
||||
Sidebar,
|
||||
InitialLoadingScreen,
|
||||
ErrorBoundary,
|
||||
UpdateScreen,
|
||||
AnimatedBackground,
|
||||
} from '@/components/layout'
|
||||
|
||||
// Dialog components
|
||||
import { ProgressDialog, DlcSelectionDialog, SettingsDialog } from '@/components/dialogs'
|
||||
import {
|
||||
ProgressDialog,
|
||||
DlcSelectionDialog,
|
||||
SettingsDialog,
|
||||
ConflictDialog,
|
||||
ReminderDialog,
|
||||
DisclaimerDialog,
|
||||
} from '@/components/dialogs'
|
||||
|
||||
// Game components
|
||||
import { GameList } from '@/components/games'
|
||||
@@ -17,6 +32,9 @@ import { GameList } from '@/components/games'
|
||||
*/
|
||||
function App() {
|
||||
const [updateComplete, setUpdateComplete] = useState(false)
|
||||
|
||||
const { showDisclaimer, handleDisclaimerClose } = useDisclaimer()
|
||||
|
||||
// Get application logic from hook
|
||||
const {
|
||||
filter,
|
||||
@@ -33,6 +51,7 @@ function App() {
|
||||
|
||||
// Get action handlers from context
|
||||
const {
|
||||
games,
|
||||
dlcDialog,
|
||||
handleDlcDialogClose,
|
||||
handleProgressDialogClose,
|
||||
@@ -45,8 +64,30 @@ function App() {
|
||||
handleSettingsOpen,
|
||||
handleSettingsClose,
|
||||
handleSmokeAPISettingsOpen,
|
||||
showToast,
|
||||
} = useAppContext()
|
||||
|
||||
// Conflict detection
|
||||
const { currentConflict, showReminder, resolveConflict, closeReminder } =
|
||||
useConflictDetection(games)
|
||||
|
||||
// Handle conflict resolution
|
||||
const handleConflictResolve = async () => {
|
||||
const resolution = resolveConflict()
|
||||
if (!resolution) return
|
||||
|
||||
// Always remove files - use the special conflict resolution command
|
||||
try {
|
||||
await invoke('resolve_platform_conflict', {
|
||||
gameId: resolution.gameId,
|
||||
conflictType: resolution.conflictType,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error resolving conflict:', error)
|
||||
showToast(`Failed to resolve conflict: ${error}`, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
// Show update screen first
|
||||
if (!updateComplete) {
|
||||
return <UpdateScreen onComplete={() => setUpdateComplete(true)} />
|
||||
@@ -73,7 +114,11 @@ function App() {
|
||||
|
||||
<div className="main-content">
|
||||
{/* Sidebar for filtering */}
|
||||
<Sidebar setFilter={setFilter} currentFilter={filter} onSettingsClick={handleSettingsOpen} />
|
||||
<Sidebar
|
||||
setFilter={setFilter}
|
||||
currentFilter={filter}
|
||||
onSettingsClick={handleSettingsOpen}
|
||||
/>
|
||||
|
||||
{/* Show error or game list */}
|
||||
{error ? (
|
||||
@@ -123,10 +168,23 @@ function App() {
|
||||
/>
|
||||
|
||||
{/* Settings Dialog */}
|
||||
<SettingsDialog
|
||||
visible ={settingsDialog.visible}
|
||||
onClose={handleSettingsClose}
|
||||
/>
|
||||
<SettingsDialog visible={settingsDialog.visible} onClose={handleSettingsClose} />
|
||||
|
||||
{/* Conflict Detection Dialog */}
|
||||
{currentConflict && (
|
||||
<ConflictDialog
|
||||
visible={true}
|
||||
gameTitle={currentConflict.gameTitle}
|
||||
conflictType={currentConflict.type}
|
||||
onConfirm={handleConflictResolve}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Steam Launch Options Reminder */}
|
||||
<ReminderDialog visible={showReminder} onClose={closeReminder} />
|
||||
|
||||
{/* Disclaimer Dialog - Shows AFTER everything is loaded */}
|
||||
<DisclaimerDialog visible={showDisclaimer} onClose={handleDisclaimerClose} />
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
77
src/components/dialogs/ConflictDialog.tsx
Normal file
77
src/components/dialogs/ConflictDialog.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
DialogActions,
|
||||
} from '@/components/dialogs'
|
||||
import { Button } from '@/components/buttons'
|
||||
import { Icon, warning } from '@/components/icons'
|
||||
|
||||
export interface ConflictDialogProps {
|
||||
visible: boolean
|
||||
gameTitle: string
|
||||
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
||||
onConfirm: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict Dialog component
|
||||
* Shows when incompatible unlocker files are detected after platform switch
|
||||
*/
|
||||
const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||
visible,
|
||||
gameTitle,
|
||||
conflictType,
|
||||
onConfirm,
|
||||
}) => {
|
||||
const getConflictMessage = () => {
|
||||
if (conflictType === 'cream-to-proton') {
|
||||
return {
|
||||
title: 'CreamLinux unlocker detected, but game is set to Proton',
|
||||
bodyPrefix: 'It looks like you previously installed CreamLinux while ',
|
||||
bodySuffix: ' was running natively. Steam is now configured to run it with Proton, so CreamLinux files will be removed automatically.',
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
title: 'SmokeAPI unlocker detected, but game is set to Native',
|
||||
bodyPrefix: 'It looks like you previously installed SmokeAPI while ',
|
||||
bodySuffix: ' was running with Proton. Steam is now configured to run it natively, so SmokeAPI files will be removed automatically.',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message = getConflictMessage()
|
||||
|
||||
return (
|
||||
<Dialog visible={visible} size="large" preventBackdropClose={true}>
|
||||
<DialogHeader hideCloseButton={true}>
|
||||
<div className="conflict-dialog-header">
|
||||
<Icon name={warning} variant="solid" size="lg" />
|
||||
<h3>{message.title}</h3>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
<div className="conflict-dialog-body">
|
||||
<p>
|
||||
{message.bodyPrefix}
|
||||
<strong>{gameTitle}</strong>
|
||||
{message.bodySuffix}
|
||||
</p>
|
||||
</div>
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogActions>
|
||||
<Button variant="primary" onClick={onConfirm}>
|
||||
OK
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConflictDialog
|
||||
69
src/components/dialogs/DisclaimerDialog.tsx
Normal file
69
src/components/dialogs/DisclaimerDialog.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import {
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
DialogActions,
|
||||
} from '@/components/dialogs'
|
||||
import { Button, AnimatedCheckbox } from '@/components/buttons'
|
||||
import { useState } from 'react'
|
||||
|
||||
export interface DisclaimerDialogProps {
|
||||
visible: boolean
|
||||
onClose: (dontShowAgain: boolean) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Disclaimer dialog that appears on app startup
|
||||
* Informs users that CreamLinux manages DLC IDs, not actual DLC files
|
||||
*/
|
||||
const DisclaimerDialog = ({ visible, onClose }: DisclaimerDialogProps) => {
|
||||
const [dontShowAgain, setDontShowAgain] = useState(false)
|
||||
|
||||
const handleOkClick = () => {
|
||||
onClose(dontShowAgain)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog visible={visible} onClose={() => onClose(false)} size="medium" preventBackdropClose>
|
||||
<DialogHeader hideCloseButton={true}>
|
||||
<div className="disclaimer-header">
|
||||
<h3>Important Notice</h3>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
<div className="disclaimer-content">
|
||||
<p>
|
||||
<strong>CreamLinux Installer</strong> does not install any DLC content files.
|
||||
</p>
|
||||
<p>
|
||||
This application manages the <strong>DLC IDs</strong> associated with DLCs you want to
|
||||
use. You must obtain the actual DLC files separately.
|
||||
</p>
|
||||
<p>
|
||||
This tool only configures which DLC IDs are recognized by the game unlockers
|
||||
(CreamLinux and SmokeAPI).
|
||||
</p>
|
||||
</div>
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogActions>
|
||||
<div className="disclaimer-footer">
|
||||
<AnimatedCheckbox
|
||||
checked={dontShowAgain}
|
||||
onChange={() => setDontShowAgain(!dontShowAgain)}
|
||||
label="Don't show this disclaimer again"
|
||||
/>
|
||||
<Button variant="primary" onClick={handleOkClick}>
|
||||
OK
|
||||
</Button>
|
||||
</div>
|
||||
</DialogActions>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default DisclaimerDialog
|
||||
56
src/components/dialogs/ReminderDialog.tsx
Normal file
56
src/components/dialogs/ReminderDialog.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogHeader,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
DialogActions,
|
||||
} from '@/components/dialogs'
|
||||
import { Button } from '@/components/buttons'
|
||||
import { Icon, info } from '@/components/icons'
|
||||
|
||||
export interface ReminderDialogProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Reminder Dialog component
|
||||
* Reminds users to remove Steam launch options after removing CreamLinux
|
||||
*/
|
||||
const ReminderDialog: React.FC<ReminderDialogProps> = ({ visible, onClose }) => {
|
||||
return (
|
||||
<Dialog visible={visible} onClose={onClose} size="small">
|
||||
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
||||
<div className="reminder-dialog-header">
|
||||
<Icon name={info} variant="solid" size="lg" />
|
||||
<h3>Reminder</h3>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogBody>
|
||||
<div className="reminder-dialog-body">
|
||||
<p>
|
||||
If you added a Steam launch option for CreamLinux, remember to remove it in Steam:
|
||||
</p>
|
||||
<ol className="reminder-steps">
|
||||
<li>Right-click the game in Steam</li>
|
||||
<li>Select "Properties"</li>
|
||||
<li>Go to "Launch Options"</li>
|
||||
<li>Remove the CreamLinux command</li>
|
||||
</ol>
|
||||
</div>
|
||||
</DialogBody>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogActions>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
Got it
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</DialogFooter>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReminderDialog
|
||||
@@ -8,6 +8,9 @@ export { default as ProgressDialog } from './ProgressDialog'
|
||||
export { default as DlcSelectionDialog } from './DlcSelectionDialog'
|
||||
export { default as SettingsDialog } from './SettingsDialog'
|
||||
export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog'
|
||||
export { default as ConflictDialog } from './ConflictDialog'
|
||||
export { default as ReminderDialog } from './ReminderDialog'
|
||||
export { default as DisclaimerDialog } from './DisclaimerDialog'
|
||||
|
||||
// Export types
|
||||
export type { DialogProps } from './Dialog'
|
||||
@@ -17,3 +20,5 @@ export type { DialogFooterProps } from './DialogFooter'
|
||||
export type { DialogActionsProps } from './DialogActions'
|
||||
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
|
||||
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
|
||||
export type { ConflictDialogProps } from './ConflictDialog'
|
||||
export type { ReminderDialogProps } from './ReminderDialog'
|
||||
@@ -4,7 +4,10 @@ export { useDlcManager } from './useDlcManager'
|
||||
export { useGameActions } from './useGameActions'
|
||||
export { useToasts } from './useToasts'
|
||||
export { useAppLogic } from './useAppLogic'
|
||||
export { useConflictDetection } from './useConflictDetection'
|
||||
export { useDisclaimer } from './useDisclaimer'
|
||||
|
||||
// Export types
|
||||
export type { ToastType, Toast, ToastOptions } from './useToasts'
|
||||
export type { DlcDialogState } from './useDlcManager'
|
||||
export type { Conflict, ConflictResolution } from './useConflictDetection'
|
||||
123
src/hooks/useConflictDetection.ts
Normal file
123
src/hooks/useConflictDetection.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Game } from '@/types'
|
||||
|
||||
export interface Conflict {
|
||||
gameId: string
|
||||
gameTitle: string
|
||||
type: 'cream-to-proton' | 'smoke-to-native'
|
||||
}
|
||||
|
||||
export interface ConflictResolution {
|
||||
gameId: string
|
||||
removeFiles: boolean
|
||||
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for detecting platform conflicts
|
||||
* Identifies when unlocker files exist for the wrong platform
|
||||
*/
|
||||
export function useConflictDetection(games: Game[]) {
|
||||
const [conflicts, setConflicts] = useState<Conflict[]>([])
|
||||
const [currentConflict, setCurrentConflict] = useState<Conflict | null>(null)
|
||||
const [showReminder, setShowReminder] = useState(false)
|
||||
const [isProcessing, setIsProcessing] = useState(false)
|
||||
const [resolvedConflicts, setResolvedConflicts] = useState<Set<string>>(new Set())
|
||||
|
||||
// Detect conflicts whenever games change
|
||||
useEffect(() => {
|
||||
const detectedConflicts: Conflict[] = []
|
||||
|
||||
games.forEach((game) => {
|
||||
// Skip if we've already resolved a conflict for this game
|
||||
if (resolvedConflicts.has(game.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Conflict 1: CreamLinux installed but game is now Proton
|
||||
if (!game.native && game.cream_installed) {
|
||||
detectedConflicts.push({
|
||||
gameId: game.id,
|
||||
gameTitle: game.title,
|
||||
type: 'cream-to-proton',
|
||||
})
|
||||
}
|
||||
|
||||
// Conflict 2: SmokeAPI installed but game is now Native
|
||||
if (game.native && game.smoke_installed) {
|
||||
detectedConflicts.push({
|
||||
gameId: game.id,
|
||||
gameTitle: game.title,
|
||||
type: 'smoke-to-native',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setConflicts(detectedConflicts)
|
||||
|
||||
// Show the first conflict if we have any and not currently processing
|
||||
if (detectedConflicts.length > 0 && !currentConflict && !isProcessing) {
|
||||
setCurrentConflict(detectedConflicts[0])
|
||||
}
|
||||
}, [games, currentConflict, isProcessing, resolvedConflicts])
|
||||
|
||||
// Handle conflict resolution
|
||||
const resolveConflict = useCallback((): ConflictResolution | null => {
|
||||
if (!currentConflict || isProcessing) return null
|
||||
|
||||
setIsProcessing(true)
|
||||
|
||||
const resolution: ConflictResolution = {
|
||||
gameId: currentConflict.gameId,
|
||||
removeFiles: true, // Always remove files
|
||||
conflictType: currentConflict.type,
|
||||
}
|
||||
|
||||
// Mark this game as resolved so we don't re-detect the conflict
|
||||
setResolvedConflicts((prev) => new Set(prev).add(currentConflict.gameId))
|
||||
|
||||
// Remove this conflict from the list
|
||||
const remainingConflicts = conflicts.filter((c) => c.gameId !== currentConflict.gameId)
|
||||
setConflicts(remainingConflicts)
|
||||
|
||||
// Close current conflict dialog immediately
|
||||
setCurrentConflict(null)
|
||||
|
||||
// Determine what to show next based on conflict type
|
||||
if (resolution.conflictType === 'cream-to-proton') {
|
||||
// CreamLinux removal - show reminder after delay
|
||||
setTimeout(() => {
|
||||
setShowReminder(true)
|
||||
setIsProcessing(false)
|
||||
}, 100)
|
||||
} else {
|
||||
// SmokeAPI removal - no reminder, just show next conflict or finish
|
||||
setTimeout(() => {
|
||||
if (remainingConflicts.length > 0) {
|
||||
setCurrentConflict(remainingConflicts[0])
|
||||
}
|
||||
setIsProcessing(false)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return resolution
|
||||
}, [currentConflict, conflicts, isProcessing])
|
||||
|
||||
// Close reminder dialog
|
||||
const closeReminder = useCallback(() => {
|
||||
setShowReminder(false)
|
||||
|
||||
// After closing reminder, check if there are more conflicts
|
||||
if (conflicts.length > 0) {
|
||||
setCurrentConflict(conflicts[0])
|
||||
}
|
||||
}, [conflicts])
|
||||
|
||||
return {
|
||||
currentConflict,
|
||||
showReminder,
|
||||
resolveConflict,
|
||||
closeReminder,
|
||||
hasConflicts: conflicts.length > 0,
|
||||
}
|
||||
}
|
||||
58
src/hooks/useDisclaimer.ts
Normal file
58
src/hooks/useDisclaimer.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Config } from '@/types/Config'
|
||||
|
||||
/**
|
||||
* Hook to manage disclaimer dialog state
|
||||
* Loads config on mount and provides methods to update it
|
||||
*/
|
||||
export function useDisclaimer() {
|
||||
const [showDisclaimer, setShowDisclaimer] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
// Load config on mount
|
||||
useEffect(() => {
|
||||
loadConfig()
|
||||
}, [])
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const config = await invoke<Config>('load_config')
|
||||
setShowDisclaimer(config.show_disclaimer)
|
||||
} catch (error) {
|
||||
console.error('Failed to load config:', error)
|
||||
// Default to showing disclaimer if config load fails
|
||||
setShowDisclaimer(true)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisclaimerClose = async (dontShowAgain: boolean) => {
|
||||
setShowDisclaimer(false)
|
||||
|
||||
if (dontShowAgain) {
|
||||
try {
|
||||
// Load the current config first
|
||||
const currentConfig = await invoke<Config>('load_config')
|
||||
|
||||
// Update the show_disclaimer field
|
||||
const updatedConfig: Config = {
|
||||
...currentConfig,
|
||||
show_disclaimer: false,
|
||||
}
|
||||
|
||||
// Save the updated config
|
||||
await invoke('update_config', { configData: updatedConfig })
|
||||
} catch (error) {
|
||||
console.error('Failed to update config:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
showDisclaimer,
|
||||
isLoading,
|
||||
handleDisclaimerClose,
|
||||
}
|
||||
}
|
||||
88
src/styles/components/dialogs/_conflict_dialog.scss
Normal file
88
src/styles/components/dialogs/_conflict_dialog.scss
Normal file
@@ -0,0 +1,88 @@
|
||||
@use '../../themes/index' as *;
|
||||
@use '../../abstracts/index' as *;
|
||||
|
||||
/*
|
||||
Conflict Dialog Styles
|
||||
Used for platform conflict detection dialogs
|
||||
*/
|
||||
|
||||
.conflict-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--warning);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.conflict-dialog-body {
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Reminder Dialog Styles
|
||||
Used for Steam launch option reminders
|
||||
*/
|
||||
|
||||
.reminder-dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--info);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.reminder-dialog-body {
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.reminder-steps {
|
||||
margin: 1rem 0 0 1.5rem;
|
||||
padding: 0;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/styles/components/dialogs/_disclaimer_dialog.scss
Normal file
38
src/styles/components/dialogs/_disclaimer_dialog.scss
Normal file
@@ -0,0 +1,38 @@
|
||||
@use '../../themes/index' as *;
|
||||
@use '../../abstracts/index' as *;
|
||||
|
||||
/*
|
||||
Disclaimer Dialog Styles
|
||||
Used for the startup disclaimer dialog
|
||||
*/
|
||||
|
||||
.disclaimer-header {
|
||||
h3 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
.disclaimer-content {
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
font-size: 0.95rem;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--bold);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disclaimer-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -3,3 +3,5 @@
|
||||
@forward './progress_dialog';
|
||||
@forward './settings_dialog';
|
||||
@forward './smokeapi_settings_dialog';
|
||||
@forward './conflict_dialog';
|
||||
@forward './disclaimer_dialog';
|
||||
|
||||
8
src/types/Config.ts
Normal file
8
src/types/Config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* User configuration structure
|
||||
* Matches the Rust Config struct
|
||||
*/
|
||||
export interface Config {
|
||||
/** Whether to show the disclaimer on startup */
|
||||
show_disclaimer: boolean
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './Game'
|
||||
export * from './DlcInfo'
|
||||
export * from './Config'
|
||||
Reference in New Issue
Block a user