diff --git a/core/overlay_window.py b/core/overlay_window.py index 47c94bf..8bbb946 100644 --- a/core/overlay_window.py +++ b/core/overlay_window.py @@ -1303,7 +1303,7 @@ class OverlayWindow(QMainWindow): return tab def _save_settings(self, dialog): - """Save plugin settings.""" + """Save plugin settings with dependency checking.""" if not self.plugin_manager: dialog.accept() return @@ -1312,13 +1312,79 @@ class OverlayWindow(QMainWindow): self._refresh_ui() self.setStyleSheet(get_global_stylesheet()) - # Save plugin enable/disable + # Check for newly enabled plugins with dependencies + from core.plugin_dependency_manager import get_dependency_manager + dep_manager = get_dependency_manager() + + plugins_to_enable = [] for plugin_id, cb in self.settings_checkboxes.items(): if cb.isChecked(): + # Check if this is a newly enabled plugin + if not self.plugin_manager.is_plugin_enabled(plugin_id): + plugins_to_enable.append(plugin_id) self.plugin_manager.enable_plugin(plugin_id) else: self.plugin_manager.disable_plugin(plugin_id) + # Check dependencies for newly enabled plugins + for plugin_id in plugins_to_enable: + # Get plugin class + all_plugins = self.plugin_manager.get_all_discovered_plugins() + if plugin_id in all_plugins: + plugin_class = all_plugins[plugin_id] + if dep_manager.has_dependencies(plugin_class): + installed, missing = dep_manager.check_all_dependencies(plugin_class) + if missing: + # Show dependency dialog + dep_text = dep_manager.get_missing_dependencies_text(plugin_class) + from PyQt6.QtWidgets import QMessageBox + + msg_box = QMessageBox(self) + msg_box.setWindowTitle("Install Dependencies?") + msg_box.setText(f"Plugin '{plugin_class.name}' requires additional dependencies:\n\n{dep_text}") + msg_box.setInformativeText("Would you like to install them now?") + msg_box.setStandardButtons( + QMessageBox.StandardButton.Yes | + QMessageBox.StandardButton.No + ) + msg_box.setDefaultButton(QMessageBox.StandardButton.Yes) + + reply = msg_box.exec() + + if reply == QMessageBox.StandardButton.Yes: + # Install dependencies + from PyQt6.QtWidgets import QProgressDialog + from PyQt6.QtCore import Qt + + progress = QProgressDialog( + f"Installing dependencies for {plugin_class.name}...", + "Cancel", + 0, + len(missing), + self + ) + progress.setWindowModality(Qt.WindowModality.WindowModal) + progress.setWindowTitle("Installing Dependencies") + + current = 0 + for dep in missing: + if progress.wasCanceled(): + break + + progress.setLabelText(f"Installing {dep.name}...") + progress.setValue(current) + + success, msg = dep_manager.install_dependency(dep.name) + if not success: + QMessageBox.warning( + self, + "Installation Failed", + f"Failed to install {dep.name}:\n{msg}" + ) + current += 1 + + progress.setValue(len(missing)) + # Reload plugins self._reload_plugins() dialog.accept() diff --git a/core/plugin_dependency_manager.py b/core/plugin_dependency_manager.py new file mode 100644 index 0000000..a46525d --- /dev/null +++ b/core/plugin_dependency_manager.py @@ -0,0 +1,185 @@ +""" +EU-Utility - Plugin Dependency Manager + +Manages plugin dependencies and auto-installs them when needed. +""" + +import sys +import subprocess +import importlib +from typing import Dict, List, Tuple, Optional, Any +from dataclasses import dataclass +from pathlib import Path +import json + + +@dataclass +class DependencyCheck: + """Result of a dependency check.""" + name: str + required: bool + installed: bool + version: Optional[str] = None + error: Optional[str] = None + + +class PluginDependencyManager: + """Manages plugin dependencies with auto-install capability.""" + + def __init__(self, plugin_manager=None): + self.plugin_manager = plugin_manager + self.install_status_file = Path("config/dependency_installs.json") + self._install_status = self._load_install_status() + + def _load_install_status(self) -> Dict: + """Load status of previously installed dependencies.""" + if self.install_status_file.exists(): + try: + with open(self.install_status_file, 'r') as f: + return json.load(f) + except: + pass + return {} + + def _save_install_status(self): + """Save status of installed dependencies.""" + try: + self.install_status_file.parent.mkdir(parents=True, exist_ok=True) + with open(self.install_status_file, 'w') as f: + json.dump(self._install_status, f, indent=2) + except Exception as e: + print(f"[DependencyManager] Failed to save status: {e}") + + def check_dependency(self, package_name: str) -> DependencyCheck: + """Check if a pip package is installed.""" + # Handle version specifiers (e.g., "package>=1.0") + base_name = package_name.split('[')[0].split('>')[0].split('<')[0].split('=')[0].strip() + + try: + module = importlib.import_module(base_name.replace('-', '_')) + version = getattr(module, '__version__', 'unknown') + return DependencyCheck( + name=package_name, + required=True, + installed=True, + version=version + ) + except ImportError: + return DependencyCheck( + name=package_name, + required=True, + installed=False + ) + except Exception as e: + return DependencyCheck( + name=package_name, + required=True, + installed=False, + error=str(e) + ) + + def check_all_dependencies(self, plugin_class) -> Tuple[List[DependencyCheck], List[DependencyCheck]]: + """Check all dependencies for a plugin. + + Returns: + Tuple of (installed_deps, missing_deps) + """ + deps = getattr(plugin_class, 'dependencies', {}) + pip_deps = deps.get('pip', []) + + installed = [] + missing = [] + + for dep in pip_deps: + check = self.check_dependency(dep) + if check.installed: + installed.append(check) + else: + missing.append(check) + + return installed, missing + + def install_dependency(self, package_name: str) -> Tuple[bool, str]: + """Install a pip package. + + Returns: + (success, message) + """ + try: + print(f"[DependencyManager] Installing {package_name}...") + result = subprocess.run( + [sys.executable, '-m', 'pip', 'install', package_name], + capture_output=True, + text=True, + timeout=300 + ) + + if result.returncode == 0: + self._install_status[package_name] = { + 'installed': True, + 'output': result.stdout[-500:] if len(result.stdout) > 500 else result.stdout + } + self._save_install_status() + return True, f"Successfully installed {package_name}" + else: + return False, f"Failed to install {package_name}: {result.stderr}" + + except subprocess.TimeoutExpired: + return False, f"Installation of {package_name} timed out" + except Exception as e: + return False, f"Error installing {package_name}: {e}" + + def install_all_missing(self, plugin_class, progress_callback=None) -> Tuple[bool, List[str]]: + """Install all missing dependencies for a plugin. + + Returns: + (all_successful, messages) + """ + _, missing = self.check_all_dependencies(plugin_class) + + if not missing: + return True, ["All dependencies already installed"] + + messages = [] + all_success = True + + for dep in missing: + if progress_callback: + progress_callback(f"Installing {dep.name}...") + + success, msg = self.install_dependency(dep.name) + messages.append(msg) + + if not success: + all_success = False + + return all_success, messages + + def get_missing_dependencies_text(self, plugin_class) -> str: + """Get a formatted text of missing dependencies.""" + _, missing = self.check_all_dependencies(plugin_class) + + if not missing: + return "All dependencies are installed." + + lines = [f"Missing dependencies ({len(missing)}):"] + for dep in missing: + lines.append(f" • {dep.name}") + + return "\n".join(lines) + + def has_dependencies(self, plugin_class) -> bool: + """Check if a plugin has declared dependencies.""" + deps = getattr(plugin_class, 'dependencies', {}) + return bool(deps.get('pip', [])) + + +# Singleton instance +_dep_manager = None + +def get_dependency_manager(plugin_manager=None) -> PluginDependencyManager: + """Get the plugin dependency manager singleton.""" + global _dep_manager + if _dep_manager is None: + _dep_manager = PluginDependencyManager(plugin_manager) + return _dep_manager diff --git a/plugins/base_plugin.py b/plugins/base_plugin.py index 6c0e3bb..63708bd 100644 --- a/plugins/base_plugin.py +++ b/plugins/base_plugin.py @@ -28,6 +28,10 @@ class BasePlugin(ABC): hotkey: Optional[str] = None # e.g., "ctrl+shift+n" enabled: bool = True + # Dependencies - override in subclass + # Format: {'pip': ['package1', 'package2>=1.0'], 'optional': {'package3': 'description'}} + dependencies: Dict[str, Any] = {} + def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]): self.overlay = overlay_window self.config = config diff --git a/plugins/game_reader_test/plugin.py b/plugins/game_reader_test/plugin.py index deeb1e2..648f71c 100644 --- a/plugins/game_reader_test/plugin.py +++ b/plugins/game_reader_test/plugin.py @@ -133,6 +133,16 @@ class GameReaderTestPlugin(BasePlugin): author = "EU-Utility" description = "Debug tool for testing OCR and screen reading" + # Dependencies for OCR functionality + dependencies = { + 'pip': ['pillow', 'numpy'], + 'optional': { + 'easyocr': 'Best OCR accuracy, auto-downloads models', + 'pytesseract': 'Alternative OCR engine', + 'paddleocr': 'Advanced OCR with layout detection' + } + } + def __init__(self, overlay_window, config): super().__init__(overlay_window, config) self.test_history = []