booru-viewer/booru_viewer/tui/grid.py

144 lines
4.6 KiB
Python

"""Thumbnail grid widget for the Textual TUI."""
from __future__ import annotations
from textual.binding import Binding
from textual.widgets import Static
from textual.reactive import reactive
from ..core.api.base import Post
from ..core.db import Database
from ..core.config import GREEN, DIM_GREEN, BG
class ThumbnailCell(Static):
"""A single post cell in the grid."""
def __init__(self, index: int, post: Post, favorited: bool = False) -> None:
self._index = index
self._post = post
self._favorited = favorited
self._selected = False
super().__init__()
def compose_content(self) -> str:
fav = " *" if self._favorited else ""
rating = self._post.rating or "?"
return (
f"#{self._post.id}{fav}\n"
f"[{rating}] s:{self._post.score}\n"
f"{self._post.width}x{self._post.height}"
)
def on_mount(self) -> None:
self.update(self.compose_content())
self._apply_style()
def set_selected(self, selected: bool) -> None:
self._selected = selected
self._apply_style()
def set_favorited(self, favorited: bool) -> None:
self._favorited = favorited
self.update(self.compose_content())
def _apply_style(self) -> None:
if self._selected:
self.styles.background = DIM_GREEN
self.styles.color = BG
self.styles.border = ("solid", GREEN)
else:
self.styles.background = BG
self.styles.color = GREEN if self._favorited else DIM_GREEN
self.styles.border = ("solid", DIM_GREEN)
def on_click(self) -> None:
self.post_message(CellClicked(self._index))
class CellClicked:
"""Message sent when a cell is clicked."""
def __init__(self, index: int) -> None:
self.index = index
class ThumbnailGrid(Static):
"""Grid of post cells with keyboard navigation."""
BINDINGS = [
Binding("j", "move_down", "Down", show=False),
Binding("k", "move_up", "Up", show=False),
Binding("h", "move_left", "Left", show=False),
Binding("l", "move_right", "Right", show=False),
Binding("down", "move_down", "Down", show=False),
Binding("up", "move_up", "Up", show=False),
Binding("left", "move_left", "Left", show=False),
Binding("right", "move_right", "Right", show=False),
]
selected_index: int = reactive(-1, init=False)
def __init__(self, **kwargs) -> None:
super().__init__(**kwargs)
self._cells: list[ThumbnailCell] = []
self._posts: list[Post] = []
self._cols = 4
self.can_focus = True
def set_posts(
self, posts: list[Post], db: Database | None = None, site_id: int | None = None
) -> None:
self._posts = posts
# Remove old cells
for cell in self._cells:
cell.remove()
self._cells.clear()
lines = []
for i, post in enumerate(posts):
fav = False
if db and site_id:
fav = db.is_favorited(site_id, post.id)
fav_marker = " *" if fav else ""
rating = post.rating or "?"
selected = " >> " if i == 0 else " "
lines.append(
f"{selected}#{post.id}{fav_marker} [{rating}] "
f"s:{post.score} {post.width}x{post.height}"
)
self.selected_index = 0 if posts else -1
self.update("\n".join(lines) if lines else "No results. Search for tags above.")
def update_favorite_status(self, index: int, favorited: bool) -> None:
"""Refresh the display for a single post's favorite status."""
if 0 <= index < len(self._posts):
self.set_posts(self._posts) # Simple refresh
def _render_list(self) -> None:
lines = []
for i, post in enumerate(self._posts):
selected = " >> " if i == self.selected_index else " "
lines.append(
f"{selected}#{post.id} [{post.rating or '?'}] "
f"s:{post.score} {post.width}x{post.height}"
)
self.update("\n".join(lines) if lines else "No results.")
def action_move_down(self) -> None:
if self._posts and self.selected_index < len(self._posts) - 1:
self.selected_index += 1
self._render_list()
def action_move_up(self) -> None:
if self._posts and self.selected_index > 0:
self.selected_index -= 1
self._render_list()
def action_move_right(self) -> None:
self.action_move_down()
def action_move_left(self) -> None:
self.action_move_up()