Refactor theming and styles: implement dynamic theme management, replace static styles with theme variables, and enhance game card UI
This commit is contained in:
parent
a31fd760d9
commit
da88ae71fe
@ -4,20 +4,25 @@ from agentm.views.login import LoginView
|
||||
from agentm import DIAMBRA_CREDENTIALS_PATH
|
||||
from agentm.utils.logger import log_with_caller
|
||||
from agentm.logic.db import initialize_database
|
||||
from agentm.theme.palette import get_theme # ← Add this import
|
||||
|
||||
|
||||
class AgentMApp(App):
|
||||
CSS_PATH = "assets/styles.tcss"
|
||||
CSS_PATH = "theme/styles.tcss"
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Called when the app starts."""
|
||||
log_with_caller("debug", "App mounted. Initializing database...")
|
||||
initialize_database()
|
||||
|
||||
# Initialize global theme instance (dark mode for now)
|
||||
log_with_caller("debug", "Initializing theme: dark")
|
||||
get_theme("dark")
|
||||
|
||||
log_with_caller("debug", "Checking for credentials...")
|
||||
|
||||
if DIAMBRA_CREDENTIALS_PATH.exists():
|
||||
token = DIAMBRA_CREDENTIALS_PATH.read_text().strip()
|
||||
|
||||
if token and len(token) > 10:
|
||||
log_with_caller("info", "Found populated DIAMBRA credentials. Launching Home.")
|
||||
self.push_screen(HomeView())
|
||||
|
||||
3
agentm/assets/game_titles.bak/doapp.txt
Normal file
3
agentm/assets/game_titles.bak/doapp.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+ +-+-+ +-+-+-+-+-+ +-+-+
|
||||
|D|E|A|D| |O|R| |A|L|I|V|E| |+|+|
|
||||
+-+-+-+-+ +-+-+ +-+-+-+-+-+ +-+-+
|
||||
3
agentm/assets/game_titles.bak/kof98umh.txt
Normal file
3
agentm/assets/game_titles.bak/kof98umh.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+ +-+-+ +-+-+-+-+-+-+-+-+
|
||||
|K|i|n|g| |o|f| |F|i|g|h|t|e|r|s|
|
||||
+-+-+-+-+ +-+-+ +-+-+-+-+-+-+-+-+
|
||||
3
agentm/assets/game_titles.bak/mvsc.txt
Normal file
3
agentm/assets/game_titles.bak/mvsc.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+-+-+ +-+-+ +-+-+-+-+-+-+
|
||||
|M|a|r|v|e|l| |v|s| |C|a|p|c|o|m|
|
||||
+-+-+-+-+-+-+ +-+-+ +-+-+-+-+-+-+
|
||||
3
agentm/assets/game_titles.bak/samsh5sp.txt
Normal file
3
agentm/assets/game_titles.bak/samsh5sp.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
|S|a|m|u|r|a|i| |S|h|o|w|d|o|w|n|
|
||||
+-+-+-+-+-+-+-+ +-+-+-+-+-+-+-+-+
|
||||
3
agentm/assets/game_titles.bak/sfiii3n.txt
Normal file
3
agentm/assets/game_titles.bak/sfiii3n.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+-+-+ +-+-+-+-+-+-+-+ +-+-+-+
|
||||
|S|t|r|e|e|t| |F|i|g|h|t|e|r| |I|I|I|
|
||||
+-+-+-+-+-+-+ +-+-+-+-+-+-+-+ +-+-+-+
|
||||
3
agentm/assets/game_titles.bak/soulclbr.txt
Normal file
3
agentm/assets/game_titles.bak/soulclbr.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+ +-+-+-+-+-+-+-+
|
||||
|S|o|u|l| |C|a|l|i|b|u|r|
|
||||
+-+-+-+-+ +-+-+-+-+-+-+-+
|
||||
3
agentm/assets/game_titles.bak/tektagt.txt
Normal file
3
agentm/assets/game_titles.bak/tektagt.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+-+-+ +-+-+-+
|
||||
|T|e|k|k|e|n| |T|a|g|
|
||||
+-+-+-+-+-+-+ +-+-+-+
|
||||
3
agentm/assets/game_titles.bak/umk3.txt
Normal file
3
agentm/assets/game_titles.bak/umk3.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+-+-+ +-+-+-+-+-+-+ +-+
|
||||
|M|o|r|t|a|l| |K|o|m|b|a|t| |3|
|
||||
+-+-+-+-+-+-+ +-+-+-+-+-+-+ +-+
|
||||
3
agentm/assets/game_titles.bak/xmvsf.txt
Normal file
3
agentm/assets/game_titles.bak/xmvsf.txt
Normal file
@ -0,0 +1,3 @@
|
||||
+-+-+-+-+-+-+ +-+-+-+-+-+-+ +-+
|
||||
|M|o|r|t|a|l| |K|o|m|b|a|t| |3|
|
||||
+-+-+-+-+-+-+ +-+-+-+-+-+-+ +-+
|
||||
29
agentm/theme/generate_theme_css.py
Normal file
29
agentm/theme/generate_theme_css.py
Normal file
@ -0,0 +1,29 @@
|
||||
from agentm.theme.palette import get_theme
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
# Load dark theme instance
|
||||
theme = get_theme("dark")
|
||||
|
||||
# Path setup
|
||||
base_path = Path(__file__).parent
|
||||
template_path = base_path / "styles.base.tcss"
|
||||
output_path = base_path / "styles.tcss"
|
||||
|
||||
# Load template
|
||||
template = template_path.read_text()
|
||||
|
||||
# Find all placeholders like {{FOREGROUND}}, {{BACKGROUND}}, etc.
|
||||
tokens = set(re.findall(r"{{\s*([A-Z0-9_]+)\s*}}", template))
|
||||
|
||||
# Replace them with actual values from theme
|
||||
for token in tokens:
|
||||
value = getattr(theme, token, None)
|
||||
if value is not None:
|
||||
template = template.replace(f"{{{{{token}}}}}", value)
|
||||
else:
|
||||
print(f"⚠️ Warning: Theme token '{token}' not found in ThemeManager")
|
||||
|
||||
# Write final output
|
||||
output_path.write_text(template)
|
||||
print(f"✅ Synced themed CSS written to: {output_path}")
|
||||
94
agentm/theme/palette.py
Normal file
94
agentm/theme/palette.py
Normal file
@ -0,0 +1,94 @@
|
||||
from typing import Literal, Optional
|
||||
|
||||
class ThemeManager:
|
||||
def __init__(self, theme: Literal["dark", "light"] = "dark"):
|
||||
self.use_theme(theme)
|
||||
|
||||
def use_theme(self, theme: str):
|
||||
if theme == "dark":
|
||||
self.ACCENT = "#ed7d3a"
|
||||
self.ACCENT_HOVER = "rgb(236, 194, 169)"
|
||||
self.BACKGROUND = "#0e0e0e"
|
||||
self.FOREGROUND = "#f0f0f0"
|
||||
self.BORDER = "#3a9bed"
|
||||
self.SURFACE_MUTED = "#8b8b8b"
|
||||
self.SUCCESS = "#4cd964"
|
||||
self.ERROR = "red"
|
||||
self.DISABLED = "#999999"
|
||||
self.DISABLED_BG = "#444444"
|
||||
self.DISABLED_BORDER = "#666666"
|
||||
else:
|
||||
self.ACCENT = "#004488"
|
||||
self.ACCENT_HOVER = "#0077cc"
|
||||
self.BACKGROUND = "#ffffff"
|
||||
self.FOREGROUND = "#000000"
|
||||
self.BORDER = "#003366"
|
||||
self.SURFACE_MUTED = "#dddddd"
|
||||
self.SUCCESS = "#007f00"
|
||||
self.ERROR = "#cc0000"
|
||||
self.DISABLED = "#cccccc"
|
||||
self.DISABLED_BG = "#f0f0f0"
|
||||
self.DISABLED_BORDER = "#aaaaaa"
|
||||
|
||||
# Shared base + primary + tonal
|
||||
self.DARK = "#000000"
|
||||
self.LIGHT = "#ffffff"
|
||||
|
||||
self.PRIMARY_0 = "#6e1106"
|
||||
self.PRIMARY_10 = "#812f1f"
|
||||
self.PRIMARY_20 = "#934937"
|
||||
self.PRIMARY_30 = "#a46251"
|
||||
self.PRIMARY_40 = "#b57b6b"
|
||||
self.PRIMARY_50 = "#c59487"
|
||||
|
||||
self.SURFACE_0 = "#121212"
|
||||
self.SURFACE_10 = "#282828"
|
||||
self.SURFACE_20 = "#3f3f3f"
|
||||
self.SURFACE_30 = "#575757"
|
||||
self.SURFACE_40 = "#717171"
|
||||
self.SURFACE_50 = "#8b8b8b"
|
||||
|
||||
self.TONAL_0 = "#1d1411"
|
||||
self.TONAL_10 = "#322927"
|
||||
self.TONAL_20 = "#48403e"
|
||||
self.TONAL_30 = "#605856"
|
||||
self.TONAL_40 = "#787270"
|
||||
self.TONAL_50 = "#928c8b"
|
||||
|
||||
self.COMPONENT_CLASSES = {
|
||||
"palette--foreground",
|
||||
"palette--background",
|
||||
"palette--accent",
|
||||
"palette--accent-hover",
|
||||
"palette--border",
|
||||
"palette--surface-muted",
|
||||
"palette--primary-0",
|
||||
"palette--primary-10",
|
||||
"palette--primary-20",
|
||||
"palette--primary-30",
|
||||
"palette--primary-40",
|
||||
"palette--primary-50",
|
||||
"palette--surface-0",
|
||||
"palette--surface-10",
|
||||
"palette--surface-20",
|
||||
"palette--surface-30",
|
||||
"palette--surface-40",
|
||||
"palette--surface-50",
|
||||
"palette--success",
|
||||
"palette--error",
|
||||
"palette--disabled",
|
||||
"palette-bg--accent",
|
||||
"palette-bg--background",
|
||||
"palette-bg--surface",
|
||||
}
|
||||
|
||||
|
||||
# === Singleton-style global instance ===
|
||||
_theme_instance: Optional[ThemeManager] = None
|
||||
|
||||
|
||||
def get_theme(theme: Literal["dark", "light"] = "dark") -> ThemeManager:
|
||||
global _theme_instance
|
||||
if _theme_instance is None:
|
||||
_theme_instance = ThemeManager(theme)
|
||||
return _theme_instance
|
||||
225
agentm/theme/styles.base.tcss
Normal file
225
agentm/theme/styles.base.tcss
Normal file
@ -0,0 +1,225 @@
|
||||
/* === Global App Styles === */
|
||||
|
||||
Screen {
|
||||
background: {{BACKGROUND}};
|
||||
color: {{FOREGROUND}};
|
||||
}
|
||||
|
||||
/* === Resets === */
|
||||
|
||||
* {
|
||||
padding: 0 1;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* === Headers === */
|
||||
|
||||
# Header, .header {
|
||||
# dock: top;
|
||||
# height: 3;
|
||||
# content-align: center middle;
|
||||
# background: {{SURFACE_10}};
|
||||
# color: {{ACCENT}};
|
||||
# text-style: bold;
|
||||
# padding: 1 2;
|
||||
# border: solid {{BORDER}};
|
||||
# }
|
||||
|
||||
/* === Buttons === */
|
||||
|
||||
Button {
|
||||
background: {{SURFACE_10}};
|
||||
color: {{ACCENT}};
|
||||
border: solid {{ACCENT}};
|
||||
padding: 1 2;
|
||||
margin: 1;
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
background: {{ACCENT}};
|
||||
color: {{BACKGROUND}};
|
||||
}
|
||||
|
||||
Button:focus {
|
||||
background: {{SURFACE_20}};
|
||||
border: solid {{ACCENT}};
|
||||
}
|
||||
|
||||
/* === Inputs === */
|
||||
|
||||
Input {
|
||||
background: {{SURFACE_10}};
|
||||
color: {{FOREGROUND}};
|
||||
border: solid {{BORDER}};
|
||||
}
|
||||
|
||||
Input:focus {
|
||||
background: {{SURFACE_20}};
|
||||
border: solid {{ACCENT}};
|
||||
}
|
||||
|
||||
/* === Alerts === */
|
||||
|
||||
.success {
|
||||
color: {{SUCCESS}};
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: {{ACCENT}};
|
||||
}
|
||||
|
||||
.error {
|
||||
color: {{ERROR}};
|
||||
}
|
||||
|
||||
/* === Login Form === */
|
||||
|
||||
#login_form {
|
||||
layout: vertical;
|
||||
align-horizontal: center;
|
||||
align-vertical: middle;
|
||||
width: 60%;
|
||||
height: auto;
|
||||
padding: 2 4;
|
||||
border: solid {{BORDER}};
|
||||
}
|
||||
|
||||
#status_message {
|
||||
color: {{ACCENT}};
|
||||
padding: 1;
|
||||
}
|
||||
|
||||
#pw_row {
|
||||
layout: horizontal;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#pw_row > * {
|
||||
margin-right: 1;
|
||||
}
|
||||
|
||||
#pw_row > *:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* === Loading Overlay === */
|
||||
|
||||
#loading_overlay {
|
||||
dock: top;
|
||||
background: {{SURFACE_10}};
|
||||
color: {{FOREGROUND}};
|
||||
padding: 1 2;
|
||||
text-style: bold;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
/* === Game Layout === */
|
||||
|
||||
.centered_layout {
|
||||
layout: vertical;
|
||||
align-horizontal: center;
|
||||
align-vertical: middle;
|
||||
padding: 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rom_rows_container {
|
||||
layout: vertical;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
.game_card {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 60;
|
||||
margin: 1;
|
||||
padding: 1;
|
||||
background: {{SURFACE_10}};
|
||||
color: {{ACCENT}};
|
||||
border: solid {{BORDER}};
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game_card:hover {
|
||||
background: {{SURFACE_20}};
|
||||
color: {{ACCENT_HOVER}};
|
||||
}
|
||||
|
||||
Static {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Horizontal {
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
.game_card:focus {
|
||||
background: {{SURFACE_20}};
|
||||
border: solid {{ACCENT}};
|
||||
}
|
||||
|
||||
.game_card:disabled {
|
||||
background: {{SURFACE_10}};
|
||||
color: {{BORDER}};
|
||||
}
|
||||
|
||||
.game_card_clicked {
|
||||
background: {{ACCENT}};
|
||||
color: {{BACKGROUND}};
|
||||
}
|
||||
|
||||
.offset_card {
|
||||
width: 11;
|
||||
height: auto;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.rom_row {
|
||||
layout: horizontal;
|
||||
align-horizontal: center;
|
||||
align-vertical: middle;
|
||||
padding: 1 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info_confirm_row {
|
||||
layout: horizontal;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
padding: 1 2;
|
||||
align-vertical: top;
|
||||
}
|
||||
|
||||
#confirm_button {
|
||||
width: 20%;
|
||||
height: auto;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
.confirm_button:hover {
|
||||
background: {{ACCENT}};
|
||||
color: {{BACKGROUND}};
|
||||
}
|
||||
|
||||
Button:disabled {
|
||||
background: {{DISABLED_BG}};
|
||||
color: {{DISABLED}};
|
||||
border: solid {{DISABLED_BORDER}};
|
||||
text-style: dim;
|
||||
}
|
||||
|
||||
.game_info {
|
||||
padding: 1 2;
|
||||
width: 100%;
|
||||
margin-right: 1;
|
||||
color: {{ACCENT}};
|
||||
}
|
||||
|
||||
#game_info_box {
|
||||
width: 80%;
|
||||
height: auto;
|
||||
}
|
||||
@ -14,21 +14,21 @@ Screen {
|
||||
|
||||
/* === Headers === */
|
||||
|
||||
Header, .header {
|
||||
dock: top;
|
||||
height: 3;
|
||||
content-align: center middle;
|
||||
background: #1f1f1f;
|
||||
color: #ed7d3a;
|
||||
text-style: bold;
|
||||
padding: 1 2;
|
||||
border: solid #3a9bed;
|
||||
}
|
||||
# Header, .header {
|
||||
# dock: top;
|
||||
# height: 3;
|
||||
# content-align: center middle;
|
||||
# background: #282828;
|
||||
# color: #ed7d3a;
|
||||
# text-style: bold;
|
||||
# padding: 1 2;
|
||||
# border: solid #3a9bed;
|
||||
# }
|
||||
|
||||
/* === Buttons === */
|
||||
|
||||
Button {
|
||||
background: #1f1f1f;
|
||||
background: #282828;
|
||||
color: #ed7d3a;
|
||||
border: solid #ed7d3a;
|
||||
padding: 1 2;
|
||||
@ -43,20 +43,20 @@ Button:hover {
|
||||
}
|
||||
|
||||
Button:focus {
|
||||
background: #2a2a2a;
|
||||
background: #3f3f3f;
|
||||
border: solid #ed7d3a;
|
||||
}
|
||||
|
||||
/* === Inputs === */
|
||||
|
||||
Input {
|
||||
background: #1f1f1f;
|
||||
background: #282828;
|
||||
color: #f0f0f0;
|
||||
border: solid #3a9bed;
|
||||
}
|
||||
|
||||
Input:focus {
|
||||
background: #2a2a2a;
|
||||
background: #3f3f3f;
|
||||
border: solid #ed7d3a;
|
||||
}
|
||||
|
||||
@ -108,7 +108,7 @@ Input:focus {
|
||||
|
||||
#loading_overlay {
|
||||
dock: top;
|
||||
background: #1f1f1f;
|
||||
background: #282828;
|
||||
color: #f0f0f0;
|
||||
padding: 1 2;
|
||||
text-style: bold;
|
||||
@ -130,42 +130,40 @@ Input:focus {
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
.rom_row {
|
||||
layout: horizontal;
|
||||
align-horizontal: center;
|
||||
align-vertical: middle;
|
||||
padding: 1 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.game_card {
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 60;
|
||||
margin: 1;
|
||||
padding: 1;
|
||||
background: #1f1f1f;
|
||||
background: #282828;
|
||||
color: #ed7d3a;
|
||||
border: solid #3a9bed;
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
.game_card:hover {
|
||||
background: #2a2a2a;
|
||||
color: #ed7d3a;
|
||||
background: #3f3f3f;
|
||||
color: rgb(236, 194, 169);
|
||||
}
|
||||
|
||||
Static {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
Horizontal {
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
.game_card:focus {
|
||||
background: #2a2a2a;
|
||||
background: #3f3f3f;
|
||||
border: solid #ed7d3a;
|
||||
}
|
||||
|
||||
.game_card:disabled {
|
||||
background: #1f1f1f;
|
||||
background: #282828;
|
||||
color: #3a9bed;
|
||||
}
|
||||
|
||||
@ -174,27 +172,20 @@ Input:focus {
|
||||
color: #0e0e0e;
|
||||
}
|
||||
|
||||
|
||||
.offset_card {
|
||||
width: 11;
|
||||
height: auto;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* ✅ Keep this for consistent row layout */
|
||||
.rom_row {
|
||||
layout: horizontal;
|
||||
align-horizontal: center;
|
||||
padding: 1 1;
|
||||
align-vertical: middle;
|
||||
padding: 1 2;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rom_row {
|
||||
layout: horizontal;
|
||||
align-horizontal: center;
|
||||
padding: 1 2; /* maybe increase vertical padding */
|
||||
}
|
||||
|
||||
|
||||
.info_confirm_row {
|
||||
layout: horizontal;
|
||||
width: 100%;
|
||||
@ -203,7 +194,6 @@ Input:focus {
|
||||
align-vertical: top;
|
||||
}
|
||||
|
||||
|
||||
#confirm_button {
|
||||
width: 20%;
|
||||
height: auto;
|
||||
@ -215,12 +205,21 @@ Input:focus {
|
||||
color: #0e0e0e;
|
||||
}
|
||||
|
||||
Button:disabled {
|
||||
background: #444444;
|
||||
color: #999999;
|
||||
border: solid #666666;
|
||||
text-style: dim;
|
||||
}
|
||||
|
||||
.game_info {
|
||||
padding: 1 2;
|
||||
width: 100%;
|
||||
margin-right: 1;
|
||||
color: #ed7d3a;
|
||||
}
|
||||
|
||||
#game_info_box {
|
||||
width: 80%;
|
||||
height: auto;
|
||||
}
|
||||
@ -6,7 +6,10 @@ 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
|
||||
from rich.table import Table
|
||||
from rich.rule import Rule
|
||||
from rich.markup import escape
|
||||
from PIL import Image, ImageFilter
|
||||
import os
|
||||
|
||||
from rich_pixels import Pixels
|
||||
@ -14,6 +17,9 @@ from rich_pixels._renderer import HalfcellRenderer
|
||||
|
||||
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
|
||||
|
||||
palette = get_theme()
|
||||
|
||||
|
||||
class GameSelected(Message):
|
||||
@ -26,7 +32,8 @@ class ProgressWidget(Widget):
|
||||
message = reactive("Loading...")
|
||||
|
||||
def render(self) -> str:
|
||||
return f"[bold cyan]{self.message}[/bold cyan]"
|
||||
return f"[bold {palette.ACCENT}]{self.message}[/]"
|
||||
|
||||
|
||||
|
||||
class GameAccordion(Static):
|
||||
@ -39,15 +46,25 @@ class GameAccordion(Static):
|
||||
|
||||
image_path = os.path.abspath(self.metadata.get("image_path", ""))
|
||||
self.image_renderable = self.load_image_scaled(image_path)
|
||||
self.ascii_title = self.load_ascii_art()
|
||||
|
||||
super().__init__(id=f"accordion_{self.safe_id}", classes="game_card")
|
||||
|
||||
def load_ascii_art(self) -> Static:
|
||||
game_id = self.metadata.get("game_id", "").lower()
|
||||
ascii_path = os.path.join("agentm", "assets", "game_titles", f"{game_id}.txt")
|
||||
try:
|
||||
with open(ascii_path, "r", encoding="utf-8") as f:
|
||||
return Static(f"[bold {palette.ACCENT}]{f.read()}[/]", markup=True)
|
||||
except FileNotFoundError:
|
||||
return Static(f"[bold {palette.ACCENT}]{self.title.upper()}[/]", markup=True)
|
||||
|
||||
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
|
||||
scale_factor = 0.098
|
||||
target_width = int(img.width * scale_factor)
|
||||
target_height = int(img.height * scale_factor)
|
||||
resized = img.resize(
|
||||
@ -66,17 +83,9 @@ class GameAccordion(Static):
|
||||
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
|
||||
)
|
||||
def compose(self):
|
||||
yield Static(self.image_renderable)
|
||||
yield self.ascii_title
|
||||
|
||||
async def on_click(self):
|
||||
await self.display_info()
|
||||
@ -85,16 +94,22 @@ class GameAccordion(Static):
|
||||
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))
|
||||
|
||||
table = Table.grid(expand=True)
|
||||
table.add_column(ratio=1)
|
||||
table.add_column()
|
||||
table.add_row("[b]Title:[/b]", meta['title'])
|
||||
table.add_row("[b]Game ID:[/b]", meta['game_id'])
|
||||
table.add_row("[b]Difficulty:[/b]", f"{meta.get('difficulty_min')} - {meta.get('difficulty_max')}")
|
||||
table.add_row("[b]Characters:[/b]", ", ".join(meta.get("characters", [])))
|
||||
table.add_row("[b]Keywords:[/b]", ", ".join(meta.get("keywords", [])))
|
||||
table.add_row("[b]SHA256:[/b]", meta["sha256"])
|
||||
|
||||
self.parent_view.shared_info_box.update(
|
||||
Panel(Group(table, Rule(style="dim")), title="Game Info", border_style=palette.BORDER, expand=True)
|
||||
)
|
||||
self.parent_view.shared_confirm_button.label = f"✅ Confirm {meta['title']}"
|
||||
self.parent_view.shared_confirm_button.disabled = False
|
||||
self.parent_view.selected_game = meta
|
||||
|
||||
|
||||
@ -109,7 +124,7 @@ class HomeView(Screen):
|
||||
|
||||
def compose(self):
|
||||
self.logo = Static(
|
||||
"[b bright_magenta]\n\n"
|
||||
f"[bold {palette.ACCENT}]\n\n"
|
||||
" █████╗ ██████╗ ███████╗ ███╗ ██╗ ████████╗ ███╗ ███╗\n"
|
||||
"██╔══██╗ ██╔════╝ ██╔════╝ ████╗ ██║ ╚══██╔══╝ ████╗ ████║\n"
|
||||
"███████║ ██║ ███╗ █████╗ ██╔██╗██║ ██║ ██╔████╔██║\n"
|
||||
@ -119,13 +134,30 @@ class HomeView(Screen):
|
||||
classes="header",
|
||||
expand=False,
|
||||
)
|
||||
|
||||
self.welcome_text = Static(
|
||||
"This is an unofficial DIAMBRA launcher to help you easily train, evaluate, and submit RL agents for fighting games.",
|
||||
classes="body",
|
||||
expand=False
|
||||
)
|
||||
|
||||
self.progress_text = ProgressWidget()
|
||||
|
||||
self.loading_container = Vertical(
|
||||
Static(f"[bold {palette.SUCCESS}]LOADING...[/]", expand=True),
|
||||
self.progress_text,
|
||||
id="loading_container"
|
||||
)
|
||||
|
||||
# This will be the main container we later modify
|
||||
self.dynamic_container = Vertical(self.loading_container, id="dynamic_content")
|
||||
|
||||
yield Vertical(
|
||||
self.logo,
|
||||
Static("[bold green]LOADING...[/bold green]", expand=True),
|
||||
self.progress_text,
|
||||
id="loading_container"
|
||||
self.welcome_text,
|
||||
self.dynamic_container,
|
||||
id="home_screen_container",
|
||||
classes="centered_layout"
|
||||
)
|
||||
|
||||
async def on_mount(self):
|
||||
@ -151,36 +183,42 @@ class HomeView(Screen):
|
||||
|
||||
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.shared_info_box = Static(
|
||||
Panel(
|
||||
"[dim]Select a Game From Above to Start[/dim]",
|
||||
title="Game Info",
|
||||
border_style=palette.BORDER,
|
||||
expand=True
|
||||
),
|
||||
id="game_info_box",
|
||||
classes="game_info",
|
||||
expand=True
|
||||
)
|
||||
self.shared_confirm_button = Button(
|
||||
"✅ Confirm",
|
||||
id="confirm_button",
|
||||
classes="confirm_button",
|
||||
disabled=True
|
||||
)
|
||||
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
|
||||
),
|
||||
new_content = Vertical(
|
||||
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"
|
||||
classes="info_confirm_row"
|
||||
)
|
||||
)
|
||||
|
||||
# Replace loading content with new UI below logo and welcome
|
||||
dynamic_container = self.query_one("#dynamic_content")
|
||||
await dynamic_container.remove_children()
|
||||
await dynamic_container.mount(new_content)
|
||||
|
||||
# Populate games
|
||||
for rom in verified_roms:
|
||||
await self.rom_scroll_row.mount(GameAccordion(
|
||||
title=rom["title"],
|
||||
@ -189,6 +227,7 @@ class HomeView(Screen):
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user