mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-31 07:42:52 -05:00
Compare commits
16 Commits
v1.3.3
...
58217d61d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58217d61d1 | ||
|
|
0f4db7bbb7 | ||
|
|
22c8f41f93 | ||
|
|
5ff51d1174 | ||
|
|
169b7d5edd | ||
|
|
41da6731a7 | ||
|
|
5f8f389687 | ||
|
|
1d8422dc65 | ||
|
|
677e3ef12d | ||
|
|
33266f3781 | ||
|
|
9703f21209 | ||
|
|
3459158d3f | ||
|
|
418b470d4a | ||
|
|
fd606cbc2e | ||
|
|
5845cf9bd8 | ||
|
|
6294b99a14 |
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/*
|
||||||
|
|||||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -1,3 +1,19 @@
|
|||||||
|
## [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
|
## [1.3.3] - 26-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
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "creamlinux",
|
"name": "creamlinux",
|
||||||
"version": "1.3.3",
|
"version": "1.3.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "creamlinux",
|
"name": "creamlinux",
|
||||||
"version": "1.3.3",
|
"version": "1.3.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "creamlinux",
|
"name": "creamlinux",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.3",
|
"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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "creamlinux-installer"
|
name = "creamlinux-installer"
|
||||||
version = "1.3.3"
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,9 @@ mod installer;
|
|||||||
mod searcher;
|
mod searcher;
|
||||||
mod unlockers;
|
mod unlockers;
|
||||||
mod smokeapi_config;
|
mod smokeapi_config;
|
||||||
|
mod config;
|
||||||
|
|
||||||
|
use crate::config::Config;
|
||||||
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
|
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
|
||||||
use dlc_manager::DlcInfoWithState;
|
use dlc_manager::DlcInfoWithState;
|
||||||
use installer::{Game, InstallerAction, InstallerType};
|
use installer::{Game, InstallerAction, InstallerType};
|
||||||
@@ -46,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);
|
||||||
@@ -658,6 +673,8 @@ fn main() {
|
|||||||
write_smokeapi_config,
|
write_smokeapi_config,
|
||||||
delete_smokeapi_config,
|
delete_smokeapi_config,
|
||||||
resolve_platform_conflict,
|
resolve_platform_conflict,
|
||||||
|
load_config,
|
||||||
|
update_config,
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
info!("Tauri application setup");
|
info!("Tauri application setup");
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
},
|
},
|
||||||
"productName": "Creamlinux",
|
"productName": "Creamlinux",
|
||||||
"mainBinaryName": "creamlinux",
|
"mainBinaryName": "creamlinux",
|
||||||
"version": "1.3.3",
|
"version": "1.3.5",
|
||||||
"identifier": "com.creamlinux.dev",
|
"identifier": "com.creamlinux.dev",
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": false,
|
"withGlobalTauri": false,
|
||||||
|
|||||||
47
src/App.tsx
47
src/App.tsx
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { useAppContext } from '@/contexts/useAppContext'
|
import { useAppContext } from '@/contexts/useAppContext'
|
||||||
import { useAppLogic, useConflictDetection } from '@/hooks'
|
import { useAppLogic, useConflictDetection, useDisclaimer } from '@/hooks'
|
||||||
import './styles/main.scss'
|
import './styles/main.scss'
|
||||||
|
|
||||||
// Layout components
|
// Layout components
|
||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
DlcSelectionDialog,
|
DlcSelectionDialog,
|
||||||
SettingsDialog,
|
SettingsDialog,
|
||||||
ConflictDialog,
|
ConflictDialog,
|
||||||
ReminderDialog,
|
DisclaimerDialog,
|
||||||
} from '@/components/dialogs'
|
} from '@/components/dialogs'
|
||||||
|
|
||||||
// Game components
|
// Game components
|
||||||
@@ -32,6 +32,8 @@ 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,
|
||||||
@@ -65,23 +67,28 @@ function App() {
|
|||||||
} = useAppContext()
|
} = useAppContext()
|
||||||
|
|
||||||
// Conflict detection
|
// Conflict detection
|
||||||
const { currentConflict, showReminder, resolveConflict, closeReminder } =
|
const { conflicts, showDialog, resolveConflict, closeDialog } =
|
||||||
useConflictDetection(games)
|
useConflictDetection(games)
|
||||||
|
|
||||||
// Handle conflict resolution
|
// Handle conflict resolution
|
||||||
const handleConflictResolve = async () => {
|
const handleConflictResolve = async (
|
||||||
const resolution = resolveConflict()
|
gameId: string,
|
||||||
if (!resolution) return
|
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
||||||
|
) => {
|
||||||
// Always remove files - use the special conflict resolution command
|
|
||||||
try {
|
try {
|
||||||
|
// Invoke backend to resolve the conflict
|
||||||
await invoke('resolve_platform_conflict', {
|
await invoke('resolve_platform_conflict', {
|
||||||
gameId: resolution.gameId,
|
gameId,
|
||||||
conflictType: resolution.conflictType,
|
conflictType,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Remove from UI
|
||||||
|
resolveConflict(gameId, conflictType)
|
||||||
|
|
||||||
|
showToast('Conflict resolved successfully', 'success')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error resolving conflict:', error)
|
console.error('Error resolving conflict:', error)
|
||||||
showToast(`Failed to resolve conflict: ${error}`, 'error')
|
showToast('Failed to resolve conflict', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,17 +175,15 @@ function App() {
|
|||||||
<SettingsDialog visible={settingsDialog.visible} onClose={handleSettingsClose} />
|
<SettingsDialog visible={settingsDialog.visible} onClose={handleSettingsClose} />
|
||||||
|
|
||||||
{/* Conflict Detection Dialog */}
|
{/* Conflict Detection Dialog */}
|
||||||
{currentConflict && (
|
<ConflictDialog
|
||||||
<ConflictDialog
|
visible={showDialog}
|
||||||
visible={true}
|
conflicts={conflicts}
|
||||||
gameTitle={currentConflict.gameTitle}
|
onResolve={handleConflictResolve}
|
||||||
conflictType={currentConflict.type}
|
onClose={closeDialog}
|
||||||
onConfirm={handleConflictResolve}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Steam Launch Options Reminder */}
|
{/* Disclaimer Dialog - Shows AFTER everything is loaded */}
|
||||||
<ReminderDialog visible={showReminder} onClose={closeReminder} />
|
<DisclaimerDialog visible={showDisclaimer} onClose={handleDisclaimerClose} />
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,66 +7,95 @@ import {
|
|||||||
DialogActions,
|
DialogActions,
|
||||||
} from '@/components/dialogs'
|
} from '@/components/dialogs'
|
||||||
import { Button } from '@/components/buttons'
|
import { Button } from '@/components/buttons'
|
||||||
import { Icon, warning } from '@/components/icons'
|
import { Icon, warning, info } from '@/components/icons'
|
||||||
|
|
||||||
|
export interface Conflict {
|
||||||
|
gameId: string
|
||||||
|
gameTitle: string
|
||||||
|
type: 'cream-to-proton' | 'smoke-to-native'
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConflictDialogProps {
|
export interface ConflictDialogProps {
|
||||||
visible: boolean
|
visible: boolean
|
||||||
gameTitle: string
|
conflicts: Conflict[]
|
||||||
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
onResolve: (gameId: string, conflictType: 'cream-to-proton' | 'smoke-to-native') => void
|
||||||
onConfirm: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conflict Dialog component
|
* Conflict Dialog component
|
||||||
* Shows when incompatible unlocker files are detected after platform switch
|
* Shows all conflicts at once with individual resolve buttons
|
||||||
*/
|
*/
|
||||||
const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
const ConflictDialog: React.FC<ConflictDialogProps> = ({
|
||||||
visible,
|
visible,
|
||||||
gameTitle,
|
conflicts,
|
||||||
conflictType,
|
onResolve,
|
||||||
onConfirm,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const getConflictMessage = () => {
|
// Check if any CreamLinux conflicts exist
|
||||||
if (conflictType === 'cream-to-proton') {
|
const hasCreamConflicts = conflicts.some((c) => c.type === 'cream-to-proton')
|
||||||
return {
|
|
||||||
title: 'CreamLinux unlocker detected, but game is set to Proton',
|
const getConflictDescription = (type: 'cream-to-proton' | 'smoke-to-native') => {
|
||||||
bodyPrefix: 'It looks like you previously installed CreamLinux while ',
|
if (type === 'cream-to-proton') {
|
||||||
bodySuffix: ' was running natively. Steam is now configured to run it with Proton, so CreamLinux files will be removed automatically.',
|
return 'Will remove existing unlocker files and restore the game to a clean state.'
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return {
|
return 'Will remove existing unlocker files and restore the game to a clean state.'
|
||||||
title: 'SmokeAPI unlocker detected, but game is set to Native',
|
|
||||||
bodyPrefix: 'It looks like you previously installed SmokeAPI while ',
|
|
||||||
bodySuffix: ' was running with Proton. Steam is now configured to run it natively, so SmokeAPI files will be removed automatically.',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = getConflictMessage()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog visible={visible} size="large" preventBackdropClose={true}>
|
<Dialog visible={visible} size="large" preventBackdropClose={true}>
|
||||||
<DialogHeader hideCloseButton={true}>
|
<DialogHeader hideCloseButton={true}>
|
||||||
<div className="conflict-dialog-header">
|
<div className="conflict-dialog-header">
|
||||||
<Icon name={warning} variant="solid" size="lg" />
|
<Icon name={warning} variant="solid" size="lg" />
|
||||||
<h3>{message.title}</h3>
|
<h3>Unlocker conflicts detected</h3>
|
||||||
</div>
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<div className="conflict-dialog-body">
|
<div className="conflict-dialog-body">
|
||||||
<p>
|
<p className="conflict-intro">
|
||||||
{message.bodyPrefix}
|
Some games have conflicting unlocker states that need attention.
|
||||||
<strong>{gameTitle}</strong>
|
|
||||||
{message.bodySuffix}
|
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
|
|
||||||
<DialogFooter>
|
<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>
|
<DialogActions>
|
||||||
<Button variant="primary" onClick={onConfirm}>
|
<Button variant="secondary" onClick={onClose}>
|
||||||
OK
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
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
|
||||||
@@ -9,7 +9,7 @@ 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 SmokeAPISettingsDialog } from './SmokeAPISettingsDialog'
|
||||||
export { default as ConflictDialog } from './ConflictDialog'
|
export { default as ConflictDialog } from './ConflictDialog'
|
||||||
export { default as ReminderDialog } from './ReminderDialog'
|
export { default as DisclaimerDialog } from './DisclaimerDialog'
|
||||||
|
|
||||||
// Export types
|
// Export types
|
||||||
export type { DialogProps } from './Dialog'
|
export type { DialogProps } from './Dialog'
|
||||||
@@ -19,5 +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 } from './ConflictDialog'
|
export type { ConflictDialogProps, Conflict } from './ConflictDialog'
|
||||||
export type { ReminderDialogProps } from './ReminderDialog'
|
|
||||||
@@ -5,6 +5,7 @@ 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 { 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'
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export interface Conflict {
|
|||||||
|
|
||||||
export interface ConflictResolution {
|
export interface ConflictResolution {
|
||||||
gameId: string
|
gameId: string
|
||||||
removeFiles: boolean
|
|
||||||
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
conflictType: 'cream-to-proton' | 'smoke-to-native'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,10 +18,9 @@ export interface ConflictResolution {
|
|||||||
*/
|
*/
|
||||||
export function useConflictDetection(games: Game[]) {
|
export function useConflictDetection(games: Game[]) {
|
||||||
const [conflicts, setConflicts] = useState<Conflict[]>([])
|
const [conflicts, setConflicts] = useState<Conflict[]>([])
|
||||||
const [currentConflict, setCurrentConflict] = useState<Conflict | null>(null)
|
const [showDialog, setShowDialog] = useState(false)
|
||||||
const [showReminder, setShowReminder] = useState(false)
|
|
||||||
const [isProcessing, setIsProcessing] = useState(false)
|
|
||||||
const [resolvedConflicts, setResolvedConflicts] = useState<Set<string>>(new Set())
|
const [resolvedConflicts, setResolvedConflicts] = useState<Set<string>>(new Set())
|
||||||
|
const [hasShownThisSession, setHasShownThisSession] = useState(false)
|
||||||
|
|
||||||
// Detect conflicts whenever games change
|
// Detect conflicts whenever games change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,69 +53,50 @@ export function useConflictDetection(games: Game[]) {
|
|||||||
|
|
||||||
setConflicts(detectedConflicts)
|
setConflicts(detectedConflicts)
|
||||||
|
|
||||||
// Show the first conflict if we have any and not currently processing
|
// Show dialog only if:
|
||||||
if (detectedConflicts.length > 0 && !currentConflict && !isProcessing) {
|
// 1. We have conflicts
|
||||||
setCurrentConflict(detectedConflicts[0])
|
// 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, currentConflict, isProcessing, resolvedConflicts])
|
}, [games, resolvedConflicts, showDialog, hasShownThisSession])
|
||||||
|
|
||||||
// Handle conflict resolution
|
// Handle resolving a single conflict
|
||||||
const resolveConflict = useCallback((): ConflictResolution | null => {
|
const resolveConflict = useCallback(
|
||||||
if (!currentConflict || isProcessing) return null
|
(gameId: string, conflictType: 'cream-to-proton' | 'smoke-to-native'): ConflictResolution => {
|
||||||
|
// Mark this game as resolved
|
||||||
|
setResolvedConflicts((prev) => new Set(prev).add(gameId))
|
||||||
|
|
||||||
setIsProcessing(true)
|
// Remove from conflicts list
|
||||||
|
setConflicts((prev) => prev.filter((c) => c.gameId !== gameId))
|
||||||
|
|
||||||
const resolution: ConflictResolution = {
|
return {
|
||||||
gameId: currentConflict.gameId,
|
gameId,
|
||||||
removeFiles: true, // Always remove files
|
conflictType,
|
||||||
conflictType: currentConflict.type,
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Auto-close dialog when all conflicts are resolved
|
||||||
|
useEffect(() => {
|
||||||
|
if (conflicts.length === 0 && showDialog) {
|
||||||
|
setShowDialog(false)
|
||||||
}
|
}
|
||||||
|
}, [conflicts.length, showDialog])
|
||||||
|
|
||||||
// Mark this game as resolved so we don't re-detect the conflict
|
// Handle dialog close
|
||||||
setResolvedConflicts((prev) => new Set(prev).add(currentConflict.gameId))
|
const closeDialog = useCallback(() => {
|
||||||
|
setShowDialog(false)
|
||||||
// Remove this conflict from the list
|
}, [])
|
||||||
const remainingConflicts = conflicts.filter((c) => c.gameId !== currentConflict.gameId)
|
|
||||||
setConflicts(remainingConflicts)
|
|
||||||
|
|
||||||
// Close current conflict dialog immediately
|
|
||||||
setCurrentConflict(null)
|
|
||||||
|
|
||||||
// Determine what to show next based on conflict type
|
|
||||||
if (resolution.conflictType === 'cream-to-proton') {
|
|
||||||
// CreamLinux removal - show reminder after delay
|
|
||||||
setTimeout(() => {
|
|
||||||
setShowReminder(true)
|
|
||||||
setIsProcessing(false)
|
|
||||||
}, 100)
|
|
||||||
} else {
|
|
||||||
// SmokeAPI removal - no reminder, just show next conflict or finish
|
|
||||||
setTimeout(() => {
|
|
||||||
if (remainingConflicts.length > 0) {
|
|
||||||
setCurrentConflict(remainingConflicts[0])
|
|
||||||
}
|
|
||||||
setIsProcessing(false)
|
|
||||||
}, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolution
|
|
||||||
}, [currentConflict, conflicts, isProcessing])
|
|
||||||
|
|
||||||
// Close reminder dialog
|
|
||||||
const closeReminder = useCallback(() => {
|
|
||||||
setShowReminder(false)
|
|
||||||
|
|
||||||
// After closing reminder, check if there are more conflicts
|
|
||||||
if (conflicts.length > 0) {
|
|
||||||
setCurrentConflict(conflicts[0])
|
|
||||||
}
|
|
||||||
}, [conflicts])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentConflict,
|
conflicts,
|
||||||
showReminder,
|
showDialog,
|
||||||
resolveConflict,
|
resolveConflict,
|
||||||
closeReminder,
|
closeDialog,
|
||||||
hasConflicts: conflicts.length > 0,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,64 +25,119 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.conflict-dialog-body {
|
.conflict-dialog-body {
|
||||||
p {
|
display: flex;
|
||||||
margin-bottom: 1rem;
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
.conflict-intro {
|
||||||
|
margin: 0;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-of-type {
|
.conflict-list {
|
||||||
margin-bottom: 0;
|
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);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
strong {
|
.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);
|
color: var(--text-primary);
|
||||||
font-weight: var(--bold);
|
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 {
|
||||||
Reminder Dialog Styles
|
|
||||||
Used for Steam launch option reminders
|
|
||||||
*/
|
|
||||||
|
|
||||||
.reminder-dialog-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
h3 {
|
background: rgba(33, 150, 243, 0.1);
|
||||||
margin: 0;
|
border: 1px solid rgba(33, 150, 243, 0.2);
|
||||||
flex: 1;
|
border-radius: 6px;
|
||||||
font-size: 1.1rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-secondary);
|
||||||
}
|
line-height: 1.4;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: var(--info);
|
color: var(--info);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.reminder-dialog-body {
|
span {
|
||||||
p {
|
flex: 1;
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reminder-steps {
|
code {
|
||||||
margin: 1rem 0 0 1.5rem;
|
padding: 0.125rem 0.375rem;
|
||||||
padding: 0;
|
background: rgba(0, 0, 0, 0.3);
|
||||||
color: var(--text-secondary);
|
border-radius: 3px;
|
||||||
line-height: 1.6;
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
li {
|
color: var(--text-primary);
|
||||||
margin-bottom: 0.5rem;
|
white-space: nowrap;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
src/styles/components/dialogs/_disclaimer_dialog.scss
Normal file
38
src/styles/components/dialogs/_disclaimer_dialog.scss
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
@use '../../themes/index' as *;
|
||||||
|
@use '../../abstracts/index' as *;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Disclaimer Dialog Styles
|
||||||
|
Used for the startup disclaimer dialog
|
||||||
|
*/
|
||||||
|
|
||||||
|
.disclaimer-header {
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.disclaimer-content {
|
||||||
|
p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--bold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
@@ -4,3 +4,4 @@
|
|||||||
@forward './settings_dialog';
|
@forward './settings_dialog';
|
||||||
@forward './smokeapi_settings_dialog';
|
@forward './smokeapi_settings_dialog';
|
||||||
@forward './conflict_dialog';
|
@forward './conflict_dialog';
|
||||||
|
@forward './disclaimer_dialog';
|
||||||
|
|||||||
8
src/types/Config.ts
Normal file
8
src/types/Config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* User configuration structure
|
||||||
|
* Matches the Rust Config struct
|
||||||
|
*/
|
||||||
|
export interface Config {
|
||||||
|
/** Whether to show the disclaimer on startup */
|
||||||
|
show_disclaimer: boolean
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './Game'
|
export * from './Game'
|
||||||
export * from './DlcInfo'
|
export * from './DlcInfo'
|
||||||
|
export * from './Config'
|
||||||
Reference in New Issue
Block a user