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
|
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."""
|
||||||
|
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
|
||||||
|
|
||||||
# Repository configuration
|
|
||||||
REPO_URL = "https://raw.githubusercontent.com/ImpulsiveFPS/EU-Utility-Plugins/main/"
|
|
||||||
INDEX_URL = REPO_URL + "plugins.json"
|
|
||||||
|
|
||||||
# Signals
|
class PluginStoreWorker(QThread):
|
||||||
plugins_loaded = pyqtSignal(list)
|
"""Background worker for plugin store operations."""
|
||||||
plugin_installed = pyqtSignal(str, bool)
|
|
||||||
plugin_removed = pyqtSignal(str, bool)
|
|
||||||
error_occurred = pyqtSignal(str)
|
|
||||||
|
|
||||||
def __init__(self, plugins_dir="user_plugins"):
|
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__()
|
super().__init__()
|
||||||
self.plugins_dir = Path(plugins_dir)
|
self.repo_url = repo_url
|
||||||
self.plugins_dir.mkdir(parents=True, exist_ok=True)
|
self.plugins_dir = plugins_dir
|
||||||
self.available_plugins = []
|
self.operation = None
|
||||||
self.installed_plugins = []
|
self.target_plugin = None
|
||||||
self._load_installed()
|
|
||||||
|
|
||||||
def fetch_plugins(self):
|
def fetch_manifest(self):
|
||||||
"""Fetch available plugins from repository."""
|
"""Queue fetch manifest operation."""
|
||||||
self.fetch_thread = PluginFetchThread(self.INDEX_URL)
|
self.operation = 'fetch_manifest'
|
||||||
self.fetch_thread.fetched.connect(self._on_plugins_fetched)
|
self.start()
|
||||||
self.fetch_thread.error.connect(self._on_fetch_error)
|
|
||||||
self.fetch_thread.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."""
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise Exception(f"Git clone failed: {result.stderr}")
|
||||||
|
|
||||||
|
# Copy plugin folder
|
||||||
|
source = clone_dir / folder
|
||||||
|
dest = self.plugins_dir / plugin_id
|
||||||
|
|
||||||
|
if dest.exists():
|
||||||
|
shutil.rmtree(dest)
|
||||||
|
|
||||||
|
self.progress_update.emit(f"Installing {plugin_id}...")
|
||||||
|
shutil.copytree(source, dest)
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
shutil.rmtree(temp_dir)
|
||||||
|
|
||||||
|
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(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")
|
||||||
|
|
||||||
def _on_plugins_fetched(self, plugins):
|
|
||||||
"""Handle fetched plugins."""
|
|
||||||
self.available_plugins = plugins
|
self.available_plugins = plugins
|
||||||
self.plugins_loaded.emit(plugins)
|
self.filtered_plugins = plugins
|
||||||
|
|
||||||
def _on_fetch_error(self, error):
|
# Update category filter
|
||||||
"""Handle fetch error."""
|
categories = sorted(set(p.category for p in plugins))
|
||||||
self.error_occurred.emit(error)
|
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)
|
||||||
|
|
||||||
def install_plugin(self, plugin_id):
|
self._render_grid()
|
||||||
"""Install a plugin from the store."""
|
|
||||||
plugin = self._find_plugin(plugin_id)
|
def _filter_plugins(self):
|
||||||
if not plugin:
|
"""Filter plugins based on search and category."""
|
||||||
self.plugin_installed.emit(plugin_id, False)
|
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
|
return
|
||||||
|
|
||||||
self.install_thread = PluginInstallThread(
|
# Create cards
|
||||||
plugin,
|
columns = 3
|
||||||
self.plugins_dir
|
for i, plugin in enumerate(self.filtered_plugins):
|
||||||
)
|
card = self._create_plugin_card(plugin)
|
||||||
self.install_thread.installed.connect(
|
row = i // columns
|
||||||
lambda success: self._on_installed(plugin_id, success)
|
col = i % columns
|
||||||
)
|
self.grid_layout.addWidget(card, row, col)
|
||||||
self.install_thread.error.connect(self._on_fetch_error)
|
|
||||||
self.install_thread.start()
|
|
||||||
|
|
||||||
def _on_installed(self, plugin_id, success):
|
def _create_plugin_card(self, plugin: PluginInfo) -> QFrame:
|
||||||
"""Handle plugin installation."""
|
"""Create a plugin card widget."""
|
||||||
if success:
|
card = QFrame()
|
||||||
self.installed_plugins.append(plugin_id)
|
card.setFixedSize(300, 200)
|
||||||
self._save_installed()
|
card.setStyleSheet("""
|
||||||
self.plugin_installed.emit(plugin_id, success)
|
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;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
def remove_plugin(self, plugin_id):
|
layout = QVBoxLayout(card)
|
||||||
"""Remove an installed plugin."""
|
layout.setSpacing(8)
|
||||||
try:
|
layout.setContentsMargins(15, 15, 15, 15)
|
||||||
plugin_dir = self.plugins_dir / plugin_id
|
|
||||||
if plugin_dir.exists():
|
|
||||||
shutil.rmtree(plugin_dir)
|
|
||||||
|
|
||||||
if plugin_id in self.installed_plugins:
|
# Header with icon and name
|
||||||
self.installed_plugins.remove(plugin_id)
|
header = QHBoxLayout()
|
||||||
self._save_installed()
|
|
||||||
|
|
||||||
self.plugin_removed.emit(plugin_id, True)
|
icon = QLabel(f"📦")
|
||||||
except Exception as e:
|
icon.setStyleSheet("font-size: 24px;")
|
||||||
self.error_occurred.emit(str(e))
|
header.addWidget(icon)
|
||||||
self.plugin_removed.emit(plugin_id, False)
|
|
||||||
|
|
||||||
def _find_plugin(self, plugin_id):
|
name_layout = QVBoxLayout()
|
||||||
"""Find plugin by ID."""
|
|
||||||
for plugin in self.available_plugins:
|
|
||||||
if plugin.get('id') == plugin_id:
|
|
||||||
return plugin
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _load_installed(self):
|
name = QLabel(plugin.name)
|
||||||
"""Load list of installed plugins."""
|
name.setStyleSheet("font-size: 14px; font-weight: bold; color: white;")
|
||||||
installed_file = self.plugins_dir / ".installed"
|
name_layout.addWidget(name)
|
||||||
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):
|
version = QLabel(f"v{plugin.version}")
|
||||||
"""Save list of installed plugins."""
|
version.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;")
|
||||||
installed_file = self.plugins_dir / ".installed"
|
name_layout.addWidget(version)
|
||||||
with open(installed_file, 'w') as f:
|
|
||||||
json.dump(self.installed_plugins, f)
|
|
||||||
|
|
||||||
def is_installed(self, plugin_id):
|
header.addLayout(name_layout, 1)
|
||||||
"""Check if a plugin is installed."""
|
|
||||||
return plugin_id in self.installed_plugins
|
|
||||||
|
|
||||||
def get_installed_plugins(self):
|
# Status badge
|
||||||
"""Get list of installed plugin IDs."""
|
if plugin.installed:
|
||||||
return self.installed_plugins.copy()
|
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)
|
||||||
|
|
||||||
class PluginFetchThread(QThread):
|
# Description
|
||||||
"""Background thread to fetch plugin index."""
|
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)
|
||||||
|
|
||||||
fetched = pyqtSignal(list)
|
# Tags
|
||||||
error = pyqtSignal(str)
|
tags = QLabel(" • ".join(plugin.tags[:3]))
|
||||||
|
tags.setStyleSheet("color: #4a9eff; font-size: 10px;")
|
||||||
|
layout.addWidget(tags)
|
||||||
|
|
||||||
def __init__(self, url):
|
# Dependencies indicator
|
||||||
super().__init__()
|
if plugin.dependencies.get('plugins') or plugin.dependencies.get('core'):
|
||||||
self.url = url
|
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)
|
||||||
|
|
||||||
def run(self):
|
layout.addStretch()
|
||||||
"""Fetch plugin index."""
|
|
||||||
try:
|
# Action button
|
||||||
http_client = get_http_client()
|
btn_layout = QHBoxLayout()
|
||||||
response = http_client.get(
|
|
||||||
self.url,
|
if plugin.installed:
|
||||||
cache_ttl=300, # 5 minute cache for plugin list
|
if plugin.enabled:
|
||||||
headers={'User-Agent': 'EU-Utility/1.0'}
|
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 response.get('json'):
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
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', []))
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.error.emit(str(e))
|
|
||||||
|
|
||||||
|
|
||||||
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']}...")
|
|
||||||
|
|
||||||
# Download zip
|
|
||||||
download_url = self.plugin.get('download_url')
|
|
||||||
if not download_url:
|
|
||||||
self.error.emit("No download URL")
|
|
||||||
self.installed.emit(False)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
temp_zip = self.install_dir / "temp.zip"
|
self.progress.show()
|
||||||
|
self.progress.setRange(0, 0)
|
||||||
|
self.progress.setFormat(f"Installing {plugin.name}...")
|
||||||
|
self.worker.download_plugin(plugin.id, plugin.folder)
|
||||||
|
|
||||||
http_client = get_http_client()
|
def _on_plugin_downloaded(self, plugin_id: str, success: bool):
|
||||||
response = http_client.get(
|
"""Handle plugin download completion."""
|
||||||
download_url,
|
self.progress.hide()
|
||||||
cache_ttl=0, # Don't cache downloads
|
|
||||||
headers={'User-Agent': 'EU-Utility/1.0'}
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(temp_zip, 'wb') as f:
|
if success:
|
||||||
f.write(response['content'])
|
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}'.")
|
||||||
|
|
||||||
self.progress.emit("Extracting...")
|
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()
|
||||||
|
|
||||||
# Extract
|
def _disable_plugin(self, plugin: PluginInfo):
|
||||||
plugin_dir = self.install_dir / self.plugin['id']
|
"""Disable an installed plugin."""
|
||||||
if plugin_dir.exists():
|
plugin_id = f"plugins.{plugin.id}.plugin"
|
||||||
shutil.rmtree(plugin_dir)
|
if hasattr(self.plugin_manager, 'enable_plugin'):
|
||||||
plugin_dir.mkdir()
|
self.plugin_manager.disable_plugin(plugin_id)
|
||||||
|
plugin.enabled = False
|
||||||
|
self._render_grid()
|
||||||
|
|
||||||
with zipfile.ZipFile(temp_zip, 'r') as zip_ref:
|
def _uninstall_plugin(self, plugin: PluginInfo):
|
||||||
zip_ref.extractall(plugin_dir)
|
"""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
|
||||||
|
)
|
||||||
|
|
||||||
# Cleanup
|
if reply == QMessageBox.StandardButton.Yes:
|
||||||
temp_zip.unlink()
|
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()
|
||||||
|
|
||||||
self.progress.emit("Installed!")
|
def _check_updates(self):
|
||||||
self.installed.emit(True)
|
"""Check for plugin updates."""
|
||||||
|
self.progress.show()
|
||||||
|
self.progress.setRange(0, 0)
|
||||||
|
self.progress.setFormat("Checking for updates...")
|
||||||
|
|
||||||
except Exception as e:
|
# TODO: Implement update checking
|
||||||
self.error.emit(str(e))
|
QTimer.singleShot(1000, self.progress.hide)
|
||||||
self.installed.emit(False)
|
QMessageBox.information(self, "Updates", "All plugins are up to date!")
|
||||||
|
|
||||||
|
def _on_progress(self, message: str):
|
||||||
|
"""Handle progress updates."""
|
||||||
|
self.progress.setFormat(message)
|
||||||
|
|
||||||
# Sample plugins.json structure for GitHub repo:
|
def _on_error(self, error: str):
|
||||||
SAMPLE_PLUGINS_JSON = {
|
"""Handle errors."""
|
||||||
"version": "1.0.0",
|
self.progress.hide()
|
||||||
"plugins": [
|
QMessageBox.critical(self, "Error", f"Plugin Store Error:\n{error}")
|
||||||
{
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
widget = QWidget()
|
# Get plugin manager from overlay
|
||||||
widget.setStyleSheet("background: transparent;")
|
plugin_manager = getattr(self.overlay, 'plugin_manager', None)
|
||||||
layout = QVBoxLayout(widget)
|
|
||||||
layout.setSpacing(15)
|
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
|
|
||||||
# Title
|
if not plugin_manager:
|
||||||
title = QLabel("🛒 Plugin Store")
|
# Fallback - show error
|
||||||
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
|
widget = QWidget()
|
||||||
layout.addWidget(title)
|
layout = QVBoxLayout(widget)
|
||||||
|
error = QLabel("Plugin Manager not available. Cannot load Plugin Store.")
|
||||||
|
error.setStyleSheet("color: #ff4757; font-size: 14px;")
|
||||||
|
layout.addWidget(error)
|
||||||
|
return widget
|
||||||
|
|
||||||
# Search
|
self.store_ui = PluginStoreUI(plugin_manager)
|
||||||
search_layout = QHBoxLayout()
|
return self.store_ui
|
||||||
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("🔍")
|
def on_hotkey(self):
|
||||||
search_btn.setFixedSize(32, 32)
|
"""Handle hotkey press."""
|
||||||
search_btn.setStyleSheet("""
|
# Show the plugin store tab
|
||||||
QPushButton {
|
if hasattr(self.overlay, 'show_plugin'):
|
||||||
background-color: #ff8c42;
|
self.overlay.show_plugin('plugins.plugin_store_ui.plugin')
|
||||||
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
|
|
||||||
|
|
||||||
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.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()
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue