start of plugin support?
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""
|
||||
Filename: plugins.py
|
||||
Description: Plugins window
|
||||
Note: Compliant with pylance strict type checking
|
||||
|
||||
Author: Tyler de Zeeuw
|
||||
License: GPL-3.0
|
||||
"""
|
||||
|
||||
# Built-in imports
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
from typing import Any, cast
|
||||
|
||||
# External library imports
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton, QTabWidget, QVBoxLayout, QWidget
|
||||
|
||||
from src.shared.shareddata import APP_NAME, PLATFORM_NAME, PLUGINS_URL
|
||||
|
||||
|
||||
class PluginsWindow(QWidget):
|
||||
"""
|
||||
Plugins window containing two tabs: Installed Plugins and Plugin Browser.
|
||||
|
||||
Args:
|
||||
parent (QWidget | None, optional): Parent widget of this window. Defaults to None.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QWidget | None = None) -> None:
|
||||
super().__init__(parent, Qt.WindowType.Window)
|
||||
self.setWindowTitle(f"{APP_NAME.upper()} - Plugins")
|
||||
self.resize(650, 500)
|
||||
|
||||
self.plugins_dir: Path = self._resolve_plugins_dir()
|
||||
self.repository_urls: list[str] = [PLUGINS_URL]
|
||||
self.remote_plugins_data: list[dict[str, Any]] = []
|
||||
self._has_fetched_remote: bool = False
|
||||
|
||||
main_layout = QVBoxLayout(self)
|
||||
|
||||
self.tab_widget = QTabWidget(self)
|
||||
self.installed_tab = self._create_installed_tab()
|
||||
self.browser_tab = self._create_browser_tab()
|
||||
|
||||
self.tab_widget.addTab(self.installed_tab, "Installed Plugins")
|
||||
self.tab_widget.addTab(self.browser_tab, "Plugin Browser")
|
||||
|
||||
self.tab_widget.setCurrentIndex(0)
|
||||
self.tab_widget.currentChanged.connect(self._on_tab_changed)
|
||||
|
||||
main_layout.addWidget(self.tab_widget)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
self.refresh_installed_plugins()
|
||||
|
||||
def _resolve_plugins_dir(self) -> Path:
|
||||
"""Determines the local plugins directory based on execution context."""
|
||||
if PLATFORM_NAME == "darwin":
|
||||
base_dir = Path(sys.executable).parent / "../../.."
|
||||
else:
|
||||
base_dir = Path.cwd()
|
||||
|
||||
return (base_dir / "plugins").resolve()
|
||||
|
||||
def _on_tab_changed(self, index: int) -> None:
|
||||
"""Triggers remote fetch only when switching specifically to the Plugin Browser tab."""
|
||||
if index == 1 and not self._has_fetched_remote:
|
||||
self.fetch_remote_plugins()
|
||||
|
||||
def refresh_installed_plugins(self) -> None:
|
||||
"""Scans local plugins directory silently without UI popups."""
|
||||
self.installed_list.clear()
|
||||
self.plugins_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
installed_plugins: list[str] = []
|
||||
|
||||
for entry in self.plugins_dir.iterdir():
|
||||
if entry.is_dir() and not entry.name.startswith((".", "__")):
|
||||
installed_plugins.append(entry.name)
|
||||
elif entry.is_file() and entry.suffix == ".py" and entry.stem != "__init__":
|
||||
installed_plugins.append(entry.stem)
|
||||
|
||||
if installed_plugins:
|
||||
self.installed_list.addItems(sorted(installed_plugins))
|
||||
else:
|
||||
self.installed_list.addItem("No plugins installed.")
|
||||
|
||||
def add_custom_repository(self) -> None:
|
||||
"""Adds a custom repository URL from the input field and refreshes the browser list."""
|
||||
url = self.repo_input.text().strip()
|
||||
if not url:
|
||||
return
|
||||
|
||||
if not (url.startswith("http://") or url.startswith("https://")):
|
||||
self._show_browser_error("Invalid URL format. Must start with http:// or https://")
|
||||
return
|
||||
|
||||
if url not in self.repository_urls:
|
||||
self.repository_urls.append(url)
|
||||
|
||||
self.repo_input.clear()
|
||||
self.fetch_remote_plugins()
|
||||
|
||||
def fetch_remote_plugins(self) -> None:
|
||||
"""
|
||||
Fetches plugins.json from all configured repository URLs directly into memory.
|
||||
Aggregates results and handles network failures gracefully in-line.
|
||||
"""
|
||||
self.browser_list.clear()
|
||||
self.browser_list.addItem("Fetching remote repositories...")
|
||||
|
||||
aggregated_plugins: list[dict[str, Any]] = []
|
||||
failed_count: int = 0
|
||||
|
||||
for url in self.repository_urls:
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={"User-Agent": f"{APP_NAME}-PluginManager"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
if response.status == 200:
|
||||
raw_data = response.read().decode("utf-8")
|
||||
plugins = json.loads(raw_data)
|
||||
if isinstance(plugins, list):
|
||||
aggregated_plugins.extend(cast(list[Any], plugins))
|
||||
else:
|
||||
failed_count += 1
|
||||
except Exception:
|
||||
failed_count += 1
|
||||
|
||||
self.remote_plugins_data = aggregated_plugins
|
||||
self._has_fetched_remote = True
|
||||
|
||||
if aggregated_plugins:
|
||||
self._populate_browser_list()
|
||||
elif failed_count == len(self.repository_urls):
|
||||
self._show_browser_error(
|
||||
"Unable to load plugins from any repository. Check your connection or URLs."
|
||||
)
|
||||
else:
|
||||
self._show_browser_error("No plugins found across configured repositories.")
|
||||
|
||||
def _show_browser_error(self, message: str) -> None:
|
||||
"""Renders error text directly in the list widget."""
|
||||
self.browser_list.clear()
|
||||
self.browser_list.addItem(message)
|
||||
|
||||
def _populate_browser_list(self) -> None:
|
||||
"""Populates list widget with parsed remote plugin data."""
|
||||
self.browser_list.clear()
|
||||
|
||||
for plugin in self.remote_plugins_data:
|
||||
name = plugin.get("name", "Unknown Plugin")
|
||||
version = plugin.get("version", "v0.0")
|
||||
desc = plugin.get("description", "No description provided.")
|
||||
|
||||
display_text = f"{name} ({version}) - {desc}"
|
||||
item = QListWidgetItem(display_text, self.browser_list)
|
||||
item.setData(Qt.ItemDataRole.UserRole, plugin)
|
||||
|
||||
def _create_installed_tab(self) -> QWidget:
|
||||
"""Creates 'Installed Plugins' tab UI."""
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
|
||||
path_label = QLabel(f"Directory: {self.plugins_dir}", tab)
|
||||
self.installed_list = QListWidget(tab)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.btn_refresh_local = QPushButton("Refresh List", tab)
|
||||
self.btn_enable_disable = QPushButton("Enable/Disable", tab)
|
||||
self.btn_uninstall = QPushButton("Uninstall", tab)
|
||||
|
||||
self.btn_refresh_local.clicked.connect(self.refresh_installed_plugins)
|
||||
|
||||
button_layout.addWidget(self.btn_refresh_local)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.btn_enable_disable)
|
||||
button_layout.addWidget(self.btn_uninstall)
|
||||
|
||||
layout.addWidget(path_label)
|
||||
layout.addWidget(self.installed_list)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
return tab
|
||||
|
||||
def _create_browser_tab(self) -> QWidget:
|
||||
"""Creates 'Plugin Browser' tab UI."""
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
|
||||
# Custom Repository Input Controls
|
||||
repo_layout = QHBoxLayout()
|
||||
self.repo_input = QLineEdit(tab)
|
||||
self.repo_input.setPlaceholderText("Enter custom plugins.json URL...")
|
||||
self.btn_add_repo = QPushButton("Add Repo", tab)
|
||||
self.btn_add_repo.clicked.connect(self.add_custom_repository)
|
||||
|
||||
repo_layout.addWidget(self.repo_input)
|
||||
repo_layout.addWidget(self.btn_add_repo)
|
||||
|
||||
label = QLabel("Available Plugins from Repositories:", tab)
|
||||
self.browser_list = QListWidget(tab)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
self.btn_fetch_remote = QPushButton("Fetch Remote Lists", tab)
|
||||
self.btn_install = QPushButton("Install Plugin", tab)
|
||||
|
||||
self.btn_fetch_remote.clicked.connect(self.fetch_remote_plugins)
|
||||
|
||||
button_layout.addWidget(self.btn_fetch_remote)
|
||||
button_layout.addStretch()
|
||||
button_layout.addWidget(self.btn_install)
|
||||
|
||||
layout.addLayout(repo_layout)
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(self.browser_list)
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
return tab
|
||||
Reference in New Issue
Block a user