Add GameImagePreview component to GameAccordion for image rendering
This commit is contained in:
parent
5611d31bd6
commit
9154f8ed3e
48
agentm/components/game_image_preview.py
Normal file
48
agentm/components/game_image_preview.py
Normal 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}[/]"
|
||||
@ -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")]
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user