diff --git a/manifest.json b/manifest.json index 2333f46..f39d2a3 100644 --- a/manifest.json +++ b/manifest.json @@ -52,22 +52,6 @@ "min_core_version": "2.0.0", "category": "Information" }, - { - "id": "dashboard", - "name": "Dashboard", - "version": "1.0.0", - "author": "ImpulsiveFPS", - "description": "Overview dashboard showing session stats, recent globals, skill gains, and quick access to all plugins.", - "folder": "plugins/dashboard/", - "icon": "grid", - "tags": ["dashboard", "overview", "stats"], - "dependencies": { - "core": ["log"], - "plugins": [] - }, - "min_core_version": "2.0.0", - "category": "Information" - }, { "id": "loot_tracker", "name": "Loot Tracker", @@ -307,38 +291,6 @@ }, "min_core_version": "2.0.0", "category": "Tools" - }, - { - "id": "universal_search", - "name": "Universal Search", - "version": "1.0.0", - "author": "ImpulsiveFPS", - "description": "Quick search across all plugins. Find items, skills, locations instantly.", - "folder": "plugins/universal_search/", - "icon": "search", - "tags": ["search", "quick", "universal"], - "dependencies": { - "core": [], - "plugins": [] - }, - "min_core_version": "2.0.0", - "category": "Tools" - }, - { - "id": "import_export", - "name": "Import/Export", - "version": "1.0.0", - "author": "ImpulsiveFPS", - "description": "Import and export your EU-Utility data. Backup and restore functionality.", - "folder": "plugins/import_export/", - "icon": "external", - "tags": ["import", "export", "backup", "data"], - "dependencies": { - "core": ["data_store"], - "plugins": [] - }, - "min_core_version": "2.0.0", - "category": "Data" } ] } diff --git a/plugins/analytics/__init__.py b/plugins/analytics/__init__.py deleted file mode 100644 index 76dab5e..0000000 --- a/plugins/analytics/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .plugin import AnalyticsPlugin - -__all__ = ['AnalyticsPlugin'] diff --git a/plugins/analytics/__pycache__/__init__.cpython-312.pyc b/plugins/analytics/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index d5af77a..0000000 Binary files a/plugins/analytics/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/plugins/analytics/__pycache__/plugin.cpython-312.pyc b/plugins/analytics/__pycache__/plugin.cpython-312.pyc deleted file mode 100644 index 222bbb8..0000000 Binary files a/plugins/analytics/__pycache__/plugin.cpython-312.pyc and /dev/null differ diff --git a/plugins/analytics/plugin.py b/plugins/analytics/plugin.py deleted file mode 100644 index e8491bb..0000000 --- a/plugins/analytics/plugin.py +++ /dev/null @@ -1,525 +0,0 @@ -# Description: Analytics and monitoring system for EU-Utility -# Privacy-focused usage tracking and performance monitoring - -""" -EU-Utility Analytics System - -Privacy-focused analytics with opt-in tracking: -- Feature usage statistics -- Performance monitoring (FPS, memory, CPU) -- Error reporting -- Health checks - -All data is stored locally by default. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QCheckBox, QProgressBar, QTableWidget, - QTableWidgetItem, QTabWidget, QGroupBox -) -from PyQt6.QtCore import QTimer, pyqtSignal, Qt -from plugins.base_plugin import BasePlugin -from datetime import datetime, timedelta -import json -import os -import psutil -import time - - -class AnalyticsPlugin(BasePlugin): - """ - Analytics and monitoring dashboard for EU-Utility. - - Tracks: - - Feature usage (opt-in) - - System performance - - Error occurrences - - Health status - """ - - name = "Analytics" - version = "1.0.0" - author = "LemonNexus" - description = "Usage analytics and performance monitoring" - icon = "bar-chart" - - def initialize(self): - """Initialize analytics system.""" - # Load settings - self.enabled = self.load_data("enabled", False) - self.track_performance = self.load_data("track_performance", True) - self.track_usage = self.load_data("track_usage", False) - - # Data storage - self.usage_data = self.load_data("usage", {}) - self.performance_data = self.load_data("performance", []) - self.error_data = self.load_data("errors", []) - - # Performance tracking - self.start_time = time.time() - self.session_events = [] - - # Setup timers - if self.track_performance: - self._setup_performance_monitoring() - - def _setup_performance_monitoring(self): - """Setup periodic performance monitoring.""" - self.performance_timer = QTimer() - self.performance_timer.timeout.connect(self._record_performance) - self.performance_timer.start(30000) # Every 30 seconds - - # Initial record - self._record_performance() - - def _record_performance(self): - """Record current system performance.""" - try: - # Get system stats - cpu_percent = psutil.cpu_percent(interval=1) - memory = psutil.virtual_memory() - - # Get process info - process = psutil.Process() - process_memory = process.memory_info().rss / 1024 / 1024 # MB - - record = { - 'timestamp': datetime.now().isoformat(), - 'cpu_percent': cpu_percent, - 'memory_percent': memory.percent, - 'memory_used_mb': process_memory, - 'uptime_seconds': time.time() - self.start_time - } - - self.performance_data.append(record) - - # Keep only last 1000 records - if len(self.performance_data) > 1000: - self.performance_data = self.performance_data[-1000:] - - self.save_data("performance", self.performance_data) - - except Exception as e: - self.log_error(f"Performance recording failed: {e}") - - def record_event(self, event_type, details=None): - """Record a usage event (if tracking enabled).""" - if not self.track_usage: - return - - event = { - 'type': event_type, - 'timestamp': datetime.now().isoformat(), - 'details': details or {} - } - - self.session_events.append(event) - - # Update usage stats - if event_type not in self.usage_data: - self.usage_data[event_type] = {'count': 0, 'last_used': None} - - self.usage_data[event_type]['count'] += 1 - self.usage_data[event_type]['last_used'] = datetime.now().isoformat() - self.save_data("usage", self.usage_data) - - def record_error(self, error_message, context=None): - """Record an error occurrence.""" - error = { - 'message': str(error_message), - 'timestamp': datetime.now().isoformat(), - 'context': context or {}, - 'session_uptime': time.time() - self.start_time - } - - self.error_data.append(error) - - # Keep only last 100 errors - if len(self.error_data) > 100: - self.error_data = self.error_data[-100:] - - self.save_data("errors", self.error_data) - - def get_ui(self): - """Create analytics dashboard UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(15) - - # Title - title = QLabel("Analytics & Monitoring") - title.setStyleSheet("font-size: 20px; font-weight: bold; color: #4a9eff;") - layout.addWidget(title) - - # Tabs - tabs = QTabWidget() - - # Overview tab - tabs.addTab(self._create_overview_tab(), "Overview") - - # Performance tab - tabs.addTab(self._create_performance_tab(), "Performance") - - # Usage tab - tabs.addTab(self._create_usage_tab(), "Usage") - - # Errors tab - tabs.addTab(self._create_errors_tab(), "Errors") - - # Settings tab - tabs.addTab(self._create_settings_tab(), "Settings") - - layout.addWidget(tabs) - - # Refresh button - refresh_btn = QPushButton("Refresh Data") - refresh_btn.clicked.connect(self._refresh_all) - layout.addWidget(refresh_btn) - - return widget - - def _create_overview_tab(self): - """Create overview dashboard.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # System health - health_group = QGroupBox("System Health") - health_layout = QVBoxLayout(health_group) - - self.health_status = QLabel("Checking...") - self.health_status.setStyleSheet("font-size: 16px; font-weight: bold;") - health_layout.addWidget(self.health_status) - - # Health metrics - self.cpu_label = QLabel("CPU: --") - self.memory_label = QLabel("Memory: --") - self.uptime_label = QLabel("Uptime: --") - - health_layout.addWidget(self.cpu_label) - health_layout.addWidget(self.memory_label) - health_layout.addWidget(self.uptime_label) - - layout.addWidget(health_group) - - # Session stats - stats_group = QGroupBox("Session Statistics") - stats_layout = QVBoxLayout(stats_group) - - self.events_label = QLabel(f"Events recorded: {len(self.session_events)}") - self.errors_label = QLabel(f"Errors: {len(self.error_data)}") - self.plugins_label = QLabel(f"Active plugins: --") - - stats_layout.addWidget(self.events_label) - stats_layout.addWidget(self.errors_label) - stats_layout.addWidget(self.plugins_label) - - layout.addWidget(stats_group) - - layout.addStretch() - - # Update immediately - self._update_overview() - - return tab - - def _create_performance_tab(self): - """Create performance monitoring tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Current stats - current_group = QGroupBox("Current Performance") - current_layout = QVBoxLayout(current_group) - - self.perf_cpu = QLabel("CPU Usage: --") - self.perf_memory = QLabel("Memory Usage: --") - self.perf_process = QLabel("Process Memory: --") - - current_layout.addWidget(self.perf_cpu) - current_layout.addWidget(self.perf_memory) - current_layout.addWidget(self.perf_process) - - layout.addWidget(current_group) - - # Historical data - history_group = QGroupBox("Performance History (Last Hour)") - history_layout = QVBoxLayout(history_group) - - self.perf_table = QTableWidget() - self.perf_table.setColumnCount(4) - self.perf_table.setHorizontalHeaderLabels(["Time", "CPU %", "Memory %", "Process MB"]) - self.perf_table.horizontalHeader().setStretchLastSection(True) - - history_layout.addWidget(self.perf_table) - layout.addWidget(history_group) - - layout.addStretch() - - self._update_performance_tab() - - return tab - - def _create_usage_tab(self): - """Create usage statistics tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Usage table - self.usage_table = QTableWidget() - self.usage_table.setColumnCount(3) - self.usage_table.setHorizontalHeaderLabels(["Feature", "Usage Count", "Last Used"]) - self.usage_table.horizontalHeader().setStretchLastSection(True) - - layout.addWidget(self.usage_table) - - # Update data - self._update_usage_tab() - - return tab - - def _create_errors_tab(self): - """Create error log tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Error table - self.error_table = QTableWidget() - self.error_table.setColumnCount(3) - self.error_table.setHorizontalHeaderLabels(["Time", "Error", "Context"]) - self.error_table.horizontalHeader().setStretchLastSection(True) - - layout.addWidget(self.error_table) - - # Clear button - clear_btn = QPushButton("Clear Error Log") - clear_btn.clicked.connect(self._clear_errors) - layout.addWidget(clear_btn) - - self._update_errors_tab() - - return tab - - def _create_settings_tab(self): - """Create analytics settings tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Privacy notice - privacy = QLabel( - "🔒 Privacy Notice:\n" - "All analytics data is stored locally on your machine. " - "No data is sent to external servers unless you explicitly configure it." - ) - privacy.setStyleSheet("background-color: #2a3a40; padding: 10px; border-radius: 4px;") - privacy.setWordWrap(True) - layout.addWidget(privacy) - - # Enable analytics - self.enable_checkbox = QCheckBox("Enable Analytics") - self.enable_checkbox.setChecked(self.enabled) - self.enable_checkbox.toggled.connect(self._on_enable_changed) - layout.addWidget(self.enable_checkbox) - - # Performance tracking - self.perf_checkbox = QCheckBox("Track System Performance") - self.perf_checkbox.setChecked(self.track_performance) - self.perf_checkbox.toggled.connect(self._on_perf_changed) - layout.addWidget(self.perf_checkbox) - - # Usage tracking - self.usage_checkbox = QCheckBox("Track Feature Usage (Opt-in)") - self.usage_checkbox.setChecked(self.track_usage) - self.usage_checkbox.toggled.connect(self._on_usage_changed) - layout.addWidget(self.usage_checkbox) - - # Data management - layout.addWidget(QLabel("Data Management:")) - - export_btn = QPushButton("Export Analytics Data") - export_btn.clicked.connect(self._export_data) - layout.addWidget(export_btn) - - clear_all_btn = QPushButton("Clear All Analytics Data") - clear_all_btn.setStyleSheet("color: #f44336;") - clear_all_btn.clicked.connect(self._clear_all_data) - layout.addWidget(clear_all_btn) - - layout.addStretch() - - return tab - - def _update_overview(self): - """Update overview tab.""" - try: - # Get current stats - cpu = psutil.cpu_percent(interval=0.5) - memory = psutil.virtual_memory() - - # Health status - if cpu < 50 and memory.percent < 80: - status = "✓ Healthy" - color = "#4caf50" - elif cpu < 80 and memory.percent < 90: - status = "⚠ Warning" - color = "#ff9800" - else: - status = "✗ Critical" - color = "#f44336" - - self.health_status.setText(status) - self.health_status.setStyleSheet(f"font-size: 16px; font-weight: bold; color: {color};") - - self.cpu_label.setText(f"CPU: {cpu:.1f}%") - self.memory_label.setText(f"Memory: {memory.percent:.1f}%") - - # Uptime - uptime = time.time() - self.start_time - hours = int(uptime // 3600) - minutes = int((uptime % 3600) // 60) - self.uptime_label.setText(f"Uptime: {hours}h {minutes}m") - - # Stats - self.events_label.setText(f"Events recorded: {len(self.session_events)}") - self.errors_label.setText(f"Total errors: {len(self.error_data)}") - - except Exception as e: - self.log_error(f"Overview update failed: {e}") - - def _update_performance_tab(self): - """Update performance tab.""" - try: - # Current stats - cpu = psutil.cpu_percent(interval=0.5) - memory = psutil.virtual_memory() - process = psutil.Process() - process_mem = process.memory_info().rss / 1024 / 1024 - - self.perf_cpu.setText(f"CPU Usage: {cpu:.1f}%") - self.perf_memory.setText(f"Memory Usage: {memory.percent:.1f}%") - self.perf_process.setText(f"Process Memory: {process_mem:.1f} MB") - - # Historical data (last 20 records) - recent_data = self.performance_data[-20:] - self.perf_table.setRowCount(len(recent_data)) - - for i, record in enumerate(reversed(recent_data)): - time_str = record['timestamp'][11:19] # HH:MM:SS - self.perf_table.setItem(i, 0, QTableWidgetItem(time_str)) - self.perf_table.setItem(i, 1, QTableWidgetItem(f"{record['cpu_percent']:.1f}%")) - self.perf_table.setItem(i, 2, QTableWidgetItem(f"{record['memory_percent']:.1f}%")) - self.perf_table.setItem(i, 3, QTableWidgetItem(f"{record['memory_used_mb']:.1f}")) - - except Exception as e: - self.log_error(f"Performance tab update failed: {e}") - - def _update_usage_tab(self): - """Update usage tab.""" - self.usage_table.setRowCount(len(self.usage_data)) - - for i, (feature, data) in enumerate(sorted(self.usage_data.items())): - self.usage_table.setItem(i, 0, QTableWidgetItem(feature)) - self.usage_table.setItem(i, 1, QTableWidgetItem(str(data['count']))) - - last_used = data.get('last_used', 'Never') - if last_used and last_used != 'Never': - last_used = last_used[:16].replace('T', ' ') # Format datetime - self.usage_table.setItem(i, 2, QTableWidgetItem(last_used)) - - def _update_errors_tab(self): - """Update errors tab.""" - self.error_table.setRowCount(len(self.error_data)) - - for i, error in enumerate(reversed(self.error_data[-50:])): # Last 50 errors - time_str = error['timestamp'][11:19] - self.error_table.setItem(i, 0, QTableWidgetItem(time_str)) - self.error_table.setItem(i, 1, QTableWidgetItem(error['message'][:50])) - self.error_table.setItem(i, 2, QTableWidgetItem(str(error.get('context', ''))[:50])) - - def _refresh_all(self): - """Refresh all tabs.""" - self._update_overview() - self._update_performance_tab() - self._update_usage_tab() - self._update_errors_tab() - - def _on_enable_changed(self, checked): - """Handle analytics enable toggle.""" - self.enabled = checked - self.save_data("enabled", checked) - - if checked and self.track_performance: - self._setup_performance_monitoring() - elif not checked and hasattr(self, 'performance_timer'): - self.performance_timer.stop() - - def _on_perf_changed(self, checked): - """Handle performance tracking toggle.""" - self.track_performance = checked - self.save_data("track_performance", checked) - - if checked and self.enabled: - self._setup_performance_monitoring() - elif hasattr(self, 'performance_timer'): - self.performance_timer.stop() - - def _on_usage_changed(self, checked): - """Handle usage tracking toggle.""" - self.track_usage = checked - self.save_data("track_usage", checked) - - def _export_data(self): - """Export analytics data.""" - data = { - 'exported_at': datetime.now().isoformat(), - 'usage': self.usage_data, - 'performance_samples': len(self.performance_data), - 'errors': len(self.error_data) - } - - # Save to file - export_path = os.path.expanduser('~/.eu-utility/analytics_export.json') - os.makedirs(os.path.dirname(export_path), exist_ok=True) - - with open(export_path, 'w') as f: - json.dump(data, f, indent=2) - - self.notify_info("Export Complete", f"Data exported to:\n{export_path}") - - def _clear_all_data(self): - """Clear all analytics data.""" - self.usage_data = {} - self.performance_data = [] - self.error_data = [] - self.session_events = [] - - self.save_data("usage", {}) - self.save_data("performance", []) - self.save_data("errors", []) - - self._refresh_all() - self.notify_info("Data Cleared", "All analytics data has been cleared.") - - def _clear_errors(self): - """Clear error log.""" - self.error_data = [] - self.save_data("errors", []) - self._update_errors_tab() - - def on_show(self): - """Update when tab shown.""" - self._refresh_all() - - def shutdown(self): - """Cleanup on shutdown.""" - if hasattr(self, 'performance_timer'): - self.performance_timer.stop() - - # Record final stats - if self.enabled: - self.save_data("final_session", { - 'session_duration': time.time() - self.start_time, - 'events_recorded': len(self.session_events), - 'timestamp': datetime.now().isoformat() - }) diff --git a/plugins/auto_updater/__init__.py b/plugins/auto_updater/__init__.py deleted file mode 100644 index 9ea3594..0000000 --- a/plugins/auto_updater/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .plugin import AutoUpdaterPlugin - -__all__ = ['AutoUpdaterPlugin'] diff --git a/plugins/auto_updater/__pycache__/__init__.cpython-312.pyc b/plugins/auto_updater/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 96d5bc9..0000000 Binary files a/plugins/auto_updater/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/plugins/auto_updater/__pycache__/plugin.cpython-312.pyc b/plugins/auto_updater/__pycache__/plugin.cpython-312.pyc deleted file mode 100644 index 8aeada4..0000000 Binary files a/plugins/auto_updater/__pycache__/plugin.cpython-312.pyc and /dev/null differ diff --git a/plugins/auto_updater/plugin.py b/plugins/auto_updater/plugin.py deleted file mode 100644 index 4602b4d..0000000 --- a/plugins/auto_updater/plugin.py +++ /dev/null @@ -1,481 +0,0 @@ -# Description: Auto-updater plugin for EU-Utility -# Checks for updates and installs them automatically - -""" -EU-Utility Auto-Updater - -Features: -- Check for updates from GitHub -- Download and install updates -- Changelog display -- Automatic rollback on failure -- Scheduled update checks -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QProgressBar, QTextEdit, QMessageBox, - QCheckBox, QGroupBox, QComboBox -) -from PyQt6.QtCore import QThread, pyqtSignal, QTimer, Qt -from plugins.base_plugin import BasePlugin -import requests -import json -import os -import shutil -import zipfile -import subprocess -import sys -from datetime import datetime - - -class UpdateWorker(QThread): - """Background worker for update operations.""" - progress = pyqtSignal(int) - status = pyqtSignal(str) - finished_signal = pyqtSignal(bool, str) - - def __init__(self, download_url, install_path, backup_path): - super().__init__() - self.download_url = download_url - self.install_path = install_path - self.backup_path = backup_path - self.temp_download = None - - def run(self): - try: - # Step 1: Download - self.status.emit("Downloading update...") - self._download() - self.progress.emit(33) - - # Step 2: Backup - self.status.emit("Creating backup...") - self._create_backup() - self.progress.emit(66) - - # Step 3: Install - self.status.emit("Installing update...") - self._install() - self.progress.emit(100) - - self.finished_signal.emit(True, "Update installed successfully. Please restart EU-Utility.") - - except Exception as e: - self.status.emit(f"Error: {str(e)}") - self._rollback() - self.finished_signal.emit(False, str(e)) - finally: - # Cleanup temp file - if self.temp_download and os.path.exists(self.temp_download): - try: - os.remove(self.temp_download) - except: - pass - - def _download(self): - """Download update package.""" - self.temp_download = os.path.join(os.path.expanduser('~/.eu-utility'), 'update.zip') - os.makedirs(os.path.dirname(self.temp_download), exist_ok=True) - - response = requests.get(self.download_url, stream=True, timeout=120) - response.raise_for_status() - - with open(self.temp_download, 'wb') as f: - for chunk in response.iter_content(chunk_size=8192): - if chunk: - f.write(chunk) - - def _create_backup(self): - """Create backup of current installation.""" - if os.path.exists(self.install_path): - os.makedirs(self.backup_path, exist_ok=True) - backup_name = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}" - backup_full_path = os.path.join(self.backup_path, backup_name) - shutil.copytree(self.install_path, backup_full_path, ignore_dangling_symlinks=True) - - def _install(self): - """Install the update.""" - # Extract update - with zipfile.ZipFile(self.temp_download, 'r') as zip_ref: - # Extract to temp location first - temp_extract = os.path.join(os.path.expanduser('~/.eu-utility'), 'update_extract') - if os.path.exists(temp_extract): - shutil.rmtree(temp_extract) - zip_ref.extractall(temp_extract) - - # Find the actual content (might be in a subdirectory) - contents = os.listdir(temp_extract) - if len(contents) == 1 and os.path.isdir(os.path.join(temp_extract, contents[0])): - source = os.path.join(temp_extract, contents[0]) - else: - source = temp_extract - - # Copy files to install path - for item in os.listdir(source): - s = os.path.join(source, item) - d = os.path.join(self.install_path, item) - - if os.path.isdir(s): - if os.path.exists(d): - shutil.rmtree(d) - shutil.copytree(s, d) - else: - shutil.copy2(s, d) - - # Cleanup - shutil.rmtree(temp_extract) - - def _rollback(self): - """Rollback to backup on failure.""" - try: - # Find most recent backup - if os.path.exists(self.backup_path): - backups = sorted(os.listdir(self.backup_path)) - if backups: - latest_backup = os.path.join(self.backup_path, backups[-1]) - - # Restore from backup - if os.path.exists(self.install_path): - shutil.rmtree(self.install_path) - shutil.copytree(latest_backup, self.install_path) - except: - pass - - -class AutoUpdaterPlugin(BasePlugin): - """ - Auto-updater for EU-Utility. - - Checks for updates from GitHub and installs them automatically. - """ - - name = "Auto Updater" - version = "1.0.0" - author = "LemonNexus" - description = "Automatic update checker and installer" - icon = "refresh" - - # GitHub repository info - GITHUB_REPO = "ImpulsiveFPS/EU-Utility" - GITHUB_API_URL = "https://api.github.com/repos/{}/releases/latest" - - def initialize(self): - """Initialize auto-updater.""" - self.current_version = "2.0.0" # Should be read from version file - self.latest_version = None - self.latest_release = None - self.worker = None - - # Settings - self.check_on_startup = self.load_data("check_on_startup", True) - self.auto_install = self.load_data("auto_install", False) - self.check_interval_hours = self.load_data("check_interval", 24) - - # Check for updates if enabled - if self.check_on_startup: - QTimer.singleShot(5000, self._check_for_updates) # Check after 5 seconds - - def get_ui(self): - """Create updater UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(15) - - # Title - title = QLabel("Auto Updater") - title.setStyleSheet("font-size: 20px; font-weight: bold; color: #4a9eff;") - layout.addWidget(title) - - # Current version - version_group = QGroupBox("Version Information") - version_layout = QVBoxLayout(version_group) - - self.current_version_label = QLabel(f"Current Version: {self.current_version}") - version_layout.addWidget(self.current_version_label) - - self.latest_version_label = QLabel("Latest Version: Checking...") - version_layout.addWidget(self.latest_version_label) - - self.status_label = QLabel("Status: Ready") - self.status_label.setStyleSheet("color: #4caf50;") - version_layout.addWidget(self.status_label) - - layout.addWidget(version_group) - - # Check for updates button - check_btn = QPushButton("Check for Updates") - check_btn.setStyleSheet(""" - QPushButton { - background-color: #4a9eff; - color: white; - padding: 12px; - font-weight: bold; - border-radius: 4px; - } - QPushButton:hover { - background-color: #5aafff; - } - """) - check_btn.clicked.connect(self._check_for_updates) - layout.addWidget(check_btn) - - # Changelog - changelog_group = QGroupBox("Changelog") - changelog_layout = QVBoxLayout(changelog_group) - - self.changelog_text = QTextEdit() - self.changelog_text.setReadOnly(True) - self.changelog_text.setPlaceholderText("Check for updates to see changelog...") - changelog_layout.addWidget(self.changelog_text) - - layout.addWidget(changelog_group) - - # Progress - self.progress_bar = QProgressBar() - self.progress_bar.setVisible(False) - layout.addWidget(self.progress_bar) - - self.progress_status = QLabel("") - layout.addWidget(self.progress_status) - - # Update button - self.update_btn = QPushButton("Download and Install Update") - self.update_btn.setStyleSheet(""" - QPushButton { - background-color: #4caf50; - color: white; - padding: 12px; - font-weight: bold; - border-radius: 4px; - } - QPushButton:hover { - background-color: #5cbf60; - } - QPushButton:disabled { - background-color: #555; - } - """) - self.update_btn.setEnabled(False) - self.update_btn.clicked.connect(self._start_update) - layout.addWidget(self.update_btn) - - # Settings - settings_group = QGroupBox("Settings") - settings_layout = QVBoxLayout(settings_group) - - self.startup_checkbox = QCheckBox("Check for updates on startup") - self.startup_checkbox.setChecked(self.check_on_startup) - self.startup_checkbox.toggled.connect(self._on_startup_changed) - settings_layout.addWidget(self.startup_checkbox) - - self.auto_checkbox = QCheckBox("Auto-install updates (not recommended)") - self.auto_checkbox.setChecked(self.auto_install) - self.auto_checkbox.toggled.connect(self._on_auto_changed) - settings_layout.addWidget(self.auto_checkbox) - - interval_layout = QHBoxLayout() - interval_layout.addWidget(QLabel("Check interval:")) - self.interval_combo = QComboBox() - self.interval_combo.addItems(["Every hour", "Every 6 hours", "Every 12 hours", "Daily", "Weekly"]) - self.interval_combo.setCurrentIndex(3) # Daily - self.interval_combo.currentIndexChanged.connect(self._on_interval_changed) - interval_layout.addWidget(self.interval_combo) - settings_layout.addLayout(interval_layout) - - layout.addWidget(settings_group) - - # Manual rollback - rollback_btn = QPushButton("Rollback to Previous Version") - rollback_btn.setStyleSheet("color: #ff9800;") - rollback_btn.clicked.connect(self._rollback_dialog) - layout.addWidget(rollback_btn) - - layout.addStretch() - - return widget - - def _check_for_updates(self): - """Check GitHub for updates.""" - self.status_label.setText("Status: Checking...") - self.status_label.setStyleSheet("color: #ff9800;") - - try: - # Query GitHub API - url = self.GITHUB_API_URL.format(self.GITHUB_REPO) - response = requests.get(url, timeout=30) - response.raise_for_status() - - self.latest_release = response.json() - self.latest_version = self.latest_release['tag_name'].lstrip('v') - - self.latest_version_label.setText(f"Latest Version: {self.latest_version}") - - # Parse changelog - changelog = self.latest_release.get('body', 'No changelog available.') - self.changelog_text.setText(changelog) - - # Compare versions - if self._version_compare(self.latest_version, self.current_version) > 0: - self.status_label.setText("Status: Update available!") - self.status_label.setStyleSheet("color: #4caf50; font-weight: bold;") - self.update_btn.setEnabled(True) - - self.notify_info( - "Update Available", - f"Version {self.latest_version} is available. Check the Auto Updater to install." - ) - else: - self.status_label.setText("Status: Up to date") - self.status_label.setStyleSheet("color: #4caf50;") - self.update_btn.setEnabled(False) - - except Exception as e: - self.status_label.setText(f"Status: Check failed") - self.status_label.setStyleSheet("color: #f44336;") - self.log_error(f"Update check failed: {e}") - - def _version_compare(self, v1, v2): - """Compare two version strings. Returns 1 if v1 > v2, -1 if v1 < v2, 0 if equal.""" - def normalize(v): - return [int(x) for x in v.split('.')] - - n1 = normalize(v1) - n2 = normalize(v2) - - for i in range(max(len(n1), len(n2))): - x1 = n1[i] if i < len(n1) else 0 - x2 = n2[i] if i < len(n2) else 0 - - if x1 > x2: - return 1 - elif x1 < x2: - return -1 - - return 0 - - def _start_update(self): - """Start the update process.""" - if not self.latest_release: - QMessageBox.warning(self.get_ui(), "No Update", "Please check for updates first.") - return - - # Get download URL - assets = self.latest_release.get('assets', []) - if not assets: - QMessageBox.critical(self.get_ui(), "Error", "No update package found.") - return - - download_url = assets[0]['browser_download_url'] - - # Confirm update - reply = QMessageBox.question( - self.get_ui(), - "Confirm Update", - f"This will update EU-Utility to version {self.latest_version}.\n\n" - "The application will need to restart after installation.\n\n" - "Continue?", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) - - if reply != QMessageBox.StandardButton.Yes: - return - - # Start update worker - install_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - backup_path = os.path.expanduser('~/.eu-utility/backups') - - self.worker = UpdateWorker(download_url, install_path, backup_path) - self.worker.progress.connect(self.progress_bar.setValue) - self.worker.status.connect(self.progress_status.setText) - self.worker.finished_signal.connect(self._on_update_finished) - - self.progress_bar.setVisible(True) - self.progress_bar.setValue(0) - self.update_btn.setEnabled(False) - - self.worker.start() - - def _on_update_finished(self, success, message): - """Handle update completion.""" - self.progress_bar.setVisible(False) - - if success: - QMessageBox.information( - self.get_ui(), - "Update Complete", - f"{message}\n\nClick OK to restart EU-Utility." - ) - self._restart_application() - else: - QMessageBox.critical( - self.get_ui(), - "Update Failed", - f"Update failed: {message}\n\nRollback was attempted." - ) - self.update_btn.setEnabled(True) - - def _restart_application(self): - """Restart the application.""" - python = sys.executable - script = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'core', 'main.py') - subprocess.Popen([python, script]) - sys.exit(0) - - def _rollback_dialog(self): - """Show rollback dialog.""" - backup_path = os.path.expanduser('~/.eu-utility/backups') - - if not os.path.exists(backup_path): - QMessageBox.information(self.get_ui(), "No Backups", "No backups found.") - return - - backups = sorted(os.listdir(backup_path)) - if not backups: - QMessageBox.information(self.get_ui(), "No Backups", "No backups found.") - return - - # Show simple rollback for now - reply = QMessageBox.question( - self.get_ui(), - "Confirm Rollback", - f"This will restore the most recent backup:\n{backups[-1]}\n\n" - "The application will restart. Continue?", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) - - if reply == QMessageBox.StandardButton.Yes: - try: - backup = os.path.join(backup_path, backups[-1]) - install_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - # Restore - if os.path.exists(install_path): - shutil.rmtree(install_path) - shutil.copytree(backup, install_path) - - QMessageBox.information( - self.get_ui(), - "Rollback Complete", - "Rollback successful. Click OK to restart." - ) - self._restart_application() - - except Exception as e: - QMessageBox.critical(self.get_ui(), "Rollback Failed", str(e)) - - def _on_startup_changed(self, checked): - """Handle startup check toggle.""" - self.check_on_startup = checked - self.save_data("check_on_startup", checked) - - def _on_auto_changed(self, checked): - """Handle auto-install toggle.""" - self.auto_install = checked - self.save_data("auto_install", checked) - - def _on_interval_changed(self, index): - """Handle check interval change.""" - intervals = [1, 6, 12, 24, 168] # hours - self.check_interval_hours = intervals[index] - self.save_data("check_interval", self.check_interval_hours) diff --git a/plugins/dashboard/__init__.py b/plugins/dashboard/__init__.py deleted file mode 100644 index 448dc14..0000000 --- a/plugins/dashboard/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Dashboard Plugin -""" - -from .plugin import DashboardPlugin - -__all__ = ["DashboardPlugin"] diff --git a/plugins/dashboard/__pycache__/__init__.cpython-312.pyc b/plugins/dashboard/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index eb94fc5..0000000 Binary files a/plugins/dashboard/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/plugins/dashboard/__pycache__/plugin.cpython-312.pyc b/plugins/dashboard/__pycache__/plugin.cpython-312.pyc deleted file mode 100644 index 2fa6db3..0000000 Binary files a/plugins/dashboard/__pycache__/plugin.cpython-312.pyc and /dev/null differ diff --git a/plugins/dashboard/plugin.py b/plugins/dashboard/plugin.py deleted file mode 100644 index ece6750..0000000 --- a/plugins/dashboard/plugin.py +++ /dev/null @@ -1,326 +0,0 @@ -""" -EU-Utility - Dashboard Plugin with Customizable Widgets - -Customizable start page with avatar statistics. -""" - -import json -from pathlib import Path -from datetime import datetime, timedelta - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QGridLayout, QFrame, QScrollArea, - QSizePolicy, QCheckBox, QDialog, QListWidget, - QListWidgetItem, QDialogButtonBox -) -from PyQt6.QtCore import Qt, QTimer -from PyQt6.QtGui import QColor, QFont - -from core.eu_styles import EU_COLORS -from plugins.base_plugin import BasePlugin - - -class DashboardPlugin(BasePlugin): - """Customizable dashboard with avatar statistics.""" - - name = "Dashboard" - version = "2.0.0" - author = "ImpulsiveFPS" - description = "Customizable start page with avatar stats" - hotkey = "ctrl+shift+home" - - # Available widgets - AVAILABLE_WIDGETS = { - 'ped_balance': {'name': 'PED Balance', 'icon': 'dollar-sign', 'default': True}, - 'skill_count': {'name': 'Skills Tracked', 'icon': 'trending-up', 'default': True}, - 'inventory_items': {'name': 'Inventory Items', 'icon': 'archive', 'default': True}, - 'current_dpp': {'name': 'Current DPP', 'icon': 'crosshair', 'default': True}, - 'total_gains_today': {'name': "Today's Skill Gains", 'icon': 'zap', 'default': True}, - 'professions_count': {'name': 'Professions', 'icon': 'award', 'default': False}, - 'missions_active': {'name': 'Active Missions', 'icon': 'map', 'default': False}, - 'codex_progress': {'name': 'Codex Progress', 'icon': 'book', 'default': False}, - 'globals_hofs': {'name': 'Globals/HOFs', 'icon': 'package', 'default': False}, - 'play_time': {'name': 'Session Time', 'icon': 'clock', 'default': False}, - } - - def initialize(self): - """Setup dashboard.""" - self.config_file = Path("data/dashboard_config.json") - self.config_file.parent.mkdir(parents=True, exist_ok=True) - - self.enabled_widgets = [] - self.widget_data = {} - - self._load_config() - self._load_data() - - # Auto-refresh timer - self.refresh_timer = QTimer() - self.refresh_timer.timeout.connect(self._refresh_data) - self.refresh_timer.start(5000) # Refresh every 5 seconds - - def _load_config(self): - """Load widget configuration.""" - if self.config_file.exists(): - try: - with open(self.config_file, 'r') as f: - config = json.load(f) - self.enabled_widgets = config.get('enabled', []) - except: - pass - - # Default: enable default widgets - if not self.enabled_widgets: - self.enabled_widgets = [ - k for k, v in self.AVAILABLE_WIDGETS.items() if v['default'] - ] - - def _save_config(self): - """Save widget configuration.""" - with open(self.config_file, 'w') as f: - json.dump({'enabled': self.enabled_widgets}, f) - - def _load_data(self): - """Load data from other plugins.""" - # Try to get data from other plugin files - data_dir = Path("data") - - # PED from inventory - inv_file = data_dir / "inventory.json" - if inv_file.exists(): - try: - with open(inv_file, 'r') as f: - data = json.load(f) - items = data.get('items', []) - total_tt = sum(item.get('tt', 0) for item in items) - self.widget_data['ped_balance'] = total_tt - except: - self.widget_data['ped_balance'] = 0 - - # Skills - skills_file = data_dir / "skill_tracker.json" - if skills_file.exists(): - try: - with open(skills_file, 'r') as f: - data = json.load(f) - self.widget_data['skill_count'] = len(data.get('skills', {})) - self.widget_data['total_gains_today'] = len([ - g for g in data.get('gains', []) - if datetime.fromisoformat(g['time']).date() == datetime.now().date() - ]) - except: - self.widget_data['skill_count'] = 0 - self.widget_data['total_gains_today'] = 0 - - # Inventory count - if inv_file.exists(): - try: - with open(inv_file, 'r') as f: - data = json.load(f) - self.widget_data['inventory_items'] = len(data.get('items', [])) - except: - self.widget_data['inventory_items'] = 0 - - # Professions - prof_file = data_dir / "professions.json" - if prof_file.exists(): - try: - with open(prof_file, 'r') as f: - data = json.load(f) - self.widget_data['professions_count'] = len(data.get('professions', {})) - except: - self.widget_data['professions_count'] = 0 - - def _refresh_data(self): - """Refresh widget data.""" - self._load_data() - if hasattr(self, 'widgets_container'): - self._update_widgets() - - def get_ui(self): - """Create dashboard UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Header with customize button - header = QHBoxLayout() - - title = QLabel("Dashboard") - title.setStyleSheet("font-size: 20px; font-weight: bold; color: white;") - header.addWidget(title) - - header.addStretch() - - customize_btn = QPushButton("Customize") - customize_btn.setStyleSheet(f""" - QPushButton {{ - background-color: {EU_COLORS['bg_secondary']}; - color: {EU_COLORS['text_secondary']}; - border: 1px solid {EU_COLORS['border_default']}; - border-radius: 4px; - padding: 8px 16px; - }} - QPushButton:hover {{ - background-color: {EU_COLORS['bg_hover']}; - border-color: {EU_COLORS['accent_orange']}; - }} - """) - customize_btn.clicked.connect(self._show_customize_dialog) - header.addWidget(customize_btn) - - layout.addLayout(header) - - # Scroll area for widgets - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setFrameShape(QFrame.Shape.NoFrame) - scroll.setStyleSheet("background: transparent; border: none;") - - self.widgets_container = QWidget() - self.widgets_layout = QGridLayout(self.widgets_container) - self.widgets_layout.setSpacing(15) - self.widgets_layout.setContentsMargins(0, 0, 0, 0) - - self._update_widgets() - - scroll.setWidget(self.widgets_container) - layout.addWidget(scroll) - - return widget - - def _update_widgets(self): - """Update widget display.""" - # Clear existing - while self.widgets_layout.count(): - item = self.widgets_layout.takeAt(0) - if item.widget(): - item.widget().deleteLater() - - # Add enabled widgets - col = 0 - row = 0 - for widget_id in self.enabled_widgets: - if widget_id in self.AVAILABLE_WIDGETS: - widget_info = self.AVAILABLE_WIDGETS[widget_id] - card = self._create_widget_card( - widget_id, - widget_info['name'], - widget_info['icon'] - ) - self.widgets_layout.addWidget(card, row, col) - - col += 1 - if col >= 2: # 2 columns - col = 0 - row += 1 - - def _create_widget_card(self, widget_id, name, icon_name): - """Create a stat widget card.""" - card = QFrame() - card.setStyleSheet(f""" - QFrame {{ - background-color: {EU_COLORS['bg_secondary']}; - border: 1px solid {EU_COLORS['border_default']}; - border-radius: 8px; - }} - """) - - layout = QVBoxLayout(card) - layout.setContentsMargins(15, 15, 15, 15) - layout.setSpacing(8) - - # Title - title = QLabel(name) - title.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 11px;") - layout.addWidget(title) - - # Value - value = self.widget_data.get(widget_id, 0) - - if widget_id == 'ped_balance': - value_text = f"{value:.2f} PED" - elif widget_id == 'play_time': - value_text = "2h 34m" # Placeholder - elif widget_id == 'current_dpp': - value_text = "3.45" - else: - value_text = str(value) - - value_label = QLabel(value_text) - value_label.setStyleSheet(f""" - color: {EU_COLORS['accent_orange']}; - font-size: 24px; - font-weight: bold; - """) - layout.addWidget(value_label) - - layout.addStretch() - return card - - def _show_customize_dialog(self): - """Show widget customization dialog.""" - dialog = QDialog() - dialog.setWindowTitle("Customize Dashboard") - dialog.setStyleSheet(f""" - QDialog {{ - background-color: {EU_COLORS['bg_secondary']}; - color: white; - }} - QLabel {{ - color: white; - }} - """) - - layout = QVBoxLayout(dialog) - - # Instructions - info = QLabel("Check widgets to display on dashboard:") - info.setStyleSheet(f"color: {EU_COLORS['text_secondary']};") - layout.addWidget(info) - - # Widget list - list_widget = QListWidget() - list_widget.setStyleSheet(f""" - QListWidget {{ - background-color: {EU_COLORS['bg_secondary']}; - color: white; - border: 1px solid {EU_COLORS['border_default']}; - }} - QListWidget::item {{ - padding: 10px; - }} - """) - - for widget_id, widget_info in self.AVAILABLE_WIDGETS.items(): - item = QListWidgetItem(widget_info['name']) - item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable) - item.setCheckState( - Qt.CheckState.Checked if widget_id in self.enabled_widgets - else Qt.CheckState.Unchecked - ) - item.setData(Qt.ItemDataRole.UserRole, widget_id) - list_widget.addItem(item) - - layout.addWidget(list_widget) - - # Buttons - buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel - ) - buttons.accepted.connect(dialog.accept) - buttons.rejected.connect(dialog.reject) - layout.addWidget(buttons) - - if dialog.exec() == QDialog.DialogCode.Accepted: - # Save selection - self.enabled_widgets = [] - for i in range(list_widget.count()): - item = list_widget.item(i) - if item.checkState() == Qt.CheckState.Checked: - self.enabled_widgets.append(item.data(Qt.ItemDataRole.UserRole)) - - self._save_config() - self._update_widgets() diff --git a/plugins/import_export/__init__.py b/plugins/import_export/__init__.py deleted file mode 100644 index e1ed8ef..0000000 --- a/plugins/import_export/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .plugin import ImportExportPlugin - -__all__ = ['ImportExportPlugin'] diff --git a/plugins/import_export/__pycache__/__init__.cpython-312.pyc b/plugins/import_export/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index eda85c6..0000000 Binary files a/plugins/import_export/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/plugins/import_export/__pycache__/plugin.cpython-312.pyc b/plugins/import_export/__pycache__/plugin.cpython-312.pyc deleted file mode 100644 index 5636354..0000000 Binary files a/plugins/import_export/__pycache__/plugin.cpython-312.pyc and /dev/null differ diff --git a/plugins/import_export/plugin.py b/plugins/import_export/plugin.py deleted file mode 100644 index ef72401..0000000 --- a/plugins/import_export/plugin.py +++ /dev/null @@ -1,333 +0,0 @@ -# Description: Universal Import/Export tool for EU-Utility -# Author: LemonNexus -# Version: 1.0.0 - -""" -Universal Import/Export tool for EU-Utility. -Export and import all plugin data for backup and migration. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QFileDialog, QProgressBar, - QListWidget, QListWidgetItem, QMessageBox, QComboBox -) -from PyQt6.QtCore import Qt, QThread, pyqtSignal -import json -import zipfile -import os -from datetime import datetime -from plugins.base_plugin import BasePlugin - - -class ExportImportWorker(QThread): - """Background worker for export/import operations.""" - progress = pyqtSignal(int) - status = pyqtSignal(str) - finished_signal = pyqtSignal(bool, str) - - def __init__(self, operation, data_store, path, selected_plugins=None): - super().__init__() - self.operation = operation - self.data_store = data_store - self.path = path - self.selected_plugins = selected_plugins or [] - - def run(self): - try: - if self.operation == "export": - self._do_export() - else: - self._do_import() - except Exception as e: - self.finished_signal.emit(False, str(e)) - - def _do_export(self): - """Export data to zip file.""" - self.status.emit("Gathering plugin data...") - - # Get all plugin data - all_data = {} - plugin_keys = self.data_store.get_all_keys("*") - - total = len(plugin_keys) - for i, key in enumerate(plugin_keys): - # Check if filtered - plugin_name = key.split('.')[0] if '.' in key else key - if self.selected_plugins and plugin_name not in self.selected_plugins: - continue - - data = self.data_store.load(key, None) - if data is not None: - all_data[key] = data - - progress = int((i + 1) / total * 50) - self.progress.emit(progress) - - self.status.emit("Creating export archive...") - - # Create zip file - with zipfile.ZipFile(self.path, 'w', zipfile.ZIP_DEFLATED) as zf: - # Add metadata - metadata = { - 'version': '2.0.0', - 'exported_at': datetime.now().isoformat(), - 'plugin_count': len(set(k.split('.')[0] for k in all_data.keys())), - } - zf.writestr('metadata.json', json.dumps(metadata, indent=2)) - - # Add data - zf.writestr('data.json', json.dumps(all_data, indent=2)) - - self.progress.emit(75) - - self.progress.emit(100) - self.finished_signal.emit(True, f"Exported {len(all_data)} data items") - - def _do_import(self): - """Import data from zip file.""" - self.status.emit("Reading archive...") - - with zipfile.ZipFile(self.path, 'r') as zf: - # Verify metadata - if 'metadata.json' not in zf.namelist(): - raise ValueError("Invalid export file: missing metadata") - - metadata = json.loads(zf.read('metadata.json')) - self.status.emit(f"Importing from version {metadata.get('version', 'unknown')}...") - - # Load data - data = json.loads(zf.read('data.json')) - self.progress.emit(25) - - # Import - total = len(data) - for i, (key, value) in enumerate(data.items()): - # Check if filtered - plugin_name = key.split('.')[0] if '.' in key else key - if self.selected_plugins and plugin_name not in self.selected_plugins: - continue - - self.data_store.save_raw(key, value) - progress = 25 + int((i + 1) / total * 75) - self.progress.emit(progress) - - self.finished_signal.emit(True, f"Imported {len(data)} data items") - - -class ImportExportPlugin(BasePlugin): - """Universal import/export tool for EU-Utility data.""" - - name = "Import/Export" - version = "1.0.0" - author = "LemonNexus" - description = "Backup and restore all plugin data" - icon = "archive" - - def initialize(self): - """Initialize plugin.""" - self.worker = None - self.default_export_dir = os.path.expanduser("~/Documents/EU-Utility/Backups") - os.makedirs(self.default_export_dir, exist_ok=True) - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(15) - - # Title - title = QLabel("Import / Export Data") - title.setStyleSheet("font-size: 18px; font-weight: bold;") - layout.addWidget(title) - - # Description - desc = QLabel("Backup and restore all your EU-Utility data") - desc.setStyleSheet("color: rgba(255,255,255,150);") - layout.addWidget(desc) - - # Plugin selection - layout.addWidget(QLabel("Select plugins to export/import:")) - self.plugin_list = QListWidget() - self.plugin_list.setSelectionMode(QListWidget.SelectionMode.MultiSelection) - self._populate_plugin_list() - layout.addWidget(self.plugin_list) - - # Select all/none buttons - select_layout = QHBoxLayout() - select_all_btn = QPushButton("Select All") - select_all_btn.clicked.connect(lambda: self.plugin_list.selectAll()) - select_layout.addWidget(select_all_btn) - - select_none_btn = QPushButton("Select None") - select_none_btn.clicked.connect(lambda: self.plugin_list.clearSelection()) - select_layout.addWidget(select_none_btn) - layout.addLayout(select_layout) - - # Progress - self.progress_bar = QProgressBar() - self.progress_bar.setVisible(False) - layout.addWidget(self.progress_bar) - - self.status_label = QLabel("") - self.status_label.setStyleSheet("color: #4a9eff;") - layout.addWidget(self.status_label) - - # Export/Import buttons - btn_layout = QHBoxLayout() - - export_btn = QPushButton("Export to File...") - export_btn.setStyleSheet(""" - QPushButton { - background-color: #4caf50; - color: white; - padding: 10px; - font-weight: bold; - } - """) - export_btn.clicked.connect(self._export) - btn_layout.addWidget(export_btn) - - import_btn = QPushButton("Import from File...") - import_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 10px; - font-weight: bold; - } - """) - import_btn.clicked.connect(self._import) - btn_layout.addWidget(import_btn) - - layout.addLayout(btn_layout) - - # Info - info = QLabel( - "Exports include:\n" - "- All plugin settings\n" - "- Session history\n" - "- Price alerts\n" - "- Custom configurations\n\n" - "Files are saved as .zip archives with JSON data." - ) - info.setStyleSheet("color: rgba(255,255,255,150); font-size: 11px;") - layout.addWidget(info) - - layout.addStretch() - return widget - - def _populate_plugin_list(self): - """Populate the plugin list.""" - # Get available plugins - plugins = [ - "loot_tracker", - "skill_scanner", - "price_alerts", - "session_exporter", - "mining_helper", - "mission_tracker", - "codex_tracker", - "auction_tracker", - "settings" - ] - - for plugin in plugins: - item = QListWidgetItem(plugin.replace('_', ' ').title()) - item.setData(Qt.ItemDataRole.UserRole, plugin) - self.plugin_list.addItem(item) - - def _get_selected_plugins(self): - """Get list of selected plugin names.""" - selected = [] - for item in self.plugin_list.selectedItems(): - selected.append(item.data(Qt.ItemDataRole.UserRole)) - return selected - - def _export(self): - """Export data to file.""" - selected = self._get_selected_plugins() - if not selected: - QMessageBox.warning(self.get_ui(), "No Selection", "Please select at least one plugin to export.") - return - - # Get save location - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - default_name = f"eu-utility-backup-{timestamp}.zip" - - path, _ = QFileDialog.getSaveFileName( - self.get_ui(), - "Export Data", - os.path.join(self.default_export_dir, default_name), - "Zip Files (*.zip)" - ) - - if not path: - return - - # Start export - self._start_worker("export", path, selected) - - def _import(self): - """Import data from file.""" - selected = self._get_selected_plugins() - if not selected: - QMessageBox.warning(self.get_ui(), "No Selection", "Please select at least one plugin to import.") - return - - # Confirm import - reply = QMessageBox.question( - self.get_ui(), - "Confirm Import", - "This will overwrite existing data for selected plugins. Continue?", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No - ) - - if reply != QMessageBox.StandardButton.Yes: - return - - # Get file location - path, _ = QFileDialog.getOpenFileName( - self.get_ui(), - "Import Data", - self.default_export_dir, - "Zip Files (*.zip)" - ) - - if not path: - return - - # Start import - self._start_worker("import", path, selected) - - def _start_worker(self, operation, path, selected): - """Start background worker.""" - # Show progress - self.progress_bar.setVisible(True) - self.progress_bar.setValue(0) - - # Get data store from API - data_store = self.api.services.get('data_store') if self.api else None - if not data_store: - QMessageBox.critical(self.get_ui(), "Error", "Data store not available") - return - - # Create and start worker - self.worker = ExportImportWorker(operation, data_store, path, selected) - self.worker.progress.connect(self.progress_bar.setValue) - self.worker.status.connect(self.status_label.setText) - self.worker.finished_signal.connect(self._on_worker_finished) - self.worker.start() - - def _on_worker_finished(self, success, message): - """Handle worker completion.""" - self.progress_bar.setVisible(False) - - if success: - self.status_label.setText(f"✓ {message}") - self.status_label.setStyleSheet("color: #4caf50;") - QMessageBox.information(self.get_ui(), "Success", message) - else: - self.status_label.setText(f"✗ {message}") - self.status_label.setStyleSheet("color: #f44336;") - QMessageBox.critical(self.get_ui(), "Error", message) 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/__pycache__/__init__.cpython-312.pyc b/plugins/plugin_store_ui/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 2f812d3..0000000 Binary files a/plugins/plugin_store_ui/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/plugins/plugin_store_ui/__pycache__/plugin.cpython-312.pyc b/plugins/plugin_store_ui/__pycache__/plugin.cpython-312.pyc deleted file mode 100644 index 7f0c6d4..0000000 Binary files a/plugins/plugin_store_ui/__pycache__/plugin.cpython-312.pyc and /dev/null differ diff --git a/plugins/plugin_store_ui/plugin.py b/plugins/plugin_store_ui/plugin.py deleted file mode 100644 index 6a92ae6..0000000 --- a/plugins/plugin_store_ui/plugin.py +++ /dev/null @@ -1,273 +0,0 @@ -""" -EU-Utility - Plugin Store UI Plugin - -Browse and install community plugins. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QLineEdit, QListWidget, QListWidgetItem, - QProgressBar, QFrame, QTextEdit -) -from PyQt6.QtCore import Qt, QThread, pyqtSignal - -from plugins.base_plugin import BasePlugin - - -class PluginStoreUIPlugin(BasePlugin): - """Browse and install community plugins.""" - - name = "Plugin Store" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Community plugin marketplace" - hotkey = "ctrl+shift+slash" - - def initialize(self): - """Setup plugin store.""" - self.available_plugins = [] - self.installed_plugins = [] - self.is_loading = False - - def get_ui(self): - """Create plugin store UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("🛒 Plugin Store") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Search - search_layout = QHBoxLayout() - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Search plugins...") - self.search_input.setStyleSheet(""" - QLineEdit { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 4px; - padding: 8px; - } - """) - search_layout.addWidget(self.search_input) - - search_btn = QPushButton("🔍") - search_btn.setFixedSize(32, 32) - search_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - border: none; - border-radius: 4px; - } - """) - search_btn.clicked.connect(self._search_plugins) - search_layout.addWidget(search_btn) - - layout.addLayout(search_layout) - - # Categories - cats_layout = QHBoxLayout() - for cat in ["All", "Hunting", "Mining", "Crafting", "Tools", "Social"]: - btn = QPushButton(cat) - btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255,255,255,15); - color: white; - border: none; - border-radius: 4px; - padding: 5px 10px; - } - QPushButton:hover { - background-color: rgba(255,255,255,30); - } - """) - cats_layout.addWidget(btn) - cats_layout.addStretch() - layout.addLayout(cats_layout) - - # Plugins list - self.plugins_list = QListWidget() - self.plugins_list.setStyleSheet(""" - QListWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - QListWidget::item { - padding: 12px; - border-bottom: 1px solid rgba(100, 110, 130, 40); - } - QListWidget::item:hover { - background-color: rgba(255, 255, 255, 10); - } - """) - self.plugins_list.itemClicked.connect(self._show_plugin_details) - layout.addWidget(self.plugins_list) - - # Sample plugins - self._load_sample_plugins() - - # Details panel - self.details_panel = QFrame() - self.details_panel.setStyleSheet(""" - QFrame { - background-color: rgba(30, 35, 45, 200); - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - """) - details_layout = QVBoxLayout(self.details_panel) - - self.detail_name = QLabel("Select a plugin") - self.detail_name.setStyleSheet("color: #ff8c42; font-size: 14px; font-weight: bold;") - details_layout.addWidget(self.detail_name) - - self.detail_desc = QLabel("") - self.detail_desc.setStyleSheet("color: rgba(255,255,255,150);") - self.detail_desc.setWordWrap(True) - details_layout.addWidget(self.detail_desc) - - self.detail_author = QLabel("") - self.detail_author.setStyleSheet("color: rgba(255,255,255,100); font-size: 10px;") - details_layout.addWidget(self.detail_author) - - self.install_btn = QPushButton("Install") - self.install_btn.setStyleSheet(""" - QPushButton { - background-color: #4caf50; - color: white; - padding: 10px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - self.install_btn.clicked.connect(self._install_plugin) - self.install_btn.setEnabled(False) - details_layout.addWidget(self.install_btn) - - layout.addWidget(self.details_panel) - - # Refresh button - refresh_btn = QPushButton("🔄 Refresh") - refresh_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255,255,255,20); - color: white; - padding: 8px; - border: none; - border-radius: 4px; - } - """) - refresh_btn.clicked.connect(self._refresh_store) - layout.addWidget(refresh_btn) - - layout.addStretch() - 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() 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/__pycache__/__init__.cpython-312.pyc b/plugins/settings/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 03633eb..0000000 Binary files a/plugins/settings/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/plugins/settings/__pycache__/plugin.cpython-312.pyc b/plugins/settings/__pycache__/plugin.cpython-312.pyc deleted file mode 100644 index 9595ddf..0000000 Binary files a/plugins/settings/__pycache__/plugin.cpython-312.pyc and /dev/null differ diff --git a/plugins/settings/plugin.py b/plugins/settings/plugin.py deleted file mode 100644 index 59659ac..0000000 --- a/plugins/settings/plugin.py +++ /dev/null @@ -1,1174 +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 plugins.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") - - # Plugins tab - plugins_tab = self._create_plugins_tab() - tabs.addTab(plugins_tab, "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_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") diff --git a/plugins/universal_search/__init__.py b/plugins/universal_search/__init__.py deleted file mode 100644 index 44d5b53..0000000 --- a/plugins/universal_search/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Universal Search Plugin for EU-Utility -""" - -from .plugin import UniversalSearchPlugin - -__all__ = ["UniversalSearchPlugin"] diff --git a/plugins/universal_search/__pycache__/__init__.cpython-312.pyc b/plugins/universal_search/__pycache__/__init__.cpython-312.pyc deleted file mode 100644 index 8befb4f..0000000 Binary files a/plugins/universal_search/__pycache__/__init__.cpython-312.pyc and /dev/null differ diff --git a/plugins/universal_search/__pycache__/plugin.cpython-312.pyc b/plugins/universal_search/__pycache__/plugin.cpython-312.pyc deleted file mode 100644 index cafa0f9..0000000 Binary files a/plugins/universal_search/__pycache__/plugin.cpython-312.pyc and /dev/null differ diff --git a/plugins/universal_search/plugin.py b/plugins/universal_search/plugin.py deleted file mode 100644 index 31f0f0c..0000000 --- a/plugins/universal_search/plugin.py +++ /dev/null @@ -1,600 +0,0 @@ -""" -EU-Utility - Universal Search Plugin - -Search across all Entropia Nexus entities - items, mobs, locations, blueprints, skills, etc. -""" - -import json -import webbrowser -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, - QLineEdit, QPushButton, QLabel, QComboBox, - QTableWidget, QTableWidgetItem, QHeaderView, - QTabWidget, QStackedWidget, QFrame -) -from PyQt6.QtCore import Qt, QThread, pyqtSignal - -from plugins.base_plugin import BasePlugin - - -class NexusEntityAPI: - """Client for Entropia Nexus Entity API.""" - - BASE_URL = "https://api.entropianexus.com" - - # Entity type to API endpoint mapping - ENDPOINTS = { - "Items": "/items", - "Weapons": "/weapons", - "Armors": "/armors", - "Blueprints": "/blueprints", - "Mobs": "/mobs", - "Locations": "/locations", - "Skills": "/skills", - "Materials": "/materials", - "Enhancers": "/enhancers", - "Medical Tools": "/medicaltools", - "Finders": "/finders", - "Excavators": "/excavators", - "Refiners": "/refiners", - "Vehicles": "/vehicles", - "Pets": "/pets", - "Decorations": "/decorations", - "Furniture": "/furniture", - "Storage": "/storagecontainers", - "Strongboxes": "/strongboxes", - "Teleporters": "/teleporters", - "Shops": "/shops", - "Vendors": "/vendors", - "Planets": "/planets", - "Areas": "/areas", - } - - @classmethod - def search_entities(cls, entity_type, query, limit=50, http_get_func=None): - """Search for entities of a specific type.""" - try: - endpoint = cls.ENDPOINTS.get(entity_type, "/items") - - # Build URL with query params - params = {'q': query, 'limit': limit, 'fuzzy': 'true'} - query_string = '&'.join(f"{k}={v}" for k, v in params.items()) - url = f"{cls.BASE_URL}{endpoint}?{query_string}" - - if http_get_func: - response = http_get_func( - url, - cache_ttl=300, # 5 minute cache - headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'} - ) - else: - # Fallback for standalone usage - import urllib.request - req = urllib.request.Request( - url, - headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'} - ) - with urllib.request.urlopen(req, timeout=15) as resp: - response = {'json': json.loads(resp.read().decode('utf-8'))} - - data = response.get('json') if response else None - return data if isinstance(data, list) else [] - - except Exception as e: - print(f"API Error ({entity_type}): {e}") - return [] - - @classmethod - def universal_search(cls, query, limit=30, http_get_func=None): - """Universal search across all entity types.""" - try: - params = {'query': query, 'limit': limit, 'fuzzy': 'true'} - query_string = '&'.join(f"{k}={v}" for k, v in params.items()) - url = f"{cls.BASE_URL}/search?{query_string}" - - if http_get_func: - response = http_get_func( - url, - cache_ttl=300, # 5 minute cache - headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'} - ) - else: - # Fallback for standalone usage - import urllib.request - req = urllib.request.Request( - url, - headers={'Accept': 'application/json', 'User-Agent': 'EU-Utility/1.0'} - ) - with urllib.request.urlopen(req, timeout=15) as resp: - response = {'json': json.loads(resp.read().decode('utf-8'))} - - data = response.get('json') if response else None - return data if isinstance(data, list) else [] - - except Exception as e: - print(f"Universal Search Error: {e}") - return [] - - @classmethod - def get_entity_url(cls, entity_type, entity_id_or_name): - """Get the web URL for an entity.""" - web_base = "https://www.entropianexus.com" - - # Map to web paths - web_paths = { - "Items": "items", - "Weapons": "items", - "Armors": "items", - "Blueprints": "blueprints", - "Mobs": "mobs", - "Locations": "locations", - "Skills": "skills", - "Materials": "items", - "Enhancers": "items", - "Medical Tools": "items", - "Finders": "items", - "Excavators": "items", - "Refiners": "items", - "Vehicles": "items", - "Pets": "items", - "Decorations": "items", - "Furniture": "items", - "Storage": "items", - "Strongboxes": "items", - "Teleporters": "locations", - "Shops": "locations", - "Vendors": "locations", - "Planets": "locations", - "Areas": "locations", - } - - path = web_paths.get(entity_type, "items") - return f"{web_base}/{path}/{entity_id_or_name}" - - -class UniversalSearchThread(QThread): - """Background thread for API searches.""" - results_ready = pyqtSignal(list, str) - error_occurred = pyqtSignal(str) - - def __init__(self, query, entity_type, universal=False, http_get_func=None): - super().__init__() - self.query = query - self.entity_type = entity_type - self.universal = universal - self.http_get_func = http_get_func - - def run(self): - """Perform API search.""" - try: - if self.universal: - results = NexusEntityAPI.universal_search(self.query, http_get_func=self.http_get_func) - else: - results = NexusEntityAPI.search_entities(self.entity_type, self.query, http_get_func=self.http_get_func) - - self.results_ready.emit(results, self.entity_type) - - except Exception as e: - self.error_occurred.emit(str(e)) - - -class UniversalSearchPlugin(BasePlugin): - """Universal search across all Nexus entities.""" - - name = "Universal Search" - version = "2.0.0" - author = "ImpulsiveFPS" - description = "Search items, mobs, locations, blueprints, skills, and more" - hotkey = "ctrl+shift+f" # F for Find - - def initialize(self): - """Setup the plugin.""" - self.search_thread = None - self.current_results = [] - self.current_entity_type = "Universal" - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(10) - - # Title - NO EMOJI - title = QLabel("Universal Search") - title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;") - layout.addWidget(title) - - # Search mode selector - mode_layout = QHBoxLayout() - mode_layout.addWidget(QLabel("Mode:")) - - self.search_mode = QComboBox() - self.search_mode.addItem("Universal (All Types)", "Universal") - self.search_mode.addItem("──────────────────", "separator") - - # Add all entity types - entity_types = [ - "Items", - "Weapons", - "Armors", - "Blueprints", - "Mobs", - "Locations", - "Skills", - "Materials", - "Enhancers", - "Medical Tools", - "Finders", - "Excavators", - "Refiners", - "Vehicles", - "Pets", - "Decorations", - "Furniture", - "Storage", - "Strongboxes", - "Teleporters", - "Shops", - "Vendors", - "Planets", - "Areas", - ] - - for etype in entity_types: - self.search_mode.addItem(f" {etype}", etype) - - self.search_mode.setStyleSheet(""" - QComboBox { - background-color: #444; - color: white; - padding: 8px; - border-radius: 4px; - min-width: 200px; - } - QComboBox::drop-down { - border: none; - } - """) - self.search_mode.currentIndexChanged.connect(self._on_mode_changed) - mode_layout.addWidget(self.search_mode) - mode_layout.addStretch() - - layout.addLayout(mode_layout) - - # Search bar - search_layout = QHBoxLayout() - - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Search for anything... (e.g., 'ArMatrix', 'Argonaut', 'Calypso')") - self.search_input.setStyleSheet(""" - QLineEdit { - background-color: #333; - color: white; - padding: 10px; - border: 2px solid #555; - border-radius: 4px; - font-size: 14px; - } - QLineEdit:focus { - border-color: #4a9eff; - } - """) - self.search_input.returnPressed.connect(self._do_search) - search_layout.addWidget(self.search_input, 1) - - search_btn = QPushButton("Search") - search_btn.setStyleSheet(""" - QPushButton { - background-color: #4a9eff; - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - font-weight: bold; - font-size: 13px; - } - QPushButton:hover { - background-color: #5aafff; - } - """) - search_btn.clicked.connect(self._do_search) - search_layout.addWidget(search_btn) - - layout.addLayout(search_layout) - - # Status - self.status_label = QLabel("Ready to search") - self.status_label.setStyleSheet("color: #666; font-size: 11px;") - layout.addWidget(self.status_label) - - # Results table - self.results_table = QTableWidget() - self.results_table.setColumnCount(4) - self.results_table.setHorizontalHeaderLabels(["Name", "Type", "Details", "ID"]) - self.results_table.horizontalHeader().setStretchLastSection(False) - self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) - self.results_table.horizontalHeader().setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) - self.results_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) - self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) - self.results_table.setColumnWidth(1, 120) - self.results_table.setColumnWidth(3, 60) - self.results_table.verticalHeader().setVisible(False) - self.results_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) - self.results_table.setStyleSheet(""" - QTableWidget { - background-color: #2a2a2a; - color: white; - border: 1px solid #444; - border-radius: 4px; - gridline-color: #333; - } - QTableWidget::item { - padding: 10px; - border-bottom: 1px solid #333; - } - QTableWidget::item:selected { - background-color: #4a9eff; - } - QTableWidget::item:hover { - background-color: #3a3a3a; - } - QHeaderView::section { - background-color: #333; - color: #aaa; - padding: 10px; - border: none; - font-weight: bold; - } - """) - self.results_table.cellDoubleClicked.connect(self._on_item_double_clicked) - self.results_table.setMaximumHeight(350) - self.results_table.setMinimumHeight(200) - layout.addWidget(self.results_table) - - # Action buttons - action_layout = QHBoxLayout() - - self.open_btn = QPushButton("Open Selected") - self.open_btn.setEnabled(False) - self.open_btn.setStyleSheet(""" - QPushButton { - background-color: #4a9eff; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - } - QPushButton:hover { - background-color: #5aafff; - } - QPushButton:disabled { - background-color: #444; - color: #666; - } - """) - self.open_btn.clicked.connect(self._open_selected) - action_layout.addWidget(self.open_btn) - - action_layout.addStretch() - - # Quick category buttons - quick_label = QLabel("Quick:") - quick_label.setStyleSheet("color: #666;") - action_layout.addWidget(quick_label) - - for category in ["Items", "Mobs", "Blueprints", "Locations"]: - btn = QPushButton(category) - btn.setStyleSheet(""" - QPushButton { - background-color: transparent; - color: #4a9eff; - border: 1px solid #4a9eff; - padding: 5px 10px; - border-radius: 3px; - } - QPushButton:hover { - background-color: #4a9eff; - color: white; - } - """) - btn.clicked.connect(lambda checked, c=category: self._quick_search(c)) - action_layout.addWidget(btn) - - layout.addLayout(action_layout) - - # Tips - tips = QLabel("Tip: Double-click result to open on Nexus website") - tips.setStyleSheet("color: #555; font-size: 10px;") - layout.addWidget(tips) - - layout.addStretch() - - return widget - - def _on_mode_changed(self): - """Handle search mode change.""" - data = self.search_mode.currentData() - if data == "separator": - # Reset to previous valid selection - self.search_mode.setCurrentIndex(0) - - def _do_search(self): - """Perform search.""" - query = self.search_input.text().strip() - if len(query) < 2: - self.status_label.setText("Enter at least 2 characters") - return - - entity_type = self.search_mode.currentData() - if entity_type == "separator": - entity_type = "Universal" - - self.current_entity_type = entity_type - universal = (entity_type == "Universal") - - # Clear previous results - self.results_table.setRowCount(0) - self.current_results = [] - self.open_btn.setEnabled(False) - self.status_label.setText(f"Searching for '{query}'...") - - # Start search thread with http_get function - self.search_thread = UniversalSearchThread( - query, entity_type, universal, - http_get_func=self.http_get - ) - self.search_thread.results_ready.connect(self._on_results) - self.search_thread.error_occurred.connect(self._on_error) - self.search_thread.start() - - def _quick_search(self, category): - """Quick search for a specific category.""" - # Set the category - index = self.search_mode.findData(category) - if index >= 0: - self.search_mode.setCurrentIndex(index) - - # If there's text in the search box, search immediately - if self.search_input.text().strip(): - self._do_search() - else: - self.search_input.setFocus() - self.status_label.setText(f"Selected: {category} - Enter search term") - - def _on_results(self, results, entity_type): - """Handle search results.""" - self.current_results = results - - if not results: - self.status_label.setText("No results found") - return - - # Populate table - self.results_table.setRowCount(len(results)) - - for row, item in enumerate(results): - # Extract data based on available fields - name = item.get('name', item.get('Name', 'Unknown')) - item_id = str(item.get('id', item.get('Id', ''))) - - # Determine type - if 'type' in item: - item_type = item['type'] - elif entity_type != "Universal": - item_type = entity_type - else: - # Try to infer from other fields - item_type = self._infer_type(item) - - # Build details string - details = self._build_details(item, item_type) - - # Set table items - self.results_table.setItem(row, 0, QTableWidgetItem(name)) - self.results_table.setItem(row, 1, QTableWidgetItem(item_type)) - self.results_table.setItem(row, 2, QTableWidgetItem(details)) - self.results_table.setItem(row, 3, QTableWidgetItem(item_id)) - - self.open_btn.setEnabled(True) - self.status_label.setText(f"Found {len(results)} results") - - def _infer_type(self, item): - """Infer entity type from item fields.""" - if 'damage' in item or 'range' in item: - return "Weapon" - elif 'protection' in item or 'durability' in item: - return "Armor" - elif 'hitpoints' in item: - return "Mob" - elif 'x' in item and 'y' in item: - return "Location" - elif 'qr' in item or 'click' in item: - return "Blueprint" - elif 'category' in item: - return item['category'] - else: - return "Item" - - def _build_details(self, item, item_type): - """Build details string based on item type.""" - details = [] - - if item_type in ["Weapon", "Weapons"]: - if 'damage' in item: - details.append(f"Dmg: {item['damage']}") - if 'range' in item: - details.append(f"Range: {item['range']}m") - if 'attacks' in item: - details.append(f"{item['attacks']} attacks") - - elif item_type in ["Armor", "Armors"]: - if 'protection' in item: - details.append(f"Prot: {item['protection']}") - if 'durability' in item: - details.append(f"Dur: {item['durability']}") - - elif item_type in ["Mob", "Mobs"]: - if 'hitpoints' in item: - details.append(f"HP: {item['hitpoints']}") - if 'damage' in item: - details.append(f"Dmg: {item['damage']}") - if 'threat' in item: - details.append(f"Threat: {item['threat']}") - - elif item_type in ["Blueprint", "Blueprints"]: - if 'qr' in item: - details.append(f"QR: {item['qr']}") - if 'click' in item: - details.append(f"Clicks: {item['click']}") - - elif item_type in ["Location", "Locations", "Teleporter", "Shop"]: - if 'planet' in item: - details.append(item['planet']) - if 'x' in item and 'y' in item: - details.append(f"[{item['x']}, {item['y']}]") - - elif item_type in ["Skill", "Skills"]: - if 'category' in item: - details.append(item['category']) - - # Add any other interesting fields - if 'level' in item: - details.append(f"Lvl: {item['level']}") - if 'weight' in item: - details.append(f"{item['weight']}kg") - - return " | ".join(details) if details else "" - - def _on_error(self, error): - """Handle search error.""" - self.status_label.setText(f"Error: {error}") - - def _on_item_double_clicked(self, row, column): - """Handle item double-click.""" - self._open_result(row) - - def _open_selected(self): - """Open selected result.""" - selected = self.results_table.selectedItems() - if selected: - row = selected[0].row() - self._open_result(row) - - def _open_result(self, row): - """Open result in browser.""" - if row < len(self.current_results): - item = self.current_results[row] - entity_id = item.get('id', item.get('Id', '')) - entity_name = item.get('name', item.get('Name', '')) - - # Use name for URL if available, otherwise ID - url_param = entity_name if entity_name else str(entity_id) - url = NexusEntityAPI.get_entity_url(self.current_entity_type, url_param) - - webbrowser.open(url) - - def on_hotkey(self): - """Focus search when hotkey pressed.""" - if hasattr(self, 'search_input'): - self.search_input.setFocus() - self.search_input.selectAll()