remove files

This commit is contained in:
Tickbase
2026-07-11 11:28:47 +02:00
parent a1be636a8c
commit a4d4091844
15 changed files with 0 additions and 1347 deletions
-44
View File
@@ -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
-56
View File
@@ -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
-113
View File
@@ -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;
}
-27
View File
@@ -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
}
-85
View File
@@ -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
View File
@@ -1 +0,0 @@
export * from './helpers'