mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-24 20:32:51 -05:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58217d61d1 | ||
|
|
0f4db7bbb7 | ||
|
|
22c8f41f93 | ||
|
|
5ff51d1174 | ||
|
|
169b7d5edd | ||
|
|
41da6731a7 | ||
|
|
5f8f389687 | ||
|
|
1d8422dc65 | ||
|
|
677e3ef12d | ||
|
|
33266f3781 | ||
|
|
9703f21209 | ||
|
|
3459158d3f | ||
|
|
418b470d4a | ||
|
|
fd606cbc2e | ||
|
|
5845cf9bd8 | ||
|
|
6294b99a14 | ||
|
|
595fe53254 | ||
|
|
3801404138 | ||
|
|
919749d0ae | ||
|
|
d4ae5d74e9 | ||
|
|
7fd3147f44 | ||
|
|
87dc328434 | ||
|
|
b227dff339 | ||
|
|
04910e84cf | ||
|
|
7960019cd9 | ||
|
|
a00cc92b70 | ||
|
|
85520f8916 | ||
|
|
ac96e7be69 | ||
|
|
3675ff8fae | ||
|
|
ab057b8d10 | ||
|
|
952749cc93 | ||
|
|
4c4e087be7 | ||
|
|
1e52c2071c | ||
|
|
fc8c69a915 | ||
|
|
2d7077a05b | ||
|
|
081d61afc7 | ||
|
|
0bfd36aea9 |
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
@@ -142,3 +142,24 @@ jobs:
|
|||||||
includeUpdaterJson: true
|
includeUpdaterJson: true
|
||||||
tauriScript: 'npm run tauri'
|
tauriScript: 'npm run tauri'
|
||||||
args: ${{ matrix.args }}
|
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
|
||||||
|
});
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,7 +14,6 @@ docs
|
|||||||
*.local
|
*.local
|
||||||
*.lock
|
*.lock
|
||||||
.env
|
.env
|
||||||
CHANGELOG.md
|
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,3 +1,37 @@
|
|||||||
|
## [1.3.5] - 09-01-2026
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Redesigned conflict detection dialog to show all conflicts at once
|
||||||
|
- Integrated Steam launch option reminder directly into the conflict dialog
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Improved UX by allowing users to resolve conflicts in any order or defer to later
|
||||||
|
|
||||||
|
## [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
|
||||||
|
- New dropdown component
|
||||||
|
- Settings dialog for SmokeAPI configuration
|
||||||
|
- Update creamlinux config functionality
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Adjusted styling for CreamLinux settings dialog
|
||||||
|
|
||||||
## [1.3.0] - 22-12-2025
|
## [1.3.0] - 22-12-2025
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Tickbase
|
Copyright (c) 2026 Tickbase
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CreamLinux
|
# 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:
|
## Watch the demo here:
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ While the core functionality is working, please be aware that this is an early r
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Novattz/creamlinux-installer.git
|
git clone https://github.com/Novattz/creamlinux-installer.git
|
||||||
cd creamlinux
|
cd creamlinux-installer
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
@@ -124,7 +124,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) f
|
|||||||
|
|
||||||
## Credits
|
## 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
|
- [SmokeAPI](https://github.com/acidicoala/SmokeAPI) - Proton support
|
||||||
- [Tauri](https://tauri.app/) - Framework for building the desktop application
|
- [Tauri](https://tauri.app/) - Framework for building the desktop application
|
||||||
- [React](https://reactjs.org/) - UI library
|
- [React](https://reactjs.org/) - UI library
|
||||||
|
|||||||
2768
package-lock.json
generated
2768
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "creamlinux",
|
"name": "creamlinux",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.0",
|
"version": "1.3.5",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "Tickbase",
|
"author": "Tickbase",
|
||||||
"repository": "https://github.com/Novattz/creamlinux-installer",
|
"repository": "https://github.com/Novattz/creamlinux-installer",
|
||||||
@@ -40,14 +40,14 @@
|
|||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"glob": "^11.0.2",
|
"glob": "^11.1.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"sass-embedded": "^1.86.3",
|
"sass-embedded": "^1.86.3",
|
||||||
"semantic-release": "^24.2.4",
|
"semantic-release": "^25.0.2",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.26.1",
|
"typescript-eslint": "^8.26.1",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.4.1",
|
||||||
"vite-plugin-svgr": "^4.3.0"
|
"vite-plugin-svgr": "^4.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "creamlinux-installer"
|
name = "creamlinux-installer"
|
||||||
version = "1.3.0"
|
version = "1.3.5"
|
||||||
description = "DLC Manager for Steam games on Linux"
|
description = "DLC Manager for Steam games on Linux"
|
||||||
authors = ["tickbase"]
|
authors = ["tickbase"]
|
||||||
license = "MIT"
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,11 @@ mod dlc_manager;
|
|||||||
mod installer;
|
mod installer;
|
||||||
mod searcher;
|
mod searcher;
|
||||||
mod unlockers;
|
mod unlockers;
|
||||||
|
mod smokeapi_config;
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
|
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
|
||||||
use dlc_manager::DlcInfoWithState;
|
use dlc_manager::DlcInfoWithState;
|
||||||
use installer::{Game, InstallerAction, InstallerType};
|
use installer::{Game, InstallerAction, InstallerType};
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, warn};
|
||||||
@@ -44,6 +48,19 @@ pub struct AppState {
|
|||||||
fetch_cancellation: Arc<AtomicBool>,
|
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]
|
#[tauri::command]
|
||||||
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
|
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
|
||||||
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
|
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
|
||||||
@@ -434,6 +451,167 @@ async fn install_cream_with_dlcs_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn read_smokeapi_config(game_path: String) -> Result<Option<smokeapi_config::SmokeAPIConfig>, String> {
|
||||||
|
info!("Reading SmokeAPI config for: {}", game_path);
|
||||||
|
smokeapi_config::read_config(&game_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn write_smokeapi_config(
|
||||||
|
game_path: String,
|
||||||
|
config: smokeapi_config::SmokeAPIConfig,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
info!("Writing SmokeAPI config for: {}", game_path);
|
||||||
|
smokeapi_config::write_config(&game_path, &config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn delete_smokeapi_config(game_path: String) -> Result<(), String> {
|
||||||
|
info!("Deleting SmokeAPI config for: {}", game_path);
|
||||||
|
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>> {
|
fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use log4rs::append::file::FileAppender;
|
use log4rs::append::file::FileAppender;
|
||||||
@@ -491,6 +669,12 @@ fn main() {
|
|||||||
get_all_dlcs_command,
|
get_all_dlcs_command,
|
||||||
clear_caches,
|
clear_caches,
|
||||||
abort_dlc_fetch,
|
abort_dlc_fetch,
|
||||||
|
read_smokeapi_config,
|
||||||
|
write_smokeapi_config,
|
||||||
|
delete_smokeapi_config,
|
||||||
|
resolve_platform_conflict,
|
||||||
|
load_config,
|
||||||
|
update_config,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
info!("Tauri application setup");
|
info!("Tauri application setup");
|
||||||
|
|||||||
@@ -256,11 +256,7 @@ fn check_creamlinux_installed(game_path: &Path) -> bool {
|
|||||||
|
|
||||||
// Check if a game has SmokeAPI installed
|
// Check if a game has SmokeAPI installed
|
||||||
fn check_smokeapi_installed(game_path: &Path, api_files: &[String]) -> bool {
|
fn check_smokeapi_installed(game_path: &Path, api_files: &[String]) -> bool {
|
||||||
if api_files.is_empty() {
|
// First check the provided api_files for backup files
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// SmokeAPI creates backups with _o.dll suffix
|
|
||||||
for api_file in api_files {
|
for api_file in api_files {
|
||||||
let api_path = game_path.join(api_file);
|
let api_path = game_path.join(api_file);
|
||||||
let api_dir = api_path.parent().unwrap_or(game_path);
|
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;
|
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
|
false
|
||||||
}
|
}
|
||||||
@@ -631,12 +649,10 @@ pub async fn find_installed_games(steamapps_paths: &[PathBuf]) -> Vec<GameInfo>
|
|||||||
// Check for CreamLinux installation
|
// Check for CreamLinux installation
|
||||||
let cream_installed = check_creamlinux_installed(&game_path);
|
let cream_installed = check_creamlinux_installed(&game_path);
|
||||||
|
|
||||||
// Check for SmokeAPI installation (only for non-native games with Steam API DLLs)
|
// Check for SmokeAPI installation
|
||||||
let smoke_installed = if !is_native && !api_files.is_empty() {
|
// For Proton games: check if api_files exist
|
||||||
check_smokeapi_installed(&game_path, &api_files)
|
// For Native games: ALSO check for orphaned backup files (proton->native switch)
|
||||||
} else {
|
let smoke_installed = check_smokeapi_installed(&game_path, &api_files);
|
||||||
false
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the game info
|
// Create the game info
|
||||||
let game_info = GameInfo {
|
let game_info = GameInfo {
|
||||||
|
|||||||
128
src-tauri/src/smokeapi_config.rs
Normal file
128
src-tauri/src/smokeapi_config.rs
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
use log::{info, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct SmokeAPIConfig {
|
||||||
|
#[serde(rename = "$schema")]
|
||||||
|
pub schema: String,
|
||||||
|
#[serde(rename = "$version")]
|
||||||
|
pub version: u32,
|
||||||
|
pub logging: bool,
|
||||||
|
pub log_steam_http: bool,
|
||||||
|
pub default_app_status: String,
|
||||||
|
pub override_app_status: HashMap<String, String>,
|
||||||
|
pub override_dlc_status: HashMap<String, String>,
|
||||||
|
pub auto_inject_inventory: bool,
|
||||||
|
pub extra_inventory_items: Vec<u32>,
|
||||||
|
pub extra_dlcs: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SmokeAPIConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
schema: "https://raw.githubusercontent.com/acidicoala/SmokeAPI/refs/tags/v4.0.0/res/SmokeAPI.schema.json".to_string(),
|
||||||
|
version: 4,
|
||||||
|
logging: false,
|
||||||
|
log_steam_http: false,
|
||||||
|
default_app_status: "unlocked".to_string(),
|
||||||
|
override_app_status: HashMap::new(),
|
||||||
|
override_dlc_status: HashMap::new(),
|
||||||
|
auto_inject_inventory: true,
|
||||||
|
extra_inventory_items: Vec::new(),
|
||||||
|
extra_dlcs: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read SmokeAPI config from a game directory
|
||||||
|
// Returns None if the config doesn't exist
|
||||||
|
pub fn read_config(game_path: &str) -> Result<Option<SmokeAPIConfig>, String> {
|
||||||
|
info!("Reading SmokeAPI config from: {}", game_path);
|
||||||
|
|
||||||
|
// Find the SmokeAPI DLL location in the game directory
|
||||||
|
let config_path = find_smokeapi_config_path(game_path)?;
|
||||||
|
|
||||||
|
if !config_path.exists() {
|
||||||
|
info!("No SmokeAPI config found at: {}", config_path.display());
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&config_path)
|
||||||
|
.map_err(|e| format!("Failed to read SmokeAPI config: {}", e))?;
|
||||||
|
|
||||||
|
let config: SmokeAPIConfig = serde_json::from_str(&content)
|
||||||
|
.map_err(|e| format!("Failed to parse SmokeAPI config: {}", e))?;
|
||||||
|
|
||||||
|
info!("Successfully read SmokeAPI config");
|
||||||
|
Ok(Some(config))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write SmokeAPI config to a game directory
|
||||||
|
pub fn write_config(game_path: &str, config: &SmokeAPIConfig) -> Result<(), String> {
|
||||||
|
info!("Writing SmokeAPI config to: {}", game_path);
|
||||||
|
|
||||||
|
let config_path = find_smokeapi_config_path(game_path)?;
|
||||||
|
|
||||||
|
let content = serde_json::to_string_pretty(config)
|
||||||
|
.map_err(|e| format!("Failed to serialize SmokeAPI config: {}", e))?;
|
||||||
|
|
||||||
|
fs::write(&config_path, content)
|
||||||
|
.map_err(|e| format!("Failed to write SmokeAPI config: {}", e))?;
|
||||||
|
|
||||||
|
info!("Successfully wrote SmokeAPI config to: {}", config_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete SmokeAPI config from a game directory
|
||||||
|
pub fn delete_config(game_path: &str) -> Result<(), String> {
|
||||||
|
info!("Deleting SmokeAPI config from: {}", game_path);
|
||||||
|
|
||||||
|
let config_path = find_smokeapi_config_path(game_path)?;
|
||||||
|
|
||||||
|
if config_path.exists() {
|
||||||
|
fs::remove_file(&config_path)
|
||||||
|
.map_err(|e| format!("Failed to delete SmokeAPI config: {}", e))?;
|
||||||
|
info!("Successfully deleted SmokeAPI config");
|
||||||
|
} else {
|
||||||
|
info!("No SmokeAPI config to delete");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the path where SmokeAPI.config.json should be located
|
||||||
|
// This is in the same directory as the SmokeAPI DLL files
|
||||||
|
fn find_smokeapi_config_path(game_path: &str) -> Result<std::path::PathBuf, String> {
|
||||||
|
let game_path_obj = Path::new(game_path);
|
||||||
|
|
||||||
|
// Search for steam_api*.dll files with _o.dll backups (indicating SmokeAPI installation)
|
||||||
|
let mut smokeapi_dir: Option<std::path::PathBuf> = None;
|
||||||
|
|
||||||
|
// Use walkdir to search recursively
|
||||||
|
for entry in walkdir::WalkDir::new(game_path_obj)
|
||||||
|
.max_depth(5)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
{
|
||||||
|
let path = entry.path();
|
||||||
|
let filename = path.file_name().unwrap_or_default().to_string_lossy();
|
||||||
|
|
||||||
|
// Look for steam_api*_o.dll (backup files created by SmokeAPI)
|
||||||
|
if filename.starts_with("steam_api") && filename.ends_with("_o.dll") {
|
||||||
|
smokeapi_dir = path.parent().map(|p| p.to_path_buf());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we found a SmokeAPI directory, return the config path
|
||||||
|
if let Some(dir) = smokeapi_dir {
|
||||||
|
Ok(dir.join("SmokeAPI.config.json"))
|
||||||
|
} else {
|
||||||
|
// Fallback to game root directory
|
||||||
|
warn!("Could not find SmokeAPI DLL directory, using game root");
|
||||||
|
Ok(game_path_obj.join("SmokeAPI.config.json"))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
},
|
},
|
||||||
"productName": "Creamlinux",
|
"productName": "Creamlinux",
|
||||||
"mainBinaryName": "creamlinux",
|
"mainBinaryName": "creamlinux",
|
||||||
"version": "1.3.0",
|
"version": "1.3.5",
|
||||||
"identifier": "com.creamlinux.dev",
|
"identifier": "com.creamlinux.dev",
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": false,
|
"withGlobalTauri": false,
|
||||||
|
|||||||
79
src/App.tsx
79
src/App.tsx
@@ -1,13 +1,27 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { useAppContext } from '@/contexts/useAppContext'
|
import { useAppContext } from '@/contexts/useAppContext'
|
||||||
import { useAppLogic } from '@/hooks'
|
import { useAppLogic, useConflictDetection, useDisclaimer } from '@/hooks'
|
||||||
import './styles/main.scss'
|
import './styles/main.scss'
|
||||||
|
|
||||||
// Layout components
|
// Layout components
|
||||||
import { Header, Sidebar, InitialLoadingScreen, ErrorBoundary, UpdateScreen, AnimatedBackground } from '@/components/layout'
|
import {
|
||||||
|
Header,
|
||||||
|
Sidebar,
|
||||||
|
InitialLoadingScreen,
|
||||||
|
ErrorBoundary,
|
||||||
|
UpdateScreen,
|
||||||
|
AnimatedBackground,
|
||||||
|
} from '@/components/layout'
|
||||||
|
|
||||||
// Dialog components
|
// Dialog components
|
||||||
import { ProgressDialog, DlcSelectionDialog, SettingsDialog } from '@/components/dialogs'
|
import {
|
||||||
|
ProgressDialog,
|
||||||
|
DlcSelectionDialog,
|
||||||
|
SettingsDialog,
|
||||||
|
ConflictDialog,
|
||||||
|
DisclaimerDialog,
|
||||||
|
} from '@/components/dialogs'
|
||||||
|
|
||||||
// Game components
|
// Game components
|
||||||
import { GameList } from '@/components/games'
|
import { GameList } from '@/components/games'
|
||||||
@@ -17,6 +31,9 @@ import { GameList } from '@/components/games'
|
|||||||
*/
|
*/
|
||||||
function App() {
|
function App() {
|
||||||
const [updateComplete, setUpdateComplete] = useState(false)
|
const [updateComplete, setUpdateComplete] = useState(false)
|
||||||
|
|
||||||
|
const { showDisclaimer, handleDisclaimerClose } = useDisclaimer()
|
||||||
|
|
||||||
// Get application logic from hook
|
// Get application logic from hook
|
||||||
const {
|
const {
|
||||||
filter,
|
filter,
|
||||||
@@ -33,6 +50,7 @@ function App() {
|
|||||||
|
|
||||||
// Get action handlers from context
|
// Get action handlers from context
|
||||||
const {
|
const {
|
||||||
|
games,
|
||||||
dlcDialog,
|
dlcDialog,
|
||||||
handleDlcDialogClose,
|
handleDlcDialogClose,
|
||||||
handleProgressDialogClose,
|
handleProgressDialogClose,
|
||||||
@@ -40,11 +58,40 @@ function App() {
|
|||||||
handleGameAction,
|
handleGameAction,
|
||||||
handleDlcConfirm,
|
handleDlcConfirm,
|
||||||
handleGameEdit,
|
handleGameEdit,
|
||||||
|
handleUpdateDlcs,
|
||||||
settingsDialog,
|
settingsDialog,
|
||||||
handleSettingsOpen,
|
handleSettingsOpen,
|
||||||
handleSettingsClose,
|
handleSettingsClose,
|
||||||
|
handleSmokeAPISettingsOpen,
|
||||||
|
showToast,
|
||||||
} = useAppContext()
|
} = useAppContext()
|
||||||
|
|
||||||
|
// Conflict detection
|
||||||
|
const { conflicts, showDialog, resolveConflict, closeDialog } =
|
||||||
|
useConflictDetection(games)
|
||||||
|
|
||||||
|
// Handle conflict resolution
|
||||||
|
const handleConflictResolve = async (
|
||||||
|
gameId: string,
|
||||||
|
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
// Invoke backend to resolve the conflict
|
||||||
|
await invoke('resolve_platform_conflict', {
|
||||||
|
gameId,
|
||||||
|
conflictType,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove from UI
|
||||||
|
resolveConflict(gameId, conflictType)
|
||||||
|
|
||||||
|
showToast('Conflict resolved successfully', 'success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error resolving conflict:', error)
|
||||||
|
showToast('Failed to resolve conflict', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Show update screen first
|
// Show update screen first
|
||||||
if (!updateComplete) {
|
if (!updateComplete) {
|
||||||
return <UpdateScreen onComplete={() => setUpdateComplete(true)} />
|
return <UpdateScreen onComplete={() => setUpdateComplete(true)} />
|
||||||
@@ -71,7 +118,11 @@ function App() {
|
|||||||
|
|
||||||
<div className="main-content">
|
<div className="main-content">
|
||||||
{/* Sidebar for filtering */}
|
{/* Sidebar for filtering */}
|
||||||
<Sidebar setFilter={setFilter} currentFilter={filter} onSettingsClick={handleSettingsOpen} />
|
<Sidebar
|
||||||
|
setFilter={setFilter}
|
||||||
|
currentFilter={filter}
|
||||||
|
onSettingsClick={handleSettingsOpen}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Show error or game list */}
|
{/* Show error or game list */}
|
||||||
{error ? (
|
{error ? (
|
||||||
@@ -86,6 +137,7 @@ function App() {
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onAction={handleGameAction}
|
onAction={handleGameAction}
|
||||||
onEdit={handleGameEdit}
|
onEdit={handleGameEdit}
|
||||||
|
onSmokeAPISettings={handleSmokeAPISettingsOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -105,20 +157,33 @@ function App() {
|
|||||||
<DlcSelectionDialog
|
<DlcSelectionDialog
|
||||||
visible={dlcDialog.visible}
|
visible={dlcDialog.visible}
|
||||||
gameTitle={dlcDialog.gameTitle}
|
gameTitle={dlcDialog.gameTitle}
|
||||||
|
gameId={dlcDialog.gameId}
|
||||||
dlcs={dlcDialog.dlcs}
|
dlcs={dlcDialog.dlcs}
|
||||||
isLoading={dlcDialog.isLoading}
|
isLoading={dlcDialog.isLoading}
|
||||||
isEditMode={dlcDialog.isEditMode}
|
isEditMode={dlcDialog.isEditMode}
|
||||||
|
isUpdating={dlcDialog.isUpdating}
|
||||||
|
updateAttempted={dlcDialog.updateAttempted}
|
||||||
loadingProgress={dlcDialog.progress}
|
loadingProgress={dlcDialog.progress}
|
||||||
estimatedTimeLeft={dlcDialog.timeLeft}
|
estimatedTimeLeft={dlcDialog.timeLeft}
|
||||||
|
newDlcsCount={dlcDialog.newDlcsCount}
|
||||||
onClose={handleDlcDialogClose}
|
onClose={handleDlcDialogClose}
|
||||||
onConfirm={handleDlcConfirm}
|
onConfirm={handleDlcConfirm}
|
||||||
|
onUpdate={handleUpdateDlcs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Settings Dialog */}
|
{/* Settings Dialog */}
|
||||||
<SettingsDialog
|
<SettingsDialog visible={settingsDialog.visible} onClose={handleSettingsClose} />
|
||||||
visible ={settingsDialog.visible}
|
|
||||||
onClose={handleSettingsClose}
|
{/* Conflict Detection Dialog */}
|
||||||
|
<ConflictDialog
|
||||||
|
visible={showDialog}
|
||||||
|
conflicts={conflicts}
|
||||||
|
onResolve={handleConflictResolve}
|
||||||
|
onClose={closeDialog}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Disclaimer Dialog - Shows AFTER everything is loaded */}
|
||||||
|
<DisclaimerDialog visible={showDisclaimer} onClose={handleDisclaimerClose} />
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
|||||||
97
src/components/common/Dropdown.tsx
Normal file
97
src/components/common/Dropdown.tsx
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { useState, useRef, useEffect } from 'react'
|
||||||
|
import { Icon, arrowUp } from '@/components/icons'
|
||||||
|
|
||||||
|
export interface DropdownOption<T = string> {
|
||||||
|
value: T
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownProps<T = string> {
|
||||||
|
label: string
|
||||||
|
description?: string
|
||||||
|
value: T
|
||||||
|
options: DropdownOption<T>[]
|
||||||
|
onChange: (value: T) => void
|
||||||
|
disabled?: boolean
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dropdown component for selecting from a list of options
|
||||||
|
*/
|
||||||
|
const Dropdown = <T extends string | number | boolean>({
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
disabled = false,
|
||||||
|
className = '',
|
||||||
|
}: DropdownProps<T>) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
// Close dropdown when clicking outside
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [isOpen])
|
||||||
|
|
||||||
|
const selectedOption = options.find((opt) => opt.value === value)
|
||||||
|
|
||||||
|
const handleSelect = (optionValue: T) => {
|
||||||
|
onChange(optionValue)
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`dropdown-container ${className}`}>
|
||||||
|
<div className="dropdown-label-container">
|
||||||
|
<label className="dropdown-label">{label}</label>
|
||||||
|
{description && <p className="dropdown-description">{description}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`dropdown ${disabled ? 'disabled' : ''}`} ref={dropdownRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="dropdown-trigger"
|
||||||
|
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<span className="dropdown-value">{selectedOption?.label || 'Select...'}</span>
|
||||||
|
<Icon
|
||||||
|
name={arrowUp}
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
className={`dropdown-icon ${isOpen ? 'open' : ''}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{isOpen && !disabled && (
|
||||||
|
<div className="dropdown-menu">
|
||||||
|
{options.map((option) => (
|
||||||
|
<button
|
||||||
|
key={String(option.value)}
|
||||||
|
type="button"
|
||||||
|
className={`dropdown-option ${option.value === value ? 'selected' : ''}`}
|
||||||
|
onClick={() => handleSelect(option.value)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Dropdown
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export { default as LoadingIndicator } from './LoadingIndicator'
|
export { default as LoadingIndicator } from './LoadingIndicator'
|
||||||
export { default as ProgressBar } from './ProgressBar'
|
export { default as ProgressBar } from './ProgressBar'
|
||||||
|
export { default as Dropdown } from './Dropdown'
|
||||||
|
|
||||||
export type { LoadingSize, LoadingType } from './LoadingIndicator'
|
export type { LoadingSize, LoadingType } from './LoadingIndicator'
|
||||||
|
export type { DropdownOption } from './Dropdown'
|
||||||
106
src/components/dialogs/ConflictDialog.tsx
Normal file
106
src/components/dialogs/ConflictDialog.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogHeader,
|
||||||
|
DialogBody,
|
||||||
|
DialogFooter,
|
||||||
|
DialogActions,
|
||||||
|
} from '@/components/dialogs'
|
||||||
|
import { Button } from '@/components/buttons'
|
||||||
|
import { Icon, warning, info } from '@/components/icons'
|
||||||
|
|
||||||
|
export interface Conflict {
|
||||||
|
gameId: string
|
||||||
|
gameTitle: string
|
||||||
|
type: 'cream-to-proton' | 'smoke-to-native'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConflictDialogProps {
|
||||||
|
visible: boolean
|
||||||
|
conflicts: Conflict[]
|
||||||
|
onResolve: (gameId: string, conflictType: 'cream-to-proton' | 'smoke-to-native') => void
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conflict Dialog component
|
||||||
|
* Shows all conflicts at once with individual resolve buttons
|
||||||
|
*/
|
||||||
|
const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||||
|
visible,
|
||||||
|
conflicts,
|
||||||
|
onResolve,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
// Check if any CreamLinux conflicts exist
|
||||||
|
const hasCreamConflicts = conflicts.some((c) => c.type === 'cream-to-proton')
|
||||||
|
|
||||||
|
const getConflictDescription = (type: 'cream-to-proton' | 'smoke-to-native') => {
|
||||||
|
if (type === 'cream-to-proton') {
|
||||||
|
return 'Will remove existing unlocker files and restore the game to a clean state.'
|
||||||
|
} else {
|
||||||
|
return 'Will remove existing unlocker files and restore the game to a clean state.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog visible={visible} size="large" preventBackdropClose={true}>
|
||||||
|
<DialogHeader hideCloseButton={true}>
|
||||||
|
<div className="conflict-dialog-header">
|
||||||
|
<Icon name={warning} variant="solid" size="lg" />
|
||||||
|
<h3>Unlocker conflicts detected</h3>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogBody>
|
||||||
|
<div className="conflict-dialog-body">
|
||||||
|
<p className="conflict-intro">
|
||||||
|
Some games have conflicting unlocker states that need attention.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="conflict-list">
|
||||||
|
{conflicts.map((conflict) => (
|
||||||
|
<div key={conflict.gameId} className="conflict-item">
|
||||||
|
<div className="conflict-info">
|
||||||
|
<div className="conflict-icon">
|
||||||
|
<Icon name={warning} variant="solid" size="md" />
|
||||||
|
</div>
|
||||||
|
<div className="conflict-details">
|
||||||
|
<h4>{conflict.gameTitle}</h4>
|
||||||
|
<p>{getConflictDescription(conflict.type)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => onResolve(conflict.gameId, conflict.type)}
|
||||||
|
className="conflict-resolve-btn"
|
||||||
|
>
|
||||||
|
Resolve
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogBody>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
{hasCreamConflicts && (
|
||||||
|
<div className="conflict-reminder">
|
||||||
|
<Icon name={info} variant="solid" size="md" />
|
||||||
|
<span>
|
||||||
|
Remember to remove <code>sh ./cream.sh %command%</code> from Steam launch options
|
||||||
|
after resolving CreamLinux conflicts.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</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
|
||||||
@@ -6,17 +6,23 @@ import DialogFooter from './DialogFooter'
|
|||||||
import DialogActions from './DialogActions'
|
import DialogActions from './DialogActions'
|
||||||
import { Button, AnimatedCheckbox } from '@/components/buttons'
|
import { Button, AnimatedCheckbox } from '@/components/buttons'
|
||||||
import { DlcInfo } from '@/types'
|
import { DlcInfo } from '@/types'
|
||||||
|
import { Icon, check, info } from '@/components/icons'
|
||||||
|
|
||||||
export interface DlcSelectionDialogProps {
|
export interface DlcSelectionDialogProps {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
gameTitle: string
|
gameTitle: string
|
||||||
|
gameId: string
|
||||||
dlcs: DlcInfo[]
|
dlcs: DlcInfo[]
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onConfirm: (selectedDlcs: DlcInfo[]) => void
|
onConfirm: (selectedDlcs: DlcInfo[]) => void
|
||||||
|
onUpdate?: (gameId: string) => void
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
isEditMode?: boolean
|
isEditMode?: boolean
|
||||||
|
isUpdating?: boolean
|
||||||
|
updateAttempted?: boolean
|
||||||
loadingProgress?: number
|
loadingProgress?: number
|
||||||
estimatedTimeLeft?: string
|
estimatedTimeLeft?: string
|
||||||
|
newDlcsCount?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,13 +33,18 @@ export interface DlcSelectionDialogProps {
|
|||||||
const DlcSelectionDialog = ({
|
const DlcSelectionDialog = ({
|
||||||
visible,
|
visible,
|
||||||
gameTitle,
|
gameTitle,
|
||||||
|
gameId,
|
||||||
dlcs,
|
dlcs,
|
||||||
onClose,
|
onClose,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
onUpdate,
|
||||||
isLoading,
|
isLoading,
|
||||||
isEditMode = false,
|
isEditMode = false,
|
||||||
|
isUpdating = false,
|
||||||
|
updateAttempted = false,
|
||||||
loadingProgress = 0,
|
loadingProgress = 0,
|
||||||
estimatedTimeLeft = '',
|
estimatedTimeLeft = '',
|
||||||
|
newDlcsCount = 0,
|
||||||
}: DlcSelectionDialogProps) => {
|
}: DlcSelectionDialogProps) => {
|
||||||
// State for DLC management
|
// State for DLC management
|
||||||
const [selectedDlcs, setSelectedDlcs] = useState<DlcInfo[]>([])
|
const [selectedDlcs, setSelectedDlcs] = useState<DlcInfo[]>([])
|
||||||
@@ -169,13 +180,13 @@ const DlcSelectionDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading && loadingProgress > 0 && (
|
{(isLoading || isUpdating) && loadingProgress > 0 && (
|
||||||
<div className="dlc-loading-progress">
|
<div className="dlc-loading-progress">
|
||||||
<div className="progress-bar-container">
|
<div className="progress-bar-container">
|
||||||
<div className="progress-bar" style={{ width: `${loadingProgress}%` }} />
|
<div className="progress-bar" style={{ width: `${loadingProgress}%` }} />
|
||||||
</div>
|
</div>
|
||||||
<div className="loading-details">
|
<div className="loading-details">
|
||||||
<span>Loading DLCs: {loadingProgress}%</span>
|
<span>{isUpdating ? 'Updating DLC list' : 'Loading DLCs'}: {loadingProgress}%</span>
|
||||||
{estimatedTimeLeft && (
|
{estimatedTimeLeft && (
|
||||||
<span className="time-left">Est. time left: {estimatedTimeLeft}</span>
|
<span className="time-left">Est. time left: {estimatedTimeLeft}</span>
|
||||||
)}
|
)}
|
||||||
@@ -211,15 +222,47 @@ const DlcSelectionDialog = ({
|
|||||||
</DialogBody>
|
</DialogBody>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
|
{/* Show update results */}
|
||||||
|
{!isUpdating && !isLoading && isEditMode && updateAttempted && (
|
||||||
|
<>
|
||||||
|
{newDlcsCount > 0 && (
|
||||||
|
<div className="dlc-update-results dlc-update-success">
|
||||||
|
<span className="update-message">
|
||||||
|
<Icon name={check} size="md" variant="solid" className="dlc-update-icon-success"/> Found {newDlcsCount} new DLC{newDlcsCount > 1 ? 's' : ''}!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{newDlcsCount === 0 && (
|
||||||
|
<div className="dlc-update-results dlc-update-info">
|
||||||
|
<span className="update-message">
|
||||||
|
<Icon name={info} size="md" variant="solid" className="dlc-update-icon-info"/> No new DLCs found. Your list is up to date!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={isLoading && loadingProgress < 10}
|
disabled={(isLoading || isUpdating) && loadingProgress < 10}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={handleConfirm} disabled={isLoading}>
|
|
||||||
|
{/* Update button - only show in edit mode */}
|
||||||
|
{isEditMode && onUpdate && (
|
||||||
|
<Button
|
||||||
|
variant="warning"
|
||||||
|
onClick={() => onUpdate(gameId)}
|
||||||
|
disabled={isLoading || isUpdating}
|
||||||
|
>
|
||||||
|
{isUpdating ? 'Updating...' : 'Update DLC List'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button variant="primary" onClick={handleConfirm} disabled={isLoading || isUpdating}>
|
||||||
{actionButtonText}
|
{actionButtonText}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
@@ -228,4 +271,4 @@ const DlcSelectionDialog = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DlcSelectionDialog
|
export default DlcSelectionDialog
|
||||||
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
|
||||||
@@ -41,7 +41,7 @@ const SettingsDialog: React.FC<SettingsDialogProps> = ({ visible, onClose }) =>
|
|||||||
<Dialog visible={visible} onClose={onClose} size="medium">
|
<Dialog visible={visible} onClose={onClose} size="medium">
|
||||||
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
||||||
<div className="settings-header">
|
<div className="settings-header">
|
||||||
<Icon name={settings} variant="solid" size="md" />
|
{/*<Icon name={settings} variant="solid" size="md" />*/}
|
||||||
<h3>Settings</h3>
|
<h3>Settings</h3>
|
||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
228
src/components/dialogs/SmokeAPISettingsDialog.tsx
Normal file
228
src/components/dialogs/SmokeAPISettingsDialog.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogHeader,
|
||||||
|
DialogBody,
|
||||||
|
DialogFooter,
|
||||||
|
DialogActions,
|
||||||
|
} from '@/components/dialogs'
|
||||||
|
import { Button, AnimatedCheckbox } from '@/components/buttons'
|
||||||
|
import { Dropdown, DropdownOption } from '@/components/common'
|
||||||
|
//import { Icon, settings } from '@/components/icons'
|
||||||
|
|
||||||
|
interface SmokeAPIConfig {
|
||||||
|
$schema: string
|
||||||
|
$version: number
|
||||||
|
logging: boolean
|
||||||
|
log_steam_http: boolean
|
||||||
|
default_app_status: 'unlocked' | 'locked' | 'original'
|
||||||
|
override_app_status: Record<string, string>
|
||||||
|
override_dlc_status: Record<string, string>
|
||||||
|
auto_inject_inventory: boolean
|
||||||
|
extra_inventory_items: number[]
|
||||||
|
extra_dlcs: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SmokeAPISettingsDialogProps {
|
||||||
|
visible: boolean
|
||||||
|
onClose: () => void
|
||||||
|
gamePath: string
|
||||||
|
gameTitle: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: SmokeAPIConfig = {
|
||||||
|
$schema:
|
||||||
|
'https://raw.githubusercontent.com/acidicoala/SmokeAPI/refs/tags/v4.0.0/res/SmokeAPI.schema.json',
|
||||||
|
$version: 4,
|
||||||
|
logging: false,
|
||||||
|
log_steam_http: false,
|
||||||
|
default_app_status: 'unlocked',
|
||||||
|
override_app_status: {},
|
||||||
|
override_dlc_status: {},
|
||||||
|
auto_inject_inventory: true,
|
||||||
|
extra_inventory_items: [],
|
||||||
|
extra_dlcs: {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP_STATUS_OPTIONS: DropdownOption<'unlocked' | 'locked' | 'original'>[] = [
|
||||||
|
{ value: 'unlocked', label: 'Unlocked' },
|
||||||
|
{ value: 'locked', label: 'Locked' },
|
||||||
|
{ value: 'original', label: 'Original' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SmokeAPI Settings Dialog
|
||||||
|
* Allows configuration of SmokeAPI for a specific game
|
||||||
|
*/
|
||||||
|
const SmokeAPISettingsDialog = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
gamePath,
|
||||||
|
gameTitle,
|
||||||
|
}: SmokeAPISettingsDialogProps) => {
|
||||||
|
const [enabled, setEnabled] = useState(false)
|
||||||
|
const [config, setConfig] = useState<SmokeAPIConfig>(DEFAULT_CONFIG)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [hasChanges, setHasChanges] = useState(false)
|
||||||
|
|
||||||
|
// Load existing config when dialog opens
|
||||||
|
const loadConfig = useCallback(async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
const existingConfig = await invoke<SmokeAPIConfig | null>('read_smokeapi_config', {
|
||||||
|
gamePath,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (existingConfig) {
|
||||||
|
setConfig(existingConfig)
|
||||||
|
setEnabled(true)
|
||||||
|
} else {
|
||||||
|
setConfig(DEFAULT_CONFIG)
|
||||||
|
setEnabled(false)
|
||||||
|
}
|
||||||
|
setHasChanges(false)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load SmokeAPI config:', error)
|
||||||
|
setConfig(DEFAULT_CONFIG)
|
||||||
|
setEnabled(false)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}, [gamePath])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible && gamePath) {
|
||||||
|
loadConfig()
|
||||||
|
}
|
||||||
|
}, [visible, gamePath, loadConfig])
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
if (enabled) {
|
||||||
|
// Save the config
|
||||||
|
await invoke('write_smokeapi_config', {
|
||||||
|
gamePath,
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Delete the config
|
||||||
|
await invoke('delete_smokeapi_config', {
|
||||||
|
gamePath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setHasChanges(false)
|
||||||
|
onClose()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save SmokeAPI config:', error)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setHasChanges(false)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateConfig = <K extends keyof SmokeAPIConfig>(key: K, value: SmokeAPIConfig[K]) => {
|
||||||
|
setConfig((prev) => ({ ...prev, [key]: value }))
|
||||||
|
setHasChanges(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog visible={visible} onClose={handleCancel} size="medium">
|
||||||
|
<DialogHeader onClose={handleCancel} hideCloseButton={true}>
|
||||||
|
<div className="settings-header">
|
||||||
|
{/*<Icon name={settings} variant="solid" size="md" />*/}
|
||||||
|
<h3>SmokeAPI Settings</h3>
|
||||||
|
</div>
|
||||||
|
<p className="dialog-subtitle">{gameTitle}</p>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogBody>
|
||||||
|
<div className="smokeapi-settings-content">
|
||||||
|
{/* Enable/Disable Section */}
|
||||||
|
<div className="settings-section">
|
||||||
|
<AnimatedCheckbox
|
||||||
|
checked={enabled}
|
||||||
|
onChange={() => {
|
||||||
|
setEnabled(!enabled)
|
||||||
|
setHasChanges(true)
|
||||||
|
}}
|
||||||
|
label="Enable SmokeAPI Configuration"
|
||||||
|
sublabel="Enable this to customize SmokeAPI settings for this game"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Settings Options */}
|
||||||
|
<div className={`settings-options ${!enabled ? 'disabled' : ''}`}>
|
||||||
|
<div className="settings-section">
|
||||||
|
<h4>General Settings</h4>
|
||||||
|
|
||||||
|
<Dropdown
|
||||||
|
label="Default App Status"
|
||||||
|
description="Specifies the default DLC status"
|
||||||
|
value={config.default_app_status}
|
||||||
|
options={APP_STATUS_OPTIONS}
|
||||||
|
onChange={(value) => updateConfig('default_app_status', value)}
|
||||||
|
disabled={!enabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-section">
|
||||||
|
<h4>Logging</h4>
|
||||||
|
|
||||||
|
<div className="checkbox-option">
|
||||||
|
<AnimatedCheckbox
|
||||||
|
checked={config.logging}
|
||||||
|
onChange={() => updateConfig('logging', !config.logging)}
|
||||||
|
label="Enable Logging"
|
||||||
|
sublabel="Enables logging to SmokeAPI.log.log file"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="checkbox-option">
|
||||||
|
<AnimatedCheckbox
|
||||||
|
checked={config.log_steam_http}
|
||||||
|
onChange={() => updateConfig('log_steam_http', !config.log_steam_http)}
|
||||||
|
label="Log Steam HTTP"
|
||||||
|
sublabel="Toggles logging of SteamHTTP traffic"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="settings-section">
|
||||||
|
<h4>Inventory</h4>
|
||||||
|
|
||||||
|
<div className="checkbox-option">
|
||||||
|
<AnimatedCheckbox
|
||||||
|
checked={config.auto_inject_inventory}
|
||||||
|
onChange={() =>
|
||||||
|
updateConfig('auto_inject_inventory', !config.auto_inject_inventory)
|
||||||
|
}
|
||||||
|
label="Auto Inject Inventory"
|
||||||
|
sublabel="Automatically inject a list of all registered inventory items when the game queries user inventory"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogBody>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="secondary" onClick={handleCancel} disabled={isLoading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={handleSave} disabled={isLoading || !hasChanges}>
|
||||||
|
{isLoading ? 'Saving...' : 'Save'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogFooter>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SmokeAPISettingsDialog
|
||||||
@@ -7,6 +7,9 @@ export { default as DialogActions } from './DialogActions'
|
|||||||
export { default as ProgressDialog } from './ProgressDialog'
|
export { default as ProgressDialog } from './ProgressDialog'
|
||||||
export { default as DlcSelectionDialog } from './DlcSelectionDialog'
|
export { default as DlcSelectionDialog } from './DlcSelectionDialog'
|
||||||
export { default as SettingsDialog } from './SettingsDialog'
|
export { default as SettingsDialog } from './SettingsDialog'
|
||||||
|
export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog'
|
||||||
|
export { default as ConflictDialog } from './ConflictDialog'
|
||||||
|
export { default as DisclaimerDialog } from './DisclaimerDialog'
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type { DialogProps } from './Dialog'
|
export type { DialogProps } from './Dialog'
|
||||||
@@ -16,3 +19,4 @@ export type { DialogFooterProps } from './DialogFooter'
|
|||||||
export type { DialogActionsProps } from './DialogActions'
|
export type { DialogActionsProps } from './DialogActions'
|
||||||
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
|
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
|
||||||
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
|
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
|
||||||
|
export type { ConflictDialogProps, Conflict } from './ConflictDialog'
|
||||||
@@ -8,13 +8,14 @@ interface GameItemProps {
|
|||||||
game: Game
|
game: Game
|
||||||
onAction: (gameId: string, action: ActionType) => Promise<void>
|
onAction: (gameId: string, action: ActionType) => Promise<void>
|
||||||
onEdit?: (gameId: string) => void
|
onEdit?: (gameId: string) => void
|
||||||
|
onSmokeAPISettings?: (gameId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Individual game card component
|
* Individual game card component
|
||||||
* Displays game information and action buttons
|
* Displays game information and action buttons
|
||||||
*/
|
*/
|
||||||
const GameItem = ({ game, onAction, onEdit }: GameItemProps) => {
|
const GameItem = ({ game, onAction, onEdit, onSmokeAPISettings }: GameItemProps) => {
|
||||||
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
const [imageUrl, setImageUrl] = useState<string | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [hasError, setHasError] = useState(false)
|
const [hasError, setHasError] = useState(false)
|
||||||
@@ -77,6 +78,13 @@ const GameItem = ({ game, onAction, onEdit }: GameItemProps) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SmokeAPI settings handler
|
||||||
|
const handleSmokeAPISettings = () => {
|
||||||
|
if (onSmokeAPISettings && game.smoke_installed) {
|
||||||
|
onSmokeAPISettings(game.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Determine background image
|
// Determine background image
|
||||||
const backgroundImage =
|
const backgroundImage =
|
||||||
!isLoading && imageUrl
|
!isLoading && imageUrl
|
||||||
@@ -156,6 +164,20 @@ const GameItem = ({ game, onAction, onEdit }: GameItemProps) => {
|
|||||||
iconOnly
|
iconOnly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Edit button - only enabled if SmokeAPI is installed */}
|
||||||
|
{game.smoke_installed && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
onClick={handleSmokeAPISettings}
|
||||||
|
disabled={!game.smoke_installed || !!game.installing}
|
||||||
|
title="Configure SmokeAPI"
|
||||||
|
className="edit-button settings-icon-button"
|
||||||
|
leftIcon={<Icon name="Settings" variant="solid" size="md" />}
|
||||||
|
iconOnly
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ interface GameListProps {
|
|||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
onAction: (gameId: string, action: ActionType) => Promise<void>
|
onAction: (gameId: string, action: ActionType) => Promise<void>
|
||||||
onEdit?: (gameId: string) => void
|
onEdit?: (gameId: string) => void
|
||||||
|
onSmokeAPISettings?: (gameId: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main game list component
|
* Main game list component
|
||||||
* Displays games in a grid with search and filtering applied
|
* Displays games in a grid with search and filtering applied
|
||||||
*/
|
*/
|
||||||
const GameList = ({ games, isLoading, onAction, onEdit }: GameListProps) => {
|
const GameList = ({ games, isLoading, onAction, onEdit, onSmokeAPISettings }: GameListProps) => {
|
||||||
const [imagesPreloaded, setImagesPreloaded] = useState(false)
|
const [imagesPreloaded, setImagesPreloaded] = useState(false)
|
||||||
|
|
||||||
// Sort games alphabetically by title
|
// Sort games alphabetically by title
|
||||||
@@ -56,7 +57,7 @@ const GameList = ({ games, isLoading, onAction, onEdit }: GameListProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="game-grid">
|
<div className="game-grid">
|
||||||
{sortedGames.map((game) => (
|
{sortedGames.map((game) => (
|
||||||
<GameItem key={game.id} game={game} onAction={onAction} onEdit={onEdit} />
|
<GameItem key={game.id} game={game} onAction={onAction} onEdit={onEdit} onSmokeAPISettings={onSmokeAPISettings} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createContext } from 'react'
|
import { createContext } from 'react'
|
||||||
import { Game, DlcInfo } from '@/types'
|
import { Game, DlcInfo } from '@/types'
|
||||||
import { ActionType } from '@/components/buttons/ActionButton'
|
import { ActionType } from '@/components/buttons/ActionButton'
|
||||||
|
import { DlcDialogState } from '@/hooks/useDlcManager'
|
||||||
|
|
||||||
// Types for context sub-components
|
// Types for context sub-components
|
||||||
export interface InstallationInstructions {
|
export interface InstallationInstructions {
|
||||||
@@ -10,17 +11,6 @@ export interface InstallationInstructions {
|
|||||||
dlc_count?: number
|
dlc_count?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DlcDialogState {
|
|
||||||
visible: boolean
|
|
||||||
gameId: string
|
|
||||||
gameTitle: string
|
|
||||||
dlcs: DlcInfo[]
|
|
||||||
isLoading: boolean
|
|
||||||
isEditMode: boolean
|
|
||||||
progress: number
|
|
||||||
timeLeft?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProgressDialogState {
|
export interface ProgressDialogState {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
title: string
|
title: string
|
||||||
@@ -30,6 +20,12 @@ export interface ProgressDialogState {
|
|||||||
instructions?: InstallationInstructions
|
instructions?: InstallationInstructions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SmokeAPISettingsDialogState {
|
||||||
|
visible: boolean
|
||||||
|
gamePath: string
|
||||||
|
gameTitle: string
|
||||||
|
}
|
||||||
|
|
||||||
// Define the context type
|
// Define the context type
|
||||||
export interface AppContextType {
|
export interface AppContextType {
|
||||||
// Game state
|
// Game state
|
||||||
@@ -43,6 +39,7 @@ export interface AppContextType {
|
|||||||
dlcDialog: DlcDialogState
|
dlcDialog: DlcDialogState
|
||||||
handleGameEdit: (gameId: string) => void
|
handleGameEdit: (gameId: string) => void
|
||||||
handleDlcDialogClose: () => void
|
handleDlcDialogClose: () => void
|
||||||
|
handleUpdateDlcs: (gameId: string) => Promise<void>
|
||||||
|
|
||||||
// Game actions
|
// Game actions
|
||||||
progressDialog: ProgressDialogState
|
progressDialog: ProgressDialogState
|
||||||
@@ -54,6 +51,11 @@ export interface AppContextType {
|
|||||||
handleSettingsOpen: () => void
|
handleSettingsOpen: () => void
|
||||||
handleSettingsClose: () => void
|
handleSettingsClose: () => void
|
||||||
|
|
||||||
|
// SmokeAPI settings
|
||||||
|
smokeAPISettingsDialog: SmokeAPISettingsDialogState
|
||||||
|
handleSmokeAPISettingsOpen: (gameId: string) => void
|
||||||
|
handleSmokeAPISettingsClose: () => void
|
||||||
|
|
||||||
// Toast notifications
|
// Toast notifications
|
||||||
showToast: (
|
showToast: (
|
||||||
message: string,
|
message: string,
|
||||||
@@ -63,4 +65,4 @@ export interface AppContextType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the context with a default value
|
// Create the context with a default value
|
||||||
export const AppContext = createContext<AppContextType | undefined>(undefined)
|
export const AppContext = createContext<AppContextType | undefined>(undefined)
|
||||||
@@ -4,6 +4,7 @@ import { useGames, useDlcManager, useGameActions, useToasts } from '@/hooks'
|
|||||||
import { DlcInfo } from '@/types'
|
import { DlcInfo } from '@/types'
|
||||||
import { ActionType } from '@/components/buttons/ActionButton'
|
import { ActionType } from '@/components/buttons/ActionButton'
|
||||||
import { ToastContainer } from '@/components/notifications'
|
import { ToastContainer } from '@/components/notifications'
|
||||||
|
import { SmokeAPISettingsDialog } from '@/components/dialogs'
|
||||||
|
|
||||||
// Context provider component
|
// Context provider component
|
||||||
interface AppProviderProps {
|
interface AppProviderProps {
|
||||||
@@ -24,6 +25,7 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
handleDlcDialogClose: closeDlcDialog,
|
handleDlcDialogClose: closeDlcDialog,
|
||||||
streamGameDlcs,
|
streamGameDlcs,
|
||||||
handleGameEdit,
|
handleGameEdit,
|
||||||
|
handleUpdateDlcs,
|
||||||
} = useDlcManager()
|
} = useDlcManager()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -38,6 +40,17 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
// Settings dialog state
|
// Settings dialog state
|
||||||
const [settingsDialog, setSettingsDialog] = useState({ visible: false })
|
const [settingsDialog, setSettingsDialog] = useState({ visible: false })
|
||||||
|
|
||||||
|
// SmokeAPI settings dialog state
|
||||||
|
const [smokeAPISettingsDialog, setSmokeAPISettingsDialog] = useState<{
|
||||||
|
visible: boolean
|
||||||
|
gamePath: string
|
||||||
|
gameTitle: string
|
||||||
|
}>({
|
||||||
|
visible: false,
|
||||||
|
gamePath: '',
|
||||||
|
gameTitle: '',
|
||||||
|
})
|
||||||
|
|
||||||
// Settings handlers
|
// Settings handlers
|
||||||
const handleSettingsOpen = () => {
|
const handleSettingsOpen = () => {
|
||||||
setSettingsDialog({ visible: true })
|
setSettingsDialog({ visible: true })
|
||||||
@@ -47,6 +60,25 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
setSettingsDialog({ visible: false })
|
setSettingsDialog({ visible: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SmokeAPI settings handlers
|
||||||
|
const handleSmokeAPISettingsOpen = (gameId: string) => {
|
||||||
|
const game = games.find((g) => g.id === gameId)
|
||||||
|
if (!game) {
|
||||||
|
showError('Game not found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSmokeAPISettingsDialog({
|
||||||
|
visible: true,
|
||||||
|
gamePath: game.path,
|
||||||
|
gameTitle: game.title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSmokeAPISettingsClose = () => {
|
||||||
|
setSmokeAPISettingsDialog((prev) => ({ ...prev, visible: false }))
|
||||||
|
}
|
||||||
|
|
||||||
// Game action handler with proper error reporting
|
// Game action handler with proper error reporting
|
||||||
const handleGameAction = async (gameId: string, action: ActionType) => {
|
const handleGameAction = async (gameId: string, action: ActionType) => {
|
||||||
const game = games.find((g) => g.id === gameId)
|
const game = games.find((g) => g.id === gameId)
|
||||||
@@ -189,6 +221,7 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
handleGameEdit(gameId, games)
|
handleGameEdit(gameId, games)
|
||||||
},
|
},
|
||||||
handleDlcDialogClose: closeDlcDialog,
|
handleDlcDialogClose: closeDlcDialog,
|
||||||
|
handleUpdateDlcs: (gameId: string) => handleUpdateDlcs(gameId),
|
||||||
|
|
||||||
// Game actions
|
// Game actions
|
||||||
progressDialog,
|
progressDialog,
|
||||||
@@ -201,6 +234,11 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
handleSettingsOpen,
|
handleSettingsOpen,
|
||||||
handleSettingsClose,
|
handleSettingsClose,
|
||||||
|
|
||||||
|
// SmokeAPI Settings
|
||||||
|
smokeAPISettingsDialog,
|
||||||
|
handleSmokeAPISettingsOpen,
|
||||||
|
handleSmokeAPISettingsClose,
|
||||||
|
|
||||||
// Toast notifications
|
// Toast notifications
|
||||||
showToast,
|
showToast,
|
||||||
}
|
}
|
||||||
@@ -209,6 +247,14 @@ export const AppProvider = ({ children }: AppProviderProps) => {
|
|||||||
<AppContext.Provider value={contextValue}>
|
<AppContext.Provider value={contextValue}>
|
||||||
{children}
|
{children}
|
||||||
<ToastContainer toasts={toasts} onDismiss={removeToast} />
|
<ToastContainer toasts={toasts} onDismiss={removeToast} />
|
||||||
|
|
||||||
|
{/* SmokeAPI Settings Dialog */}
|
||||||
|
<SmokeAPISettingsDialog
|
||||||
|
visible={smokeAPISettingsDialog.visible}
|
||||||
|
onClose={handleSmokeAPISettingsClose}
|
||||||
|
gamePath={smokeAPISettingsDialog.gamePath}
|
||||||
|
gameTitle={smokeAPISettingsDialog.gameTitle}
|
||||||
|
/>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,10 @@ export { useDlcManager } from './useDlcManager'
|
|||||||
export { useGameActions } from './useGameActions'
|
export { useGameActions } from './useGameActions'
|
||||||
export { useToasts } from './useToasts'
|
export { useToasts } from './useToasts'
|
||||||
export { useAppLogic } from './useAppLogic'
|
export { useAppLogic } from './useAppLogic'
|
||||||
|
export { useConflictDetection } from './useConflictDetection'
|
||||||
|
export { useDisclaimer } from './useDisclaimer'
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type { ToastType, Toast, ToastOptions } from './useToasts'
|
export type { ToastType, Toast, ToastOptions } from './useToasts'
|
||||||
export type { DlcDialogState } from './useDlcManager'
|
export type { DlcDialogState } from './useDlcManager'
|
||||||
|
export type { Conflict, ConflictResolution } from './useConflictDetection'
|
||||||
102
src/hooks/useConflictDetection.ts
Normal file
102
src/hooks/useConflictDetection.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
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
|
||||||
|
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 [showDialog, setShowDialog] = useState(false)
|
||||||
|
const [resolvedConflicts, setResolvedConflicts] = useState<Set<string>>(new Set())
|
||||||
|
const [hasShownThisSession, setHasShownThisSession] = useState(false)
|
||||||
|
|
||||||
|
// 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 dialog only if:
|
||||||
|
// 1. We have conflicts
|
||||||
|
// 2. Dialog isn't already visible
|
||||||
|
// 3. We haven't shown it this session
|
||||||
|
if (detectedConflicts.length > 0 && !showDialog && !hasShownThisSession) {
|
||||||
|
setShowDialog(true)
|
||||||
|
setHasShownThisSession(true)
|
||||||
|
}
|
||||||
|
}, [games, resolvedConflicts, showDialog, hasShownThisSession])
|
||||||
|
|
||||||
|
// Handle resolving a single conflict
|
||||||
|
const resolveConflict = useCallback(
|
||||||
|
(gameId: string, conflictType: 'cream-to-proton' | 'smoke-to-native'): ConflictResolution => {
|
||||||
|
// Mark this game as resolved
|
||||||
|
setResolvedConflicts((prev) => new Set(prev).add(gameId))
|
||||||
|
|
||||||
|
// Remove from conflicts list
|
||||||
|
setConflicts((prev) => prev.filter((c) => c.gameId !== gameId))
|
||||||
|
|
||||||
|
return {
|
||||||
|
gameId,
|
||||||
|
conflictType,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-close dialog when all conflicts are resolved
|
||||||
|
useEffect(() => {
|
||||||
|
if (conflicts.length === 0 && showDialog) {
|
||||||
|
setShowDialog(false)
|
||||||
|
}
|
||||||
|
}, [conflicts.length, showDialog])
|
||||||
|
|
||||||
|
// Handle dialog close
|
||||||
|
const closeDialog = useCallback(() => {
|
||||||
|
setShowDialog(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
conflicts,
|
||||||
|
showDialog,
|
||||||
|
resolveConflict,
|
||||||
|
closeDialog,
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,13 @@ export interface DlcDialogState {
|
|||||||
enabledDlcs: string[]
|
enabledDlcs: string[]
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
isEditMode: boolean
|
isEditMode: boolean
|
||||||
|
isUpdating: boolean
|
||||||
|
updateAttempted: boolean
|
||||||
progress: number
|
progress: number
|
||||||
progressMessage: string
|
progressMessage: string
|
||||||
timeLeft: string
|
timeLeft: string
|
||||||
error: string | null
|
error: string | null
|
||||||
|
newDlcsCount: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,10 +39,13 @@ export function useDlcManager() {
|
|||||||
enabledDlcs: [],
|
enabledDlcs: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isEditMode: false,
|
isEditMode: false,
|
||||||
|
isUpdating: false,
|
||||||
|
updateAttempted: false,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
progressMessage: '',
|
progressMessage: '',
|
||||||
timeLeft: '',
|
timeLeft: '',
|
||||||
error: null,
|
error: null,
|
||||||
|
newDlcsCount: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set up event listeners for DLC streaming
|
// Set up event listeners for DLC streaming
|
||||||
@@ -80,6 +86,7 @@ export function useDlcManager() {
|
|||||||
setDlcDialog((prev) => ({
|
setDlcDialog((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
isUpdating: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Reset fetch state
|
// Reset fetch state
|
||||||
@@ -177,10 +184,13 @@ export function useDlcManager() {
|
|||||||
enabledDlcs: [],
|
enabledDlcs: [],
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isEditMode: true,
|
isEditMode: true,
|
||||||
|
isUpdating: false,
|
||||||
|
updateAttempted: false,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
progressMessage: 'Reading DLC configuration...',
|
progressMessage: 'Reading DLC configuration...',
|
||||||
timeLeft: '',
|
timeLeft: '',
|
||||||
error: null,
|
error: null,
|
||||||
|
newDlcsCount: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Always get a fresh copy from the config file
|
// Always get a fresh copy from the config file
|
||||||
@@ -302,6 +312,58 @@ export function useDlcManager() {
|
|||||||
}
|
}
|
||||||
}, [dlcDialog.dlcs, dlcDialog.enabledDlcs])
|
}, [dlcDialog.dlcs, dlcDialog.enabledDlcs])
|
||||||
|
|
||||||
|
// Function to update DLC list (refetch from Steam API)
|
||||||
|
const handleUpdateDlcs = async (gameId: string) => {
|
||||||
|
try {
|
||||||
|
// Store current app IDs to identify new DLCs later
|
||||||
|
const currentAppIds = new Set(dlcDialog.dlcs.map((dlc) => dlc.appid))
|
||||||
|
|
||||||
|
// Set updating state and clear DLCs
|
||||||
|
setDlcDialog((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isUpdating: true,
|
||||||
|
isLoading: true,
|
||||||
|
updateAttempted: true,
|
||||||
|
progress: 0,
|
||||||
|
progressMessage: 'Checking for new DLCs...',
|
||||||
|
newDlcsCount: 0,
|
||||||
|
dlcs: [], // Clear current DLCs to start fresh
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Mark that we're fetching DLCs for this game
|
||||||
|
setIsFetchingDlcs(true)
|
||||||
|
activeDlcFetchId.current = gameId
|
||||||
|
|
||||||
|
// Start streaming DLCs
|
||||||
|
await streamGameDlcs(gameId)
|
||||||
|
|
||||||
|
// After streaming completes, calculate new DLCs
|
||||||
|
// Wait a bit longer to ensure all DLCs have been added
|
||||||
|
setTimeout(() => {
|
||||||
|
setDlcDialog((prev) => {
|
||||||
|
// Count how many DLCs are new (not in the original list)
|
||||||
|
const actualNewCount = prev.dlcs.filter(dlc => !currentAppIds.has(dlc.appid)).length
|
||||||
|
|
||||||
|
console.log(`Update complete: Found ${actualNewCount} new DLCs out of ${prev.dlcs.length} total`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
newDlcsCount: actualNewCount,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, 1500) // Increased timeout to ensure all DLCs are processed
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating DLCs:', error)
|
||||||
|
setDlcDialog((prev) => ({
|
||||||
|
...prev,
|
||||||
|
error: `Failed to update DLCs: ${error}`,
|
||||||
|
isLoading: false,
|
||||||
|
isUpdating: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dlcDialog,
|
dlcDialog,
|
||||||
setDlcDialog,
|
setDlcDialog,
|
||||||
@@ -309,6 +371,7 @@ export function useDlcManager() {
|
|||||||
streamGameDlcs,
|
streamGameDlcs,
|
||||||
handleGameEdit,
|
handleGameEdit,
|
||||||
handleDlcDialogClose,
|
handleDlcDialogClose,
|
||||||
|
handleUpdateDlcs,
|
||||||
forceReload,
|
forceReload,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
127
src/styles/components/common/_dropdown.scss
Normal file
127
src/styles/components/common/_dropdown.scss
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
@use '../../themes/index' as *;
|
||||||
|
@use '../../abstracts/index' as *;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Dropdown component styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
.dropdown-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-label-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-label {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-description {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: var(--border-dark);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-normal) var(--easing-ease-out);
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
border-color: var(--border);
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(var(--primary-color), 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-value {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-icon {
|
||||||
|
transition: transform var(--duration-normal) var(--easing-ease-out);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transform: rotate(180deg);
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.5rem);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: var(--elevated-bg);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
z-index: var(--z-modal);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
@include custom-scrollbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-option {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all var(--duration-normal) var(--easing-ease-out);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: rgba(var(--primary-color), 0.2);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--border-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
@forward './loading';
|
@forward './loading';
|
||||||
@forward './progress_bar';
|
@forward './progress_bar';
|
||||||
|
@forward './dropdown';
|
||||||
|
|||||||
143
src/styles/components/dialogs/_conflict_dialog.scss
Normal file
143
src/styles/components/dialogs/_conflict_dialog.scss
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
@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 {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.conflict-intro {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0; // Enable text truncation
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: rgba(255, 193, 7, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-details {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 0.25rem 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: var(--semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-resolve-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conflict-reminder {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: rgba(33, 150, 243, 0.1);
|
||||||
|
border: 1px solid rgba(33, 150, 243, 0.2);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: var(--info);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
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%;
|
||||||
|
}
|
||||||
@@ -154,6 +154,40 @@
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update results message
|
||||||
|
.dlc-update-results {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background-color: var(--elevated-bg);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
|
||||||
|
.update-message {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dlc-update-success {
|
||||||
|
.update-message {
|
||||||
|
.dlc-update-icon-success {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.dlc-update-info {
|
||||||
|
.update-message {
|
||||||
|
.dlc-update-icon-info {
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Game information in DLC dialog
|
// Game information in DLC dialog
|
||||||
.dlc-game-info {
|
.dlc-game-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -2,3 +2,6 @@
|
|||||||
@forward './dlc_dialog';
|
@forward './dlc_dialog';
|
||||||
@forward './progress_dialog';
|
@forward './progress_dialog';
|
||||||
@forward './settings_dialog';
|
@forward './settings_dialog';
|
||||||
|
@forward './smokeapi_settings_dialog';
|
||||||
|
@forward './conflict_dialog';
|
||||||
|
@forward './disclaimer_dialog';
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
.settings-section {
|
.settings-section {
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1.1rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
|
|||||||
66
src/styles/components/dialogs/_smokeapi_settings_dialog.scss
Normal file
66
src/styles/components/dialogs/_smokeapi_settings_dialog.scss
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
@use '../../themes/index' as *;
|
||||||
|
@use '../../abstracts/index' as *;
|
||||||
|
|
||||||
|
/*
|
||||||
|
SmokeAPI Settings Dialog styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
.dialog-subtitle {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smokeapi-settings-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
transition: opacity var(--duration-normal) var(--easing-ease-out);
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-soft);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-option {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--border-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated-checkbox {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.checkbox-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-sublabel {
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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 './Game'
|
||||||
export * from './DlcInfo'
|
export * from './DlcInfo'
|
||||||
|
export * from './Config'
|
||||||
Reference in New Issue
Block a user