update refactoring and add new features

This commit is contained in:
2026-07-24 20:55:58 -07:00
parent 7f12bc7f40
commit 03fbbbcf28
69 changed files with 16332 additions and 17600 deletions
+41 -158
View File
@@ -45,22 +45,6 @@ def _rich():
return _rich_mod
# Optional Textual imports - lazily loaded to avoid pulling in ~300ms of textual
# at import time when the TUI is not being used.
import importlib.util as _importlib_util
TEXTUAL_AVAILABLE: bool = _importlib_util.find_spec("textual") is not None
# Tree is populated lazily on first call to build_metadata_tree().
_textual_Tree: Any = None
def _get_textual_Tree() -> Any:
global _textual_Tree
if _textual_Tree is None:
from textual.widgets import Tree as _Tree
_textual_Tree = _Tree
return _textual_Tree
# Import ResultModel from the API for typing; avoid runtime redefinition issues
from typing import TYPE_CHECKING
@@ -69,6 +53,8 @@ if TYPE_CHECKING:
else:
ResultModel = None # type: ignore[assignment]
# Reuse the existing format_bytes helper under a clearer alias
from SYS.utils import format_bytes as format_mb
@@ -587,30 +573,6 @@ class InputOption:
}
@dataclass
class TUIResultCard:
"""Represents a result as a UI card with title, metadata, and actions.
Used in hub-ui and TUI contexts to render individual search results
as grouped components with visual structure.
"""
title: str
subtitle: Optional[str] = None
metadata: Optional[Dict[str, str]] = None
media_kind: Optional[str] = None
tag: Optional[List[str]] = None
file_hash: Optional[str] = None
file_size: Optional[str] = None
duration: Optional[str] = None
def __post_init__(self):
"""Initialize default values."""
if self.metadata is None:
self.metadata = {}
if self.tag is None:
self.tag = []
@dataclass
class Column:
@@ -2113,122 +2075,6 @@ class Table:
return self.rows[idx]
return None
# TUI-specific formatting methods
def to_datatable_rows(self, source: str = "unknown") -> List[List[str]]:
"""Convert results to rows suitable for Textual DataTable widget.
Args:
source: Source type for formatting context (openlibrary, soulseek, etc.)
Returns:
List of row value lists
"""
rows = []
for result in self.rows:
row_values = self._format_datatable_row(result, source)
rows.append(row_values)
return rows
def _format_datatable_row(self,
row: Row,
source: str = "unknown") -> List[str]:
"""Format a ResultRow for DataTable display.
Args:
row: ResultRow to format
source: Source type
Returns:
List of column values as strings
"""
# Extract values from row columns
values = [col.value for col in row.columns]
# Truncate to reasonable lengths for table display
return [v[:60] if len(v) > 60 else v for v in values]
def to_result_cards(self) -> List[TUIResultCard]:
"""Convert all rows to TUIResultCard objects for card-based UI display.
Returns:
List of TUIResultCard objects
"""
cards = []
for row in self.rows:
card = self._row_to_card(row)
cards.append(card)
return cards
def _row_to_card(self, row: Row) -> TUIResultCard:
"""Convert a ResultRow to a TUIResultCard.
Args:
row: ResultRow to convert
Returns:
TUIResultCard with extracted metadata
"""
# Build metadata dict from row columns
metadata = {}
title = ""
for col in row.columns:
if col.name.lower() == "title":
title = col.value
metadata[col.name] = col.value
# Extract tag values if present
tag = []
if "Tag" in metadata:
tag_val = metadata["Tag"]
if tag_val:
tag = [t.strip() for t in tag_val.split(",")][:5]
# Try to find useful metadata fields
subtitle = metadata.get("Artist", metadata.get("Author", ""))
media_kind = metadata.get("Type", metadata.get("Media Kind", ""))
file_size = metadata.get("Size", "")
duration = metadata.get("Duration", "")
file_hash = metadata.get("Hash", "")
return TUIResultCard(
title=title or "Unknown",
subtitle=subtitle,
metadata=metadata,
media_kind=media_kind,
tag=tag,
file_hash=file_hash or None,
file_size=file_size or None,
duration=duration or None,
)
def build_metadata_tree(self, tree_widget: "Tree") -> None:
"""Populate a Textual Tree widget with result metadata hierarchy.
Args:
tree_widget: Textual Tree widget to populate
Raises:
ImportError: If Textual not available
"""
if not TEXTUAL_AVAILABLE:
raise ImportError("Textual not available for tree building")
tree_widget.reset(self.title or "Results")
root = tree_widget.root
# Add each row as a top-level node
for i, row in enumerate(self.rows, 1):
row_node = root.add(f"[bold]Result {i}[/bold]")
# Add columns as children
for col in row.columns:
value_str = col.value
if len(value_str) > 100:
value_str = value_str[:97] + "..."
row_node.add_leaf(f"[cyan]{col.name}[/cyan]: {value_str}")
def _format_size(size: Any, integer_only: bool = False) -> str:
"""Format file size as human-readable string.
@@ -2577,9 +2423,46 @@ class ItemDetailView(Table):
return f"{label[:max_len - 3]}..."
return label
def _looks_like_local_path(value: Any) -> bool:
text = str(value or "").strip()
if not text:
return False
try:
p = Path(text)
if p.exists():
return True
return text.startswith("/") or text.startswith("\\\\") or (len(text) >= 2 and text[1] == ":" and text[2:3] in ("\\", "/"))
except Exception:
return False
def _short_path_label(path_value: str, *, max_len: int = 68) -> str:
text = str(path_value or "").strip()
if not text:
return ""
if len(text) <= max_len:
return text
parts = text.replace("\\", "/").rstrip("/").split("/")
if len(parts) >= 3:
head = parts[0]
tail = parts[-1]
mid = "..."
label = f"{head}/{mid}/{tail}"
if len(label) > max_len:
return f"{text[:max_len - 3]}..."
return label
return f"{text[:max_len - 3]}..."
def _render_detail_value(key: str, value: Any) -> Any:
# Keep terminal links compact while preserving full click target.
if str(key or "").strip().lower() in {"path", "url"} and _looks_like_http_url(value):
key_lower = str(key or "").strip().lower()
if key_lower == "path" and _looks_like_local_path(value):
full_path = str(value).strip()
try:
file_uri = Path(full_path).as_uri()
except Exception:
return str(value)
label = _short_path_label(full_path)
return _rich().Text(label, style=f"underline cyan link {file_uri}")
if key_lower in {"path", "url"} and _looks_like_http_url(value):
full_url = str(value).strip()
label = _short_link_label(full_url)
return _rich().Text(label, style=f"underline cyan link {full_url}")