8c8079fe19
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
169 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
};
|