Files
lanspread/crates/lanspread-tauri-deno-ts/src/components/modals/GameDetailModal.tsx
T
ddidderr 8c8079fe19 feat: pass profile settings to launch scripts
Add launcher profile settings for username and language, then thread those
values into the Windows script launch path. The game setup, game start, and
server start scripts now share the same argument shape:

- game path: local
- game id
- language: en or de
- player name

Expose a local can_host_server flag in the games payload by checking for
server_start.cmd in an installed game's root directory. The detail modal uses
that flag to show Start Server only for installed games with the script, and
the new start_server command invokes server_start.cmd with the same sanitized
settings used by game_setup.cmd and game_start.cmd.

Test Plan:
- just fmt
- just test
- just frontend-test
- just build
- just clippy
- git diff --check

Refs: design/README.md
2026-05-21 09:40:23 +02:00

169 lines
6.3 KiB
TypeScript

import { Modal } from '../Modal';
import { Icon } from '../Icon';
import { GameCover } from '../grid/GameCover';
import { StateChip } from '../StateChip';
import { ActionButton } from '../ActionButton';
import { Game, InstallStatus } from '../../lib/types';
import { deriveState, isInProgress } from '../../lib/gameState';
import { formatBytes, formatEtiVersion, formatPlayers } from '../../lib/format';
interface Props {
game: Game;
thumbnailUrl: string | null;
onClose: () => void;
onPrimary: (game: Game) => void;
onUninstall: (game: Game) => void;
onRemoveDownload: (game: Game) => void;
onCancelDownload: (game: Game) => void;
onStartServer: (game: Game) => void;
onViewFiles: (game: Game) => void;
}
const tagsFromGame = (game: Game): string[] => {
const tags: string[] = [];
if (game.genre) tags.push(game.genre);
if (game.publisher) tags.push(game.publisher);
if (game.release_year) tags.push(game.release_year);
return tags;
};
const statusLabelFor = (game: Game): string => {
switch (deriveState(game)) {
case 'installed': return 'Installed';
case 'local': return 'Downloaded';
case 'downloading': return 'Downloading';
case 'busy': return 'Working…';
case 'none': return 'Not downloaded';
}
};
export const GameDetailModal = ({
game,
thumbnailUrl,
onClose,
onPrimary,
onUninstall,
onRemoveDownload,
onCancelDownload,
onStartServer,
onViewFiles,
}: Props) => {
const tags = tagsFromGame(game);
const canRemoveDownload = game.downloaded
&& !game.installed
&& !isInProgress(game.install_status);
const canViewFiles = game.downloaded
|| game.installed
|| game.install_status === InstallStatus.Downloading;
return (
<Modal onClose={onClose}>
<button className="modal-close" type="button" onClick={onClose} aria-label="Close">
<Icon.close />
</button>
<div className="modal-hero">
<GameCover game={game} aspect="banner" thumbnailUrl={thumbnailUrl} hideTitle />
<div className="modal-hero-fade" />
<div className="modal-hero-text">
{tags.length > 0 && (
<div className="modal-tags">
{tags.map(t => <span key={t} className="modal-tag">{t}</span>)}
</div>
)}
<h2 className="modal-title">{game.name}</h2>
</div>
<div className="modal-state">
<StateChip game={game} showNone />
</div>
</div>
<div className="modal-body">
<div className="modal-meta">
<div className="meta-cell">
<div className="meta-label">Size</div>
<div className="meta-value">{formatBytes(game.size)}</div>
</div>
<div className="meta-cell">
<div className="meta-label">Players</div>
<div className="meta-value">
<Icon.users /> {formatPlayers(game.max_players)}
</div>
</div>
<div className="meta-cell">
<div className="meta-label">Version</div>
<div className="meta-value meta-mono">
{formatEtiVersion(game.local_version ?? game.eti_game_version)}
</div>
</div>
<div className="meta-cell">
<div className="meta-label">Status</div>
<div className="meta-value">{statusLabelFor(game)}</div>
</div>
</div>
{game.description && (
<p className="modal-desc">{game.description}</p>
)}
{game.status_message && (
<p className={`modal-status${game.status_level === 'error' ? ' is-error' : ''}`}>
{game.status_message}
</p>
)}
<div className="modal-actions">
<ActionButton
game={game}
size="lg"
onClick={() => onPrimary(game)}
onCancelDownload={onCancelDownload}
/>
{game.installed && game.can_host_server === true && (
<button
type="button"
className="act-btn act-lg act-server"
onClick={() => onStartServer(game)}
>
<Icon.server />
<span className="act-label">Start Server</span>
</button>
)}
{game.installed && (
<button
type="button"
className="ghost-btn ghost-danger"
onClick={() => onUninstall(game)}
>
<Icon.trash />
<span>Uninstall</span>
</button>
)}
{canRemoveDownload && (
<button
type="button"
className="ghost-btn ghost-danger"
onClick={() => onRemoveDownload(game)}
>
<Icon.trash />
<span>Remove files</span>
</button>
)}
{canViewFiles && (
<>
<div className="modal-actions-spacer" />
<button
type="button"
className="ghost-btn"
onClick={() => onViewFiles(game)}
>
<Icon.folder />
<span>View Files</span>
</button>
</>
)}
</div>
</div>
</Modal>
);
};