689 lines
25 KiB
Python
689 lines
25 KiB
Python
"""
|
|
EU-Utility - Plugin Store
|
|
|
|
Fetches and installs plugins from remote repositories.
|
|
"""
|
|
|
|
import json
|
|
import shutil
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
from dataclasses import dataclass
|
|
from urllib.parse import urljoin
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
QScrollArea, QLineEdit, QComboBox, QFrame, QGridLayout,
|
|
QProgressBar, QMessageBox, QTextEdit
|
|
)
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
|
from PyQt6.QtGui import QFont
|
|
|
|
|
|
@dataclass
|
|
class PluginInfo:
|
|
"""Plugin metadata from repository."""
|
|
id: str
|
|
name: str
|
|
version: str
|
|
author: str
|
|
description: str
|
|
folder: str
|
|
icon: str
|
|
tags: List[str]
|
|
dependencies: Dict
|
|
min_core_version: str
|
|
category: str
|
|
installed: bool = False
|
|
update_available: bool = False
|
|
enabled: bool = False
|
|
|
|
|
|
class PluginStoreWorker(QThread):
|
|
"""Background worker for plugin store operations."""
|
|
|
|
manifest_loaded = pyqtSignal(list) # List[PluginInfo]
|
|
plugin_downloaded = pyqtSignal(str, bool) # plugin_id, success
|
|
progress_update = pyqtSignal(str)
|
|
error = pyqtSignal(str)
|
|
|
|
def __init__(self, repo_url: str, plugins_dir: Path):
|
|
super().__init__()
|
|
self.repo_url = repo_url
|
|
self.plugins_dir = plugins_dir
|
|
self.operation = None
|
|
self.target_plugin = None
|
|
|
|
def fetch_manifest(self):
|
|
"""Queue fetch manifest operation."""
|
|
self.operation = 'fetch_manifest'
|
|
self.start()
|
|
|
|
def download_plugin(self, plugin_id: str, folder: str):
|
|
"""Queue download operation."""
|
|
self.operation = 'download'
|
|
self.target_plugin = {'id': plugin_id, 'folder': folder}
|
|
self.start()
|
|
|
|
def check_updates(self, installed_plugins: Dict):
|
|
"""Queue update check operation."""
|
|
self.operation = 'check_updates'
|
|
self.installed_plugins = installed_plugins
|
|
self.start()
|
|
|
|
def run(self):
|
|
"""Execute queued operation."""
|
|
try:
|
|
if self.operation == 'fetch_manifest':
|
|
self._do_fetch_manifest()
|
|
elif self.operation == 'download':
|
|
self._do_download()
|
|
elif self.operation == 'check_updates':
|
|
self._do_check_updates()
|
|
except Exception as e:
|
|
self.error.emit(str(e))
|
|
|
|
def _do_fetch_manifest(self):
|
|
"""Fetch plugin manifest from repository."""
|
|
self.progress_update.emit("Fetching plugin list...")
|
|
|
|
try:
|
|
# Try HTTP first
|
|
import urllib.request
|
|
manifest_url = f"{self.repo_url}/raw/branch/main/manifest.json"
|
|
|
|
with urllib.request.urlopen(manifest_url, timeout=10) as response:
|
|
data = json.loads(response.read().decode('utf-8'))
|
|
except Exception as e:
|
|
# Fall back to local file for development
|
|
self.progress_update.emit("Using local manifest...")
|
|
manifest_path = Path(__file__).parent.parent.parent / "manifest.json"
|
|
if manifest_path.exists():
|
|
with open(manifest_path, 'r') as f:
|
|
data = json.load(f)
|
|
else:
|
|
raise Exception(f"Could not fetch manifest: {e}")
|
|
|
|
plugins = []
|
|
for p in data.get('plugins', []):
|
|
plugins.append(PluginInfo(
|
|
id=p['id'],
|
|
name=p['name'],
|
|
version=p['version'],
|
|
author=p['author'],
|
|
description=p['description'],
|
|
folder=p['folder'],
|
|
icon=p.get('icon', 'box'),
|
|
tags=p.get('tags', []),
|
|
dependencies=p.get('dependencies', {}),
|
|
min_core_version=p.get('min_core_version', '2.0.0'),
|
|
category=p.get('category', 'Other')
|
|
))
|
|
|
|
self.manifest_loaded.emit(plugins)
|
|
|
|
def _do_download(self):
|
|
"""Download and install a plugin using raw files (no git clone)."""
|
|
plugin_id = self.target_plugin['id']
|
|
folder = self.target_plugin['folder']
|
|
|
|
self.progress_update.emit(f"Downloading {plugin_id}...")
|
|
|
|
try:
|
|
import urllib.request
|
|
|
|
# Download files directly from raw git
|
|
dest = self.plugins_dir / plugin_id
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create __init__.py
|
|
init_url = f"{self.repo_url}/raw/branch/main/{folder}/__init__.py"
|
|
try:
|
|
urllib.request.urlretrieve(init_url, dest / "__init__.py")
|
|
except:
|
|
(dest / "__init__.py").touch()
|
|
|
|
# Download plugin.py
|
|
plugin_url = f"{self.repo_url}/raw/branch/main/{folder}/plugin.py"
|
|
self.progress_update.emit(f"Downloading {plugin_id}/plugin.py...")
|
|
urllib.request.urlretrieve(plugin_url, dest / "plugin.py")
|
|
|
|
self.plugin_downloaded.emit(plugin_id, True)
|
|
|
|
except Exception as e:
|
|
self.error.emit(str(e))
|
|
self.plugin_downloaded.emit(plugin_id, False)
|
|
|
|
def _do_check_updates(self):
|
|
"""Check for available plugin updates."""
|
|
self.progress_update.emit("Checking for updates...")
|
|
|
|
# Fetch manifest
|
|
self._do_fetch_manifest()
|
|
|
|
# Compare versions
|
|
updates_available = []
|
|
for plugin_id, local_info in self.installed_plugins.items():
|
|
# Compare with remote
|
|
pass
|
|
|
|
|
|
class PluginStoreUI(QWidget):
|
|
"""Plugin Store user interface."""
|
|
|
|
DEFAULT_REPO = "https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo"
|
|
|
|
def __init__(self, plugin_manager, parent=None):
|
|
super().__init__(parent)
|
|
self.plugin_manager = plugin_manager
|
|
self.plugins_dir = Path("plugins")
|
|
self.available_plugins: List[PluginInfo] = []
|
|
self.filtered_plugins: List[PluginInfo] = []
|
|
|
|
self._setup_ui()
|
|
self._setup_worker()
|
|
self._load_plugins()
|
|
|
|
def _setup_worker(self):
|
|
"""Setup background worker."""
|
|
self.worker = PluginStoreWorker(self.DEFAULT_REPO, self.plugins_dir)
|
|
self.worker.manifest_loaded.connect(self._on_manifest_loaded)
|
|
self.worker.plugin_downloaded.connect(self._on_plugin_downloaded)
|
|
self.worker.progress_update.connect(self._on_progress)
|
|
self.worker.error.connect(self._on_error)
|
|
|
|
def _setup_ui(self):
|
|
"""Create the UI."""
|
|
layout = QVBoxLayout(self)
|
|
layout.setSpacing(15)
|
|
layout.setContentsMargins(0, 0, 0, 0) # Remove margins to fill parent
|
|
|
|
# Header
|
|
header = QLabel("🔌 Plugin Store")
|
|
header.setStyleSheet("font-size: 24px; font-weight: bold; color: white;")
|
|
layout.addWidget(header)
|
|
|
|
# Info
|
|
info = QLabel("Browse and install plugins to extend EU-Utility functionality.")
|
|
info.setStyleSheet("color: rgba(255,255,255,150); font-size: 12px;")
|
|
layout.addWidget(info)
|
|
|
|
# Progress bar
|
|
self.progress = QProgressBar()
|
|
self.progress.setTextVisible(True)
|
|
self.progress.setStyleSheet("""
|
|
QProgressBar {
|
|
background-color: rgba(30, 35, 45, 200);
|
|
border: 1px solid rgba(100, 110, 130, 80);
|
|
border-radius: 4px;
|
|
color: white;
|
|
}
|
|
QProgressBar::chunk {
|
|
background-color: #4ecdc4;
|
|
border-radius: 4px;
|
|
}
|
|
""")
|
|
self.progress.hide()
|
|
layout.addWidget(self.progress)
|
|
|
|
# Search and filter bar
|
|
filter_layout = QHBoxLayout()
|
|
|
|
self.search_input = QLineEdit()
|
|
self.search_input.setPlaceholderText("🔍 Search plugins...")
|
|
self.search_input.setMinimumWidth(300)
|
|
self.search_input.setStyleSheet("""
|
|
QLineEdit {
|
|
background-color: rgba(30, 35, 45, 200);
|
|
color: white;
|
|
border: 1px solid rgba(100, 110, 130, 80);
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
}
|
|
""")
|
|
self.search_input.textChanged.connect(self._filter_plugins)
|
|
filter_layout.addWidget(self.search_input, 2)
|
|
|
|
self.category_combo = QComboBox()
|
|
self.category_combo.addItem("All Categories")
|
|
self.category_combo.setMinimumWidth(150)
|
|
self.category_combo.setStyleSheet("""
|
|
QComboBox {
|
|
background-color: rgba(30, 35, 45, 200);
|
|
color: white;
|
|
border: 1px solid rgba(100, 110, 130, 80);
|
|
border-radius: 4px;
|
|
padding: 10px;
|
|
}
|
|
QComboBox::drop-down {
|
|
border: none;
|
|
}
|
|
QComboBox QAbstractItemView {
|
|
background-color: #1a1f2e;
|
|
color: white;
|
|
selection-background-color: #4a9eff;
|
|
}
|
|
""")
|
|
self.category_combo.currentTextChanged.connect(self._filter_plugins)
|
|
filter_layout.addWidget(self.category_combo, 1)
|
|
|
|
self.refresh_btn = QPushButton("🔄 Refresh")
|
|
self.refresh_btn.setMinimumWidth(100)
|
|
self.refresh_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #4a9eff;
|
|
color: white;
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #3a8eef;
|
|
}
|
|
""")
|
|
self.refresh_btn.clicked.connect(self._load_plugins)
|
|
filter_layout.addWidget(self.refresh_btn)
|
|
|
|
self.check_updates_btn = QPushButton("📥 Check Updates")
|
|
self.check_updates_btn.setMinimumWidth(120)
|
|
self.check_updates_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #ff8c42;
|
|
color: white;
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #e67e3a;
|
|
}
|
|
""")
|
|
self.check_updates_btn.clicked.connect(self._check_updates)
|
|
filter_layout.addWidget(self.check_updates_btn)
|
|
|
|
layout.addLayout(filter_layout)
|
|
|
|
# Plugin grid - make it expand to fill space
|
|
self.scroll = QScrollArea()
|
|
self.scroll.setWidgetResizable(True)
|
|
self.scroll.setFrameShape(QFrame.Shape.NoFrame)
|
|
self.scroll.setStyleSheet("background: transparent; border: none;")
|
|
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
|
|
self.grid_widget = QWidget()
|
|
self.grid_layout = QGridLayout(self.grid_widget)
|
|
self.grid_layout.setSpacing(20)
|
|
self.grid_layout.setContentsMargins(10, 10, 10, 10)
|
|
self.grid_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
|
|
|
|
self.scroll.setWidget(self.grid_widget)
|
|
layout.addWidget(self.scroll, 1) # Add stretch factor to fill space
|
|
|
|
def _load_plugins(self):
|
|
"""Load available plugins from repository."""
|
|
self.progress.show()
|
|
self.progress.setRange(0, 0) # Indeterminate
|
|
self.progress.setFormat("Loading plugins...")
|
|
self.worker.fetch_manifest()
|
|
|
|
def _on_manifest_loaded(self, plugins: List[PluginInfo]):
|
|
"""Handle loaded plugin manifest."""
|
|
self.progress.hide()
|
|
|
|
# Check installed status
|
|
for p in plugins:
|
|
p.installed = (self.plugins_dir / p.id).exists()
|
|
if p.installed and hasattr(self.plugin_manager, 'is_plugin_enabled'):
|
|
p.enabled = self.plugin_manager.is_plugin_enabled(f"plugins.{p.id}.plugin")
|
|
|
|
self.available_plugins = plugins
|
|
self.filtered_plugins = plugins
|
|
|
|
# Update category filter
|
|
categories = sorted(set(p.category for p in plugins))
|
|
current = self.category_combo.currentText()
|
|
self.category_combo.clear()
|
|
self.category_combo.addItem("All Categories")
|
|
self.category_combo.addItems(categories)
|
|
if current in categories:
|
|
self.category_combo.setCurrentText(current)
|
|
|
|
self._render_grid()
|
|
|
|
def _filter_plugins(self):
|
|
"""Filter plugins based on search and category."""
|
|
search = self.search_input.text().lower()
|
|
category = self.category_combo.currentText()
|
|
|
|
self.filtered_plugins = []
|
|
for p in self.available_plugins:
|
|
# Category filter
|
|
if category != "All Categories" and p.category != category:
|
|
continue
|
|
|
|
# Search filter
|
|
if search:
|
|
match = (
|
|
search in p.name.lower() or
|
|
search in p.description.lower() or
|
|
search in p.author.lower() or
|
|
any(search in t.lower() for t in p.tags)
|
|
)
|
|
if not match:
|
|
continue
|
|
|
|
self.filtered_plugins.append(p)
|
|
|
|
self._render_grid()
|
|
|
|
def _render_grid(self):
|
|
"""Render the plugin grid."""
|
|
# Clear existing
|
|
while self.grid_layout.count():
|
|
item = self.grid_layout.takeAt(0)
|
|
if item.widget():
|
|
item.widget().deleteLater()
|
|
|
|
if not self.filtered_plugins:
|
|
no_results = QLabel("No plugins found matching your criteria.")
|
|
no_results.setStyleSheet("color: rgba(255,255,255,100); font-size: 14px; padding: 50px;")
|
|
no_results.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.grid_layout.addWidget(no_results, 0, 0)
|
|
return
|
|
|
|
# Create cards - use minimum 2 columns, expand to fill
|
|
# Calculate columns based on available width
|
|
available_width = self.scroll.width() - 40 # Account for margins
|
|
card_width = 340 # Card width + spacing
|
|
columns = max(2, available_width // card_width)
|
|
|
|
for i, plugin in enumerate(self.filtered_plugins):
|
|
card = self._create_plugin_card(plugin)
|
|
row = i // columns
|
|
col = i % columns
|
|
self.grid_layout.addWidget(card, row, col)
|
|
# Make columns stretch evenly
|
|
self.grid_layout.setColumnStretch(col, 1)
|
|
|
|
def _create_plugin_card(self, plugin: PluginInfo) -> QFrame:
|
|
"""Create a plugin card widget."""
|
|
card = QFrame()
|
|
card.setMinimumSize(320, 220)
|
|
card.setMaximumWidth(400)
|
|
card.setStyleSheet("""
|
|
QFrame {
|
|
background-color: rgba(35, 40, 55, 200);
|
|
border: 1px solid rgba(100, 110, 130, 80);
|
|
border-radius: 8px;
|
|
}
|
|
QFrame:hover {
|
|
border: 1px solid #4a9eff;
|
|
}
|
|
""")
|
|
|
|
layout = QVBoxLayout(card)
|
|
layout.setSpacing(8)
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
|
|
# Header with icon and name
|
|
header = QHBoxLayout()
|
|
|
|
icon = QLabel(f"📦")
|
|
icon.setStyleSheet("font-size: 24px;")
|
|
header.addWidget(icon)
|
|
|
|
name_layout = QVBoxLayout()
|
|
|
|
name = QLabel(plugin.name)
|
|
name.setStyleSheet("font-size: 14px; font-weight: bold; color: white;")
|
|
name_layout.addWidget(name)
|
|
|
|
version = QLabel(f"v{plugin.version}")
|
|
version.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
|
|
name_layout.addWidget(version)
|
|
|
|
header.addLayout(name_layout, 1)
|
|
|
|
# Status badge
|
|
if plugin.installed:
|
|
if plugin.enabled:
|
|
status = QLabel("✅ Enabled")
|
|
status.setStyleSheet("color: #4ecdc4; font-size: 10px; font-weight: bold;")
|
|
else:
|
|
status = QLabel("📦 Installed")
|
|
status.setStyleSheet("color: #ff8c42; font-size: 10px; font-weight: bold;")
|
|
header.addWidget(status)
|
|
|
|
layout.addLayout(header)
|
|
|
|
# Description
|
|
desc = QLabel(plugin.description[:100] + "..." if len(plugin.description) > 100 else plugin.description)
|
|
desc.setStyleSheet("color: rgba(255,255,255,150); font-size: 11px;")
|
|
desc.setWordWrap(True)
|
|
layout.addWidget(desc)
|
|
|
|
# Tags
|
|
tags = QLabel(" • ".join(plugin.tags[:3]))
|
|
tags.setStyleSheet("color: #4a9eff; font-size: 10px;")
|
|
layout.addWidget(tags)
|
|
|
|
# Dependencies indicator
|
|
if plugin.dependencies.get('plugins') or plugin.dependencies.get('core'):
|
|
deps_count = len(plugin.dependencies.get('plugins', [])) + len(plugin.dependencies.get('core', []))
|
|
deps_label = QPushButton(f"🔗 {deps_count} dependencies")
|
|
deps_label.setStyleSheet("""
|
|
QPushButton {
|
|
color: #ffd93d;
|
|
font-size: 10px;
|
|
background-color: transparent;
|
|
border: none;
|
|
text-align: left;
|
|
padding: 0px;
|
|
}
|
|
QPushButton:hover {
|
|
color: #ffed8a;
|
|
text-decoration: underline;
|
|
}
|
|
""")
|
|
deps_label.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
deps_label.clicked.connect(lambda checked, p=plugin: self._show_dependencies_dialog(p))
|
|
layout.addWidget(deps_label)
|
|
|
|
layout.addStretch()
|
|
|
|
# Action button
|
|
btn_layout = QHBoxLayout()
|
|
|
|
if plugin.installed:
|
|
if plugin.enabled:
|
|
btn = QPushButton("Disable")
|
|
btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: rgba(255,255,255,20);
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: rgba(255,255,255,40);
|
|
}
|
|
""")
|
|
btn.clicked.connect(lambda checked, p=plugin: self._disable_plugin(p))
|
|
else:
|
|
btn = QPushButton("Enable")
|
|
btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #4ecdc4;
|
|
color: #141f23;
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #3dbdb4;
|
|
}
|
|
""")
|
|
btn.clicked.connect(lambda checked, p=plugin: self._enable_plugin(p))
|
|
|
|
# Uninstall button
|
|
uninstall_btn = QPushButton("🗑")
|
|
uninstall_btn.setFixedSize(32, 32)
|
|
uninstall_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: rgba(255,71,71,100);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 4px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: rgba(255,71,71,150);
|
|
}
|
|
""")
|
|
uninstall_btn.setToolTip("Uninstall plugin")
|
|
uninstall_btn.clicked.connect(lambda checked, p=plugin: self._uninstall_plugin(p))
|
|
btn_layout.addWidget(uninstall_btn)
|
|
else:
|
|
btn = QPushButton("Install")
|
|
btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #4a9eff;
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #3a8eef;
|
|
}
|
|
""")
|
|
btn.clicked.connect(lambda checked, p=plugin: self._install_plugin(p))
|
|
|
|
btn_layout.addWidget(btn)
|
|
btn_layout.addStretch()
|
|
layout.addLayout(btn_layout)
|
|
|
|
return card
|
|
|
|
def _install_plugin(self, plugin: PluginInfo):
|
|
"""Install a plugin."""
|
|
# Check dependencies
|
|
deps = plugin.dependencies.get('plugins', [])
|
|
if deps:
|
|
msg = f"This plugin requires:\n"
|
|
for dep in deps:
|
|
msg += f" • {dep}\n"
|
|
msg += "\nThese will be installed automatically.\n\nContinue?"
|
|
|
|
reply = QMessageBox.question(
|
|
self, "Install Dependencies", msg,
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
)
|
|
|
|
if reply != QMessageBox.StandardButton.Yes:
|
|
return
|
|
|
|
self.progress.show()
|
|
self.progress.setRange(0, 0)
|
|
self.progress.setFormat(f"Installing {plugin.name}...")
|
|
self.worker.download_plugin(plugin.id, plugin.folder)
|
|
|
|
def _on_plugin_downloaded(self, plugin_id: str, success: bool):
|
|
"""Handle plugin download completion."""
|
|
self.progress.hide()
|
|
|
|
if success:
|
|
QMessageBox.information(self, "Success", f"Plugin '{plugin_id}' installed successfully!\n\nRestart EU-Utility to use the plugin.")
|
|
self._load_plugins() # Refresh
|
|
else:
|
|
QMessageBox.critical(self, "Error", f"Failed to install plugin '{plugin_id}'.")
|
|
|
|
def _enable_plugin(self, plugin: PluginInfo):
|
|
"""Enable an installed plugin."""
|
|
plugin_id = f"plugins.{plugin.id}.plugin"
|
|
if hasattr(self.plugin_manager, 'enable_plugin'):
|
|
self.plugin_manager.enable_plugin(plugin_id)
|
|
plugin.enabled = True
|
|
self._render_grid()
|
|
|
|
def _disable_plugin(self, plugin: PluginInfo):
|
|
"""Disable an installed plugin."""
|
|
plugin_id = f"plugins.{plugin.id}.plugin"
|
|
if hasattr(self.plugin_manager, 'enable_plugin'):
|
|
self.plugin_manager.disable_plugin(plugin_id)
|
|
plugin.enabled = False
|
|
self._render_grid()
|
|
|
|
def _uninstall_plugin(self, plugin: PluginInfo):
|
|
"""Uninstall a plugin."""
|
|
reply = QMessageBox.question(
|
|
self, "Confirm Uninstall",
|
|
f"Are you sure you want to uninstall '{plugin.name}'?\n\nThis will delete all plugin data.",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
)
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
plugin_path = self.plugins_dir / plugin.id
|
|
if plugin_path.exists():
|
|
shutil.rmtree(plugin_path)
|
|
QMessageBox.information(self, "Uninstalled", f"'{plugin.name}' has been uninstalled.")
|
|
self._load_plugins()
|
|
|
|
def _show_dependencies_dialog(self, plugin: PluginInfo):
|
|
"""Show dialog with plugin dependencies."""
|
|
deps = plugin.dependencies
|
|
|
|
msg = f"<h3>🔌 {plugin.name} Dependencies</h3>"
|
|
|
|
# Core dependencies
|
|
core_deps = deps.get('core', [])
|
|
if core_deps:
|
|
msg += "<h4>Core Services Required:</h4><ul>"
|
|
for dep in core_deps:
|
|
msg += f"<li>✅ {dep} (built-in)</li>"
|
|
msg += "</ul>"
|
|
|
|
# Plugin dependencies
|
|
plugin_deps = deps.get('plugins', [])
|
|
if plugin_deps:
|
|
msg += "<h4>Plugins Required:</h4><ul>"
|
|
for dep in plugin_deps:
|
|
dep_name = dep.split('.')[-1].replace('_', ' ').title()
|
|
msg += f"<li>📦 {dep_name}</li>"
|
|
msg += "</ul>"
|
|
msg += "<p><i>These will be automatically installed when you install this plugin.</i></p>"
|
|
|
|
if not core_deps and not plugin_deps:
|
|
msg += "<p>This plugin has no dependencies.</p>"
|
|
|
|
dialog = QMessageBox(self)
|
|
dialog.setWindowTitle(f"Dependencies - {plugin.name}")
|
|
dialog.setTextFormat(Qt.TextFormat.RichText)
|
|
dialog.setText(msg)
|
|
dialog.setIcon(QMessageBox.Icon.Information)
|
|
dialog.exec()
|
|
|
|
def _check_updates(self):
|
|
"""Check for plugin updates."""
|
|
self.progress.show()
|
|
self.progress.setRange(0, 0)
|
|
self.progress.setFormat("Checking for updates...")
|
|
|
|
# TODO: Implement update checking
|
|
QTimer.singleShot(1000, self.progress.hide)
|
|
QMessageBox.information(self, "Updates", "All plugins are up to date!")
|
|
|
|
def _on_progress(self, message: str):
|
|
"""Handle progress updates."""
|
|
self.progress.setFormat(message)
|
|
|
|
def _on_error(self, error: str):
|
|
"""Handle errors."""
|
|
self.progress.hide()
|
|
QMessageBox.critical(self, "Error", f"Plugin Store Error:\n{error}")
|