Add GameImagePreview component to GameAccordion for image rendering

This commit is contained in:
mscrnt 2025-05-25 07:28:50 -07:00
parent 5611d31bd6
commit 9154f8ed3e
2 changed files with 57 additions and 31 deletions

View File

@ -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}[/]"

View File

@ -19,6 +19,7 @@ from agentm.utils.logger import log_with_caller
from agentm.logic.roms import get_verified_roms, GAME_FILES from agentm.logic.roms import get_verified_roms, GAME_FILES
from agentm.theme.palette import get_theme from agentm.theme.palette import get_theme
from agentm.components.footer import AgentMFooter from agentm.components.footer import AgentMFooter
from agentm.components.game_image_preview import GameImagePreview
palette = get_theme() palette = get_theme()
@ -39,51 +40,27 @@ class ProgressWidget(Widget):
class GameAccordion(Static): class GameAccordion(Static):
def __init__(self, title: str, rom_file: str, metadata: dict, parent_view): 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.title = title
self.rom_file = rom_file self.rom_file = rom_file
self.metadata = metadata self.metadata = metadata
self.parent_view = parent_view 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( self.title_label = Static(
f"[b {palette.ACCENT}]{escape(self.title.upper())}[/]\n", f"[b {palette.ACCENT}]{escape(self.title.upper())}[/]\n",
classes="game_title", classes="game_title",
markup=True markup=True
) )
super().__init__(id=f"accordion_{self.safe_id}", classes="game_card") self.image_path = os.path.abspath(self.metadata.get("image_path", ""))
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}]"
def compose(self): def compose(self):
yield self.title_label yield self.title_label
yield Static(self.image_renderable) yield GameImagePreview(image_path=self.image_path)
async def on_click(self): async def on_click(self):
await self.display_info() await self.display_info()
@ -111,6 +88,7 @@ class GameAccordion(Static):
self.parent_view.selected_game = meta self.parent_view.selected_game = meta
class HomeView(Screen): class HomeView(Screen):
BINDINGS = [("escape", "app.quit", "Quit")] BINDINGS = [("escape", "app.quit", "Quit")]