feat: Plugin Store - Framework-only architecture
BREAKING CHANGE: EU-Utility is now a framework-only application.
All user-facing features have been moved to separate plugin repository.
NEW FEATURES:
1. Plugin Store Core Module (core/plugin_store.py)
- PluginStoreWorker: Background operations (fetch, download, updates)
- PluginStoreUI: Grid-based plugin browser with cards
- PluginInfo dataclass for plugin metadata
- Fetches from remote git repository
2. Plugin Store UI Features:
- Grid layout with plugin cards (300x200px each)
- Search/filter by name, description, tags
- Category filter dropdown
- Visual indicators:
* 📦 Plugin icon (emoji-based)
* Version badge
* Status badges (✅ Enabled, 📦 Installed)
* Tag display
* 🔗 Dependency count with tooltip
- Install/Enable/Disable/Uninstall buttons
- Progress bar for operations
- Refresh and Check Updates buttons
3. Settings Integration:
- New 'Plugin Store' tab in Settings
- Moved plugin management to 'My Plugins' tab
- Plugin Store uses core module directly
4. Plugin Store UI Plugin (plugins/plugin_store_ui/):
- Standalone plugin for overlay integration
- Hotkey: Ctrl+Shift+P (configurable)
ARCHITECTURE CHANGES:
- EU-Utility Core: Framework only (PluginAPI, services, overlay)
- Plugin Repository: https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo
- Plugins installed via Store → user plugins/ directory
- Local plugins/ folder still supported for development
MANIFEST FORMAT:
USER WORKFLOW:
1. Open Settings → Plugin Store
2. Browse/search available plugins
3. Click Install (with dependency confirmation)
4. Restart EU-Utility
5. Enable plugin in 'My Plugins' tab
DEVELOPER WORKFLOW:
1. Develop plugin locally in plugins/
2. Test with core framework
3. Submit to plugin repository
4. Users install via Store
This enables limitless modularity - users only install
what they need, developers can publish independently.
This commit is contained in:
parent
09ad30c223
commit
725590e247
|
|
@ -1,253 +1,648 @@
|
|||
"""
|
||||
EU-Utility - Plugin Store
|
||||
|
||||
Fetch and install community plugins from GitHub.
|
||||
Fetches and installs plugins from remote repositories.
|
||||
"""
|
||||
|
||||
import json
|
||||
import zipfile
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from PyQt6.QtCore import QObject, QThread, pyqtSignal
|
||||
from typing import Dict, List, Optional
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from core.http_client import get_http_client
|
||||
|
||||
|
||||
class PluginStore(QObject):
|
||||
"""Community plugin repository manager."""
|
||||
|
||||
# Repository configuration
|
||||
REPO_URL = "https://raw.githubusercontent.com/ImpulsiveFPS/EU-Utility-Plugins/main/"
|
||||
INDEX_URL = REPO_URL + "plugins.json"
|
||||
|
||||
# Signals
|
||||
plugins_loaded = pyqtSignal(list)
|
||||
plugin_installed = pyqtSignal(str, bool)
|
||||
plugin_removed = pyqtSignal(str, bool)
|
||||
error_occurred = pyqtSignal(str)
|
||||
|
||||
def __init__(self, plugins_dir="user_plugins"):
|
||||
super().__init__()
|
||||
self.plugins_dir = Path(plugins_dir)
|
||||
self.plugins_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.available_plugins = []
|
||||
self.installed_plugins = []
|
||||
self._load_installed()
|
||||
|
||||
def fetch_plugins(self):
|
||||
"""Fetch available plugins from repository."""
|
||||
self.fetch_thread = PluginFetchThread(self.INDEX_URL)
|
||||
self.fetch_thread.fetched.connect(self._on_plugins_fetched)
|
||||
self.fetch_thread.error.connect(self._on_fetch_error)
|
||||
self.fetch_thread.start()
|
||||
|
||||
def _on_plugins_fetched(self, plugins):
|
||||
"""Handle fetched plugins."""
|
||||
self.available_plugins = plugins
|
||||
self.plugins_loaded.emit(plugins)
|
||||
|
||||
def _on_fetch_error(self, error):
|
||||
"""Handle fetch error."""
|
||||
self.error_occurred.emit(error)
|
||||
|
||||
def install_plugin(self, plugin_id):
|
||||
"""Install a plugin from the store."""
|
||||
plugin = self._find_plugin(plugin_id)
|
||||
if not plugin:
|
||||
self.plugin_installed.emit(plugin_id, False)
|
||||
return
|
||||
|
||||
self.install_thread = PluginInstallThread(
|
||||
plugin,
|
||||
self.plugins_dir
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QScrollArea, QLineEdit, QComboBox, QFrame, QGridLayout,
|
||||
QProgressBar, QMessageBox, QTextEdit
|
||||
)
|
||||
self.install_thread.installed.connect(
|
||||
lambda success: self._on_installed(plugin_id, success)
|
||||
)
|
||||
self.install_thread.error.connect(self._on_fetch_error)
|
||||
self.install_thread.start()
|
||||
|
||||
def _on_installed(self, plugin_id, success):
|
||||
"""Handle plugin installation."""
|
||||
if success:
|
||||
self.installed_plugins.append(plugin_id)
|
||||
self._save_installed()
|
||||
self.plugin_installed.emit(plugin_id, success)
|
||||
|
||||
def remove_plugin(self, plugin_id):
|
||||
"""Remove an installed plugin."""
|
||||
try:
|
||||
plugin_dir = self.plugins_dir / plugin_id
|
||||
if plugin_dir.exists():
|
||||
shutil.rmtree(plugin_dir)
|
||||
|
||||
if plugin_id in self.installed_plugins:
|
||||
self.installed_plugins.remove(plugin_id)
|
||||
self._save_installed()
|
||||
|
||||
self.plugin_removed.emit(plugin_id, True)
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
self.plugin_removed.emit(plugin_id, False)
|
||||
|
||||
def _find_plugin(self, plugin_id):
|
||||
"""Find plugin by ID."""
|
||||
for plugin in self.available_plugins:
|
||||
if plugin.get('id') == plugin_id:
|
||||
return plugin
|
||||
return None
|
||||
|
||||
def _load_installed(self):
|
||||
"""Load list of installed plugins."""
|
||||
installed_file = self.plugins_dir / ".installed"
|
||||
if installed_file.exists():
|
||||
try:
|
||||
with open(installed_file, 'r') as f:
|
||||
self.installed_plugins = json.load(f)
|
||||
except:
|
||||
self.installed_plugins = []
|
||||
|
||||
def _save_installed(self):
|
||||
"""Save list of installed plugins."""
|
||||
installed_file = self.plugins_dir / ".installed"
|
||||
with open(installed_file, 'w') as f:
|
||||
json.dump(self.installed_plugins, f)
|
||||
|
||||
def is_installed(self, plugin_id):
|
||||
"""Check if a plugin is installed."""
|
||||
return plugin_id in self.installed_plugins
|
||||
|
||||
def get_installed_plugins(self):
|
||||
"""Get list of installed plugin IDs."""
|
||||
return self.installed_plugins.copy()
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||||
from PyQt6.QtGui import QFont
|
||||
|
||||
|
||||
class PluginFetchThread(QThread):
|
||||
"""Background thread to fetch plugin index."""
|
||||
@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
|
||||
|
||||
fetched = pyqtSignal(list)
|
||||
|
||||
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, url):
|
||||
def __init__(self, repo_url: str, plugins_dir: Path):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
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):
|
||||
"""Fetch plugin index."""
|
||||
"""Execute queued operation."""
|
||||
try:
|
||||
http_client = get_http_client()
|
||||
response = http_client.get(
|
||||
self.url,
|
||||
cache_ttl=300, # 5 minute cache for plugin list
|
||||
headers={'User-Agent': 'EU-Utility/1.0'}
|
||||
)
|
||||
|
||||
if response.get('json'):
|
||||
data = response['json']
|
||||
self.fetched.emit(data.get('plugins', []))
|
||||
else:
|
||||
# Try to parse JSON from text
|
||||
data = json.loads(response['text'])
|
||||
self.fetched.emit(data.get('plugins', []))
|
||||
|
||||
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...")
|
||||
|
||||
class PluginInstallThread(QThread):
|
||||
"""Background thread to install a plugin."""
|
||||
|
||||
installed = pyqtSignal(bool)
|
||||
error = pyqtSignal(str)
|
||||
progress = pyqtSignal(str)
|
||||
|
||||
def __init__(self, plugin, install_dir):
|
||||
super().__init__()
|
||||
self.plugin = plugin
|
||||
self.install_dir = Path(install_dir)
|
||||
|
||||
def run(self):
|
||||
"""Install plugin."""
|
||||
try:
|
||||
self.progress.emit(f"Downloading {self.plugin['name']}...")
|
||||
# Try HTTP first
|
||||
import urllib.request
|
||||
manifest_url = f"{self.repo_url}/raw/branch/main/manifest.json"
|
||||
|
||||
# Download zip
|
||||
download_url = self.plugin.get('download_url')
|
||||
if not download_url:
|
||||
self.error.emit("No download URL")
|
||||
self.installed.emit(False)
|
||||
return
|
||||
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}")
|
||||
|
||||
temp_zip = self.install_dir / "temp.zip"
|
||||
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')
|
||||
))
|
||||
|
||||
http_client = get_http_client()
|
||||
response = http_client.get(
|
||||
download_url,
|
||||
cache_ttl=0, # Don't cache downloads
|
||||
headers={'User-Agent': 'EU-Utility/1.0'}
|
||||
self.manifest_loaded.emit(plugins)
|
||||
|
||||
def _do_download(self):
|
||||
"""Download and install a plugin."""
|
||||
plugin_id = self.target_plugin['id']
|
||||
folder = self.target_plugin['folder']
|
||||
|
||||
self.progress_update.emit(f"Downloading {plugin_id}...")
|
||||
|
||||
# Create temp directory
|
||||
temp_dir = Path("temp_download")
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
|
||||
try:
|
||||
# For git repositories, clone and copy folder
|
||||
import subprocess
|
||||
|
||||
clone_dir = temp_dir / "repo"
|
||||
if clone_dir.exists():
|
||||
shutil.rmtree(clone_dir)
|
||||
|
||||
# Clone the repository
|
||||
self.progress_update.emit(f"Cloning repository...")
|
||||
result = subprocess.run(
|
||||
['git', 'clone', '--depth', '1', self.repo_url, str(clone_dir)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
with open(temp_zip, 'wb') as f:
|
||||
f.write(response['content'])
|
||||
if result.returncode != 0:
|
||||
raise Exception(f"Git clone failed: {result.stderr}")
|
||||
|
||||
self.progress.emit("Extracting...")
|
||||
# Copy plugin folder
|
||||
source = clone_dir / folder
|
||||
dest = self.plugins_dir / plugin_id
|
||||
|
||||
# Extract
|
||||
plugin_dir = self.install_dir / self.plugin['id']
|
||||
if plugin_dir.exists():
|
||||
shutil.rmtree(plugin_dir)
|
||||
plugin_dir.mkdir()
|
||||
if dest.exists():
|
||||
shutil.rmtree(dest)
|
||||
|
||||
with zipfile.ZipFile(temp_zip, 'r') as zip_ref:
|
||||
zip_ref.extractall(plugin_dir)
|
||||
self.progress_update.emit(f"Installing {plugin_id}...")
|
||||
shutil.copytree(source, dest)
|
||||
|
||||
# Clean up
|
||||
temp_zip.unlink()
|
||||
shutil.rmtree(temp_dir)
|
||||
|
||||
self.progress.emit("Installed!")
|
||||
self.installed.emit(True)
|
||||
self.plugin_downloaded.emit(plugin_id, True)
|
||||
|
||||
except Exception as e:
|
||||
self.error.emit(str(e))
|
||||
self.installed.emit(False)
|
||||
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
|
||||
|
||||
|
||||
# Sample plugins.json structure for GitHub repo:
|
||||
SAMPLE_PLUGINS_JSON = {
|
||||
"version": "1.0.0",
|
||||
"plugins": [
|
||||
{
|
||||
"id": "loot_tracker",
|
||||
"name": "Loot Tracker",
|
||||
"description": "Track and analyze hunting loot",
|
||||
"version": "1.0.0",
|
||||
"author": "ImpulsiveFPS",
|
||||
"category": "hunting",
|
||||
"download_url": "https://github.com/.../loot_tracker.zip",
|
||||
"icon": "🎁",
|
||||
"min_app_version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"id": "mining_helper",
|
||||
"name": "Mining Helper",
|
||||
"description": "Track mining finds and claims",
|
||||
"version": "1.1.0",
|
||||
"author": "Community",
|
||||
"category": "mining",
|
||||
"download_url": "https://github.com/.../mining_helper.zip",
|
||||
"icon": "⛏️",
|
||||
"min_app_version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"id": "market_analyzer",
|
||||
"name": "Market Analyzer",
|
||||
"description": "Analyze auction prices and trends",
|
||||
"version": "0.9.0",
|
||||
"author": "EU Community",
|
||||
"category": "trading",
|
||||
"download_url": "https://github.com/.../market_analyzer.zip",
|
||||
"icon": "📈",
|
||||
"min_app_version": "1.0.0"
|
||||
},
|
||||
]
|
||||
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(20, 20, 20, 20)
|
||||
|
||||
# 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.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.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.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.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
|
||||
self.scroll = QScrollArea()
|
||||
self.scroll.setWidgetResizable(True)
|
||||
self.scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||
self.scroll.setStyleSheet("background: transparent; border: none;")
|
||||
|
||||
self.grid_widget = QWidget()
|
||||
self.grid_layout = QGridLayout(self.grid_widget)
|
||||
self.grid_layout.setSpacing(15)
|
||||
self.grid_layout.setAlignment(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft)
|
||||
|
||||
self.scroll.setWidget(self.grid_widget)
|
||||
layout.addWidget(self.scroll)
|
||||
|
||||
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
|
||||
columns = 3
|
||||
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)
|
||||
|
||||
def _create_plugin_card(self, plugin: PluginInfo) -> QFrame:
|
||||
"""Create a plugin card widget."""
|
||||
card = QFrame()
|
||||
card.setFixedSize(300, 200)
|
||||
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 = QLabel(f"🔗 {deps_count} dependencies")
|
||||
deps_label.setStyleSheet("color: #ffd93d; font-size: 10px;")
|
||||
deps_text = "Requires: " + ", ".join(
|
||||
plugin.dependencies.get('plugins', []) + plugin.dependencies.get('core', [])
|
||||
)
|
||||
deps_label.setToolTip(deps_text[:200])
|
||||
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 _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}")
|
||||
|
|
|
|||
|
|
@ -1,273 +1,55 @@
|
|||
"""
|
||||
EU-Utility - Plugin Store UI Plugin
|
||||
|
||||
Browse and install community plugins.
|
||||
Provides the Plugin Store interface for browsing and installing plugins.
|
||||
"""
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||
QPushButton, QLineEdit, QListWidget, QListWidgetItem,
|
||||
QProgressBar, QFrame, QTextEdit
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
|
||||
|
||||
from plugins.base_plugin import BasePlugin
|
||||
from core.plugin_store import PluginStoreUI
|
||||
|
||||
|
||||
class PluginStoreUIPlugin(BasePlugin):
|
||||
"""Browse and install community plugins."""
|
||||
"""Plugin Store for browsing and installing plugins from repositories."""
|
||||
|
||||
name = "Plugin Store"
|
||||
version = "1.0.0"
|
||||
author = "ImpulsiveFPS"
|
||||
description = "Community plugin marketplace"
|
||||
hotkey = "ctrl+shift+slash"
|
||||
description = "Browse, install, and manage plugins from the official repository"
|
||||
icon = "shopping-bag"
|
||||
hotkeys = [
|
||||
{
|
||||
'action': 'open_store',
|
||||
'description': 'Open Plugin Store',
|
||||
'default': 'ctrl+shift+p',
|
||||
'config_key': 'pluginstore_open'
|
||||
}
|
||||
]
|
||||
|
||||
def initialize(self):
|
||||
"""Setup plugin store."""
|
||||
self.available_plugins = []
|
||||
self.installed_plugins = []
|
||||
self.is_loading = False
|
||||
self.store_ui = None
|
||||
|
||||
def get_ui(self):
|
||||
"""Create plugin store UI."""
|
||||
# Get plugin manager from overlay
|
||||
plugin_manager = getattr(self.overlay, 'plugin_manager', None)
|
||||
|
||||
if not plugin_manager:
|
||||
# Fallback - show error
|
||||
widget = QWidget()
|
||||
widget.setStyleSheet("background: transparent;")
|
||||
layout = QVBoxLayout(widget)
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Title
|
||||
title = QLabel("🛒 Plugin Store")
|
||||
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
|
||||
layout.addWidget(title)
|
||||
|
||||
# Search
|
||||
search_layout = QHBoxLayout()
|
||||
self.search_input = QLineEdit()
|
||||
self.search_input.setPlaceholderText("Search plugins...")
|
||||
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: 8px;
|
||||
}
|
||||
""")
|
||||
search_layout.addWidget(self.search_input)
|
||||
|
||||
search_btn = QPushButton("🔍")
|
||||
search_btn.setFixedSize(32, 32)
|
||||
search_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #ff8c42;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
search_btn.clicked.connect(self._search_plugins)
|
||||
search_layout.addWidget(search_btn)
|
||||
|
||||
layout.addLayout(search_layout)
|
||||
|
||||
# Categories
|
||||
cats_layout = QHBoxLayout()
|
||||
for cat in ["All", "Hunting", "Mining", "Crafting", "Tools", "Social"]:
|
||||
btn = QPushButton(cat)
|
||||
btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: rgba(255,255,255,15);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: rgba(255,255,255,30);
|
||||
}
|
||||
""")
|
||||
cats_layout.addWidget(btn)
|
||||
cats_layout.addStretch()
|
||||
layout.addLayout(cats_layout)
|
||||
|
||||
# Plugins list
|
||||
self.plugins_list = QListWidget()
|
||||
self.plugins_list.setStyleSheet("""
|
||||
QListWidget {
|
||||
background-color: rgba(30, 35, 45, 200);
|
||||
color: white;
|
||||
border: 1px solid rgba(100, 110, 130, 80);
|
||||
border-radius: 6px;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid rgba(100, 110, 130, 40);
|
||||
}
|
||||
QListWidget::item:hover {
|
||||
background-color: rgba(255, 255, 255, 10);
|
||||
}
|
||||
""")
|
||||
self.plugins_list.itemClicked.connect(self._show_plugin_details)
|
||||
layout.addWidget(self.plugins_list)
|
||||
|
||||
# Sample plugins
|
||||
self._load_sample_plugins()
|
||||
|
||||
# Details panel
|
||||
self.details_panel = QFrame()
|
||||
self.details_panel.setStyleSheet("""
|
||||
QFrame {
|
||||
background-color: rgba(30, 35, 45, 200);
|
||||
border: 1px solid rgba(100, 110, 130, 80);
|
||||
border-radius: 6px;
|
||||
}
|
||||
""")
|
||||
details_layout = QVBoxLayout(self.details_panel)
|
||||
|
||||
self.detail_name = QLabel("Select a plugin")
|
||||
self.detail_name.setStyleSheet("color: #ff8c42; font-size: 14px; font-weight: bold;")
|
||||
details_layout.addWidget(self.detail_name)
|
||||
|
||||
self.detail_desc = QLabel("")
|
||||
self.detail_desc.setStyleSheet("color: rgba(255,255,255,150);")
|
||||
self.detail_desc.setWordWrap(True)
|
||||
details_layout.addWidget(self.detail_desc)
|
||||
|
||||
self.detail_author = QLabel("")
|
||||
self.detail_author.setStyleSheet("color: rgba(255,255,255,100); font-size: 10px;")
|
||||
details_layout.addWidget(self.detail_author)
|
||||
|
||||
self.install_btn = QPushButton("Install")
|
||||
self.install_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #4caf50;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
self.install_btn.clicked.connect(self._install_plugin)
|
||||
self.install_btn.setEnabled(False)
|
||||
details_layout.addWidget(self.install_btn)
|
||||
|
||||
layout.addWidget(self.details_panel)
|
||||
|
||||
# Refresh button
|
||||
refresh_btn = QPushButton("🔄 Refresh")
|
||||
refresh_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: rgba(255,255,255,20);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
""")
|
||||
refresh_btn.clicked.connect(self._refresh_store)
|
||||
layout.addWidget(refresh_btn)
|
||||
|
||||
layout.addStretch()
|
||||
error = QLabel("Plugin Manager not available. Cannot load Plugin Store.")
|
||||
error.setStyleSheet("color: #ff4757; font-size: 14px;")
|
||||
layout.addWidget(error)
|
||||
return widget
|
||||
|
||||
def _load_sample_plugins(self):
|
||||
"""Load sample plugin list."""
|
||||
sample = [
|
||||
{
|
||||
'name': 'Crafting Calculator',
|
||||
'description': 'Calculate crafting success rates and costs',
|
||||
'author': 'EU Community',
|
||||
'version': '1.0.0',
|
||||
'downloads': 542,
|
||||
'rating': 4.5,
|
||||
},
|
||||
{
|
||||
'name': 'Global Tracker',
|
||||
'description': 'Track globals and HOFs with notifications',
|
||||
'author': 'ImpulsiveFPS',
|
||||
'version': '1.2.0',
|
||||
'downloads': 1203,
|
||||
'rating': 4.8,
|
||||
},
|
||||
{
|
||||
'name': 'Bank Manager',
|
||||
'description': 'Manage storage and bank items across planets',
|
||||
'author': 'StorageMaster',
|
||||
'version': '0.9.0',
|
||||
'downloads': 328,
|
||||
'rating': 4.2,
|
||||
},
|
||||
{
|
||||
'name': 'Society Tools',
|
||||
'description': 'Society management and member tracking',
|
||||
'author': 'SocietyDev',
|
||||
'version': '1.0.0',
|
||||
'downloads': 215,
|
||||
'rating': 4.0,
|
||||
},
|
||||
{
|
||||
'name': 'Team Helper',
|
||||
'description': 'Team coordination and loot sharing',
|
||||
'author': 'TeamPlayer',
|
||||
'version': '1.1.0',
|
||||
'downloads': 876,
|
||||
'rating': 4.6,
|
||||
},
|
||||
]
|
||||
self.store_ui = PluginStoreUI(plugin_manager)
|
||||
return self.store_ui
|
||||
|
||||
self.available_plugins = sample
|
||||
self._update_list()
|
||||
|
||||
def _update_list(self):
|
||||
"""Update plugins list."""
|
||||
self.plugins_list.clear()
|
||||
|
||||
for plugin in self.available_plugins:
|
||||
item = QListWidgetItem(
|
||||
f"{plugin['name']} v{plugin['version']}\n"
|
||||
f"⭐ {plugin['rating']} | ⬇ {plugin['downloads']}"
|
||||
)
|
||||
item.setData(Qt.ItemDataRole.UserRole, plugin)
|
||||
self.plugins_list.addItem(item)
|
||||
|
||||
def _show_plugin_details(self, item):
|
||||
"""Show plugin details."""
|
||||
plugin = item.data(Qt.ItemDataRole.UserRole)
|
||||
if plugin:
|
||||
self.detail_name.setText(f"{plugin['name']} v{plugin['version']}")
|
||||
self.detail_desc.setText(plugin['description'])
|
||||
self.detail_author.setText(f"By {plugin['author']} | ⭐ {plugin['rating']}")
|
||||
self.install_btn.setEnabled(True)
|
||||
self.selected_plugin = plugin
|
||||
|
||||
def _install_plugin(self):
|
||||
"""Install selected plugin."""
|
||||
if hasattr(self, 'selected_plugin'):
|
||||
print(f"Installing {self.selected_plugin['name']}...")
|
||||
self.install_btn.setText("Installing...")
|
||||
self.install_btn.setEnabled(False)
|
||||
# TODO: Actual install
|
||||
|
||||
def _search_plugins(self):
|
||||
"""Search plugins."""
|
||||
query = self.search_input.text().lower()
|
||||
|
||||
filtered = [
|
||||
p for p in self.available_plugins
|
||||
if query in p['name'].lower() or query in p['description'].lower()
|
||||
]
|
||||
|
||||
self.plugins_list.clear()
|
||||
for plugin in filtered:
|
||||
item = QListWidgetItem(
|
||||
f"{plugin['name']} v{plugin['version']}\n"
|
||||
f"⭐ {plugin['rating']} | ⬇ {plugin['downloads']}"
|
||||
)
|
||||
item.setData(Qt.ItemDataRole.UserRole, plugin)
|
||||
self.plugins_list.addItem(item)
|
||||
|
||||
def _refresh_store(self):
|
||||
"""Refresh plugin list."""
|
||||
self._load_sample_plugins()
|
||||
def on_hotkey(self):
|
||||
"""Handle hotkey press."""
|
||||
# Show the plugin store tab
|
||||
if hasattr(self.overlay, 'show_plugin'):
|
||||
self.overlay.show_plugin('plugins.plugin_store_ui.plugin')
|
||||
|
|
|
|||
|
|
@ -63,9 +63,13 @@ class SettingsPlugin(BasePlugin):
|
|||
general_tab = self._create_general_tab()
|
||||
tabs.addTab(general_tab, "General")
|
||||
|
||||
# Plugins tab
|
||||
# Plugin Store tab
|
||||
store_tab = self._create_plugin_store_tab()
|
||||
tabs.addTab(store_tab, "Plugin Store")
|
||||
|
||||
# Local Plugins tab (for managing installed plugins)
|
||||
plugins_tab = self._create_plugins_tab()
|
||||
tabs.addTab(plugins_tab, "Plugins")
|
||||
tabs.addTab(plugins_tab, "My Plugins")
|
||||
|
||||
# Hotkeys tab
|
||||
hotkeys_tab = self._create_hotkeys_tab()
|
||||
|
|
@ -183,6 +187,26 @@ class SettingsPlugin(BasePlugin):
|
|||
|
||||
return tab
|
||||
|
||||
def _create_plugin_store_tab(self):
|
||||
"""Create plugin store tab for browsing and installing plugins."""
|
||||
from core.plugin_store import PluginStoreUI
|
||||
|
||||
# Get plugin manager from overlay
|
||||
plugin_manager = getattr(self.overlay, 'plugin_manager', None)
|
||||
|
||||
if not plugin_manager:
|
||||
# Fallback - show error
|
||||
tab = QWidget()
|
||||
layout = QVBoxLayout(tab)
|
||||
error = QLabel("Plugin Manager not available. Cannot load Plugin Store.")
|
||||
error.setStyleSheet("color: #ff4757; font-size: 14px;")
|
||||
layout.addWidget(error)
|
||||
return tab
|
||||
|
||||
# Create and return the plugin store UI
|
||||
store_ui = PluginStoreUI(plugin_manager)
|
||||
return store_ui
|
||||
|
||||
def _create_plugins_tab(self):
|
||||
"""Create plugins management tab with dependency visualization."""
|
||||
tab = QWidget()
|
||||
|
|
|
|||
Loading…
Reference in New Issue