From a81e5caf64dc29453ce8509190019a3c6b6dcbf6 Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 15 Feb 2026 02:56:14 +0000 Subject: [PATCH] fix: Built-in Plugin Store, dependency dialog, removed settings/plugin_store plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGES: 1. Fixed plugin download to use raw git files instead of git clone - Avoids Windows permission issues with .git/objects - Downloads __init__.py and plugin.py directly 2. Added clickable dependencies button showing full dependency dialog - Shows Core Services Required - Shows Plugins Required - Lists what will be auto-installed 3. Integrated Plugin Store into Settings dialog - Added 🔌 Store tab to Settings - Plugin Store is now built-in, not a plugin 4. Removed plugins/settings/ and plugins/plugin_store_ui/ - Settings is now built into overlay_window.py - Users access via Settings button 5. Added _create_plugin_store_tab() method to OverlayWindow NOTE: Pull latest EU-Utility-Plugins-Repo to get Clock Widget --- core/overlay_window.py | 20 + core/plugin_store.py | 103 ++- plugins/plugin_store_ui/__init__.py | 7 - plugins/plugin_store_ui/plugin.py | 55 -- plugins/settings/__init__.py | 7 - plugins/settings/plugin.py | 1198 --------------------------- 6 files changed, 85 insertions(+), 1305 deletions(-) delete mode 100644 plugins/plugin_store_ui/__init__.py delete mode 100644 plugins/plugin_store_ui/plugin.py delete mode 100644 plugins/settings/__init__.py delete mode 100644 plugins/settings/plugin.py diff --git a/core/overlay_window.py b/core/overlay_window.py index 96a1d3f..fbc2d0f 100644 --- a/core/overlay_window.py +++ b/core/overlay_window.py @@ -743,6 +743,10 @@ class OverlayWindow(QMainWindow): plugins_tab = self._create_plugins_settings_tab() tabs.addTab(plugins_tab, "Plugins") + # Plugin Store tab + store_tab = self._create_plugin_store_tab() + tabs.addTab(store_tab, "🔌 Store") + # Hotkeys tab hotkeys_tab = self._create_hotkeys_settings_tab() tabs.addTab(hotkeys_tab, "Hotkeys") @@ -944,6 +948,22 @@ class OverlayWindow(QMainWindow): return tab + def _create_plugin_store_tab(self): + """Create plugin store tab for browsing and installing plugins.""" + from core.plugin_store import PluginStoreUI + + if hasattr(self, 'plugin_manager') and self.plugin_manager: + return PluginStoreUI(self.plugin_manager) + + # Fallback - show error + from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel + 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 + def _add_plugin_row(self, layout, plugin_id, plugin_class, colors, prefix): """Add a plugin row to the layout.""" try: diff --git a/core/plugin_store.py b/core/plugin_store.py index 062e139..0834820 100644 --- a/core/plugin_store.py +++ b/core/plugin_store.py @@ -124,48 +124,30 @@ class PluginStoreWorker(QThread): self.manifest_loaded.emit(plugins) def _do_download(self): - """Download and install a plugin.""" + """Download and install a plugin using raw files (no git clone).""" plugin_id = self.target_plugin['id'] folder = self.target_plugin['folder'] self.progress_update.emit(f"Downloading {plugin_id}...") - # Create temp directory - temp_dir = Path("temp_download") - temp_dir.mkdir(exist_ok=True) - try: - # For git repositories, clone and copy folder - import subprocess + import urllib.request - 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 + # Download files directly from raw git dest = self.plugins_dir / plugin_id + dest.mkdir(parents=True, exist_ok=True) - if dest.exists(): - shutil.rmtree(dest) + # Create __init__.py + init_url = f"{self.repo_url}/raw/branch/main/{folder}/__init__.py" + try: + urllib.request.urlretrieve(init_url, dest / "__init__.py") + except: + (dest / "__init__.py").touch() - self.progress_update.emit(f"Installing {plugin_id}...") - shutil.copytree(source, dest) - - # Clean up - shutil.rmtree(temp_dir) + # Download plugin.py + plugin_url = f"{self.repo_url}/raw/branch/main/{folder}/plugin.py" + self.progress_update.emit(f"Downloading {plugin_id}/plugin.py...") + urllib.request.urlretrieve(plugin_url, dest / "plugin.py") self.plugin_downloaded.emit(plugin_id, True) @@ -478,12 +460,23 @@ class PluginStoreUI(QWidget): # 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]) + deps_label = QPushButton(f"🔗 {deps_count} dependencies") + deps_label.setStyleSheet(""" + QPushButton { + color: #ffd93d; + font-size: 10px; + background-color: transparent; + border: none; + text-align: left; + padding: 0px; + } + QPushButton:hover { + color: #ffed8a; + text-decoration: underline; + } + """) + deps_label.setCursor(Qt.CursorShape.PointingHandCursor) + deps_label.clicked.connect(lambda checked, p=plugin: self._show_dependencies_dialog(p)) layout.addWidget(deps_label) layout.addStretch() @@ -628,6 +621,40 @@ class PluginStoreUI(QWidget): QMessageBox.information(self, "Uninstalled", f"'{plugin.name}' has been uninstalled.") self._load_plugins() + def _show_dependencies_dialog(self, plugin: PluginInfo): + """Show dialog with plugin dependencies.""" + deps = plugin.dependencies + + msg = f"

🔌 {plugin.name} Dependencies

" + + # Core dependencies + core_deps = deps.get('core', []) + if core_deps: + msg += "

Core Services Required:

" + + # Plugin dependencies + plugin_deps = deps.get('plugins', []) + if plugin_deps: + msg += "

Plugins Required:

" + msg += "

These will be automatically installed when you install this plugin.

" + + if not core_deps and not plugin_deps: + msg += "

This plugin has no dependencies.

" + + dialog = QMessageBox(self) + dialog.setWindowTitle(f"Dependencies - {plugin.name}") + dialog.setTextFormat(Qt.TextFormat.RichText) + dialog.setText(msg) + dialog.setIcon(QMessageBox.Icon.Information) + dialog.exec() + def _check_updates(self): """Check for plugin updates.""" self.progress.show() diff --git a/plugins/plugin_store_ui/__init__.py b/plugins/plugin_store_ui/__init__.py deleted file mode 100644 index e9d630b..0000000 --- a/plugins/plugin_store_ui/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Plugin Store UI Plugin -""" - -from .plugin import PluginStoreUIPlugin - -__all__ = ["PluginStoreUIPlugin"] diff --git a/plugins/plugin_store_ui/plugin.py b/plugins/plugin_store_ui/plugin.py deleted file mode 100644 index 6b6007c..0000000 --- a/plugins/plugin_store_ui/plugin.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -EU-Utility - Plugin Store UI Plugin - -Provides the Plugin Store interface for browsing and installing plugins. -""" - -from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel - -from core.base_plugin import BasePlugin -from core.plugin_store import PluginStoreUI - - -class PluginStoreUIPlugin(BasePlugin): - """Plugin Store for browsing and installing plugins from repositories.""" - - name = "Plugin Store" - version = "1.0.0" - author = "ImpulsiveFPS" - 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.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() - 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 - - self.store_ui = PluginStoreUI(plugin_manager) - return self.store_ui - - 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') diff --git a/plugins/settings/__init__.py b/plugins/settings/__init__.py deleted file mode 100644 index b8a1ea7..0000000 --- a/plugins/settings/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Settings Plugin -""" - -from .plugin import SettingsPlugin - -__all__ = ["SettingsPlugin"] diff --git a/plugins/settings/plugin.py b/plugins/settings/plugin.py deleted file mode 100644 index 2b8882b..0000000 --- a/plugins/settings/plugin.py +++ /dev/null @@ -1,1198 +0,0 @@ -""" -EU-Utility - Settings UI Plugin - -Settings menu for configuring EU-Utility. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QCheckBox, QLineEdit, QComboBox, - QSlider, QTabWidget, QGroupBox, QListWidget, - QListWidgetItem, QFrame, QFileDialog, QScrollArea -) -from PyQt6.QtCore import Qt, QTimer - -from core.settings import get_settings -from core.base_plugin import BasePlugin - - -class SettingsPlugin(BasePlugin): - """EU-Utility settings and configuration.""" - - name = "Settings" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Configure EU-Utility preferences" - hotkey = "ctrl+shift+comma" - - def initialize(self): - """Setup settings.""" - self.settings = get_settings() - - def get_ui(self): - """Create settings UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(10) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("Settings") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Tabs - tabs = QTabWidget() - tabs.setStyleSheet(""" - QTabBar::tab { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,150); - padding: 10px 20px; - border-top-left-radius: 6px; - border-top-right-radius: 6px; - } - QTabBar::tab:selected { - background-color: #ff8c42; - color: white; - font-weight: bold; - } - """) - - # General tab - general_tab = self._create_general_tab() - tabs.addTab(general_tab, "General") - - # 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, "My Plugins") - - # Hotkeys tab - hotkeys_tab = self._create_hotkeys_tab() - tabs.addTab(hotkeys_tab, "Hotkeys") - - # Overlay tab - overlay_tab = self._create_overlay_tab() - tabs.addTab(overlay_tab, "Overlays") - - # Data tab - data_tab = self._create_data_tab() - tabs.addTab(data_tab, "Data") - - layout.addWidget(tabs) - - # Save/Reset buttons - btn_layout = QHBoxLayout() - - save_btn = QPushButton("Save Settings") - save_btn.setStyleSheet(""" - QPushButton { - background-color: #4caf50; - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - save_btn.clicked.connect(self._save_settings) - btn_layout.addWidget(save_btn) - - reset_btn = QPushButton("Reset to Default") - reset_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255,255,255,20); - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - } - """) - reset_btn.clicked.connect(self._reset_settings) - btn_layout.addWidget(reset_btn) - - btn_layout.addStretch() - layout.addLayout(btn_layout) - - return widget - - def _create_general_tab(self): - """Create general settings tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - layout.setSpacing(15) - - # Appearance - appear_group = QGroupBox("Appearance") - appear_group.setStyleSheet(self._group_style()) - appear_layout = QVBoxLayout(appear_group) - - # Theme - theme_layout = QHBoxLayout() - theme_layout.addWidget(QLabel("Theme:")) - self.theme_combo = QComboBox() - self.theme_combo.addItems(["Dark (EU Style)", "Light", "Auto"]) - self.theme_combo.setCurrentText(self.settings.get('theme', 'Dark (EU Style)')) - theme_layout.addWidget(self.theme_combo) - theme_layout.addStretch() - appear_layout.addLayout(theme_layout) - - # Opacity - opacity_layout = QHBoxLayout() - opacity_layout.addWidget(QLabel("Overlay Opacity:")) - self.opacity_slider = QSlider(Qt.Orientation.Horizontal) - self.opacity_slider.setMinimum(50) - self.opacity_slider.setMaximum(100) - self.opacity_slider.setValue(int(self.settings.get('overlay_opacity', 0.9) * 100)) - opacity_layout.addWidget(self.opacity_slider) - self.opacity_label = QLabel(f"{self.opacity_slider.value()}%") - opacity_layout.addWidget(self.opacity_label) - opacity_layout.addStretch() - appear_layout.addLayout(opacity_layout) - - # Icon size - icon_layout = QHBoxLayout() - icon_layout.addWidget(QLabel("Icon Size:")) - self.icon_combo = QComboBox() - self.icon_combo.addItems(["Small (20px)", "Medium (24px)", "Large (32px)"]) - icon_layout.addWidget(self.icon_combo) - icon_layout.addStretch() - appear_layout.addLayout(icon_layout) - - layout.addWidget(appear_group) - - # Behavior - behavior_group = QGroupBox("Behavior") - behavior_group.setStyleSheet(self._group_style()) - behavior_layout = QVBoxLayout(behavior_group) - - self.auto_start_cb = QCheckBox("Start with Windows") - self.auto_start_cb.setChecked(self.settings.get('auto_start', False)) - behavior_layout.addWidget(self.auto_start_cb) - - self.minimize_cb = QCheckBox("Minimize to tray on close") - self.minimize_cb.setChecked(self.settings.get('minimize_to_tray', True)) - behavior_layout.addWidget(self.minimize_cb) - - self.tooltips_cb = QCheckBox("Show tooltips") - self.tooltips_cb.setChecked(self.settings.get('show_tooltips', True)) - behavior_layout.addWidget(self.tooltips_cb) - - layout.addWidget(behavior_group) - layout.addStretch() - - 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() - layout = QVBoxLayout(tab) - layout.setSpacing(15) - - # Info label - info = QLabel("Manage plugins. Hover over dependency icons to see requirements. Changes take effect immediately.") - info.setStyleSheet("color: rgba(255,255,255,150);") - layout.addWidget(info) - - # Dependency legend - legend_layout = QHBoxLayout() - legend_layout.addWidget(QLabel("Legend:")) - - # Required by others indicator - req_label = QLabel("⚠️ Required") - req_label.setStyleSheet("color: #ffd93d; font-size: 11px;") - req_label.setToolTip("This plugin is required by other enabled plugins") - legend_layout.addWidget(req_label) - - # Has dependencies indicator - dep_label = QLabel("🔗 Has deps") - dep_label.setStyleSheet("color: #4ecdc4; font-size: 11px;") - dep_label.setToolTip("This plugin requires other plugins to function") - legend_layout.addWidget(dep_label) - - # Auto-enabled indicator - auto_label = QLabel("🔄 Auto") - auto_label.setStyleSheet("color: #ff8c42; font-size: 11px;") - auto_label.setToolTip("Auto-enabled due to dependency") - legend_layout.addWidget(auto_label) - - legend_layout.addStretch() - layout.addLayout(legend_layout) - - # Scroll area for plugin list - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.Shape.NoFrame) - scroll.setStyleSheet("background: transparent; border: none;") - - scroll_content = QWidget() - plugins_layout = QVBoxLayout(scroll_content) - plugins_layout.setSpacing(8) - plugins_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - self.plugin_checkboxes = {} - self.plugin_dependency_labels = {} - self.plugin_rows = {} - - # Get all discovered plugins from plugin manager - if hasattr(self.overlay, 'plugin_manager'): - plugin_manager = self.overlay.plugin_manager - all_plugins = plugin_manager.get_all_discovered_plugins() - - # Build dependency maps - self._build_dependency_maps(all_plugins) - - # Sort by name - sorted_plugins = sorted(all_plugins.items(), key=lambda x: x[1].name) - - for plugin_id, plugin_class in sorted_plugins: - plugin_row = self._create_plugin_row(plugin_id, plugin_class, plugin_manager) - plugins_layout.addLayout(plugin_row) - - # Separator - sep = QFrame() - sep.setFrameShape(QFrame.Shape.HLine) - sep.setStyleSheet("background-color: rgba(100, 110, 130, 40);") - sep.setFixedHeight(1) - plugins_layout.addWidget(sep) - - plugins_layout.addStretch() - scroll.setWidget(scroll_content) - layout.addWidget(scroll) - - # Buttons - btn_layout = QHBoxLayout() - - enable_all_btn = QPushButton("Enable All") - enable_all_btn.setStyleSheet(""" - QPushButton { - background-color: #4caf50; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - } - """) - enable_all_btn.clicked.connect(self._enable_all_plugins) - btn_layout.addWidget(enable_all_btn) - - disable_all_btn = QPushButton("Disable All") - disable_all_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255,255,255,20); - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - } - """) - disable_all_btn.clicked.connect(self._disable_all_plugins) - btn_layout.addWidget(disable_all_btn) - - # Dependency info button - deps_info_btn = QPushButton("📋 Dependency Report") - deps_info_btn.setStyleSheet(""" - QPushButton { - background-color: #4a9eff; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - } - """) - deps_info_btn.clicked.connect(self._show_dependency_report) - btn_layout.addWidget(deps_info_btn) - - btn_layout.addStretch() - layout.addLayout(btn_layout) - - return tab - - def _build_dependency_maps(self, all_plugins): - """Build maps of plugin dependencies.""" - self.plugin_deps = {} # plugin_id -> list of plugin_ids it depends on - self.plugin_dependents = {} # plugin_id -> list of plugin_ids that depend on it - - for plugin_id, plugin_class in all_plugins.items(): - deps = getattr(plugin_class, 'dependencies', {}) - plugin_deps_list = deps.get('plugins', []) - - self.plugin_deps[plugin_id] = plugin_deps_list - - # Build reverse map - for dep_id in plugin_deps_list: - if dep_id not in self.plugin_dependents: - self.plugin_dependents[dep_id] = [] - self.plugin_dependents[dep_id].append(plugin_id) - - def _create_plugin_row(self, plugin_id, plugin_class, plugin_manager): - """Create a plugin row with dependency indicators.""" - row = QHBoxLayout() - row.setSpacing(10) - - # Checkbox - cb = QCheckBox(plugin_class.name) - is_enabled = plugin_manager.is_plugin_enabled(plugin_id) - is_auto_enabled = plugin_manager.is_auto_enabled(plugin_id) if hasattr(plugin_manager, 'is_auto_enabled') else False - - cb.setChecked(is_enabled) - cb.setStyleSheet(""" - QCheckBox { - color: white; - spacing: 8px; - } - QCheckBox::indicator { - width: 18px; - height: 18px; - } - QCheckBox::indicator:disabled { - background-color: #ff8c42; - } - """) - - # Disable checkbox if auto-enabled - if is_auto_enabled: - cb.setEnabled(False) - cb.setText(f"{plugin_class.name} (auto)") - - # Connect to enable/disable - cb.stateChanged.connect( - lambda state, pid=plugin_id: self._toggle_plugin(pid, state == Qt.CheckState.Checked.value) - ) - self.plugin_checkboxes[plugin_id] = cb - row.addWidget(cb) - - # Dependency indicators - indicators_layout = QHBoxLayout() - indicators_layout.setSpacing(4) - - # Check if this plugin has dependencies - deps = self.plugin_deps.get(plugin_id, []) - if deps: - deps_btn = QPushButton("🔗") - deps_btn.setFixedSize(24, 24) - deps_btn.setStyleSheet(""" - QPushButton { - background-color: transparent; - color: #4ecdc4; - border: none; - font-size: 12px; - } - QPushButton:hover { - background-color: rgba(78, 205, 196, 30); - border-radius: 4px; - } - """) - deps_btn.setToolTip(self._format_dependencies_tooltip(plugin_id, deps)) - indicators_layout.addWidget(deps_btn) - - # Check if other plugins depend on this one - dependents = self.plugin_dependents.get(plugin_id, []) - enabled_dependents = [d for d in dependents if plugin_manager.is_plugin_enabled(d)] - if enabled_dependents: - req_btn = QPushButton("⚠️") - req_btn.setFixedSize(24, 24) - req_btn.setStyleSheet(""" - QPushButton { - background-color: transparent; - color: #ffd93d; - border: none; - font-size: 12px; - } - QPushButton:hover { - background-color: rgba(255, 217, 61, 30); - border-radius: 4px; - } - """) - req_btn.setToolTip(self._format_dependents_tooltip(plugin_id, enabled_dependents)) - indicators_layout.addWidget(req_btn) - - # Check if auto-enabled - if is_auto_enabled: - auto_btn = QPushButton("🔄") - auto_btn.setFixedSize(24, 24) - auto_btn.setStyleSheet(""" - QPushButton { - background-color: transparent; - color: #ff8c42; - border: none; - font-size: 12px; - } - QPushButton:hover { - background-color: rgba(255, 140, 66, 30); - border-radius: 4px; - } - """) - # Find what enabled this plugin - enabler = self._find_enabler(plugin_id, plugin_manager) - auto_btn.setToolTip(f"Auto-enabled by: {enabler or 'dependency resolution'}") - indicators_layout.addWidget(auto_btn) - - row.addLayout(indicators_layout) - - # Version - version_label = QLabel(f"v{plugin_class.version}") - version_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;") - row.addWidget(version_label) - - # Description - desc_label = QLabel(f"- {plugin_class.description}") - desc_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;") - desc_label.setWordWrap(True) - row.addWidget(desc_label, 1) - - row.addStretch() - - # Store row reference for updates - self.plugin_rows[plugin_id] = row - - return row - - def _format_dependencies_tooltip(self, plugin_id, deps): - """Format tooltip for dependencies.""" - lines = ["This plugin requires:"] - for dep_id in deps: - dep_name = dep_id.split('.')[-1].replace('_', ' ').title() - lines.append(f" • {dep_name}") - lines.append("") - lines.append("These will be auto-enabled when you enable this plugin.") - return "\n".join(lines) - - def _format_dependents_tooltip(self, plugin_id, dependents): - """Format tooltip for plugins that depend on this one.""" - lines = ["Required by enabled plugins:"] - for dep_id in dependents: - dep_name = dep_id.split('.')[-1].replace('_', ' ').title() - lines.append(f" • {dep_name}") - lines.append("") - lines.append("Disable these first to disable this plugin.") - return "\n".join(lines) - - def _find_enabler(self, plugin_id, plugin_manager): - """Find which plugin auto-enabled this one.""" - # Check all enabled plugins to see which one depends on this - for other_id, other_class in plugin_manager.get_all_discovered_plugins().items(): - if plugin_manager.is_plugin_enabled(other_id): - deps = getattr(other_class, 'dependencies', {}).get('plugins', []) - if plugin_id in deps: - return other_class.name - return None - - def _show_dependency_report(self): - """Show a dialog with full dependency report.""" - from PyQt6.QtWidgets import QDialog, QTextEdit, QVBoxLayout, QPushButton - - dialog = QDialog() - dialog.setWindowTitle("Plugin Dependency Report") - dialog.setMinimumSize(600, 400) - dialog.setStyleSheet(""" - QDialog { - background-color: #1a1f2e; - } - QTextEdit { - background-color: #232837; - color: white; - border: 1px solid rgba(100, 110, 130, 80); - padding: 10px; - } - QPushButton { - background-color: #4a9eff; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - } - """) - - layout = QVBoxLayout(dialog) - - text_edit = QTextEdit() - text_edit.setReadOnly(True) - text_edit.setHtml(self._generate_dependency_report()) - layout.addWidget(text_edit) - - close_btn = QPushButton("Close") - close_btn.clicked.connect(dialog.close) - layout.addWidget(close_btn) - - dialog.exec() - - def _create_hotkeys_tab(self): - """Create hotkeys configuration tab - dynamically discovers hotkeys from plugins.""" - tab = QWidget() - layout = QVBoxLayout(tab) - layout.setSpacing(15) - - # Info label - info = QLabel("Hotkeys are advertised by plugins. Changes apply on next restart.") - info.setStyleSheet("color: rgba(255,255,255,150);") - layout.addWidget(info) - - # Scroll area for hotkeys - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.Shape.NoFrame) - scroll.setStyleSheet("background: transparent; border: none;") - - scroll_content = QWidget() - hotkeys_layout = QVBoxLayout(scroll_content) - hotkeys_layout.setSpacing(10) - hotkeys_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - self.hotkey_inputs = {} - - # Collect hotkeys from all plugins - plugin_hotkeys = self._collect_plugin_hotkeys() - - # Group by plugin - for plugin_name, hotkeys in sorted(plugin_hotkeys.items()): - # Plugin group - group = QGroupBox(plugin_name) - group.setStyleSheet(self._group_style()) - group_layout = QVBoxLayout(group) - - for hotkey_info in hotkeys: - row = QHBoxLayout() - - # Description - desc = hotkey_info.get('description', hotkey_info['action']) - desc_label = QLabel(f"{desc}:") - desc_label.setStyleSheet("color: white; min-width: 150px;") - row.addWidget(desc_label) - - # Hotkey input - input_field = QLineEdit() - input_field.setText(hotkey_info['current']) - input_field.setPlaceholderText(hotkey_info['default']) - input_field.setStyleSheet(""" - QLineEdit { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - padding: 5px; - min-width: 150px; - } - """) - - # Store reference with config key - config_key = hotkey_info['config_key'] - self.hotkey_inputs[config_key] = { - 'input': input_field, - 'default': hotkey_info['default'], - 'plugin': plugin_name, - 'action': hotkey_info['action'] - } - - row.addWidget(input_field) - - # Reset button - reset_btn = QPushButton("↺") - reset_btn.setFixedSize(28, 28) - reset_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255,255,255,20); - color: white; - border: none; - border-radius: 4px; - font-size: 12px; - } - QPushButton:hover { - background-color: rgba(255,255,255,40); - } - """) - reset_btn.setToolTip(f"Reset to default: {hotkey_info['default']}") - reset_btn.clicked.connect(lambda checked, inp=input_field, default=hotkey_info['default']: inp.setText(default)) - row.addWidget(reset_btn) - - row.addStretch() - group_layout.addLayout(row) - - hotkeys_layout.addWidget(group) - - # Core hotkeys section (always present) - core_group = QGroupBox("Core System") - core_group.setStyleSheet(self._group_style()) - core_layout = QVBoxLayout(core_group) - - core_hotkeys = [ - ("Toggle Overlay", "hotkey_toggle", "ctrl+shift+u", "Show/hide the EU-Utility overlay"), - ("Universal Search", "hotkey_search", "ctrl+shift+f", "Quick search across all plugins"), - ] - - for label, config_key, default, description in core_hotkeys: - row = QHBoxLayout() - - desc_label = QLabel(f"{label}:") - desc_label.setStyleSheet("color: white; min-width: 150px;") - row.addWidget(desc_label) - - input_field = QLineEdit() - current = self.settings.get(config_key, default) - input_field.setText(current) - input_field.setPlaceholderText(default) - input_field.setStyleSheet(""" - QLineEdit { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - padding: 5px; - min-width: 150px; - } - """) - - self.hotkey_inputs[config_key] = { - 'input': input_field, - 'default': default, - 'plugin': 'Core', - 'action': label - } - - row.addWidget(input_field) - - reset_btn = QPushButton("↺") - reset_btn.setFixedSize(28, 28) - reset_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255,255,255,20); - color: white; - border: none; - border-radius: 4px; - font-size: 12px; - } - QPushButton:hover { - background-color: rgba(255,255,255,40); - } - """) - reset_btn.setToolTip(f"Reset to default: {default}") - reset_btn.clicked.connect(lambda checked, inp=input_field, default=default: inp.setText(default)) - row.addWidget(reset_btn) - - row.addStretch() - core_layout.addLayout(row) - - hotkeys_layout.addWidget(core_group) - hotkeys_layout.addStretch() - - scroll.setWidget(scroll_content) - layout.addWidget(scroll) - - return tab - - def _collect_plugin_hotkeys(self) -> dict: - """Collect hotkeys from all discovered plugins. - - Returns: - Dict mapping plugin name to list of hotkey info dicts - """ - plugin_hotkeys = {} - - if not hasattr(self.overlay, 'plugin_manager'): - return plugin_hotkeys - - plugin_manager = self.overlay.plugin_manager - all_plugins = plugin_manager.get_all_discovered_plugins() - - for plugin_id, plugin_class in all_plugins.items(): - hotkeys = getattr(plugin_class, 'hotkeys', None) - - if not hotkeys: - # Try legacy single hotkey attribute - single_hotkey = getattr(plugin_class, 'hotkey', None) - if single_hotkey: - hotkeys = [{ - 'action': 'toggle', - 'description': f"Toggle {plugin_class.name}", - 'default': single_hotkey, - 'config_key': f"hotkey_{plugin_id.split('.')[-1]}" - }] - - if hotkeys: - plugin_name = plugin_class.name - plugin_hotkeys[plugin_name] = [] - - for i, hk in enumerate(hotkeys): - # Support both dict format and simple string - if isinstance(hk, dict): - hotkey_info = { - 'action': hk.get('action', f'action_{i}'), - 'description': hk.get('description', hk.get('action', f'Action {i}')), - 'default': hk.get('default', ''), - 'config_key': hk.get('config_key', f"hotkey_{plugin_id.split('.')[-1]}_{i}") - } - else: - # Simple string format - legacy - hotkey_info = { - 'action': f'hotkey_{i}', - 'description': f"Hotkey {i+1}", - 'default': str(hk), - 'config_key': f"hotkey_{plugin_id.split('.')[-1]}_{i}" - } - - # Get current value from settings - hotkey_info['current'] = self.settings.get(hotkey_info['config_key'], hotkey_info['default']) - - plugin_hotkeys[plugin_name].append(hotkey_info) - - return plugin_hotkeys - - def _create_overlay_tab(self): - """Create overlay widgets configuration tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - layout.setSpacing(15) - - overlays_group = QGroupBox("In-Game Overlays") - overlays_group.setStyleSheet(self._group_style()) - overlays_layout = QVBoxLayout(overlays_group) - - overlays = [ - ("Spotify Player", "spotify", True), - ("Mission Tracker", "mission", False), - ("Skill Gains", "skillgain", False), - ("DPP Tracker", "dpp", False), - ] - - for name, key, enabled in overlays: - cb = QCheckBox(name) - cb.setChecked(enabled) - overlays_layout.addWidget(cb) - - # Reset positions - reset_pos_btn = QPushButton("↺ Reset All Positions") - reset_pos_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255,255,255,20); - color: white; - padding: 8px; - border: none; - border-radius: 4px; - } - """) - overlays_layout.addWidget(reset_pos_btn) - - layout.addWidget(overlays_group) - layout.addStretch() - - return tab - - def _create_data_tab(self): - """Create data management tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - layout.setSpacing(15) - - data_group = QGroupBox("Data Management") - data_group.setStyleSheet(self._group_style()) - data_layout = QVBoxLayout(data_group) - - # Export - export_btn = QPushButton("📤 Export All Data") - export_btn.setStyleSheet(""" - QPushButton { - background-color: #4a9eff; - color: white; - padding: 10px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - export_btn.clicked.connect(self._export_data) - data_layout.addWidget(export_btn) - - # Import - import_btn = QPushButton("📥 Import Data") - import_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255,255,255,20); - color: white; - padding: 10px; - border: none; - border-radius: 4px; - } - """) - import_btn.clicked.connect(self._import_data) - data_layout.addWidget(import_btn) - - # Clear - clear_btn = QPushButton("Clear All Data") - clear_btn.setStyleSheet(""" - QPushButton { - background-color: #f44336; - color: white; - padding: 10px; - border: none; - border-radius: 4px; - } - """) - clear_btn.clicked.connect(self._clear_data) - data_layout.addWidget(clear_btn) - - # Retention - retention_layout = QHBoxLayout() - retention_layout.addWidget(QLabel("Data retention:")) - self.retention_combo = QComboBox() - self.retention_combo.addItems(["7 days", "30 days", "90 days", "Forever"]) - retention_layout.addWidget(self.retention_combo) - retention_layout.addStretch() - data_layout.addLayout(retention_layout) - - layout.addWidget(data_group) - layout.addStretch() - - return tab - - def _group_style(self): - """Get group box style.""" - return """ - QGroupBox { - color: rgba(255,255,255,200); - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - margin-top: 10px; - font-weight: bold; - font-size: 12px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px; - } - """ - - def _save_settings(self): - """Save all settings.""" - # General - self.settings.set('theme', self.theme_combo.currentText()) - self.settings.set('overlay_opacity', self.opacity_slider.value() / 100) - self.settings.set('auto_start', self.auto_start_cb.isChecked()) - self.settings.set('minimize_to_tray', self.minimize_cb.isChecked()) - self.settings.set('show_tooltips', self.tooltips_cb.isChecked()) - - # Hotkeys - new structure with dict values - for config_key, hotkey_data in self.hotkey_inputs.items(): - if isinstance(hotkey_data, dict): - input_field = hotkey_data['input'] - self.settings.set(config_key, input_field.text()) - else: - # Legacy format - direct QLineEdit reference - self.settings.set(config_key, hotkey_data.text()) - - print("Settings saved!") - - def _reset_settings(self): - """Reset to defaults.""" - self.settings.reset() - print("Settings reset to defaults!") - - def _export_data(self): - """Export all data.""" - from PyQt6.QtWidgets import QFileDialog - - filepath, _ = QFileDialog.getSaveFileName( - None, "Export EU-Utility Data", "eu_utility_backup.json", "JSON (*.json)" - ) - - if filepath: - import shutil - data_dir = Path("data") - if data_dir.exists(): - # Create export - import json - export_data = {} - for f in data_dir.glob("*.json"): - with open(f, 'r') as file: - export_data[f.stem] = json.load(file) - - with open(filepath, 'w') as file: - json.dump(export_data, file, indent=2) - - def _import_data(self): - """Import data.""" - pass - - def _clear_data(self): - """Clear all data.""" - pass - - def _toggle_plugin(self, plugin_id: str, enable: bool): - """Enable or disable a plugin with dependency handling.""" - if not hasattr(self.overlay, 'plugin_manager'): - return - - plugin_manager = self.overlay.plugin_manager - - if enable: - # Get dependencies that will be auto-enabled - deps_to_enable = self._get_missing_dependencies(plugin_id, plugin_manager) - - if deps_to_enable: - # Show confirmation dialog - from PyQt6.QtWidgets import QMessageBox - - dep_names = [pid.split('.')[-1].replace('_', ' ').title() for pid in deps_to_enable] - msg = f"Enabling this plugin will also enable:\n\n" - msg += "\n".join(f" • {name}" for name in dep_names) - msg += "\n\nContinue?" - - reply = QMessageBox.question( - None, "Enable Dependencies", msg, - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) - - if reply != QMessageBox.StandardButton.Yes: - # Uncheck the box - self.plugin_checkboxes[plugin_id].setChecked(False) - return - - success = plugin_manager.enable_plugin(plugin_id) - if success: - print(f"[Settings] Enabled plugin: {plugin_id}") - # Refresh UI to show auto-enabled plugins - self._refresh_plugin_list() - else: - print(f"[Settings] Failed to enable plugin: {plugin_id}") - else: - # Check if other enabled plugins depend on this one - dependents = self.plugin_dependents.get(plugin_id, []) - enabled_dependents = [d for d in dependents if plugin_manager.is_plugin_enabled(d)] - - if enabled_dependents: - # Show warning - from PyQt6.QtWidgets import QMessageBox - - dep_names = [pid.split('.')[-1].replace('_', ' ').title() for pid in enabled_dependents] - msg = f"Cannot disable: This plugin is required by:\n\n" - msg += "\n".join(f" • {name}" for name in dep_names) - msg += "\n\nDisable those plugins first." - - QMessageBox.warning(None, "Dependency Warning", msg) - - # Recheck the box - self.plugin_checkboxes[plugin_id].setChecked(True) - return - - success = plugin_manager.disable_plugin(plugin_id) - if success: - print(f"[Settings] Disabled plugin: {plugin_id}") - self._refresh_plugin_list() - - def _get_missing_dependencies(self, plugin_id: str, plugin_manager) -> list: - """Get list of dependencies that need to be enabled.""" - deps = self.plugin_deps.get(plugin_id, []) - missing = [] - - for dep_id in deps: - if not plugin_manager.is_plugin_enabled(dep_id): - missing.append(dep_id) - - return missing - - def _refresh_plugin_list(self): - """Refresh the plugin list UI.""" - if not hasattr(self.overlay, 'plugin_manager'): - return - - plugin_manager = self.overlay.plugin_manager - - for plugin_id, cb in self.plugin_checkboxes.items(): - is_enabled = plugin_manager.is_plugin_enabled(plugin_id) - is_auto_enabled = plugin_manager.is_auto_enabled(plugin_id) if hasattr(plugin_manager, 'is_auto_enabled') else False - - cb.setChecked(is_enabled) - - if is_auto_enabled: - cb.setEnabled(False) - # Update text to show auto status - plugin_class = plugin_manager.get_all_discovered_plugins().get(plugin_id) - if plugin_class: - cb.setText(f"{plugin_class.name} (auto)") - else: - cb.setEnabled(True) - plugin_class = plugin_manager.get_all_discovered_plugins().get(plugin_id) - if plugin_class: - cb.setText(plugin_class.name) - - def _generate_dependency_report(self) -> str: - """Generate HTML dependency report.""" - if not hasattr(self.overlay, 'plugin_manager'): - return "

No plugin manager available

" - - plugin_manager = self.overlay.plugin_manager - all_plugins = plugin_manager.get_all_discovered_plugins() - - html = [""] - html.append("

📋 Plugin Dependency Report

") - html.append("
") - - # Summary section - total = len(all_plugins) - enabled = sum(1 for pid in all_plugins if plugin_manager.is_plugin_enabled(pid)) - html.append(f"

Total Plugins: {total} | Enabled: {enabled}

") - html.append("
") - - # Plugins with dependencies - html.append("

🔗 Plugins with Dependencies

") - html.append("") - html.append("
") - - # Plugins required by others - html.append("

⚠️ Plugins Required by Others

") - html.append("") - html.append("
") - - # Dependency chain visualization - html.append("

🔄 Dependency Chains

") - html.append("") - html.append("") - - return "\n".join(html) - - def _get_dependency_chain(self, plugin_id: str, visited=None) -> list: - """Get the dependency chain for a plugin.""" - if visited is None: - visited = set() - - if plugin_id in visited: - return [plugin_id] # Circular dependency - - visited.add(plugin_id) - chain = [plugin_id] - - # Get what this plugin depends on - deps = self.plugin_deps.get(plugin_id, []) - for dep_id in deps: - if dep_id not in visited: - chain.extend(self._get_dependency_chain(dep_id, visited)) - - return chain - - def _enable_all_plugins(self): - """Enable all plugins with dependency resolution.""" - if not hasattr(self.overlay, 'plugin_manager'): - return - - plugin_manager = self.overlay.plugin_manager - all_plugins = plugin_manager.get_all_discovered_plugins() - - # Sort plugins so dependencies are enabled first - sorted_plugins = self._sort_plugins_by_dependencies(all_plugins) - - enabled_count = 0 - for plugin_id in sorted_plugins: - cb = self.plugin_checkboxes.get(plugin_id) - if cb: - cb.setChecked(True) - success = plugin_manager.enable_plugin(plugin_id) - if success: - enabled_count += 1 - - self._refresh_plugin_list() - print(f"[Settings] Enabled {enabled_count} plugins") - - def _sort_plugins_by_dependencies(self, all_plugins: dict) -> list: - """Sort plugins so dependencies come before dependents.""" - plugin_ids = list(all_plugins.keys()) - - # Build dependency graph - graph = {pid: set(self.plugin_deps.get(pid, [])) for pid in plugin_ids} - - # Topological sort - sorted_list = [] - visited = set() - temp_mark = set() - - def visit(pid): - if pid in temp_mark: - return # Circular dependency, skip - if pid in visited: - return - - temp_mark.add(pid) - for dep in graph.get(pid, set()): - if dep in graph: - visit(dep) - temp_mark.remove(pid) - visited.add(pid) - sorted_list.append(pid) - - for pid in plugin_ids: - visit(pid) - - return sorted_list - - def _disable_all_plugins(self): - """Disable all plugins in reverse dependency order.""" - if not hasattr(self.overlay, 'plugin_manager'): - return - - plugin_manager = self.overlay.plugin_manager - all_plugins = plugin_manager.get_all_discovered_plugins() - - # Sort so dependents are disabled first (reverse of enable order) - sorted_plugins = self._sort_plugins_by_dependencies(all_plugins) - sorted_plugins.reverse() - - disabled_count = 0 - for plugin_id in sorted_plugins: - cb = self.plugin_checkboxes.get(plugin_id) - if cb: - cb.setChecked(False) - success = plugin_manager.disable_plugin(plugin_id) - if success: - disabled_count += 1 - - self._refresh_plugin_list() - print(f"[Settings] Disabled {disabled_count} plugins")