agent_m/agentm/views/home.py
mscrnt a31fd760d9 Enhance game card styles and add image handling in the home view
- Updated styles for game cards, including hover and focus states.
- Integrated image loading for game cards, scaling images appropriately.
- Modified GameAccordion to display images and improved layout handling.
- Added image path management in ROM verification logic.
2025-05-21 12:22:08 -07:00

195 lines
7.9 KiB
Python

from textual.screen import Screen
from textual.widgets import Static, Button
from textual.containers import Vertical, Horizontal, HorizontalScroll
from textual.message import Message
from textual.reactive import reactive
from textual.widget import Widget
from rich.panel import Panel
from rich.console import Group
from PIL import Image, ImageEnhance, ImageFilter
import os
from rich_pixels import Pixels
from rich_pixels._renderer import HalfcellRenderer
from agentm.utils.logger import log_with_caller
from agentm.logic.roms import get_verified_roms, GAME_FILES
class GameSelected(Message):
def __init__(self, sender: Widget, metadata: dict):
self.metadata = metadata
super().__init__(sender)
class ProgressWidget(Widget):
message = reactive("Loading...")
def render(self) -> str:
return f"[bold cyan]{self.message}[/bold cyan]"
class GameAccordion(Static):
def __init__(self, title: str, rom_file: str, metadata: dict, parent_view):
self.title = title
self.rom_file = rom_file
self.metadata = metadata
self.parent_view = parent_view
self.safe_id = rom_file.replace(".", "_").replace("-", "_")
image_path = os.path.abspath(self.metadata.get("image_path", ""))
self.image_renderable = self.load_image_scaled(image_path)
super().__init__(id=f"accordion_{self.safe_id}", classes="game_card")
def load_image_scaled(self, path: str):
try:
with Image.open(path) as img:
if img.mode != "RGBA":
img = img.convert("RGBA")
scale_factor = 0.10
target_width = int(img.width * scale_factor)
target_height = int(img.height * scale_factor)
resized = img.resize(
(target_width, target_height),
resample=Image.Resampling.LANCZOS
)
resized = resized.filter(ImageFilter.UnsharpMask(radius=1, percent=150, threshold=3))
self._render_width = target_width
self._render_height = target_height // 2
return Pixels.from_image(
resized,
renderer=HalfcellRenderer(default_color="black"),
)
except Exception as e:
self._render_width = 24
self._render_height = 16
return f"[red]Failed to load image[/red]\n[dim]{e}]"
def render(self):
return Panel(
Group(
self.image_renderable,
"", # spacer
f"[center][bold orange1][u]{self.title.upper()}[/u][/bold orange1][/center]\n",
),
border_style="bright_magenta",
padding=(1, 2),
expand=False
)
async def on_click(self):
await self.display_info()
self.parent_view.highlight_selected(self)
async def display_info(self):
meta = self.metadata
log_with_caller("debug", f"Showing shared info for {self.rom_file}")
info_lines = [
f"[b]Title:[/b] {meta['title']}",
f"[b]Game ID:[/b] {meta['game_id']}",
f"[b]Difficulty:[/b] {meta.get('difficulty_min')} - {meta.get('difficulty_max')}",
f"[b]Characters:[/b] {', '.join(meta.get('characters', []))}",
f"[b]Keywords:[/b] {', '.join(meta.get('keywords', []))}",
f"[b]SHA256:[/b] {meta['sha256']}",
]
self.parent_view.shared_info_box.update("\n".join(info_lines))
self.parent_view.shared_confirm_button.label = f"✅ Confirm {meta['title']}"
self.parent_view.selected_game = meta
class HomeView(Screen):
BINDINGS = [("escape", "app.quit", "Quit")]
def highlight_selected(self, selected_widget: GameAccordion):
for card in self.rom_scroll_row.children:
if isinstance(card, GameAccordion):
card.remove_class("game_card_clicked")
selected_widget.add_class("game_card_clicked")
def compose(self):
self.logo = Static(
"[b bright_magenta]\n\n"
" █████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗ ███╗ ███╗\n"
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
"██╔══██║ ██║ ██║ ██╔══╝ ██║╚████║ ██║ ██║╚██╔╝██║\n"
"██║ ██║ ╚██████╔╝ ███████╗ ██║ ╚███║ ██║ ██║ ██║\n"
"╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═╝ ╚══╝ ╚═╝ ╚═╝ ╚═╝\n[/]",
classes="header",
expand=False,
)
self.progress_text = ProgressWidget()
yield Vertical(
self.logo,
Static("[bold green]LOADING...[/bold green]", expand=True),
self.progress_text,
id="loading_container"
)
async def on_mount(self):
log_with_caller("debug", "HomeView mounted. Starting ROM verification.")
self.selected_game = None
self.run_worker(self.run_verification, thread=True, exclusive=True, name="rom-verification")
def run_verification(self):
total = len(GAME_FILES)
verified_roms = get_verified_roms()
for idx, rom in enumerate(verified_roms, start=1):
self.app.call_from_thread(
lambda title=rom['title'], idx=idx: setattr(
self.progress_text, "message",
f"Processing {title} ({idx}/{total})"
)
)
import time
time.sleep(0.01)
self.app.call_from_thread(lambda: self.display_verified_roms(verified_roms))
async def display_verified_roms(self, verified_roms):
log_with_caller("info", f"ROM verification complete. Total: {len(verified_roms)}")
self.query_one("#loading_container").remove()
self.shared_info_box = Static("", id="game_info_box", classes="game_info")
self.shared_confirm_button = Button("✅ Confirm", id="confirm_button", classes="confirm_button")
self.rom_scroll_row = HorizontalScroll(id="rom_scroll_row", classes="rom_row")
await self.mount(
Vertical(
self.logo,
Static("🎮 Welcome to Agent M", classes="header", expand=False),
Static(
"This is an unofficial DIAMBRA launcher to help you easily train, evaluate, and submit RL agents for fighting games.\n\n"
"Verified ROMs are shown below. Click one to view game info and confirm selection.",
classes="body",
expand=False
),
self.rom_scroll_row,
Horizontal(
self.shared_info_box,
self.shared_confirm_button,
id="info_row",
classes="info_confirm_row",
),
id="home_screen_container",
classes="centered_layout"
)
)
for rom in verified_roms:
await self.rom_scroll_row.mount(GameAccordion(
title=rom["title"],
rom_file=rom["rom_file"],
metadata=rom,
parent_view=self
))
async def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "confirm_button" and self.selected_game:
await self.app.push_screen("training", self.selected_game)