update refactoring and add new features
This commit is contained in:
+41
-158
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user