mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-08-01 19:18:25 -04:00
remove files
This commit is contained in:
@@ -1,44 +0,0 @@
|
|||||||
// This module contains helper functions for file operations during installation
|
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use std::io;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
// Copy a file with backup
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn copy_with_backup(src: &Path, dest: &Path) -> io::Result<()> {
|
|
||||||
// If destination exists, create a backup
|
|
||||||
if dest.exists() {
|
|
||||||
let backup = dest.with_extension("bak");
|
|
||||||
fs::copy(dest, &backup)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::copy(src, dest)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safely remove a file (doesn't error if it doesn't exist)
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn safe_remove(path: &Path) -> io::Result<()> {
|
|
||||||
if path.exists() {
|
|
||||||
fs::remove_file(path)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a file executable (Unix only)
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn make_executable(path: &Path) -> io::Result<()> {
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
|
|
||||||
let mut perms = fs::metadata(path)?.permissions();
|
|
||||||
perms.set_mode(0o755);
|
|
||||||
fs::set_permissions(path, perms)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
pub fn make_executable(_path: &Path) -> io::Result<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -1,97 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogHeader,
|
|
||||||
DialogBody,
|
|
||||||
DialogFooter,
|
|
||||||
DialogActions,
|
|
||||||
} from '@/components/dialogs'
|
|
||||||
import { Button } from '@/components/buttons'
|
|
||||||
import { Icon, info } from '@/components/icons'
|
|
||||||
import { EpicGame } from '@/types/EpicGame'
|
|
||||||
|
|
||||||
export interface EpicUnlockerSelectionDialogProps {
|
|
||||||
visible: boolean
|
|
||||||
game: EpicGame | null
|
|
||||||
onClose: () => void
|
|
||||||
onSelectScreamAPI: () => void
|
|
||||||
onSelectKoaloader: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unlocker selection dialog for Epic games.
|
|
||||||
* Recommended: ScreamAPI (direct EOSSDK replacement).
|
|
||||||
* Alternative: Koaloader + ScreamAPI (proxy DLL injection).
|
|
||||||
*/
|
|
||||||
const EpicUnlockerSelectionDialog: React.FC<EpicUnlockerSelectionDialogProps> = ({
|
|
||||||
visible,
|
|
||||||
game,
|
|
||||||
onClose,
|
|
||||||
onSelectScreamAPI,
|
|
||||||
onSelectKoaloader,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Dialog visible={visible} onClose={onClose} size="medium">
|
|
||||||
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
|
||||||
<div className="unlocker-selection-header">
|
|
||||||
<h3>Choose Unlocker</h3>
|
|
||||||
</div>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<DialogBody>
|
|
||||||
<div className="unlocker-selection-content">
|
|
||||||
<p className="game-title-info">
|
|
||||||
Select which unlocker to install for <strong>{game?.title}</strong>:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="unlocker-options">
|
|
||||||
<div className="unlocker-option recommended">
|
|
||||||
<div className="option-header">
|
|
||||||
<h4>ScreamAPI</h4>
|
|
||||||
<span className="recommended-badge">Recommended</span>
|
|
||||||
</div>
|
|
||||||
<p className="option-description">
|
|
||||||
Replaces the EOS SDK DLL directly with ScreamAPI. Works for most Epic games and
|
|
||||||
requires no additional files. DLC unlocking is automatic.
|
|
||||||
</p>
|
|
||||||
<Button variant="primary" onClick={onSelectScreamAPI} fullWidth>
|
|
||||||
Install ScreamAPI
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="unlocker-option">
|
|
||||||
<div className="option-header">
|
|
||||||
<h4>Koaloader + ScreamAPI</h4>
|
|
||||||
<span className="alternative-badge">Alternative</span>
|
|
||||||
</div>
|
|
||||||
<p className="option-description">
|
|
||||||
Uses a proxy DLL to inject ScreamAPI without modifying the EOS SDK. Try this if the
|
|
||||||
recommended method doesn't work for your game.
|
|
||||||
</p>
|
|
||||||
<Button variant="secondary" onClick={onSelectKoaloader} fullWidth>
|
|
||||||
Install Koaloader
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="selection-info">
|
|
||||||
<Icon name={info} variant="solid" size="md" />
|
|
||||||
<span>
|
|
||||||
You can always uninstall and try the other option if one doesn't work properly.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogBody>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogActions>
|
|
||||||
<Button variant="secondary" onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</DialogFooter>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EpicUnlockerSelectionDialog
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
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'
|
|
||||||
|
|
||||||
interface ScreamAPIConfig {
|
|
||||||
$schema: string
|
|
||||||
$version: number
|
|
||||||
logging: boolean
|
|
||||||
log_eos: boolean
|
|
||||||
block_metrics: boolean
|
|
||||||
namespace_id: string
|
|
||||||
default_dlc_status: 'unlocked' | 'locked' | 'original'
|
|
||||||
override_dlc_status: Record<string, string>
|
|
||||||
extra_graphql_endpoints: string[]
|
|
||||||
extra_entitlements: Record<string, string>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScreamAPISettingsDialogProps {
|
|
||||||
visible: boolean
|
|
||||||
onClose: () => void
|
|
||||||
gamePath: string
|
|
||||||
gameTitle: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG: ScreamAPIConfig = {
|
|
||||||
$schema:
|
|
||||||
'https://raw.githubusercontent.com/acidicoala/ScreamAPI/master/res/ScreamAPI.schema.json',
|
|
||||||
$version: 3,
|
|
||||||
logging: false,
|
|
||||||
log_eos: false,
|
|
||||||
block_metrics: false,
|
|
||||||
namespace_id: '',
|
|
||||||
default_dlc_status: 'unlocked',
|
|
||||||
override_dlc_status: {},
|
|
||||||
extra_graphql_endpoints: [],
|
|
||||||
extra_entitlements: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
const DLC_STATUS_OPTIONS: DropdownOption<'unlocked' | 'locked' | 'original'>[] = [
|
|
||||||
{ value: 'unlocked', label: 'Unlocked' },
|
|
||||||
{ value: 'locked', label: 'Locked' },
|
|
||||||
{ value: 'original', label: 'Original' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const ScreamAPISettingsDialog = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
gamePath,
|
|
||||||
gameTitle,
|
|
||||||
}: ScreamAPISettingsDialogProps) => {
|
|
||||||
const [enabled, setEnabled] = useState(false)
|
|
||||||
const [config, setConfig] = useState<ScreamAPIConfig>(DEFAULT_CONFIG)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [hasChanges, setHasChanges] = useState(false)
|
|
||||||
|
|
||||||
const loadConfig = useCallback(async () => {
|
|
||||||
setIsLoading(true)
|
|
||||||
try {
|
|
||||||
const existingConfig = await invoke<ScreamAPIConfig | null>('read_screamapi_config', {
|
|
||||||
gamePath,
|
|
||||||
})
|
|
||||||
if (existingConfig) {
|
|
||||||
setConfig(existingConfig)
|
|
||||||
setEnabled(true)
|
|
||||||
} else {
|
|
||||||
setConfig(DEFAULT_CONFIG)
|
|
||||||
setEnabled(false)
|
|
||||||
}
|
|
||||||
setHasChanges(false)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load ScreamAPI 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) {
|
|
||||||
await invoke('write_screamapi_config', { gamePath, config })
|
|
||||||
} else {
|
|
||||||
await invoke('delete_screamapi_config', { gamePath })
|
|
||||||
}
|
|
||||||
setHasChanges(false)
|
|
||||||
onClose()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save ScreamAPI config:', error)
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setHasChanges(false)
|
|
||||||
onClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateConfig = <K extends keyof ScreamAPIConfig>(key: K, value: ScreamAPIConfig[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">
|
|
||||||
<h3>ScreamAPI Settings</h3>
|
|
||||||
</div>
|
|
||||||
<p className="dialog-subtitle">{gameTitle}</p>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<DialogBody>
|
|
||||||
<div className="smokeapi-settings-content">
|
|
||||||
<div className="settings-section">
|
|
||||||
<AnimatedCheckbox
|
|
||||||
checked={enabled}
|
|
||||||
onChange={() => {
|
|
||||||
setEnabled(!enabled)
|
|
||||||
setHasChanges(true)
|
|
||||||
}}
|
|
||||||
label="Enable ScreamAPI Configuration"
|
|
||||||
sublabel="Enable this to customise ScreamAPI settings for this game"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`settings-options ${!enabled ? 'disabled' : ''}`}>
|
|
||||||
<div className="settings-section">
|
|
||||||
<h4>General Settings</h4>
|
|
||||||
|
|
||||||
<Dropdown
|
|
||||||
label="Default DLC Status"
|
|
||||||
description="Specifies the default DLC unlock status"
|
|
||||||
value={config.default_dlc_status}
|
|
||||||
options={DLC_STATUS_OPTIONS}
|
|
||||||
onChange={(value) => updateConfig('default_dlc_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 ScreamAPI.log.log file"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="checkbox-option">
|
|
||||||
<AnimatedCheckbox
|
|
||||||
checked={config.log_eos}
|
|
||||||
onChange={() => updateConfig('log_eos', !config.log_eos)}
|
|
||||||
label="Log EOS SDK"
|
|
||||||
sublabel="Intercept and log EOS SDK calls (requires logging enabled)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="settings-section">
|
|
||||||
<h4>Privacy</h4>
|
|
||||||
|
|
||||||
<div className="checkbox-option">
|
|
||||||
<AnimatedCheckbox
|
|
||||||
checked={config.block_metrics}
|
|
||||||
onChange={() => updateConfig('block_metrics', !config.block_metrics)}
|
|
||||||
label="Block Metrics"
|
|
||||||
sublabel="Block game analytics/usage reporting to Epic Online Services"
|
|
||||||
/>
|
|
||||||
</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 ScreamAPISettingsDialog
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogHeader,
|
|
||||||
DialogBody,
|
|
||||||
DialogFooter,
|
|
||||||
DialogActions,
|
|
||||||
} from '@/components/dialogs'
|
|
||||||
import { Button } from '@/components/buttons'
|
|
||||||
import { Icon, settings } from '@/components/icons'
|
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
|
||||||
visible: boolean
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Settings Dialog component
|
|
||||||
* Contains application settings and configuration options
|
|
||||||
*/
|
|
||||||
const SettingsDialog: React.FC<SettingsDialogProps> = ({ visible, onClose }) => {
|
|
||||||
const [appVersion, setAppVersion] = useState<string>('Loading...')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Fetch app version when component mounts
|
|
||||||
const fetchVersion = async () => {
|
|
||||||
try {
|
|
||||||
const version = await getVersion()
|
|
||||||
setAppVersion(version)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch app version:', error)
|
|
||||||
setAppVersion('Unknown')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchVersion()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog visible={visible} onClose={onClose} size="medium">
|
|
||||||
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
|
||||||
<div className="settings-header">
|
|
||||||
{/*<Icon name={settings} variant="solid" size="md" />*/}
|
|
||||||
<h3>Settings</h3>
|
|
||||||
</div>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<DialogBody>
|
|
||||||
<div className="settings-content">
|
|
||||||
<div className="settings-section">
|
|
||||||
<h4>General Settings</h4>
|
|
||||||
<p className="settings-description">
|
|
||||||
Configure your CreamLinux preferences and application behavior.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="settings-placeholder">
|
|
||||||
<div className="placeholder-icon"> <Icon name={settings} variant="solid" size="xl" /> </div>
|
|
||||||
<div className="placeholder-text">
|
|
||||||
<h5>Settings Coming Soon</h5>
|
|
||||||
<p>
|
|
||||||
Working on adding customizable settings to improve your experience.
|
|
||||||
Future options may include:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Custom Steam library paths</li>
|
|
||||||
<li>Automatic update settings</li>
|
|
||||||
<li>Scan frequency options</li>
|
|
||||||
<li>DLC catalog</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="settings-section">
|
|
||||||
<h4>About CreamLinux</h4>
|
|
||||||
<div className="app-info">
|
|
||||||
<div className="info-row">
|
|
||||||
<span className="info-label">Version:</span>
|
|
||||||
<span className="info-value">{appVersion}</span>
|
|
||||||
</div>
|
|
||||||
<div className="info-row">
|
|
||||||
<span className="info-label">Build:</span>
|
|
||||||
<span className="info-value">Beta</span>
|
|
||||||
</div>
|
|
||||||
<div className="info-row">
|
|
||||||
<span className="info-label">Repository:</span>
|
|
||||||
<a
|
|
||||||
href="https://github.com/Novattz/creamlinux-installer"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="info-link"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogBody>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogActions>
|
|
||||||
<Button variant="secondary" onClick={onClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</DialogFooter>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SettingsDialog
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogHeader,
|
|
||||||
DialogBody,
|
|
||||||
DialogFooter,
|
|
||||||
DialogActions,
|
|
||||||
} from '@/components/dialogs'
|
|
||||||
import { Button } from '@/components/buttons'
|
|
||||||
import { Icon, info } from '@/components/icons'
|
|
||||||
import VotesDisplay, { GameVotes } from '@/components/common/VotesDisplay'
|
|
||||||
|
|
||||||
export interface UnlockerSelectionDialogProps {
|
|
||||||
visible: boolean
|
|
||||||
gameId: string | null
|
|
||||||
gameTitle: string | null
|
|
||||||
onClose: () => void
|
|
||||||
onSelectCreamLinux: () => void
|
|
||||||
onSelectSmokeAPI: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unlocker Selection Dialog component
|
|
||||||
* Allows users to choose between CreamLinux and SmokeAPI for native Linux games.
|
|
||||||
* Fetches and displays community vote data per unlocker.
|
|
||||||
*/
|
|
||||||
const UnlockerSelectionDialog: React.FC<UnlockerSelectionDialogProps> = ({
|
|
||||||
visible,
|
|
||||||
gameId,
|
|
||||||
gameTitle,
|
|
||||||
onClose,
|
|
||||||
onSelectCreamLinux,
|
|
||||||
onSelectSmokeAPI,
|
|
||||||
}) => {
|
|
||||||
const [creamVotes, setCreamVotes] = useState<GameVotes | null>(null)
|
|
||||||
const [smokeVotes, setSmokeVotes] = useState<GameVotes | null>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!visible || !gameId) {
|
|
||||||
setCreamVotes(null)
|
|
||||||
setSmokeVotes(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
invoke<GameVotes[]>('get_game_votes', { gameId })
|
|
||||||
.then((results) => {
|
|
||||||
setCreamVotes(results.find((v) => v.unlocker === 'creamlinux') ?? null)
|
|
||||||
setSmokeVotes(results.find((v) => v.unlocker === 'smokeapi') ?? null)
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
// Votes are non-critical — silently fall back to "No votes yet"
|
|
||||||
setCreamVotes(null)
|
|
||||||
setSmokeVotes(null)
|
|
||||||
})
|
|
||||||
}, [visible, gameId])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog visible={visible} onClose={onClose} size="medium">
|
|
||||||
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
|
||||||
<div className="unlocker-selection-header">
|
|
||||||
<h3>Choose Unlocker</h3>
|
|
||||||
</div>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<DialogBody>
|
|
||||||
<div className="unlocker-selection-content">
|
|
||||||
<p className="game-title-info">
|
|
||||||
Select which unlocker to install for <strong>{gameTitle}</strong>:
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="unlocker-options">
|
|
||||||
<div className="unlocker-option recommended">
|
|
||||||
<div className="option-header">
|
|
||||||
<h4>CreamLinux</h4>
|
|
||||||
<span className="recommended-badge">Recommended</span>
|
|
||||||
</div>
|
|
||||||
<p className="option-description">
|
|
||||||
Native Linux DLC unlocker. Works best with most native Linux games and provides
|
|
||||||
better compatibility.
|
|
||||||
</p>
|
|
||||||
<VotesDisplay votes={creamVotes} />
|
|
||||||
<Button variant="primary" onClick={onSelectCreamLinux} fullWidth>
|
|
||||||
Install CreamLinux
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="unlocker-option">
|
|
||||||
<div className="option-header">
|
|
||||||
<h4>SmokeAPI</h4>
|
|
||||||
<span className="alternative-badge">Alternative</span>
|
|
||||||
</div>
|
|
||||||
<p className="option-description">
|
|
||||||
Cross-platform DLC unlocker. Try this if CreamLinux doesn't work for your game.
|
|
||||||
Automatically fetches DLC information.
|
|
||||||
</p>
|
|
||||||
<VotesDisplay votes={smokeVotes} />
|
|
||||||
<Button variant="secondary" onClick={onSelectSmokeAPI} fullWidth>
|
|
||||||
Install SmokeAPI
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="selection-info">
|
|
||||||
<Icon name={info} variant="solid" size="md" />
|
|
||||||
<span>
|
|
||||||
You can always uninstall and try the other option if one doesn't work properly.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogBody>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogActions>
|
|
||||||
<Button variant="secondary" onClick={onClose}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</DialogFooter>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UnlockerSelectionDialog
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animated background component that draws an interactive particle effect
|
|
||||||
*/
|
|
||||||
const AnimatedBackground = () => {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canvas = canvasRef.current
|
|
||||||
if (!canvas) return
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
if (!ctx) return
|
|
||||||
|
|
||||||
// Set canvas size to match window
|
|
||||||
const setCanvasSize = () => {
|
|
||||||
canvas.width = window.innerWidth
|
|
||||||
canvas.height = window.innerHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
setCanvasSize()
|
|
||||||
window.addEventListener('resize', setCanvasSize)
|
|
||||||
|
|
||||||
// Create particles
|
|
||||||
const particles: Particle[] = []
|
|
||||||
const particleCount = 30
|
|
||||||
|
|
||||||
interface Particle {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
size: number
|
|
||||||
speedX: number
|
|
||||||
speedY: number
|
|
||||||
opacity: number
|
|
||||||
color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color palette matching theme
|
|
||||||
const colors = [
|
|
||||||
'rgba(74, 118, 196, 0.5)', // primary blue
|
|
||||||
'rgba(155, 125, 255, 0.5)', // purple
|
|
||||||
'rgba(251, 177, 60, 0.5)', // gold
|
|
||||||
]
|
|
||||||
|
|
||||||
// Create initial particles
|
|
||||||
for (let i = 0; i < particleCount; i++) {
|
|
||||||
particles.push({
|
|
||||||
x: Math.random() * canvas.width,
|
|
||||||
y: Math.random() * canvas.height,
|
|
||||||
size: Math.random() * 3 + 1,
|
|
||||||
speedX: Math.random() * 0.2 - 0.1,
|
|
||||||
speedY: Math.random() * 0.2 - 0.1,
|
|
||||||
opacity: Math.random() * 0.07 + 0.03,
|
|
||||||
color: colors[Math.floor(Math.random() * colors.length)],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animation loop
|
|
||||||
const animate = () => {
|
|
||||||
// Clear canvas with transparent black to create fade effect
|
|
||||||
ctx.fillStyle = 'rgba(15, 15, 15, 0.1)'
|
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
|
||||||
|
|
||||||
// Update and draw particles
|
|
||||||
particles.forEach((particle) => {
|
|
||||||
// Update position
|
|
||||||
particle.x += particle.speedX
|
|
||||||
particle.y += particle.speedY
|
|
||||||
|
|
||||||
// Wrap around edges
|
|
||||||
if (particle.x < 0) particle.x = canvas.width
|
|
||||||
if (particle.x > canvas.width) particle.x = 0
|
|
||||||
if (particle.y < 0) particle.y = canvas.height
|
|
||||||
if (particle.y > canvas.height) particle.y = 0
|
|
||||||
|
|
||||||
// Draw particle
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2)
|
|
||||||
ctx.fillStyle = particle.color.replace('0.5', `${particle.opacity}`)
|
|
||||||
ctx.fill()
|
|
||||||
|
|
||||||
// Connect particles that are close to each other
|
|
||||||
particles.forEach((otherParticle) => {
|
|
||||||
const dx = particle.x - otherParticle.x
|
|
||||||
const dy = particle.y - otherParticle.y
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
|
||||||
|
|
||||||
if (distance < 100) {
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.strokeStyle = particle.color.replace('0.5', `${particle.opacity * 0.5}`)
|
|
||||||
ctx.lineWidth = 0.2
|
|
||||||
ctx.moveTo(particle.x, particle.y)
|
|
||||||
ctx.lineTo(otherParticle.x, otherParticle.y)
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
requestAnimationFrame(animate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start animation
|
|
||||||
animate()
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', setCanvasSize)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return <canvas ref={canvasRef} className="animated-background" aria-hidden="true" />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AnimatedBackground
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
@use '../../themes/index' as *;
|
|
||||||
@use '../../abstracts/index' as *;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Settings dialog styles
|
|
||||||
*/
|
|
||||||
|
|
||||||
.settings-header {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-section {
|
|
||||||
h4 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom: 1px solid var(--border-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-description {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-placeholder {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: var(--border-dark);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--border-soft);
|
|
||||||
|
|
||||||
.placeholder-icon {
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder-text {
|
|
||||||
h5 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
text-align: left;
|
|
||||||
color: var(--text-soft);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
max-width: 300px;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '•';
|
|
||||||
color: var(--primary-color);
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-info {
|
|
||||||
background-color: var(--border-dark);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid var(--border-soft);
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
border-bottom: 1px solid var(--border-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-link {
|
|
||||||
color: var(--primary-color);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--secondary-color);
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings button in sidebar
|
|
||||||
.settings-button {
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
transition: all var(--duration-normal) var(--easing-ease-out);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 500;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
|
||||||
color: var(--text-primary);
|
|
||||||
border-color: var(--border-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .settings-icon {
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
@use '../../themes/index' as *;
|
|
||||||
@use '../../abstracts/index' as *;
|
|
||||||
|
|
||||||
/*
|
|
||||||
Animated background styles
|
|
||||||
*/
|
|
||||||
.animated-background {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: var(--z-bg);
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
Home page specific styles
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Currently empty since most styles are component-based
|
|
||||||
// Will be used for any specific home page layouts or adjustments
|
|
||||||
|
|
||||||
.home-page {
|
|
||||||
// Page-specific styles can be added here
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page-specific media queries
|
|
||||||
@include media-sm {
|
|
||||||
// Small screen adjustments
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-md {
|
|
||||||
// Medium screen adjustments
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-lg {
|
|
||||||
// Large screen adjustments
|
|
||||||
}
|
|
||||||
|
|
||||||
@include media-xl {
|
|
||||||
// Extra large screen adjustments
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* General-purpose utility functions
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats a timestamp in seconds to a human-readable string
|
|
||||||
* @param seconds Number of seconds
|
|
||||||
* @returns Formatted string (e.g., "5m 30s" or "30s")
|
|
||||||
*/
|
|
||||||
export function formatTime(seconds: number): string {
|
|
||||||
if (seconds < 60) {
|
|
||||||
return `${Math.round(seconds)}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
const minutes = Math.floor(seconds / 60)
|
|
||||||
const remainingSeconds = Math.round(seconds % 60)
|
|
||||||
|
|
||||||
if (remainingSeconds === 0) {
|
|
||||||
return `${minutes}m`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${minutes}m ${remainingSeconds}s`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Truncates a string if it exceeds the specified length
|
|
||||||
* @param str String to truncate
|
|
||||||
* @param maxLength Maximum length before truncation
|
|
||||||
* @param suffix Suffix to append to truncated string (default: "...")
|
|
||||||
* @returns Truncated string
|
|
||||||
*/
|
|
||||||
export function truncateString(str: string, maxLength: number, suffix: string = '...'): string {
|
|
||||||
if (str.length <= maxLength) {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
|
|
||||||
return str.substring(0, maxLength - suffix.length) + suffix
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debounces a function to limit how often it's called
|
|
||||||
* @param fn Function to debounce
|
|
||||||
* @param delay Delay in milliseconds
|
|
||||||
* @returns Debounced function
|
|
||||||
*/
|
|
||||||
export function debounce<T extends (...args: unknown[]) => unknown>(
|
|
||||||
fn: T,
|
|
||||||
delay: number
|
|
||||||
): (...args: Parameters<T>) => void {
|
|
||||||
let timer: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
return function (...args: Parameters<T>) {
|
|
||||||
if (timer) {
|
|
||||||
clearTimeout(timer)
|
|
||||||
}
|
|
||||||
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
fn(...args)
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a throttled function that only invokes the provided function at most once per specified interval
|
|
||||||
* @param fn Function to throttle
|
|
||||||
* @param limit Interval in milliseconds
|
|
||||||
* @returns Throttled function
|
|
||||||
*/
|
|
||||||
export function throttle<T extends (...args: unknown[]) => unknown>(
|
|
||||||
fn: T,
|
|
||||||
limit: number
|
|
||||||
): (...args: Parameters<T>) => void {
|
|
||||||
let lastCall = 0
|
|
||||||
|
|
||||||
return function (...args: Parameters<T>) {
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
if (now - lastCall < limit) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
lastCall = now
|
|
||||||
return fn(...args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './helpers'
|
|
||||||
Reference in New Issue
Block a user