From 3675ff8faefceb7160fd85b78e2226535919317b Mon Sep 17 00:00:00 2001 From: Novattz Date: Tue, 23 Dec 2025 01:58:30 +0100 Subject: [PATCH] add smokeapi settings dialog & styling #67 --- src/App.tsx | 2 + .../dialogs/SmokeAPISettingsDialog.tsx | 228 ++++++++++++++++++ src/components/dialogs/index.ts | 1 + src/contexts/AppContext.tsx | 11 + src/contexts/AppProvider.tsx | 46 +++- src/styles/components/dialogs/_index.scss | 1 + .../dialogs/_smokeapi_settings_dialog.scss | 66 +++++ 7 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 src/components/dialogs/SmokeAPISettingsDialog.tsx create mode 100644 src/styles/components/dialogs/_smokeapi_settings_dialog.scss diff --git a/src/App.tsx b/src/App.tsx index 2ee8808..42838c1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -43,6 +43,7 @@ function App() { settingsDialog, handleSettingsOpen, handleSettingsClose, + handleSmokeAPISettingsOpen, } = useAppContext() // Show update screen first @@ -86,6 +87,7 @@ function App() { isLoading={isLoading} onAction={handleGameAction} onEdit={handleGameEdit} + onSmokeAPISettings={handleSmokeAPISettingsOpen} /> )} diff --git a/src/components/dialogs/SmokeAPISettingsDialog.tsx b/src/components/dialogs/SmokeAPISettingsDialog.tsx new file mode 100644 index 0000000..92f3c76 --- /dev/null +++ b/src/components/dialogs/SmokeAPISettingsDialog.tsx @@ -0,0 +1,228 @@ +import { useState, useEffect, useCallback } from 'react' +import { invoke } from '@tauri-apps/api/core' +import { + Dialog, + DialogHeader, + DialogBody, + DialogFooter, + DialogActions, +} from '@/components/dialogs' +import { Button, AnimatedCheckbox } from '@/components/buttons' +import { Dropdown, DropdownOption } from '@/components/common' +//import { Icon, settings } from '@/components/icons' + +interface SmokeAPIConfig { + $schema: string + $version: number + logging: boolean + log_steam_http: boolean + default_app_status: 'unlocked' | 'locked' | 'original' + override_app_status: Record + override_dlc_status: Record + auto_inject_inventory: boolean + extra_inventory_items: number[] + extra_dlcs: Record +} + +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(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('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 = (key: K, value: SmokeAPIConfig[K]) => { + setConfig((prev) => ({ ...prev, [key]: value })) + setHasChanges(true) + } + + return ( + + +
+ {/**/} +

SmokeAPI Settings

+
+

{gameTitle}

+
+ + +
+ {/* Enable/Disable Section */} +
+ { + setEnabled(!enabled) + setHasChanges(true) + }} + label="Enable SmokeAPI Configuration" + sublabel="Enable this to customize SmokeAPI settings for this game" + /> +
+ + {/* Settings Options */} +
+
+

General Settings

+ + updateConfig('default_app_status', value)} + disabled={!enabled} + /> +
+ +
+

Logging

+ +
+ updateConfig('logging', !config.logging)} + label="Enable Logging" + sublabel="Enables logging to SmokeAPI.log.log file" + /> +
+ +
+ updateConfig('log_steam_http', !config.log_steam_http)} + label="Log Steam HTTP" + sublabel="Toggles logging of SteamHTTP traffic" + /> +
+
+ +
+

Inventory

+ +
+ + 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" + /> +
+
+
+
+
+ + + + + + + +
+ ) +} + +export default SmokeAPISettingsDialog \ No newline at end of file diff --git a/src/components/dialogs/index.ts b/src/components/dialogs/index.ts index a57acd3..c5ec29f 100644 --- a/src/components/dialogs/index.ts +++ b/src/components/dialogs/index.ts @@ -7,6 +7,7 @@ export { default as DialogActions } from './DialogActions' export { default as ProgressDialog } from './ProgressDialog' export { default as DlcSelectionDialog } from './DlcSelectionDialog' export { default as SettingsDialog } from './SettingsDialog' +export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog' // Export types export type { DialogProps } from './Dialog' diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index 6251b7e..9f4232e 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -30,6 +30,12 @@ export interface ProgressDialogState { instructions?: InstallationInstructions } +export interface SmokeAPISettingsDialogState { + visible: boolean + gamePath: string + gameTitle: string +} + // Define the context type export interface AppContextType { // Game state @@ -54,6 +60,11 @@ export interface AppContextType { handleSettingsOpen: () => void handleSettingsClose: () => void + // SmokeAPI settings + smokeAPISettingsDialog: SmokeAPISettingsDialogState + handleSmokeAPISettingsOpen: (gameId: string) => void + handleSmokeAPISettingsClose: () => void + // Toast notifications showToast: ( message: string, diff --git a/src/contexts/AppProvider.tsx b/src/contexts/AppProvider.tsx index 06a9dc3..df09bd5 100644 --- a/src/contexts/AppProvider.tsx +++ b/src/contexts/AppProvider.tsx @@ -4,6 +4,7 @@ import { useGames, useDlcManager, useGameActions, useToasts } from '@/hooks' import { DlcInfo } from '@/types' import { ActionType } from '@/components/buttons/ActionButton' import { ToastContainer } from '@/components/notifications' +import { SmokeAPISettingsDialog } from '@/components/dialogs' // Context provider component interface AppProviderProps { @@ -38,6 +39,17 @@ export const AppProvider = ({ children }: AppProviderProps) => { // Settings dialog state const [settingsDialog, setSettingsDialog] = useState({ visible: false }) + // SmokeAPI settings dialog state + const [smokeAPISettingsDialog, setSmokeAPISettingsDialog] = useState<{ + visible: boolean + gamePath: string + gameTitle: string + }>({ + visible: false, + gamePath: '', + gameTitle: '', + }) + // Settings handlers const handleSettingsOpen = () => { setSettingsDialog({ visible: true }) @@ -47,6 +59,25 @@ export const AppProvider = ({ children }: AppProviderProps) => { setSettingsDialog({ visible: false }) } + // SmokeAPI settings handlers + const handleSmokeAPISettingsOpen = (gameId: string) => { + const game = games.find((g) => g.id === gameId) + if (!game) { + showError('Game not found') + return + } + + setSmokeAPISettingsDialog({ + visible: true, + gamePath: game.path, + gameTitle: game.title, + }) + } + + const handleSmokeAPISettingsClose = () => { + setSmokeAPISettingsDialog((prev) => ({ ...prev, visible: false })) + } + // Game action handler with proper error reporting const handleGameAction = async (gameId: string, action: ActionType) => { const game = games.find((g) => g.id === gameId) @@ -201,6 +232,11 @@ export const AppProvider = ({ children }: AppProviderProps) => { handleSettingsOpen, handleSettingsClose, + // SmokeAPI Settings + smokeAPISettingsDialog, + handleSmokeAPISettingsOpen, + handleSmokeAPISettingsClose, + // Toast notifications showToast, } @@ -209,6 +245,14 @@ export const AppProvider = ({ children }: AppProviderProps) => { {children} + + {/* SmokeAPI Settings Dialog */} + ) -} +} \ No newline at end of file diff --git a/src/styles/components/dialogs/_index.scss b/src/styles/components/dialogs/_index.scss index efd2938..d0b8be2 100644 --- a/src/styles/components/dialogs/_index.scss +++ b/src/styles/components/dialogs/_index.scss @@ -2,3 +2,4 @@ @forward './dlc_dialog'; @forward './progress_dialog'; @forward './settings_dialog'; +@forward './smokeapi_settings_dialog'; diff --git a/src/styles/components/dialogs/_smokeapi_settings_dialog.scss b/src/styles/components/dialogs/_smokeapi_settings_dialog.scss new file mode 100644 index 0000000..5e9ec36 --- /dev/null +++ b/src/styles/components/dialogs/_smokeapi_settings_dialog.scss @@ -0,0 +1,66 @@ +@use '../../themes/index' as *; +@use '../../abstracts/index' as *; + +/* + SmokeAPI Settings Dialog styles +*/ + +.dialog-subtitle { + color: var(--text-secondary); + font-weight: 500; + margin-top: 0.25rem; + font-weight: normal; +} + +.smokeapi-settings-content { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.settings-options { + display: flex; + flex-direction: column; + gap: 1.5rem; + transition: opacity var(--duration-normal) var(--easing-ease-out); + + &.disabled { + opacity: 0.4; + pointer-events: none; + } +} + +.settings-section { + display: flex; + flex-direction: column; + gap: 1rem; + + h4 { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-soft); + } +} + +.checkbox-option { + padding: 0.5rem 0; + + &:not(:last-child) { + border-bottom: 1px solid var(--border-soft); + } + + .animated-checkbox { + width: 100%; + + .checkbox-content { + flex: 1; + } + + .checkbox-sublabel { + margin-top: 0.25rem; + } + } +}