Add tool marketplace UI enhancements
- Browse all tools on page load without search - Category filter dropdown (Text, Developer, Data, etc.) - Sort options (downloads, rating, newest, name) - Star ratings display in results table - Clickable tags for filtering - Installed indicator (✓) for local tools - Update available indicator (↑) for newer versions - Pagination controls for large result sets - Publisher reputation info in details Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
4fe2d26244
commit
518a04a8b0
13
README.md
13
README.md
|
|
@ -431,9 +431,16 @@ The graphical interface provides a modern desktop experience:
|
||||||
- Test tools before saving
|
- Test tools before saving
|
||||||
|
|
||||||
### Registry Browser
|
### Registry Browser
|
||||||
- Search community tools by name or keyword
|
- **Browse all** tools on page load (no search required)
|
||||||
- View tool details, downloads, and ratings
|
- **Search** by name, description, or keyword
|
||||||
- One-click install to your local machine
|
- **Filter** by category (Text, Developer, Data, Other)
|
||||||
|
- **Sort** by popularity, rating, newest, or name
|
||||||
|
- **Star ratings** displayed in results and details
|
||||||
|
- **Clickable tags** to filter by tag
|
||||||
|
- **Installed indicator** (✓) shows which tools you have
|
||||||
|
- **Update available** (↑) when newer version exists
|
||||||
|
- **Pagination** for browsing large result sets
|
||||||
|
- **Publisher info** showing reputation and total downloads
|
||||||
|
|
||||||
### Provider Management
|
### Provider Management
|
||||||
- Add and configure AI providers
|
- Add and configure AI providers
|
||||||
|
|
|
||||||
|
|
@ -3,30 +3,45 @@
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLineEdit,
|
QWidget, QVBoxLayout, QHBoxLayout, QLineEdit,
|
||||||
QPushButton, QTableWidget, QTableWidgetItem, QLabel,
|
QPushButton, QTableWidget, QTableWidgetItem, QLabel,
|
||||||
QHeaderView, QGroupBox, QTextEdit, QSplitter, QMessageBox
|
QHeaderView, QGroupBox, QTextEdit, QSplitter, QMessageBox,
|
||||||
|
QComboBox, QFrame
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, QThread, Signal
|
from PySide6.QtCore import Qt, QThread, Signal
|
||||||
|
from PySide6.QtGui import QColor
|
||||||
|
|
||||||
from ...registry_client import RegistryClient, RegistryError
|
from ...registry_client import RegistryClient, RegistryError
|
||||||
from ...config import load_config
|
from ...config import load_config
|
||||||
|
from ...tool import list_tools, load_tool
|
||||||
|
|
||||||
|
|
||||||
class SearchWorker(QThread):
|
class SearchWorker(QThread):
|
||||||
"""Background worker for registry search."""
|
"""Background worker for registry search."""
|
||||||
finished = Signal(list)
|
finished = Signal(object) # PaginatedResponse
|
||||||
error = Signal(str)
|
error = Signal(str)
|
||||||
|
|
||||||
def __init__(self, query: str, page: int = 1):
|
def __init__(self, query: str = "", category: str = None, sort: str = "downloads",
|
||||||
|
tags: list = None, page: int = 1, per_page: int = 20):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.query = query
|
self.query = query
|
||||||
|
self.category = category
|
||||||
|
self.sort = sort
|
||||||
|
self.tags = tags
|
||||||
self.page = page
|
self.page = page
|
||||||
|
self.per_page = per_page
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
client = RegistryClient()
|
client = RegistryClient()
|
||||||
result = client.search_tools(self.query, page=self.page, per_page=20)
|
result = client.search_tools(
|
||||||
# result is a PaginatedResponse with data attribute
|
self.query,
|
||||||
self.finished.emit(result.data)
|
category=self.category if self.category and self.category != "All" else None,
|
||||||
|
tags=self.tags,
|
||||||
|
page=self.page,
|
||||||
|
per_page=self.per_page,
|
||||||
|
sort=self.sort,
|
||||||
|
include_facets=True
|
||||||
|
)
|
||||||
|
self.finished.emit(result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error.emit(str(e))
|
self.error.emit(str(e))
|
||||||
|
|
||||||
|
|
@ -59,6 +74,10 @@ class RegistryPage(QWidget):
|
||||||
self._search_worker = None
|
self._search_worker = None
|
||||||
self._install_worker = None
|
self._install_worker = None
|
||||||
self._selected_tool = None
|
self._selected_tool = None
|
||||||
|
self._current_page = 1
|
||||||
|
self._total_pages = 1
|
||||||
|
self._current_tags = []
|
||||||
|
self._installed_tools = {} # name -> version
|
||||||
self._setup_ui()
|
self._setup_ui()
|
||||||
|
|
||||||
def _setup_ui(self):
|
def _setup_ui(self):
|
||||||
|
|
@ -68,26 +87,70 @@ class RegistryPage(QWidget):
|
||||||
layout.setSpacing(16)
|
layout.setSpacing(16)
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
|
header = QHBoxLayout()
|
||||||
title = QLabel("Tool Registry")
|
title = QLabel("Tool Registry")
|
||||||
title.setObjectName("heading")
|
title.setObjectName("heading")
|
||||||
layout.addWidget(title)
|
header.addWidget(title)
|
||||||
|
header.addStretch()
|
||||||
|
|
||||||
# Search bar
|
# Browse all button
|
||||||
search_box = QWidget()
|
self.btn_browse = QPushButton("Browse All")
|
||||||
search_layout = QHBoxLayout(search_box)
|
self.btn_browse.clicked.connect(self._browse_all)
|
||||||
search_layout.setContentsMargins(0, 0, 0, 0)
|
header.addWidget(self.btn_browse)
|
||||||
search_layout.setSpacing(8)
|
|
||||||
|
|
||||||
|
layout.addLayout(header)
|
||||||
|
|
||||||
|
# Search and filters row
|
||||||
|
filters_box = QWidget()
|
||||||
|
filters_layout = QHBoxLayout(filters_box)
|
||||||
|
filters_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
filters_layout.setSpacing(12)
|
||||||
|
|
||||||
|
# Search input
|
||||||
self.search_input = QLineEdit()
|
self.search_input = QLineEdit()
|
||||||
self.search_input.setPlaceholderText("Search tools...")
|
self.search_input.setPlaceholderText("Search tools...")
|
||||||
self.search_input.returnPressed.connect(self._do_search)
|
self.search_input.returnPressed.connect(self._do_search)
|
||||||
search_layout.addWidget(self.search_input, 1)
|
filters_layout.addWidget(self.search_input, 2)
|
||||||
|
|
||||||
|
# Category filter
|
||||||
|
cat_label = QLabel("Category:")
|
||||||
|
filters_layout.addWidget(cat_label)
|
||||||
|
self.category_combo = QComboBox()
|
||||||
|
self.category_combo.addItems(["All", "Text", "Developer", "Data", "Other"])
|
||||||
|
self.category_combo.setMinimumWidth(100)
|
||||||
|
self.category_combo.currentTextChanged.connect(self._on_filter_changed)
|
||||||
|
filters_layout.addWidget(self.category_combo)
|
||||||
|
|
||||||
|
# Sort dropdown
|
||||||
|
sort_label = QLabel("Sort:")
|
||||||
|
filters_layout.addWidget(sort_label)
|
||||||
|
self.sort_combo = QComboBox()
|
||||||
|
self.sort_combo.addItem("Most Popular", "downloads")
|
||||||
|
self.sort_combo.addItem("Highest Rated", "rating")
|
||||||
|
self.sort_combo.addItem("Newest", "newest")
|
||||||
|
self.sort_combo.addItem("Name (A-Z)", "name")
|
||||||
|
self.sort_combo.setMinimumWidth(120)
|
||||||
|
self.sort_combo.currentIndexChanged.connect(self._on_filter_changed)
|
||||||
|
filters_layout.addWidget(self.sort_combo)
|
||||||
|
|
||||||
|
# Search button
|
||||||
self.btn_search = QPushButton("Search")
|
self.btn_search = QPushButton("Search")
|
||||||
self.btn_search.clicked.connect(self._do_search)
|
self.btn_search.clicked.connect(self._do_search)
|
||||||
search_layout.addWidget(self.btn_search)
|
filters_layout.addWidget(self.btn_search)
|
||||||
|
|
||||||
layout.addWidget(search_box)
|
layout.addWidget(filters_box)
|
||||||
|
|
||||||
|
# Active tags display
|
||||||
|
self.tags_widget = QWidget()
|
||||||
|
self.tags_layout = QHBoxLayout(self.tags_widget)
|
||||||
|
self.tags_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.tags_layout.setSpacing(8)
|
||||||
|
self.tags_label = QLabel("Filtering by tags:")
|
||||||
|
self.tags_label.setStyleSheet("color: #718096;")
|
||||||
|
self.tags_layout.addWidget(self.tags_label)
|
||||||
|
self.tags_layout.addStretch()
|
||||||
|
self.tags_widget.hide()
|
||||||
|
layout.addWidget(self.tags_widget)
|
||||||
|
|
||||||
# Content splitter
|
# Content splitter
|
||||||
splitter = QSplitter(Qt.Horizontal)
|
splitter = QSplitter(Qt.Horizontal)
|
||||||
|
|
@ -98,18 +161,46 @@ class RegistryPage(QWidget):
|
||||||
left_layout.setContentsMargins(0, 0, 0, 0)
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
self.results_table = QTableWidget()
|
self.results_table = QTableWidget()
|
||||||
self.results_table.setColumnCount(4)
|
self.results_table.setColumnCount(6)
|
||||||
self.results_table.setHorizontalHeaderLabels(["Name", "Owner", "Downloads", "Version"])
|
self.results_table.setHorizontalHeaderLabels(["", "Name", "Owner", "Rating", "Downloads", "Version"])
|
||||||
self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
|
self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed)
|
||||||
self.results_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeToContents)
|
self.results_table.setColumnWidth(0, 30) # Installed indicator
|
||||||
|
self.results_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
|
||||||
self.results_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
self.results_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
||||||
self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
||||||
|
self.results_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeToContents)
|
||||||
|
self.results_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeToContents)
|
||||||
self.results_table.setSelectionBehavior(QTableWidget.SelectRows)
|
self.results_table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||||
self.results_table.setSelectionMode(QTableWidget.SingleSelection)
|
self.results_table.setSelectionMode(QTableWidget.SingleSelection)
|
||||||
self.results_table.verticalHeader().setVisible(False)
|
self.results_table.verticalHeader().setVisible(False)
|
||||||
self.results_table.itemSelectionChanged.connect(self._on_selection_changed)
|
self.results_table.itemSelectionChanged.connect(self._on_selection_changed)
|
||||||
left_layout.addWidget(self.results_table)
|
left_layout.addWidget(self.results_table)
|
||||||
|
|
||||||
|
# Pagination controls
|
||||||
|
pagination = QWidget()
|
||||||
|
pag_layout = QHBoxLayout(pagination)
|
||||||
|
pag_layout.setContentsMargins(0, 8, 0, 0)
|
||||||
|
|
||||||
|
self.btn_prev = QPushButton("← Previous")
|
||||||
|
self.btn_prev.clicked.connect(self._prev_page)
|
||||||
|
self.btn_prev.setEnabled(False)
|
||||||
|
pag_layout.addWidget(self.btn_prev)
|
||||||
|
|
||||||
|
pag_layout.addStretch()
|
||||||
|
|
||||||
|
self.page_label = QLabel("Page 1 of 1")
|
||||||
|
self.page_label.setStyleSheet("color: #718096;")
|
||||||
|
pag_layout.addWidget(self.page_label)
|
||||||
|
|
||||||
|
pag_layout.addStretch()
|
||||||
|
|
||||||
|
self.btn_next = QPushButton("Next →")
|
||||||
|
self.btn_next.clicked.connect(self._next_page)
|
||||||
|
self.btn_next.setEnabled(False)
|
||||||
|
pag_layout.addWidget(self.btn_next)
|
||||||
|
|
||||||
|
left_layout.addWidget(pagination)
|
||||||
|
|
||||||
splitter.addWidget(left)
|
splitter.addWidget(left)
|
||||||
|
|
||||||
# Right: Tool details
|
# Right: Tool details
|
||||||
|
|
@ -123,18 +214,31 @@ class RegistryPage(QWidget):
|
||||||
self.details_text = QTextEdit()
|
self.details_text = QTextEdit()
|
||||||
self.details_text.setReadOnly(True)
|
self.details_text.setReadOnly(True)
|
||||||
self.details_text.setPlaceholderText("Select a tool to view details")
|
self.details_text.setPlaceholderText("Select a tool to view details")
|
||||||
|
self.details_text.anchorClicked = self._on_tag_clicked
|
||||||
|
self.details_text.setOpenExternalLinks(False)
|
||||||
details_layout.addWidget(self.details_text)
|
details_layout.addWidget(self.details_text)
|
||||||
|
|
||||||
right_layout.addWidget(details_box, 1)
|
right_layout.addWidget(details_box, 1)
|
||||||
|
|
||||||
# Install button
|
# Action buttons
|
||||||
|
actions = QHBoxLayout()
|
||||||
|
|
||||||
self.btn_install = QPushButton("Install")
|
self.btn_install = QPushButton("Install")
|
||||||
self.btn_install.clicked.connect(self._install_tool)
|
self.btn_install.clicked.connect(self._install_tool)
|
||||||
self.btn_install.setEnabled(False)
|
self.btn_install.setEnabled(False)
|
||||||
right_layout.addWidget(self.btn_install)
|
actions.addWidget(self.btn_install)
|
||||||
|
|
||||||
|
self.btn_update = QPushButton("Update Available")
|
||||||
|
self.btn_update.clicked.connect(self._install_tool)
|
||||||
|
self.btn_update.setEnabled(False)
|
||||||
|
self.btn_update.setStyleSheet("background-color: #48bb78; color: white;")
|
||||||
|
self.btn_update.hide()
|
||||||
|
actions.addWidget(self.btn_update)
|
||||||
|
|
||||||
|
right_layout.addLayout(actions)
|
||||||
|
|
||||||
splitter.addWidget(right)
|
splitter.addWidget(right)
|
||||||
splitter.setSizes([500, 500])
|
splitter.setSizes([550, 450])
|
||||||
|
|
||||||
layout.addWidget(splitter, 1)
|
layout.addWidget(splitter, 1)
|
||||||
|
|
||||||
|
|
@ -144,43 +248,147 @@ class RegistryPage(QWidget):
|
||||||
layout.addWidget(self.status_label)
|
layout.addWidget(self.status_label)
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Refresh on page enter."""
|
"""Refresh on page enter - load popular tools."""
|
||||||
pass # Could auto-search popular tools
|
self._load_installed_tools()
|
||||||
|
self._browse_all()
|
||||||
|
|
||||||
|
def _load_installed_tools(self):
|
||||||
|
"""Load list of installed tools and their versions."""
|
||||||
|
self._installed_tools = {}
|
||||||
|
for name in list_tools():
|
||||||
|
tool = load_tool(name)
|
||||||
|
if tool:
|
||||||
|
self._installed_tools[name] = tool.version or "1.0.0"
|
||||||
|
|
||||||
|
def _browse_all(self):
|
||||||
|
"""Browse all tools (no search query)."""
|
||||||
|
self.search_input.clear()
|
||||||
|
self._current_page = 1
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
|
def _on_filter_changed(self):
|
||||||
|
"""Handle filter/sort change."""
|
||||||
|
self._current_page = 1
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
def _do_search(self):
|
def _do_search(self):
|
||||||
"""Perform search."""
|
"""Perform search with current filters."""
|
||||||
query = self.search_input.text().strip()
|
query = self.search_input.text().strip()
|
||||||
if not query:
|
category = self.category_combo.currentText()
|
||||||
return
|
sort = self.sort_combo.currentData()
|
||||||
|
|
||||||
self.btn_search.setEnabled(False)
|
self.btn_search.setEnabled(False)
|
||||||
|
self.btn_browse.setEnabled(False)
|
||||||
self.status_label.setText("Searching...")
|
self.status_label.setText("Searching...")
|
||||||
self.results_table.setRowCount(0)
|
self.results_table.setRowCount(0)
|
||||||
|
|
||||||
self._search_worker = SearchWorker(query)
|
self._search_worker = SearchWorker(
|
||||||
|
query=query,
|
||||||
|
category=category,
|
||||||
|
sort=sort,
|
||||||
|
tags=self._current_tags if self._current_tags else None,
|
||||||
|
page=self._current_page,
|
||||||
|
per_page=20
|
||||||
|
)
|
||||||
self._search_worker.finished.connect(self._on_search_complete)
|
self._search_worker.finished.connect(self._on_search_complete)
|
||||||
self._search_worker.error.connect(self._on_search_error)
|
self._search_worker.error.connect(self._on_search_error)
|
||||||
self._search_worker.start()
|
self._search_worker.start()
|
||||||
|
|
||||||
def _on_search_complete(self, tools: list):
|
def _on_search_complete(self, result):
|
||||||
"""Handle search results."""
|
"""Handle search results."""
|
||||||
self.btn_search.setEnabled(True)
|
self.btn_search.setEnabled(True)
|
||||||
self.status_label.setText(f"Found {len(tools)} tools")
|
self.btn_browse.setEnabled(True)
|
||||||
|
|
||||||
|
tools = result.data
|
||||||
|
self._total_pages = result.total_pages or 1
|
||||||
|
total = result.total or len(tools)
|
||||||
|
|
||||||
|
self.status_label.setText(f"Found {total} tools")
|
||||||
|
self.page_label.setText(f"Page {self._current_page} of {self._total_pages}")
|
||||||
|
|
||||||
|
# Update pagination buttons
|
||||||
|
self.btn_prev.setEnabled(self._current_page > 1)
|
||||||
|
self.btn_next.setEnabled(self._current_page < self._total_pages)
|
||||||
|
|
||||||
self.results_table.setRowCount(len(tools))
|
self.results_table.setRowCount(len(tools))
|
||||||
for row, tool in enumerate(tools):
|
for row, tool in enumerate(tools):
|
||||||
name_item = QTableWidgetItem(tool.get("name", ""))
|
tool_name = tool.get("name", "")
|
||||||
|
|
||||||
|
# Installed indicator
|
||||||
|
installed_item = QTableWidgetItem()
|
||||||
|
if tool_name in self._installed_tools:
|
||||||
|
installed_version = self._installed_tools[tool_name]
|
||||||
|
registry_version = tool.get("version", "1.0.0")
|
||||||
|
if installed_version != registry_version:
|
||||||
|
installed_item.setText("↑") # Update available
|
||||||
|
installed_item.setToolTip(f"Update available: {installed_version} → {registry_version}")
|
||||||
|
installed_item.setForeground(QColor("#48bb78"))
|
||||||
|
else:
|
||||||
|
installed_item.setText("✓")
|
||||||
|
installed_item.setToolTip("Installed")
|
||||||
|
installed_item.setForeground(QColor("#4299e1"))
|
||||||
|
installed_item.setTextAlignment(Qt.AlignCenter)
|
||||||
|
self.results_table.setItem(row, 0, installed_item)
|
||||||
|
|
||||||
|
# Name
|
||||||
|
name_item = QTableWidgetItem(tool_name)
|
||||||
name_item.setData(Qt.UserRole, tool)
|
name_item.setData(Qt.UserRole, tool)
|
||||||
self.results_table.setItem(row, 0, name_item)
|
self.results_table.setItem(row, 1, name_item)
|
||||||
self.results_table.setItem(row, 1, QTableWidgetItem(tool.get("owner", "")))
|
|
||||||
self.results_table.setItem(row, 2, QTableWidgetItem(str(tool.get("downloads", 0))))
|
# Owner
|
||||||
self.results_table.setItem(row, 3, QTableWidgetItem(tool.get("version", "1.0.0")))
|
self.results_table.setItem(row, 2, QTableWidgetItem(tool.get("owner", "")))
|
||||||
|
|
||||||
|
# Rating (stars)
|
||||||
|
rating = tool.get("average_rating", 0) or 0
|
||||||
|
rating_count = tool.get("rating_count", 0) or 0
|
||||||
|
stars = self._rating_to_stars(rating)
|
||||||
|
rating_item = QTableWidgetItem(f"{stars} ({rating_count})")
|
||||||
|
rating_item.setToolTip(f"{rating:.1f}/5 from {rating_count} ratings")
|
||||||
|
self.results_table.setItem(row, 3, rating_item)
|
||||||
|
|
||||||
|
# Downloads
|
||||||
|
downloads = tool.get("downloads", 0)
|
||||||
|
downloads_str = self._format_downloads(downloads)
|
||||||
|
self.results_table.setItem(row, 4, QTableWidgetItem(downloads_str))
|
||||||
|
|
||||||
|
# Version
|
||||||
|
self.results_table.setItem(row, 5, QTableWidgetItem(tool.get("version", "1.0.0")))
|
||||||
|
|
||||||
|
def _rating_to_stars(self, rating: float) -> str:
|
||||||
|
"""Convert rating to star display."""
|
||||||
|
if rating <= 0:
|
||||||
|
return "☆☆☆☆☆"
|
||||||
|
full_stars = int(rating)
|
||||||
|
half_star = rating - full_stars >= 0.5
|
||||||
|
empty_stars = 5 - full_stars - (1 if half_star else 0)
|
||||||
|
return "★" * full_stars + ("½" if half_star else "") + "☆" * empty_stars
|
||||||
|
|
||||||
|
def _format_downloads(self, downloads: int) -> str:
|
||||||
|
"""Format download count."""
|
||||||
|
if downloads >= 1000000:
|
||||||
|
return f"{downloads/1000000:.1f}M"
|
||||||
|
elif downloads >= 1000:
|
||||||
|
return f"{downloads/1000:.1f}K"
|
||||||
|
return str(downloads)
|
||||||
|
|
||||||
def _on_search_error(self, error: str):
|
def _on_search_error(self, error: str):
|
||||||
"""Handle search error."""
|
"""Handle search error."""
|
||||||
self.btn_search.setEnabled(True)
|
self.btn_search.setEnabled(True)
|
||||||
|
self.btn_browse.setEnabled(True)
|
||||||
self.status_label.setText(f"Error: {error}")
|
self.status_label.setText(f"Error: {error}")
|
||||||
|
|
||||||
|
def _prev_page(self):
|
||||||
|
"""Go to previous page."""
|
||||||
|
if self._current_page > 1:
|
||||||
|
self._current_page -= 1
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
|
def _next_page(self):
|
||||||
|
"""Go to next page."""
|
||||||
|
if self._current_page < self._total_pages:
|
||||||
|
self._current_page += 1
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
def _on_selection_changed(self):
|
def _on_selection_changed(self):
|
||||||
"""Handle selection change."""
|
"""Handle selection change."""
|
||||||
items = self.results_table.selectedItems()
|
items = self.results_table.selectedItems()
|
||||||
|
|
@ -188,35 +396,141 @@ class RegistryPage(QWidget):
|
||||||
self._selected_tool = None
|
self._selected_tool = None
|
||||||
self.details_text.clear()
|
self.details_text.clear()
|
||||||
self.btn_install.setEnabled(False)
|
self.btn_install.setEnabled(False)
|
||||||
|
self.btn_update.hide()
|
||||||
return
|
return
|
||||||
|
|
||||||
row = items[0].row()
|
row = items[0].row()
|
||||||
name_item = self.results_table.item(row, 0)
|
name_item = self.results_table.item(row, 1)
|
||||||
tool = name_item.data(Qt.UserRole)
|
tool = name_item.data(Qt.UserRole)
|
||||||
self._selected_tool = tool
|
self._selected_tool = tool
|
||||||
self._show_tool_details(tool)
|
self._show_tool_details(tool)
|
||||||
self.btn_install.setEnabled(True)
|
|
||||||
|
# Check if installed / update available
|
||||||
|
tool_name = tool.get("name", "")
|
||||||
|
if tool_name in self._installed_tools:
|
||||||
|
installed_version = self._installed_tools[tool_name]
|
||||||
|
registry_version = tool.get("version", "1.0.0")
|
||||||
|
if installed_version != registry_version:
|
||||||
|
self.btn_install.hide()
|
||||||
|
self.btn_update.show()
|
||||||
|
self.btn_update.setEnabled(True)
|
||||||
|
self.btn_update.setText(f"Update ({installed_version} → {registry_version})")
|
||||||
|
else:
|
||||||
|
self.btn_install.setText("Reinstall")
|
||||||
|
self.btn_install.setEnabled(True)
|
||||||
|
self.btn_install.show()
|
||||||
|
self.btn_update.hide()
|
||||||
|
else:
|
||||||
|
self.btn_install.setText("Install")
|
||||||
|
self.btn_install.setEnabled(True)
|
||||||
|
self.btn_install.show()
|
||||||
|
self.btn_update.hide()
|
||||||
|
|
||||||
def _show_tool_details(self, tool: dict):
|
def _show_tool_details(self, tool: dict):
|
||||||
"""Show tool details."""
|
"""Show tool details."""
|
||||||
lines = []
|
lines = []
|
||||||
lines.append(f"<h2>{tool.get('owner', '')}/{tool.get('name', '')}</h2>")
|
owner = tool.get('owner', '')
|
||||||
|
name = tool.get('name', '')
|
||||||
|
|
||||||
|
lines.append(f"<h2>{owner}/{name}</h2>")
|
||||||
|
|
||||||
if tool.get("description"):
|
if tool.get("description"):
|
||||||
lines.append(f"<p>{tool.get('description')}</p>")
|
lines.append(f"<p style='color: #4a5568;'>{tool.get('description')}</p>")
|
||||||
|
|
||||||
|
# Rating display
|
||||||
|
rating = tool.get("average_rating", 0) or 0
|
||||||
|
rating_count = tool.get("rating_count", 0) or 0
|
||||||
|
stars = self._rating_to_stars(rating)
|
||||||
|
lines.append(f"<p><strong>Rating:</strong> {stars} ({rating:.1f}/5 from {rating_count} reviews)</p>")
|
||||||
|
|
||||||
lines.append(f"<p><strong>Version:</strong> {tool.get('version', '1.0.0')}</p>")
|
lines.append(f"<p><strong>Version:</strong> {tool.get('version', '1.0.0')}</p>")
|
||||||
lines.append(f"<p><strong>Downloads:</strong> {tool.get('downloads', 0)}</p>")
|
lines.append(f"<p><strong>Downloads:</strong> {tool.get('downloads', 0):,}</p>")
|
||||||
|
|
||||||
if tool.get("category"):
|
if tool.get("category"):
|
||||||
lines.append(f"<p><strong>Category:</strong> {tool.get('category')}</p>")
|
lines.append(f"<p><strong>Category:</strong> {tool.get('category')}</p>")
|
||||||
|
|
||||||
|
# Clickable tags
|
||||||
if tool.get("tags"):
|
if tool.get("tags"):
|
||||||
tags = ", ".join(tool.get("tags", []))
|
tags_html = []
|
||||||
lines.append(f"<p><strong>Tags:</strong> {tags}</p>")
|
for tag in tool.get("tags", []):
|
||||||
|
tags_html.append(
|
||||||
|
f'<a href="tag:{tag}" style="background: #edf2f7; padding: 2px 8px; '
|
||||||
|
f'border-radius: 4px; text-decoration: none; color: #4a5568; margin-right: 4px;">{tag}</a>'
|
||||||
|
)
|
||||||
|
lines.append(f"<p><strong>Tags:</strong> {''.join(tags_html)}</p>")
|
||||||
|
|
||||||
|
# Publisher info
|
||||||
|
if tool.get("publisher_reputation"):
|
||||||
|
rep = tool.get("publisher_reputation", {})
|
||||||
|
lines.append(f"<p style='margin-top: 16px; color: #718096; font-size: 12px;'>"
|
||||||
|
f"Publisher: @{owner} · {rep.get('total_tools', 0)} tools · "
|
||||||
|
f"{rep.get('total_downloads', 0):,} total downloads</p>")
|
||||||
|
|
||||||
|
# Installed status
|
||||||
|
tool_name = tool.get("name", "")
|
||||||
|
if tool_name in self._installed_tools:
|
||||||
|
installed_version = self._installed_tools[tool_name]
|
||||||
|
registry_version = tool.get("version", "1.0.0")
|
||||||
|
if installed_version != registry_version:
|
||||||
|
lines.append(f"<p style='color: #48bb78; font-weight: 600;'>"
|
||||||
|
f"✓ Installed (v{installed_version}) - Update available!</p>")
|
||||||
|
else:
|
||||||
|
lines.append(f"<p style='color: #4299e1;'>✓ Installed (v{installed_version})</p>")
|
||||||
|
|
||||||
self.details_text.setHtml("\n".join(lines))
|
self.details_text.setHtml("\n".join(lines))
|
||||||
|
|
||||||
|
def _on_tag_clicked(self, url):
|
||||||
|
"""Handle tag click to filter by tag."""
|
||||||
|
tag = url.toString().replace("tag:", "")
|
||||||
|
if tag not in self._current_tags:
|
||||||
|
self._current_tags.append(tag)
|
||||||
|
self._update_tags_display()
|
||||||
|
self._current_page = 1
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
|
def _update_tags_display(self):
|
||||||
|
"""Update the active tags display."""
|
||||||
|
# Clear existing tag buttons
|
||||||
|
while self.tags_layout.count() > 2:
|
||||||
|
item = self.tags_layout.takeAt(2)
|
||||||
|
if item.widget():
|
||||||
|
item.widget().deleteLater()
|
||||||
|
|
||||||
|
if self._current_tags:
|
||||||
|
self.tags_widget.show()
|
||||||
|
for tag in self._current_tags:
|
||||||
|
tag_btn = QPushButton(f"{tag} ×")
|
||||||
|
tag_btn.setStyleSheet(
|
||||||
|
"background: #4299e1; color: white; border: none; "
|
||||||
|
"padding: 4px 8px; border-radius: 4px;"
|
||||||
|
)
|
||||||
|
tag_btn.clicked.connect(lambda checked, t=tag: self._remove_tag(t))
|
||||||
|
self.tags_layout.insertWidget(self.tags_layout.count() - 1, tag_btn)
|
||||||
|
|
||||||
|
# Add clear all button
|
||||||
|
clear_btn = QPushButton("Clear all")
|
||||||
|
clear_btn.setStyleSheet("color: #e53e3e;")
|
||||||
|
clear_btn.setFlat(True)
|
||||||
|
clear_btn.clicked.connect(self._clear_tags)
|
||||||
|
self.tags_layout.insertWidget(self.tags_layout.count() - 1, clear_btn)
|
||||||
|
else:
|
||||||
|
self.tags_widget.hide()
|
||||||
|
|
||||||
|
def _remove_tag(self, tag: str):
|
||||||
|
"""Remove a tag from the filter."""
|
||||||
|
if tag in self._current_tags:
|
||||||
|
self._current_tags.remove(tag)
|
||||||
|
self._update_tags_display()
|
||||||
|
self._current_page = 1
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
|
def _clear_tags(self):
|
||||||
|
"""Clear all tag filters."""
|
||||||
|
self._current_tags = []
|
||||||
|
self._update_tags_display()
|
||||||
|
self._current_page = 1
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
def _install_tool(self):
|
def _install_tool(self):
|
||||||
"""Install selected tool."""
|
"""Install selected tool."""
|
||||||
if not self._selected_tool:
|
if not self._selected_tool:
|
||||||
|
|
@ -226,6 +540,7 @@ class RegistryPage(QWidget):
|
||||||
name = self._selected_tool.get("name", "")
|
name = self._selected_tool.get("name", "")
|
||||||
|
|
||||||
self.btn_install.setEnabled(False)
|
self.btn_install.setEnabled(False)
|
||||||
|
self.btn_update.setEnabled(False)
|
||||||
self.status_label.setText(f"Installing {owner}/{name}...")
|
self.status_label.setText(f"Installing {owner}/{name}...")
|
||||||
|
|
||||||
self._install_worker = InstallWorker(owner, name)
|
self._install_worker = InstallWorker(owner, name)
|
||||||
|
|
@ -238,10 +553,23 @@ class RegistryPage(QWidget):
|
||||||
self.btn_install.setEnabled(True)
|
self.btn_install.setEnabled(True)
|
||||||
self.status_label.setText(f"Installed {tool_id}")
|
self.status_label.setText(f"Installed {tool_id}")
|
||||||
self.main_window.show_status(f"Installed {tool_id}")
|
self.main_window.show_status(f"Installed {tool_id}")
|
||||||
|
|
||||||
|
# Refresh installed tools list
|
||||||
|
self._load_installed_tools()
|
||||||
|
|
||||||
|
# Update the display
|
||||||
|
if self._selected_tool:
|
||||||
|
self._show_tool_details(self._selected_tool)
|
||||||
|
self._on_selection_changed()
|
||||||
|
|
||||||
|
# Refresh the table to update indicators
|
||||||
|
self._do_search()
|
||||||
|
|
||||||
QMessageBox.information(self, "Success", f"Successfully installed {tool_id}")
|
QMessageBox.information(self, "Success", f"Successfully installed {tool_id}")
|
||||||
|
|
||||||
def _on_install_error(self, error: str):
|
def _on_install_error(self, error: str):
|
||||||
"""Handle install error."""
|
"""Handle install error."""
|
||||||
self.btn_install.setEnabled(True)
|
self.btn_install.setEnabled(True)
|
||||||
|
self.btn_update.setEnabled(True)
|
||||||
self.status_label.setText(f"Error: {error}")
|
self.status_label.setText(f"Error: {error}")
|
||||||
QMessageBox.critical(self, "Install Error", f"Failed to install tool:\n{error}")
|
QMessageBox.critical(self, "Install Error", f"Failed to install tool:\n{error}")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue