diff --git a/agentm/components/game_image_preview.py b/agentm/components/game_image_preview.py new file mode 100644 index 0000000..9c99b83 --- /dev/null +++ b/agentm/components/game_image_preview.py @@ -0,0 +1,48 @@ +from textual.widgets import Static +from PIL import Image, ImageFilter +from rich_pixels import Pixels +from rich_pixels._renderer import HalfcellRenderer +from pathlib import Path + +class GameImagePreview(Static): + def __init__( + self, + image_path: str, + *, + scale_factor: float = 0.098, + fallback_text: str = "[red]Failed to load image[/red]", + **kwargs + ): + self.image_path = image_path + self.scale_factor = scale_factor + self.fallback_text = fallback_text + renderable = self.load_and_process_image() + + # THIS is the key line that makes the image render: + super().__init__(renderable, **kwargs) + + def load_and_process_image(self): + path = Path(self.image_path) + if not path.exists() or not path.is_file(): + return f"{self.fallback_text}\n[dim]Not found: {self.image_path}[/]" + + try: + with Image.open(path) as img: + if img.mode != "RGBA": + img = img.convert("RGBA") + + resized = img.resize( + ( + int(img.width * self.scale_factor), + int(img.height * self.scale_factor), + ), + resample=Image.Resampling.LANCZOS, + ) + resized = resized.filter(ImageFilter.UnsharpMask(radius=1, percent=150, threshold=3)) + + return Pixels.from_image( + resized, + renderer=HalfcellRenderer(default_color="black"), + ) + except Exception as e: + return f"{self.fallback_text}\n[dim]{e}[/]" diff --git a/agentm/views/home.py b/agentm/views/home.py index 2d7db24..6b63cab 100644 --- a/agentm/views/home.py +++ b/agentm/views/home.py @@ -19,6 +19,7 @@ from agentm.utils.logger import log_with_caller from agentm.logic.roms import get_verified_roms, GAME_FILES from agentm.theme.palette import get_theme from agentm.components.footer import AgentMFooter +from agentm.components.game_image_preview import GameImagePreview palette = get_theme() @@ -39,51 +40,27 @@ class ProgressWidget(Widget): class GameAccordion(Static): def __init__(self, title: str, rom_file: str, metadata: dict, parent_view): + super().__init__( + id=f"accordion_{rom_file.replace('.', '_').replace('-', '_')}", + classes="game_card" + ) + 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) - - # Title self.title_label = Static( f"[b {palette.ACCENT}]{escape(self.title.upper())}[/]\n", classes="game_title", markup=True ) - 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.098 - 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}]" + self.image_path = os.path.abspath(self.metadata.get("image_path", "")) def compose(self): yield self.title_label - yield Static(self.image_renderable) + yield GameImagePreview(image_path=self.image_path) async def on_click(self): await self.display_info() @@ -111,6 +88,7 @@ class GameAccordion(Static): self.parent_view.selected_game = meta + class HomeView(Screen): BINDINGS = [("escape", "app.quit", "Quit")]