start of plugin support?

This commit is contained in:
2026-09-01 02:14:47 -07:00
parent 815f342ead
commit ca203fcb56
9 changed files with 476 additions and 243 deletions
+224
View File
@@ -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