Universal settings dialog

This commit is contained in:
Tickbase
2026-07-11 12:20:03 +02:00
parent e8906d6ec0
commit e2b715dd6b
2 changed files with 330 additions and 0 deletions
@@ -0,0 +1,194 @@
import { useState, useEffect, useCallback } from 'react'
import { invoke } from '@tauri-apps/api/core'
import Dialog from './Dialog'
import DialogHeader from './DialogHeader'
import DialogBody from './DialogBody'
import DialogFooter from './DialogFooter'
import DialogActions from './DialogActions'
import { Button, AnimatedCheckbox } from '@/components/buttons'
import { Dropdown, DropdownOption } from '@/components/common'
export type ApiSettingsField =
| {
kind: 'dropdown'
key: string
label: string
description: string
options: DropdownOption<string>[]
}
| {
kind: 'checkbox'
key: string
label: string
sublabel: string
}
export interface ApiSettingsSection {
title: string
fields: ApiSettingsField[]
}
/**
* Describes an unlocker's config dialog: what sections/fields to render and
* which Tauri commands read/write/delete its config file. One dialog
* component renders any config that fits this shape, instead of a
* near-identical component per unlocker.
*/
export interface ApiSettingsSpec {
dialogTitle: string
enableLabel: string
enableSublabel: string
defaultConfig: Record<string, unknown>
sections: ApiSettingsSection[]
readCommand: string
writeCommand: string
deleteCommand: string
}
export interface ApiSettingsDialogProps {
visible: boolean
onClose: () => void
gamePath: string
gameTitle: string
spec: ApiSettingsSpec
}
/**
* Generic settings dialog for an unlocker's config file (SmokeAPI,
* ScreamAPI, ...). Renders whatever sections/fields the spec describes and
* reads/writes/deletes the config through the spec's Tauri commands.
*/
const ApiSettingsDialog = ({ visible, onClose, gamePath, gameTitle, spec }: ApiSettingsDialogProps) => {
const [enabled, setEnabled] = useState(false)
const [config, setConfig] = useState<Record<string, unknown>>(spec.defaultConfig)
const [isLoading, setIsLoading] = useState(false)
const [hasChanges, setHasChanges] = useState(false)
const loadConfig = useCallback(async () => {
setIsLoading(true)
try {
const existingConfig = await invoke<Record<string, unknown> | null>(spec.readCommand, {
gamePath,
})
if (existingConfig) {
setConfig(existingConfig)
setEnabled(true)
} else {
setConfig(spec.defaultConfig)
setEnabled(false)
}
setHasChanges(false)
} catch (error) {
console.error(`Failed to load ${spec.dialogTitle}:`, error)
setConfig(spec.defaultConfig)
setEnabled(false)
} finally {
setIsLoading(false)
}
}, [gamePath, spec])
useEffect(() => {
if (visible && gamePath) {
loadConfig()
}
}, [visible, gamePath, loadConfig])
const handleSave = async () => {
setIsLoading(true)
try {
if (enabled) {
await invoke(spec.writeCommand, { gamePath, config })
} else {
await invoke(spec.deleteCommand, { gamePath })
}
setHasChanges(false)
onClose()
} catch (error) {
console.error(`Failed to save ${spec.dialogTitle}:`, error)
} finally {
setIsLoading(false)
}
}
const handleCancel = () => {
setHasChanges(false)
onClose()
}
const updateConfig = (key: string, value: unknown) => {
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>{spec.dialogTitle}</h3>
</div>
<p className="dialog-subtitle">{gameTitle}</p>
</DialogHeader>
<DialogBody>
<div className="api-settings-content">
<div className="settings-section">
<AnimatedCheckbox
checked={enabled}
onChange={() => {
setEnabled(!enabled)
setHasChanges(true)
}}
label={spec.enableLabel}
sublabel={spec.enableSublabel}
/>
</div>
<div className={`settings-options ${!enabled ? 'disabled' : ''}`}>
{spec.sections.map((section) => (
<div className="settings-section" key={section.title}>
<h4>{section.title}</h4>
{section.fields.map((field) =>
field.kind === 'dropdown' ? (
<Dropdown
key={field.key}
label={field.label}
description={field.description}
value={config[field.key] as string}
options={field.options}
onChange={(value) => updateConfig(field.key, value)}
disabled={!enabled}
/>
) : (
<div className="checkbox-option" key={field.key}>
<AnimatedCheckbox
checked={Boolean(config[field.key])}
onChange={() => updateConfig(field.key, !config[field.key])}
label={field.label}
sublabel={field.sublabel}
/>
</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 ApiSettingsDialog
+136
View File
@@ -0,0 +1,136 @@
import { DropdownOption } from '@/components/common'
import { ApiSettingsSpec } from './ApiSettingsDialog'
const DLC_STATUS_OPTIONS: DropdownOption<string>[] = [
{ value: 'unlocked', label: 'Unlocked' },
{ value: 'locked', label: 'Locked' },
{ value: 'original', label: 'Original' },
]
export const smokeApiSettingsSpec: ApiSettingsSpec = {
dialogTitle: 'SmokeAPI Settings',
enableLabel: 'Enable SmokeAPI Configuration',
enableSublabel: 'Enable this to customize SmokeAPI settings for this game',
defaultConfig: {
$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: {},
},
readCommand: 'read_smokeapi_config',
writeCommand: 'write_smokeapi_config',
deleteCommand: 'delete_smokeapi_config',
sections: [
{
title: 'General Settings',
fields: [
{
kind: 'dropdown',
key: 'default_app_status',
label: 'Default App Status',
description: 'Specifies the default DLC status',
options: DLC_STATUS_OPTIONS,
},
],
},
{
title: 'Logging',
fields: [
{
kind: 'checkbox',
key: 'logging',
label: 'Enable Logging',
sublabel: 'Enables logging to SmokeAPI.log.log file',
},
{
kind: 'checkbox',
key: 'log_steam_http',
label: 'Log Steam HTTP',
sublabel: 'Toggles logging of SteamHTTP traffic',
},
],
},
{
title: 'Inventory',
fields: [
{
kind: 'checkbox',
key: 'auto_inject_inventory',
label: 'Auto Inject Inventory',
sublabel:
'Automatically inject a list of all registered inventory items when the game queries user inventory',
},
],
},
],
}
export const screamApiSettingsSpec: ApiSettingsSpec = {
dialogTitle: 'ScreamAPI Settings',
enableLabel: 'Enable ScreamAPI Configuration',
enableSublabel: 'Enable this to customise ScreamAPI settings for this game',
defaultConfig: {
$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: {},
},
readCommand: 'read_screamapi_config',
writeCommand: 'write_screamapi_config',
deleteCommand: 'delete_screamapi_config',
sections: [
{
title: 'General Settings',
fields: [
{
kind: 'dropdown',
key: 'default_dlc_status',
label: 'Default DLC Status',
description: 'Specifies the default DLC unlock status',
options: DLC_STATUS_OPTIONS,
},
],
},
{
title: 'Logging',
fields: [
{
kind: 'checkbox',
key: 'logging',
label: 'Enable Logging',
sublabel: 'Enables logging to ScreamAPI.log.log file',
},
{
kind: 'checkbox',
key: 'log_eos',
label: 'Log EOS SDK',
sublabel: 'Intercept and log EOS SDK calls (requires logging enabled)',
},
],
},
{
title: 'Privacy',
fields: [
{
kind: 'checkbox',
key: 'block_metrics',
label: 'Block Metrics',
sublabel: 'Block game analytics/usage reporting to Epic Online Services',
},
],
},
],
}