updated steam

This commit is contained in:
Frank Harris 2026-01-17 10:18:59 -06:00
parent 8857f441e7
commit b2b46b23db
4 changed files with 426 additions and 163 deletions

View file

@ -9,6 +9,7 @@ class AdminWorkshopController
private array $lang;
private ?array $adapterFormOverride = null;
private ?string $adapterFormGameKey = null;
private array $gameGroups = [];
public function __construct(OGPDatabase $db)
{
@ -28,29 +29,25 @@ class AdminWorkshopController
echo '<link rel="stylesheet" type="text/css" href="modules/steam_workshop/steam_workshop.css" />';
$this->gameGroups = $this->service->listWorkshopGameGroups();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$this->processPost();
}
$gameKeys = $this->service->listAvailableGameKeys();
$mappings = $this->service->getAdapterMappings();
$adapters = $this->service->loadAdapters();
$adapterOptions = $this->service->getAdapterOptions();
$gameRows = $this->buildGameRows($gameKeys);
$activeGame = $this->resolveActiveGameKey();
$adapterForm = $activeGame !== ''
? $this->service->getAdapterFormData($activeGame, $this->adapterFormOverride)
: null;
$gameRows = $this->buildGameRows($mappings);
$requestedGame = $this->sanitizeGameKeyInput($_GET['adapter_game'] ?? '');
$activeGame = $this->adapterFormGameKey !== null ? $this->adapterFormGameKey : $requestedGame;
$this->render('admin/index', [
'lang' => $this->lang,
'gameKeys' => $gameKeys,
'mappings' => $mappings,
'adapters' => $adapters,
'adapterOptions' => $adapterOptions,
'gameRows' => $gameRows,
'adapterForm' => $adapterForm,
'activeGameKey' => $activeGame,
]);
}
@ -78,7 +75,22 @@ class AdminWorkshopController
if (!is_array($payload)) {
$payload = [];
}
$this->service->saveAdapterMappings($payload);
$fanOut = [];
$groupIndex = $this->indexGameGroups();
foreach ($payload as $groupKey => $adapterKey) {
$groupKey = (string)$groupKey;
$adapterKey = (string)$adapterKey;
if (!isset($groupIndex[$groupKey])) {
continue;
}
foreach ($groupIndex[$groupKey] as $gameKey) {
$fanOut[$gameKey] = $adapterKey;
}
}
$this->service->saveAdapterMappings($fanOut);
print_success($this->lang['message_mappings_saved'] ?? 'Adapter mappings saved.');
}
@ -97,7 +109,7 @@ class AdminWorkshopController
try {
$this->service->saveGameAdapter($gameKey, $payload);
$this->service->upsertAdapterMapping($gameKey, $gameKey);
$this->propagateAdapterMapping($gameKey);
print_success($this->lang['message_adapter_saved'] ?? 'Adapter saved.');
$this->adapterFormOverride = null;
$this->adapterFormGameKey = null;
@ -125,36 +137,85 @@ class AdminWorkshopController
}
if ($this->service->deleteGameAdapter($gameKey)) {
$this->service->removeAdapterMapping($gameKey, $gameKey);
$this->clearGroupMappings($gameKey);
print_success($this->lang['message_adapter_deleted'] ?? 'Adapter deleted.');
} else {
print_failure($this->lang['error_adapter_delete_failed'] ?? 'Unable to delete adapter.');
}
}
private function buildGameRows(array $gameKeys): array
private function buildGameRows(array $mappings): array
{
$rows = [];
foreach ($gameKeys as $gameKey) {
foreach ($this->gameGroups as $group) {
$primaryKey = $group['primary_game_key'];
$override = ($this->adapterFormGameKey === $primaryKey) ? $this->adapterFormOverride : null;
$mappingValues = [];
foreach ($group['game_keys'] as $gameKey) {
if (isset($mappings[$gameKey]) && $mappings[$gameKey] !== '') {
$mappingValues[$mappings[$gameKey]] = true;
}
}
$rows[] = [
'game_key' => $gameKey,
'exists' => $this->service->gameAdapterExists($gameKey),
'adapter' => $this->service->getGameAdapter($gameKey),
'updated_at' => $this->service->getGameAdapterUpdatedAt($gameKey),
'group_key' => $group['group_key'],
'app_id' => $group['app_id'],
'game_name' => $group['game_name'],
'game_keys' => $group['game_keys'],
'primary_game_key' => $primaryKey,
'mixed_mapping' => count($mappingValues) > 1,
'selected_adapter' => count($mappingValues) === 1 ? array_key_first($mappingValues) : '',
'exists' => $this->service->gameAdapterExists($primaryKey),
'adapter' => $this->service->getGameAdapter($primaryKey),
'updated_at' => $this->service->getGameAdapterUpdatedAt($primaryKey),
'form' => $this->service->getAdapterFormData($primaryKey, $override),
];
}
return $rows;
}
private function resolveActiveGameKey(): string
private function indexGameGroups(): array
{
if ($this->adapterFormGameKey !== null) {
return $this->adapterFormGameKey;
$index = [];
foreach ($this->gameGroups as $group) {
$index[$group['group_key']] = $group['game_keys'];
}
$queryKey = $_GET['adapter_game'] ?? '';
return $this->sanitizeGameKeyInput($queryKey);
return $index;
}
private function propagateAdapterMapping(string $primaryGameKey): void
{
foreach ($this->gameGroups as $group) {
if (!in_array($primaryGameKey, $group['game_keys'], true)) {
continue;
}
foreach ($group['game_keys'] as $gameKey) {
$this->service->upsertAdapterMapping($gameKey, $primaryGameKey);
}
return;
}
$this->service->upsertAdapterMapping($primaryGameKey, $primaryGameKey);
}
private function clearGroupMappings(string $primaryGameKey): void
{
foreach ($this->gameGroups as $group) {
if (!in_array($primaryGameKey, $group['game_keys'], true)) {
continue;
}
foreach ($group['game_keys'] as $gameKey) {
$this->service->removeAdapterMapping($gameKey, $primaryGameKey);
}
return;
}
$this->service->removeAdapterMapping($primaryGameKey, $primaryGameKey);
}
private function sanitizeGameKeyInput($value): string

View file

@ -497,23 +497,63 @@ class SteamWorkshopService
return unlink($path);
}
public function listAvailableGameKeys(): array
public function listWorkshopGameGroups(): array
{
$keys = [];
$configDir = defined('SERVER_CONFIG_LOCATION') ? SERVER_CONFIG_LOCATION : __DIR__ . '/../../config_games/server_configs';
$groups = [];
foreach (glob($configDir . '/*.xml') as $file) {
$xml = @simplexml_load_file($file);
if ($xml === false) {
continue;
}
if (isset($xml->game_key)) {
$keys[] = trim((string)$xml->game_key);
$gameKey = isset($xml->game_key) ? trim((string)$xml->game_key) : '';
if ($gameKey === '') {
continue;
}
$appId = $this->parseSteamAppIdFromConfig($xml);
if ($appId === null) {
continue;
}
$groupKey = $this->buildWorkshopGroupKey($appId);
if (!isset($groups[$groupKey])) {
$gameName = isset($xml->game_name) ? trim((string)$xml->game_name) : '';
$groups[$groupKey] = [
'group_key' => $groupKey,
'app_id' => $appId,
'game_name' => $gameName !== '' ? $gameName : $gameKey,
'game_keys' => [],
];
}
$groups[$groupKey]['game_keys'][] = $gameKey;
}
$keys = array_filter(array_unique($keys));
sort($keys);
return array_values($keys);
foreach ($groups as &$group) {
$group['game_keys'] = array_values(array_unique($group['game_keys']));
sort($group['game_keys']);
$group['primary_game_key'] = $group['game_keys'][0];
}
unset($group);
usort($groups, static function (array $a, array $b): int {
return strcmp($a['game_name'], $b['game_name']);
});
return array_values($groups);
}
public function listAvailableGameKeys(): array
{
$keys = [];
foreach ($this->listWorkshopGameGroups() as $group) {
$keys = array_merge($keys, $group['game_keys']);
}
return array_values(array_unique($keys));
}
private function sanitizeInterval(?int $minutes): int
@ -676,4 +716,36 @@ class SteamWorkshopService
file_put_contents($this->adapterMapFile, json_encode([]));
}
}
private function parseSteamAppIdFromConfig($xml): ?string
{
if (!isset($xml->mods) || !isset($xml->mods->mod)) {
return null;
}
$candidate = null;
foreach ($xml->mods->mod as $mod) {
$installerName = trim((string)($mod->installer_name ?? ''));
if ($installerName === '' || preg_match('/\D/', $installerName)) {
continue;
}
$modName = strtolower(trim((string)($mod->name ?? '')));
$modKey = strtolower(trim((string)($mod['key'] ?? '')));
if ($modKey === 'default' || $modName === 'none' || $modName === '') {
return $installerName;
}
if ($candidate === null) {
$candidate = $installerName;
}
}
return $candidate;
}
private function buildWorkshopGroupKey(string $appId): string
{
return 'steamapp_' . $appId;
}
}

View file

@ -123,3 +123,107 @@
.sw-toggle input {
width: auto;
}
.sw-game-table__wrapper {
margin-top: 1rem;
overflow-x: auto;
}
.sw-game-table__row td {
vertical-align: top;
}
.sw-game-label {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.sw-game-label__title {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.4rem;
}
.sw-game-label__key {
font-weight: 600;
}
.sw-game-label__name {
font-weight: 600;
}
.sw-game-label__hint {
color: #666;
}
.sw-game-label__hint--warning {
color: #b15a00;
}
.sw-badge {
display: inline-flex;
align-items: center;
padding: 0.1rem 0.5rem;
border-radius: 999px;
font-size: 0.75rem;
}
.sw-badge--custom {
background: #eef4ff;
color: #0b5ed7;
}
.sw-badge--app {
background: #f1f3f5;
color: #444;
}
.sw-game-variants {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.sw-chip {
background: #f4f6f8;
border-radius: 999px;
padding: 0.1rem 0.55rem;
font-size: 0.8rem;
}
.sw-actions {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.sw-inline-delete {
display: inline;
}
.sw-game-table__form-row {
display: none;
}
.sw-game-table__form-row.is-open {
display: table-row;
}
.sw-inline-form {
background: #f6f8fb;
border: 1px solid #d0dae9;
border-radius: 6px;
padding: 1rem;
}
.sw-checkbox {
display: flex;
align-items: center;
gap: 0.4rem;
}
.sw-admin__mapping-actions {
margin-top: 1rem;
}

View file

@ -1,169 +1,162 @@
<?php
declare(strict_types=1);
/** @var array $lang */
/** @var array $gameKeys */
/** @var array $mappings */
/** @var array $gameRows */
/** @var array $adapterOptions */
/** @var array $adapters */
/** @var array $gameRows */
/** @var array|null $adapterForm */
/** @var string $activeGameKey */
?>
<div class="sw-admin">
<h3><?php echo htmlspecialchars($lang['admin_heading_game_mapping'] ?? 'Game type adapter mapping'); ?></h3>
<p><?php echo htmlspecialchars($lang['admin_subheading_game_mapping'] ?? 'Select which adapter will manage Steam Workshop installs for each supported game.'); ?></p>
<div class="sw-admin__intro">
<h3><?php echo htmlspecialchars($lang['admin_heading_game_mapping'] ?? 'Game type adapter mapping'); ?></h3>
<p><?php echo htmlspecialchars($lang['admin_subheading_game_mapping'] ?? 'Assign an adapter and edit its XML without leaving the table.'); ?></p>
</div>
<form method="post" class="sw-form">
<form id="sw-mapping-form" method="post">
<input type="hidden" name="admin_action" value="save_mappings">
<table class="table sw-mods__table">
</form>
<div class="sw-game-table__wrapper">
<table class="table sw-game-table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['admin_col_game_key'] ?? 'Game key'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_adapter'] ?? 'Adapter'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_adapter'] ?? 'Mapping'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_status'] ?? 'Adapter status'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_updated'] ?? 'Last updated'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($gameKeys)): ?>
<?php if (empty($gameRows)): ?>
<tr>
<td colspan="2"><?php echo htmlspecialchars($lang['admin_no_game_keys'] ?? 'No game definitions were found in modules/config_games/server_configs.'); ?></td>
<td colspan="5"><?php echo htmlspecialchars($lang['admin_no_game_keys'] ?? 'No Steam Workshop-enabled game definitions were detected.'); ?></td>
</tr>
<?php else: ?>
<?php foreach ($gameKeys as $gameKey): ?>
<tr>
<td><?php echo htmlspecialchars($gameKey); ?></td>
<?php foreach ($gameRows as $row): ?>
<?php
$groupKey = $row['group_key'];
$primaryKey = $row['primary_game_key'];
$selectValue = $row['selected_adapter'] ?: ($row['exists'] ? $primaryKey : '');
$statusLabel = $row['exists']
? ($row['adapter']['name'] ?? $primaryKey)
: ($lang['status_no_adapter'] ?? 'No adapter');
$isOpen = ($activeGameKey !== '' && $activeGameKey === $primaryKey);
$formId = 'adapter-panel-' . preg_replace('/[^a-z0-9_-]/i', '', $groupKey);
$form = $row['form'];
?>
<tr class="sw-game-table__row">
<td>
<select name="mapping[<?php echo htmlspecialchars($gameKey); ?>]">
<div class="sw-game-label">
<div class="sw-game-label__title">
<span class="sw-game-label__name"><?php echo htmlspecialchars($row['game_name']); ?></span>
<span class="sw-badge sw-badge--app">App ID <?php echo htmlspecialchars($row['app_id']); ?></span>
<?php if ($row['exists']): ?>
<span class="sw-badge sw-badge--custom"><?php echo htmlspecialchars($lang['badge_custom_xml'] ?? 'Custom XML'); ?></span>
<?php endif; ?>
</div>
<div class="sw-game-variants">
<?php foreach ($row['game_keys'] as $variantKey): ?>
<span class="sw-chip"><?php echo htmlspecialchars($variantKey); ?></span>
<?php endforeach; ?>
</div>
</div>
<small class="sw-game-label__hint"><?php echo htmlspecialchars($lang['admin_hint_inline_edit'] ?? 'Use the toggle to edit the XML inline.'); ?></small>
</td>
<td>
<select form="sw-mapping-form" name="mapping[<?php echo htmlspecialchars($groupKey); ?>]">
<option value="">--</option>
<?php foreach ($adapterOptions as $key => $label): ?>
<option value="<?php echo htmlspecialchars($key); ?>" <?php echo (isset($mappings[$gameKey]) && $mappings[$gameKey] === $key) ? 'selected' : ''; ?>>
<option value="<?php echo htmlspecialchars($key); ?>" <?php echo ($selectValue === $key) ? 'selected' : ''; ?>>
<?php echo htmlspecialchars($label); ?>
</option>
<?php endforeach; ?>
</select>
<?php if (!empty($row['mixed_mapping'])): ?>
<small class="sw-game-label__hint sw-game-label__hint--warning"><?php echo htmlspecialchars($lang['admin_hint_mixed_mapping'] ?? 'Different adapters assigned across variants. Saving will sync them.'); ?></small>
<?php endif; ?>
</td>
<td><?php echo htmlspecialchars($statusLabel); ?></td>
<td>
<?php if (!empty($row['updated_at'])): ?>
<?php echo htmlspecialchars(date('Y-m-d H:i', (int)$row['updated_at'])); ?>
<?php else: ?>
&mdash;
<?php endif; ?>
</td>
<td class="sw-actions">
<button type="button" class="btn secondary js-toggle-adapter" data-target="<?php echo htmlspecialchars($formId); ?>" aria-expanded="<?php echo $isOpen ? 'true' : 'false'; ?>">
<?php echo htmlspecialchars($row['exists'] ? ($lang['button_edit_adapter'] ?? 'Edit adapter') : ($lang['button_create_adapter'] ?? 'Create adapter')); ?>
</button>
<?php if ($row['exists']): ?>
<form method="post" class="sw-inline-delete">
<input type="hidden" name="admin_action" value="delete_adapter">
<input type="hidden" name="game_key" value="<?php echo htmlspecialchars($primaryKey); ?>">
<button type="submit" class="btn danger" onclick="return confirm('<?php echo htmlspecialchars($lang['confirm_delete_adapter'] ?? 'Delete this adapter?'); ?>');">
<?php echo htmlspecialchars($lang['button_delete_adapter'] ?? 'Delete'); ?>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<tr id="<?php echo htmlspecialchars($formId); ?>" class="sw-game-table__form-row <?php echo $isOpen ? 'is-open' : ''; ?>">
<td colspan="5">
<form method="post" class="sw-form sw-inline-form">
<input type="hidden" name="admin_action" value="save_adapter">
<input type="hidden" name="game_key" value="<?php echo htmlspecialchars($form['game_key']); ?>">
<div class="sw-form__grid">
<label>
<?php echo htmlspecialchars($lang['label_game_key'] ?? 'Game key'); ?>
<input type="text" value="<?php echo htmlspecialchars($form['game_key']); ?>" readonly>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_name'] ?? 'Adapter display name'); ?>
<input type="text" name="adapter[name]" value="<?php echo htmlspecialchars($form['name']); ?>" required>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_app_id'] ?? 'Steam App ID'); ?>
<input type="text" name="adapter[steam_app_id]" value="<?php echo htmlspecialchars($form['steam_app_id']); ?>" required>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_mods_dir'] ?? 'Mods directory'); ?>
<input type="text" name="adapter[mods_dir]" value="<?php echo htmlspecialchars($form['mods_dir']); ?>" required>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_keys_dir'] ?? 'Keys directory (optional)'); ?>
<input type="text" name="adapter[keys_dir]" value="<?php echo htmlspecialchars($form['keys_dir']); ?>">
</label>
<label class="sw-checkbox">
<input type="checkbox" name="adapter[supports_hot_reload]" value="1" <?php echo !empty($form['supports_hot_reload']) ? 'checked' : ''; ?>>
<span><?php echo htmlspecialchars($lang['label_adapter_hot_reload'] ?? 'Supports hot reload'); ?></span>
</label>
</div>
<label>
<?php echo htmlspecialchars($lang['label_adapter_activation'] ?? 'Activation template'); ?>
<textarea name="adapter[activation_template]" rows="3"><?php echo htmlspecialchars($form['activation_template']); ?></textarea>
</label>
<label>
<?php echo htmlspecialchars($lang['label_adapter_notes'] ?? 'Notes'); ?>
<textarea name="adapter[notes]" rows="2"><?php echo htmlspecialchars($form['notes']); ?></textarea>
</label>
<div class="sw-form__actions">
<button class="btn primary" type="submit"><?php echo htmlspecialchars($lang['button_save_adapter'] ?? 'Save adapter'); ?></button>
<button type="button" class="btn js-toggle-adapter" data-target="<?php echo htmlspecialchars($formId); ?>"><?php echo htmlspecialchars($lang['button_cancel'] ?? 'Cancel'); ?></button>
</div>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<div class="sw-form__actions">
<button class="btn primary" type="submit"><?php echo htmlspecialchars($lang['button_save']); ?></button>
</div>
</form>
</div>
<h3><?php echo htmlspecialchars($lang['admin_heading_per_game'] ?? 'Per-game adapters'); ?></h3>
<p><?php echo htmlspecialchars($lang['admin_subheading_per_game'] ?? 'Each game key gets its own adapter XML. Create, edit, or delete them below.'); ?></p>
<table class="table sw-mods__table">
<thead>
<tr>
<th><?php echo htmlspecialchars($lang['admin_col_game_key'] ?? 'Game key'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_status'] ?? 'Status'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_updated'] ?? 'Last updated'); ?></th>
<th><?php echo htmlspecialchars($lang['admin_col_actions'] ?? 'Actions'); ?></th>
</tr>
</thead>
<tbody>
<?php if (empty($gameRows)): ?>
<tr>
<td colspan="4"><?php echo htmlspecialchars($lang['admin_no_game_keys'] ?? 'No game definitions were found in modules/config_games/server_configs.'); ?></td>
</tr>
<?php else: ?>
<?php foreach ($gameRows as $row): ?>
<?php $exists = !empty($row['exists']); ?>
<tr>
<td><?php echo htmlspecialchars($row['game_key']); ?></td>
<td>
<?php if ($exists): ?>
<?php echo htmlspecialchars($row['adapter']['name'] ?? $row['game_key']); ?>
<?php else: ?>
<?php echo htmlspecialchars($lang['status_no_adapter'] ?? 'No adapter'); ?>
<?php endif; ?>
</td>
<td>
<?php if ($exists && !empty($row['updated_at'])): ?>
<?php echo htmlspecialchars(date('Y-m-d H:i', (int)$row['updated_at'])); ?>
<?php else: ?>
&mdash;
<?php endif; ?>
</td>
<td class="sw-actions">
<a class="btn secondary" href="?m=steam_workshop&amp;p=workshop_admin&amp;adapter_game=<?php echo urlencode($row['game_key']); ?>#adapter-form">
<?php echo htmlspecialchars($exists ? ($lang['button_edit_adapter'] ?? 'Edit') : ($lang['button_create_adapter'] ?? 'Create')); ?>
</a>
<?php if ($exists): ?>
<form method="post" style="display:inline;">
<input type="hidden" name="admin_action" value="delete_adapter">
<input type="hidden" name="game_key" value="<?php echo htmlspecialchars($row['game_key']); ?>">
<button type="submit" class="btn danger" onclick="return confirm('<?php echo htmlspecialchars($lang['confirm_delete_adapter'] ?? 'Delete this adapter?'); ?>');">
<?php echo htmlspecialchars($lang['button_delete_adapter'] ?? 'Delete'); ?>
</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
<div id="adapter-form" class="sw-adapter-form">
<?php if ($adapterForm): ?>
<h3><?php echo htmlspecialchars(sprintf($lang['admin_heading_edit_adapter'] ?? 'Editing adapter for %s', $adapterForm['game_key'])); ?></h3>
<form method="post" class="sw-form">
<input type="hidden" name="admin_action" value="save_adapter">
<input type="hidden" name="game_key" value="<?php echo htmlspecialchars($adapterForm['game_key']); ?>">
<div class="sw-form__row">
<label><?php echo htmlspecialchars($lang['label_game_key'] ?? 'Game key'); ?></label>
<input type="text" value="<?php echo htmlspecialchars($adapterForm['game_key']); ?>" readonly>
</div>
<div class="sw-form__row">
<label><?php echo htmlspecialchars($lang['label_adapter_name'] ?? 'Adapter display name'); ?></label>
<input type="text" name="adapter[name]" value="<?php echo htmlspecialchars($adapterForm['name']); ?>" required>
</div>
<div class="sw-form__row">
<label><?php echo htmlspecialchars($lang['label_adapter_app_id'] ?? 'Steam App ID'); ?></label>
<input type="text" name="adapter[steam_app_id]" value="<?php echo htmlspecialchars($adapterForm['steam_app_id']); ?>" required>
</div>
<div class="sw-form__row">
<label><?php echo htmlspecialchars($lang['label_adapter_mods_dir'] ?? 'Mods directory'); ?></label>
<input type="text" name="adapter[mods_dir]" value="<?php echo htmlspecialchars($adapterForm['mods_dir']); ?>" required>
</div>
<div class="sw-form__row">
<label><?php echo htmlspecialchars($lang['label_adapter_keys_dir'] ?? 'Keys directory (optional)'); ?></label>
<input type="text" name="adapter[keys_dir]" value="<?php echo htmlspecialchars($adapterForm['keys_dir']); ?>">
</div>
<div class="sw-form__row">
<label class="checkbox">
<input type="checkbox" name="adapter[supports_hot_reload]" value="1" <?php echo !empty($adapterForm['supports_hot_reload']) ? 'checked' : ''; ?> >
<span><?php echo htmlspecialchars($lang['label_adapter_hot_reload'] ?? 'Supports hot reload'); ?></span>
</label>
</div>
<div class="sw-form__row">
<label><?php echo htmlspecialchars($lang['label_adapter_activation'] ?? 'Activation template'); ?></label>
<textarea name="adapter[activation_template]" rows="4"><?php echo htmlspecialchars($adapterForm['activation_template']); ?></textarea>
</div>
<div class="sw-form__row">
<label><?php echo htmlspecialchars($lang['label_adapter_notes'] ?? 'Notes'); ?></label>
<textarea name="adapter[notes]" rows="3"><?php echo htmlspecialchars($adapterForm['notes']); ?></textarea>
</div>
<div class="sw-form__actions">
<button class="btn primary" type="submit"><?php echo htmlspecialchars($lang['button_save_adapter'] ?? 'Save adapter'); ?></button>
<a class="btn" href="?m=steam_workshop&amp;p=workshop_admin"><?php echo htmlspecialchars($lang['button_cancel'] ?? 'Cancel'); ?></a>
</div>
</form>
<?php else: ?>
<p><?php echo htmlspecialchars($lang['admin_hint_select_game'] ?? 'Select a game above to start editing its adapter.'); ?></p>
<?php endif; ?>
<div class="sw-form__actions sw-admin__mapping-actions">
<button class="btn primary" type="submit" form="sw-mapping-form"><?php echo htmlspecialchars($lang['button_save']); ?></button>
</div>
<h3><?php echo htmlspecialchars($lang['admin_heading_adapters'] ?? 'Available adapters'); ?></h3>
@ -192,3 +185,36 @@ declare(strict_types=1);
</tbody>
</table>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const toggleRow = function (targetId) {
const row = document.getElementById(targetId);
if (!row) {
return;
}
row.classList.toggle('is-open');
const expanded = row.classList.contains('is-open');
const toggleButtons = document.querySelectorAll('.js-toggle-adapter[data-target="' + targetId + '"]');
toggleButtons.forEach(btn => btn.setAttribute('aria-expanded', expanded ? 'true' : 'false'));
if (expanded) {
const focusable = row.querySelector('input:not([type="hidden"]), textarea, select');
if (focusable) {
focusable.focus();
}
}
};
document.querySelectorAll('.js-toggle-adapter').forEach(button => {
button.addEventListener('click', function () {
const targetId = button.getAttribute('data-target');
if (targetId) {
toggleRow(targetId);
}
});
});
});
</script>