16 Commits

Author SHA1 Message Date
Novattz
58217d61d1 changelog 2026-01-09 20:44:10 +01:00
Novattz
0f4db7bbb7 gitignore 2026-01-09 20:44:02 +01:00
Novattz
22c8f41f93 bump version 2026-01-09 20:41:11 +01:00
Novattz
5ff51d1174 Remove reminder #92 2026-01-09 20:40:35 +01:00
Novattz
169b7d5edd redesign conflict dialog #92 2026-01-09 20:37:55 +01:00
Novattz
41da6731a7 update workflow 2026-01-03 00:37:31 +01:00
Novattz
5f8f389687 version bump 2026-01-03 00:31:25 +01:00
Novattz
1d8422dc65 changelog 2026-01-03 00:31:01 +01:00
Novattz
677e3ef12d disclaimer hook #87 2026-01-03 00:26:23 +01:00
Novattz
33266f3781 index #87 2026-01-03 00:26:00 +01:00
Novattz
9703f21209 disclaimer dialog & styles #87 2026-01-03 00:25:40 +01:00
Novattz
3459158d3f config types #88 2026-01-03 00:24:56 +01:00
Novattz
418b470d4a format 2026-01-03 00:24:23 +01:00
Novattz
fd606cbc2e config manager #88 2026-01-03 00:23:47 +01:00
Tickbase
5845cf9bd8 Update README for clarity and corrections 2026-01-02 19:57:25 +01:00
Tickbase
6294b99a14 Update LICENSE.md 2026-01-01 21:44:50 +01:00
23 changed files with 574 additions and 160 deletions

View File

@@ -142,3 +142,24 @@ jobs:
includeUpdaterJson: true
tauriScript: 'npm run tauri'
args: ${{ matrix.args }}
publish-release:
name: Publish release
needs: [create-release, build-tauri]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- name: Publish GitHub release (unset draft)
uses: actions/github-script@v6
with:
script: |
const release_id = Number("${{ needs.create-release.outputs.release_id }}");
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id,
draft: false
});

1
.gitignore vendored
View File

@@ -14,7 +14,6 @@ docs
*.local
*.lock
.env
CHANGELOG.md
# Editor directories and files
.vscode/*

View File

@@ -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
### Added

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Tickbase
Copyright (c) 2026 Tickbase
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,6 +1,6 @@
# CreamLinux
CreamLinux is a GUI application for Linux that simplifies the management of DLC in Steam games. It provides a user-friendly interface to install and configure CreamAPI (for native Linux games) and SmokeAPI (for Windows games running through Proton).
CreamLinux is a GUI application for Linux that simplifies the management of DLC IDs in Steam games. It provides a user-friendly interface to install and configure CreamAPI (for native Linux games) and SmokeAPI (for Windows games running through Proton).
## Watch the demo here:
@@ -61,7 +61,7 @@ While the core functionality is working, please be aware that this is an early r
```bash
git clone https://github.com/Novattz/creamlinux-installer.git
cd creamlinux
cd creamlinux-installer
```
2. Install dependencies:
@@ -124,7 +124,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE.md) f
## Credits
- [Creamlinux](https://github.com/anticitizn/creamlinux) - Native DLC support
- [Creamlinux](https://github.com/anticitizn/creamlinux) - Native support
- [SmokeAPI](https://github.com/acidicoala/SmokeAPI) - Proton support
- [Tauri](https://tauri.app/) - Framework for building the desktop application
- [React](https://reactjs.org/) - UI library

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "creamlinux",
"version": "1.3.3",
"version": "1.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "creamlinux",
"version": "1.3.3",
"version": "1.3.5",
"license": "MIT",
"dependencies": {
"@tauri-apps/api": "^2.5.0",

View File

@@ -1,7 +1,7 @@
{
"name": "creamlinux",
"private": true,
"version": "1.3.3",
"version": "1.3.5",
"type": "module",
"author": "Tickbase",
"repository": "https://github.com/Novattz/creamlinux-installer",

View File

@@ -1,6 +1,6 @@
[package]
name = "creamlinux-installer"
version = "1.3.3"
version = "1.3.5"
description = "DLC Manager for Steam games on Linux"
authors = ["tickbase"]
license = "MIT"

118
src-tauri/src/config.rs Normal file
View 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);
}
}

View File

@@ -9,7 +9,9 @@ mod installer;
mod searcher;
mod unlockers;
mod smokeapi_config;
mod config;
use crate::config::Config;
use crate::unlockers::{CreamLinux, SmokeAPI, Unlocker};
use dlc_manager::DlcInfoWithState;
use installer::{Game, InstallerAction, InstallerType};
@@ -46,6 +48,19 @@ pub struct AppState {
fetch_cancellation: Arc<AtomicBool>,
}
// Load the current configuration
#[tauri::command]
fn load_config() -> Result<Config, String> {
config::load_config()
}
// Update configuration
#[tauri::command]
fn update_config(config_data: Config) -> Result<Config, String> {
config::save_config(&config_data)?;
Ok(config_data)
}
#[tauri::command]
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
@@ -658,6 +673,8 @@ fn main() {
write_smokeapi_config,
delete_smokeapi_config,
resolve_platform_conflict,
load_config,
update_config,
])
.setup(|app| {
info!("Tauri application setup");

View File

@@ -19,7 +19,7 @@
},
"productName": "Creamlinux",
"mainBinaryName": "creamlinux",
"version": "1.3.3",
"version": "1.3.5",
"identifier": "com.creamlinux.dev",
"app": {
"withGlobalTauri": false,

View File

@@ -1,7 +1,7 @@
import { useState } from 'react'
import { invoke } from '@tauri-apps/api/core'
import { useAppContext } from '@/contexts/useAppContext'
import { useAppLogic, useConflictDetection } from '@/hooks'
import { useAppLogic, useConflictDetection, useDisclaimer } from '@/hooks'
import './styles/main.scss'
// Layout components
@@ -20,7 +20,7 @@ import {
DlcSelectionDialog,
SettingsDialog,
ConflictDialog,
ReminderDialog,
DisclaimerDialog,
} from '@/components/dialogs'
// Game components
@@ -32,6 +32,8 @@ import { GameList } from '@/components/games'
function App() {
const [updateComplete, setUpdateComplete] = useState(false)
const { showDisclaimer, handleDisclaimerClose } = useDisclaimer()
// Get application logic from hook
const {
filter,
@@ -65,23 +67,28 @@ function App() {
} = useAppContext()
// Conflict detection
const { currentConflict, showReminder, resolveConflict, closeReminder } =
const { conflicts, showDialog, resolveConflict, closeDialog } =
useConflictDetection(games)
// Handle conflict resolution
const handleConflictResolve = async () => {
const resolution = resolveConflict()
if (!resolution) return
// Always remove files - use the special conflict resolution command
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: resolution.gameId,
conflictType: resolution.conflictType,
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}`, 'error')
showToast('Failed to resolve conflict', 'error')
}
}
@@ -168,17 +175,15 @@ function App() {
<SettingsDialog visible={settingsDialog.visible} onClose={handleSettingsClose} />
{/* Conflict Detection Dialog */}
{currentConflict && (
<ConflictDialog
visible={true}
gameTitle={currentConflict.gameTitle}
conflictType={currentConflict.type}
onConfirm={handleConflictResolve}
/>
)}
<ConflictDialog
visible={showDialog}
conflicts={conflicts}
onResolve={handleConflictResolve}
onClose={closeDialog}
/>
{/* Steam Launch Options Reminder */}
<ReminderDialog visible={showReminder} onClose={closeReminder} />
{/* Disclaimer Dialog - Shows AFTER everything is loaded */}
<DisclaimerDialog visible={showDisclaimer} onClose={handleDisclaimerClose} />
</div>
</ErrorBoundary>
)

View File

@@ -7,66 +7,95 @@ import {
DialogActions,
} from '@/components/dialogs'
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 {
visible: boolean
gameTitle: string
conflictType: 'cream-to-proton' | 'smoke-to-native'
onConfirm: () => void
conflicts: Conflict[]
onResolve: (gameId: string, conflictType: 'cream-to-proton' | 'smoke-to-native') => void
onClose: () => void
}
/**
* 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> = ({
visible,
gameTitle,
conflictType,
onConfirm,
conflicts,
onResolve,
onClose,
}) => {
const getConflictMessage = () => {
if (conflictType === 'cream-to-proton') {
return {
title: 'CreamLinux unlocker detected, but game is set to Proton',
bodyPrefix: 'It looks like you previously installed CreamLinux while ',
bodySuffix: ' was running natively. Steam is now configured to run it with Proton, so CreamLinux files will be removed automatically.',
}
// 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 {
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.',
}
return 'Will remove existing unlocker files and restore the game to a clean state.'
}
}
const message = getConflictMessage()
return (
<Dialog visible={visible} size="large" preventBackdropClose={true}>
<DialogHeader hideCloseButton={true}>
<div className="conflict-dialog-header">
<Icon name={warning} variant="solid" size="lg" />
<h3>{message.title}</h3>
<h3>Unlocker conflicts detected</h3>
</div>
</DialogHeader>
<DialogBody>
<div className="conflict-dialog-body">
<p>
{message.bodyPrefix}
<strong>{gameTitle}</strong>
{message.bodySuffix}
<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="primary" onClick={onConfirm}>
OK
<Button variant="secondary" onClick={onClose}>
Close
</Button>
</DialogActions>
</DialogFooter>

View 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

View File

@@ -9,7 +9,7 @@ export { default as DlcSelectionDialog } from './DlcSelectionDialog'
export { default as SettingsDialog } from './SettingsDialog'
export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog'
export { default as ConflictDialog } from './ConflictDialog'
export { default as ReminderDialog } from './ReminderDialog'
export { default as DisclaimerDialog } from './DisclaimerDialog'
// Export types
export type { DialogProps } from './Dialog'
@@ -19,5 +19,4 @@ export type { DialogFooterProps } from './DialogFooter'
export type { DialogActionsProps } from './DialogActions'
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
export type { ConflictDialogProps } from './ConflictDialog'
export type { ReminderDialogProps } from './ReminderDialog'
export type { ConflictDialogProps, Conflict } from './ConflictDialog'

View File

@@ -5,6 +5,7 @@ export { useGameActions } from './useGameActions'
export { useToasts } from './useToasts'
export { useAppLogic } from './useAppLogic'
export { useConflictDetection } from './useConflictDetection'
export { useDisclaimer } from './useDisclaimer'
// Export types
export type { ToastType, Toast, ToastOptions } from './useToasts'

View File

@@ -9,7 +9,6 @@ export interface Conflict {
export interface ConflictResolution {
gameId: string
removeFiles: boolean
conflictType: 'cream-to-proton' | 'smoke-to-native'
}
@@ -19,10 +18,9 @@ export interface ConflictResolution {
*/
export function useConflictDetection(games: Game[]) {
const [conflicts, setConflicts] = useState<Conflict[]>([])
const [currentConflict, setCurrentConflict] = useState<Conflict | null>(null)
const [showReminder, setShowReminder] = useState(false)
const [isProcessing, setIsProcessing] = useState(false)
const [showDialog, setShowDialog] = useState(false)
const [resolvedConflicts, setResolvedConflicts] = useState<Set<string>>(new Set())
const [hasShownThisSession, setHasShownThisSession] = useState(false)
// Detect conflicts whenever games change
useEffect(() => {
@@ -55,69 +53,50 @@ export function useConflictDetection(games: Game[]) {
setConflicts(detectedConflicts)
// Show the first conflict if we have any and not currently processing
if (detectedConflicts.length > 0 && !currentConflict && !isProcessing) {
setCurrentConflict(detectedConflicts[0])
// 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, currentConflict, isProcessing, resolvedConflicts])
}, [games, resolvedConflicts, showDialog, hasShownThisSession])
// Handle conflict resolution
const resolveConflict = useCallback((): ConflictResolution | null => {
if (!currentConflict || isProcessing) return null
// 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))
setIsProcessing(true)
// Remove from conflicts list
setConflicts((prev) => prev.filter((c) => c.gameId !== gameId))
const resolution: ConflictResolution = {
gameId: currentConflict.gameId,
removeFiles: true, // Always remove files
conflictType: currentConflict.type,
return {
gameId,
conflictType,
}
},
[]
)
// 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
setResolvedConflicts((prev) => new Set(prev).add(currentConflict.gameId))
// Remove this conflict from the list
const remainingConflicts = conflicts.filter((c) => c.gameId !== currentConflict.gameId)
setConflicts(remainingConflicts)
// Close current conflict dialog immediately
setCurrentConflict(null)
// Determine what to show next based on conflict type
if (resolution.conflictType === 'cream-to-proton') {
// CreamLinux removal - show reminder after delay
setTimeout(() => {
setShowReminder(true)
setIsProcessing(false)
}, 100)
} else {
// SmokeAPI removal - no reminder, just show next conflict or finish
setTimeout(() => {
if (remainingConflicts.length > 0) {
setCurrentConflict(remainingConflicts[0])
}
setIsProcessing(false)
}, 100)
}
return resolution
}, [currentConflict, conflicts, isProcessing])
// Close reminder dialog
const closeReminder = useCallback(() => {
setShowReminder(false)
// After closing reminder, check if there are more conflicts
if (conflicts.length > 0) {
setCurrentConflict(conflicts[0])
}
}, [conflicts])
// Handle dialog close
const closeDialog = useCallback(() => {
setShowDialog(false)
}, [])
return {
currentConflict,
showReminder,
conflicts,
showDialog,
resolveConflict,
closeReminder,
closeDialog,
hasConflicts: conflicts.length > 0,
}
}

View 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,
}
}

View File

@@ -25,64 +25,119 @@
}
.conflict-dialog-body {
p {
margin-bottom: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
.conflict-intro {
margin: 0;
color: var(--text-secondary);
line-height: 1.5;
}
&:last-of-type {
margin-bottom: 0;
.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);
}
}
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);
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;
}
}
/*
Reminder Dialog Styles
Used for Steam launch option reminders
*/
.reminder-dialog-header {
.conflict-reminder {
display: flex;
align-items: center;
gap: 0.75rem;
h3 {
margin: 0;
flex: 1;
font-size: 1.1rem;
color: var(--text-primary);
}
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;
}
}
.reminder-dialog-body {
p {
margin-bottom: 1rem;
color: var(--text-secondary);
line-height: 1.5;
span {
flex: 1;
}
.reminder-steps {
margin: 1rem 0 0 1.5rem;
padding: 0;
color: var(--text-secondary);
line-height: 1.6;
li {
margin-bottom: 0.5rem;
&:last-child {
margin-bottom: 0;
}
}
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;
}
}

View 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%;
}

View File

@@ -4,3 +4,4 @@
@forward './settings_dialog';
@forward './smokeapi_settings_dialog';
@forward './conflict_dialog';
@forward './disclaimer_dialog';

8
src/types/Config.ts Normal file
View 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
}

View File

@@ -1,2 +1,3 @@
export * from './Game'
export * from './DlcInfo'
export * from './Config'