diff --git a/src/components/dialogs/ApiSettingsDialog.tsx b/src/components/dialogs/ApiSettingsDialog.tsx new file mode 100644 index 0000000..c582d50 --- /dev/null +++ b/src/components/dialogs/ApiSettingsDialog.tsx @@ -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[] + } + | { + 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 + 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>(spec.defaultConfig) + const [isLoading, setIsLoading] = useState(false) + const [hasChanges, setHasChanges] = useState(false) + + const loadConfig = useCallback(async () => { + setIsLoading(true) + try { + const existingConfig = await invoke | 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 ( + + +
+

{spec.dialogTitle}

+
+

{gameTitle}

+
+ + +
+
+ { + setEnabled(!enabled) + setHasChanges(true) + }} + label={spec.enableLabel} + sublabel={spec.enableSublabel} + /> +
+ +
+ {spec.sections.map((section) => ( +
+

{section.title}

+ + {section.fields.map((field) => + field.kind === 'dropdown' ? ( + updateConfig(field.key, value)} + disabled={!enabled} + /> + ) : ( +
+ updateConfig(field.key, !config[field.key])} + label={field.label} + sublabel={field.sublabel} + /> +
+ ) + )} +
+ ))} +
+
+
+ + + + + + + +
+ ) +} + +export default ApiSettingsDialog diff --git a/src/components/dialogs/apiSettingsSpecs.ts b/src/components/dialogs/apiSettingsSpecs.ts new file mode 100644 index 0000000..1815786 --- /dev/null +++ b/src/components/dialogs/apiSettingsSpecs.ts @@ -0,0 +1,136 @@ +import { DropdownOption } from '@/components/common' +import { ApiSettingsSpec } from './ApiSettingsDialog' + +const DLC_STATUS_OPTIONS: DropdownOption[] = [ + { 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', + }, + ], + }, + ], +}