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:
LemonNexus 2026-02-15 01:43:25 +00:00
parent 09ad30c223
commit 725590e247
3 changed files with 680 additions and 479 deletions

View File

@ -1,253 +1,648 @@
""" """
EU-Utility - Plugin Store EU-Utility - Plugin Store
Fetch and install community plugins from GitHub. Fetches and installs plugins from remote repositories.
""" """
import json import json
import zipfile
import shutil import shutil
import zipfile
from pathlib import Path 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 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
class PluginStore(QObject): @dataclass
"""Community plugin repository manager.""" class PluginInfo:
"""Plugin metadata from repository."""
# Repository configuration id: str
REPO_URL = "https://raw.githubusercontent.com/ImpulsiveFPS/EU-Utility-Plugins/main/" name: str
INDEX_URL = REPO_URL + "plugins.json" version: str
author: str
# Signals description: str
plugins_loaded = pyqtSignal(list) folder: str
plugin_installed = pyqtSignal(str, bool) icon: str
plugin_removed = pyqtSignal(str, bool) tags: List[str]
error_occurred = pyqtSignal(str) dependencies: Dict
min_core_version: str
def __init__(self, plugins_dir="user_plugins"): category: str
super().__init__() installed: bool = False
self.plugins_dir = Path(plugins_dir) update_available: bool = False
self.plugins_dir.mkdir(parents=True, exist_ok=True) enabled: bool = False
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
)
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()
class PluginFetchThread(QThread): class PluginStoreWorker(QThread):
"""Background thread to fetch plugin index.""" """Background worker for plugin store operations."""
fetched = pyqtSignal(list) manifest_loaded = pyqtSignal(list) # List[PluginInfo]
plugin_downloaded = pyqtSignal(str, bool) # plugin_id, success
progress_update = pyqtSignal(str)
error = pyqtSignal(str) error = pyqtSignal(str)
def __init__(self, url): def __init__(self, repo_url: str, plugins_dir: Path):
super().__init__() 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): def run(self):
"""Fetch plugin index.""" """Execute queued operation."""
try: try:
http_client = get_http_client() if self.operation == 'fetch_manifest':
response = http_client.get( self._do_fetch_manifest()
self.url, elif self.operation == 'download':
cache_ttl=300, # 5 minute cache for plugin list self._do_download()
headers={'User-Agent': 'EU-Utility/1.0'} elif self.operation == 'check_updates':
) self._do_check_updates()
except Exception as e:
self.error.emit(str(e))
if response.get('json'): def _do_fetch_manifest(self):
data = response['json'] """Fetch plugin manifest from repository."""
self.fetched.emit(data.get('plugins', [])) 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: else:
# Try to parse JSON from text raise Exception(f"Could not fetch manifest: {e}")
data = json.loads(response['text'])
self.fetched.emit(data.get('plugins', []))
except Exception as e: plugins = []
self.error.emit(str(e)) 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)
class PluginInstallThread(QThread): def _do_download(self):
"""Background thread to install a plugin.""" """Download and install a plugin."""
plugin_id = self.target_plugin['id']
folder = self.target_plugin['folder']
installed = pyqtSignal(bool) self.progress_update.emit(f"Downloading {plugin_id}...")
error = pyqtSignal(str)
progress = pyqtSignal(str)
def __init__(self, plugin, install_dir): # Create temp directory
super().__init__() temp_dir = Path("temp_download")
self.plugin = plugin temp_dir.mkdir(exist_ok=True)
self.install_dir = Path(install_dir)
def run(self):
"""Install plugin."""
try: try:
self.progress.emit(f"Downloading {self.plugin['name']}...") # For git repositories, clone and copy folder
import subprocess
# Download zip clone_dir = temp_dir / "repo"
download_url = self.plugin.get('download_url') if clone_dir.exists():
if not download_url: shutil.rmtree(clone_dir)
self.error.emit("No download URL")
self.installed.emit(False)
return
temp_zip = self.install_dir / "temp.zip" # Clone the repository
self.progress_update.emit(f"Cloning repository...")
http_client = get_http_client() result = subprocess.run(
response = http_client.get( ['git', 'clone', '--depth', '1', self.repo_url, str(clone_dir)],
download_url, capture_output=True,
cache_ttl=0, # Don't cache downloads text=True,
headers={'User-Agent': 'EU-Utility/1.0'} timeout=60
) )
with open(temp_zip, 'wb') as f: if result.returncode != 0:
f.write(response['content']) 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 if dest.exists():
plugin_dir = self.install_dir / self.plugin['id'] shutil.rmtree(dest)
if plugin_dir.exists():
shutil.rmtree(plugin_dir)
plugin_dir.mkdir()
with zipfile.ZipFile(temp_zip, 'r') as zip_ref: self.progress_update.emit(f"Installing {plugin_id}...")
zip_ref.extractall(plugin_dir) shutil.copytree(source, dest)
# Cleanup # Clean up
temp_zip.unlink() shutil.rmtree(temp_dir)
self.progress.emit("Installed!") self.plugin_downloaded.emit(plugin_id, True)
self.installed.emit(True)
except Exception as e: except Exception as e:
self.error.emit(str(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: class PluginStoreUI(QWidget):
SAMPLE_PLUGINS_JSON = { """Plugin Store user interface."""
"version": "1.0.0",
"plugins": [ DEFAULT_REPO = "https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo"
{
"id": "loot_tracker", def __init__(self, plugin_manager, parent=None):
"name": "Loot Tracker", super().__init__(parent)
"description": "Track and analyze hunting loot", self.plugin_manager = plugin_manager
"version": "1.0.0", self.plugins_dir = Path("plugins")
"author": "ImpulsiveFPS", self.available_plugins: List[PluginInfo] = []
"category": "hunting", self.filtered_plugins: List[PluginInfo] = []
"download_url": "https://github.com/.../loot_tracker.zip",
"icon": "🎁", self._setup_ui()
"min_app_version": "1.0.0" self._setup_worker()
}, self._load_plugins()
{
"id": "mining_helper", def _setup_worker(self):
"name": "Mining Helper", """Setup background worker."""
"description": "Track mining finds and claims", self.worker = PluginStoreWorker(self.DEFAULT_REPO, self.plugins_dir)
"version": "1.1.0", self.worker.manifest_loaded.connect(self._on_manifest_loaded)
"author": "Community", self.worker.plugin_downloaded.connect(self._on_plugin_downloaded)
"category": "mining", self.worker.progress_update.connect(self._on_progress)
"download_url": "https://github.com/.../mining_helper.zip", self.worker.error.connect(self._on_error)
"icon": "⛏️",
"min_app_version": "1.0.0" def _setup_ui(self):
}, """Create the UI."""
{ layout = QVBoxLayout(self)
"id": "market_analyzer", layout.setSpacing(15)
"name": "Market Analyzer", layout.setContentsMargins(20, 20, 20, 20)
"description": "Analyze auction prices and trends",
"version": "0.9.0", # Header
"author": "EU Community", header = QLabel("🔌 Plugin Store")
"category": "trading", header.setStyleSheet("font-size: 24px; font-weight: bold; color: white;")
"download_url": "https://github.com/.../market_analyzer.zip", layout.addWidget(header)
"icon": "📈",
"min_app_version": "1.0.0" # 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}")

View File

@ -1,273 +1,55 @@
""" """
EU-Utility - Plugin Store UI Plugin 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 ( from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QLineEdit, QListWidget, QListWidgetItem,
QProgressBar, QFrame, QTextEdit
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from plugins.base_plugin import BasePlugin from plugins.base_plugin import BasePlugin
from core.plugin_store import PluginStoreUI
class PluginStoreUIPlugin(BasePlugin): class PluginStoreUIPlugin(BasePlugin):
"""Browse and install community plugins.""" """Plugin Store for browsing and installing plugins from repositories."""
name = "Plugin Store" name = "Plugin Store"
version = "1.0.0" version = "1.0.0"
author = "ImpulsiveFPS" author = "ImpulsiveFPS"
description = "Community plugin marketplace" description = "Browse, install, and manage plugins from the official repository"
hotkey = "ctrl+shift+slash" icon = "shopping-bag"
hotkeys = [
{
'action': 'open_store',
'description': 'Open Plugin Store',
'default': 'ctrl+shift+p',
'config_key': 'pluginstore_open'
}
]
def initialize(self): def initialize(self):
"""Setup plugin store.""" """Setup plugin store."""
self.available_plugins = [] self.store_ui = None
self.installed_plugins = []
self.is_loading = False
def get_ui(self): def get_ui(self):
"""Create plugin store UI.""" """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 = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget) layout = QVBoxLayout(widget)
layout.setSpacing(15) error = QLabel("Plugin Manager not available. Cannot load Plugin Store.")
layout.setContentsMargins(0, 0, 0, 0) error.setStyleSheet("color: #ff4757; font-size: 14px;")
layout.addWidget(error)
# 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()
return widget return widget
def _load_sample_plugins(self): self.store_ui = PluginStoreUI(plugin_manager)
"""Load sample plugin list.""" return self.store_ui
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.available_plugins = sample def on_hotkey(self):
self._update_list() """Handle hotkey press."""
# Show the plugin store tab
def _update_list(self): if hasattr(self.overlay, 'show_plugin'):
"""Update plugins list.""" self.overlay.show_plugin('plugins.plugin_store_ui.plugin')
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()

View File

@ -63,9 +63,13 @@ class SettingsPlugin(BasePlugin):
general_tab = self._create_general_tab() general_tab = self._create_general_tab()
tabs.addTab(general_tab, "General") 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() plugins_tab = self._create_plugins_tab()
tabs.addTab(plugins_tab, "Plugins") tabs.addTab(plugins_tab, "My Plugins")
# Hotkeys tab # Hotkeys tab
hotkeys_tab = self._create_hotkeys_tab() hotkeys_tab = self._create_hotkeys_tab()
@ -183,6 +187,26 @@ class SettingsPlugin(BasePlugin):
return tab 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): def _create_plugins_tab(self):
"""Create plugins management tab with dependency visualization.""" """Create plugins management tab with dependency visualization."""
tab = QWidget() tab = QWidget()