From 7d13dd1a298132f9fd6db68db386c4d25c949f7a Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Sun, 15 Feb 2026 01:53:09 +0000 Subject: [PATCH] cleanup: Remove all plugins moved to separate repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All user-facing plugins have been moved to: https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo REMOVED FROM CORE (30+ plugins): - analytics, auction_tracker, auto_screenshot, auto_updater - calculator, chat_logger, codex_tracker, crafting_calc - dashboard, discord_presence, dpp_calculator, enhancer_calc - event_bus_example, game_reader, game_reader_test, global_tracker - import_export, inventory_manager, log_parser_test, loot_tracker - mining_helper, mission_tracker, nexus_search, price_alerts - profession_scanner, session_exporter, skill_scanner - spotify_controller, tp_runner, universal_search ALSO REMOVED: - plugins/base_plugin.py (was duplicate, should be in package root) - plugins/__pycache__ (shouldn't be in git) REMAINING IN CORE: - plugins/__init__.py - plugins/settings/ (essential for configuration) - plugins/plugin_store_ui/ (essential for plugin installation) EU-Utility is now a pure framework. Users install plugins via Settings โ†’ Plugin Store or manually to the plugins/ folder. This separation enables: - Independent plugin development - Modular installation (only what you need) - Community contributions via plugin repo - Cleaner core codebase focused on framework --- plugins/analytics/__init__.py | 3 - plugins/analytics/plugin.py | 525 ---------- plugins/auction_tracker/__init__.py | 7 - plugins/auction_tracker/plugin.py | 261 ----- plugins/auto_screenshot/__init__.py | 10 - plugins/auto_screenshot/plugin.py | 735 -------------- plugins/auto_updater/__init__.py | 3 - plugins/auto_updater/plugin.py | 481 --------- plugins/base_plugin.py | 1193 ----------------------- plugins/calculator/__init__.py | 7 - plugins/calculator/plugin.py | 386 -------- plugins/chat_logger/__init__.py | 7 - plugins/chat_logger/plugin.py | 285 ------ plugins/codex_tracker/__init__.py | 7 - plugins/codex_tracker/plugin.py | 218 ----- plugins/crafting_calc/__init__.py | 7 - plugins/crafting_calc/plugin.py | 219 ----- plugins/dashboard/__init__.py | 7 - plugins/dashboard/plugin.py | 326 ------- plugins/discord_presence/__init__.py | 3 - plugins/discord_presence/plugin.py | 217 ----- plugins/dpp_calculator/__init__.py | 7 - plugins/dpp_calculator/plugin.py | 230 ----- plugins/enhancer_calc/__init__.py | 7 - plugins/enhancer_calc/plugin.py | 160 --- plugins/event_bus_example/__init__.py | 4 - plugins/event_bus_example/plugin.py | 211 ---- plugins/game_reader/__init__.py | 7 - plugins/game_reader/plugin.py | 261 ----- plugins/game_reader_test/__init__.py | 2 - plugins/game_reader_test/plugin.py | 1197 ----------------------- plugins/global_tracker/__init__.py | 7 - plugins/global_tracker/plugin.py | 257 ----- plugins/import_export/__init__.py | 3 - plugins/import_export/plugin.py | 333 ------- plugins/inventory_manager/__init__.py | 7 - plugins/inventory_manager/plugin.py | 219 ----- plugins/log_parser_test/__init__.py | 2 - plugins/log_parser_test/plugin.py | 384 -------- plugins/loot_tracker/__init__.py | 7 - plugins/loot_tracker/plugin.py | 224 ----- plugins/mining_helper/__init__.py | 7 - plugins/mining_helper/plugin.py | 273 ------ plugins/mission_tracker/__init__.py | 7 - plugins/mission_tracker/plugin.py | 315 ------ plugins/nexus_search/__init__.py | 7 - plugins/nexus_search/plugin.py | 442 --------- plugins/price_alerts/__init__.py | 10 - plugins/price_alerts/plugin.py | 693 ------------- plugins/profession_scanner/__init__.py | 7 - plugins/profession_scanner/plugin.py | 247 ----- plugins/session_exporter/__init__.py | 9 - plugins/session_exporter/plugin.py | 643 ------------ plugins/skill_scanner/__init__.py | 7 - plugins/skill_scanner/plugin.py | 1240 ------------------------ plugins/spotify_controller/__init__.py | 7 - plugins/spotify_controller/plugin.py | 473 --------- plugins/tp_runner/__init__.py | 7 - plugins/tp_runner/plugin.py | 219 ----- plugins/universal_search/__init__.py | 7 - plugins/universal_search/plugin.py | 600 ------------ 61 files changed, 13656 deletions(-) delete mode 100644 plugins/analytics/__init__.py delete mode 100644 plugins/analytics/plugin.py delete mode 100644 plugins/auction_tracker/__init__.py delete mode 100644 plugins/auction_tracker/plugin.py delete mode 100644 plugins/auto_screenshot/__init__.py delete mode 100644 plugins/auto_screenshot/plugin.py delete mode 100644 plugins/auto_updater/__init__.py delete mode 100644 plugins/auto_updater/plugin.py delete mode 100644 plugins/base_plugin.py delete mode 100644 plugins/calculator/__init__.py delete mode 100644 plugins/calculator/plugin.py delete mode 100644 plugins/chat_logger/__init__.py delete mode 100644 plugins/chat_logger/plugin.py delete mode 100644 plugins/codex_tracker/__init__.py delete mode 100644 plugins/codex_tracker/plugin.py delete mode 100644 plugins/crafting_calc/__init__.py delete mode 100644 plugins/crafting_calc/plugin.py delete mode 100644 plugins/dashboard/__init__.py delete mode 100644 plugins/dashboard/plugin.py delete mode 100644 plugins/discord_presence/__init__.py delete mode 100644 plugins/discord_presence/plugin.py delete mode 100644 plugins/dpp_calculator/__init__.py delete mode 100644 plugins/dpp_calculator/plugin.py delete mode 100644 plugins/enhancer_calc/__init__.py delete mode 100644 plugins/enhancer_calc/plugin.py delete mode 100644 plugins/event_bus_example/__init__.py delete mode 100644 plugins/event_bus_example/plugin.py delete mode 100644 plugins/game_reader/__init__.py delete mode 100644 plugins/game_reader/plugin.py delete mode 100644 plugins/game_reader_test/__init__.py delete mode 100644 plugins/game_reader_test/plugin.py delete mode 100644 plugins/global_tracker/__init__.py delete mode 100644 plugins/global_tracker/plugin.py delete mode 100644 plugins/import_export/__init__.py delete mode 100644 plugins/import_export/plugin.py delete mode 100644 plugins/inventory_manager/__init__.py delete mode 100644 plugins/inventory_manager/plugin.py delete mode 100644 plugins/log_parser_test/__init__.py delete mode 100644 plugins/log_parser_test/plugin.py delete mode 100644 plugins/loot_tracker/__init__.py delete mode 100644 plugins/loot_tracker/plugin.py delete mode 100644 plugins/mining_helper/__init__.py delete mode 100644 plugins/mining_helper/plugin.py delete mode 100644 plugins/mission_tracker/__init__.py delete mode 100644 plugins/mission_tracker/plugin.py delete mode 100644 plugins/nexus_search/__init__.py delete mode 100644 plugins/nexus_search/plugin.py delete mode 100644 plugins/price_alerts/__init__.py delete mode 100644 plugins/price_alerts/plugin.py delete mode 100644 plugins/profession_scanner/__init__.py delete mode 100644 plugins/profession_scanner/plugin.py delete mode 100644 plugins/session_exporter/__init__.py delete mode 100644 plugins/session_exporter/plugin.py delete mode 100644 plugins/skill_scanner/__init__.py delete mode 100644 plugins/skill_scanner/plugin.py delete mode 100644 plugins/spotify_controller/__init__.py delete mode 100644 plugins/spotify_controller/plugin.py delete mode 100644 plugins/tp_runner/__init__.py delete mode 100644 plugins/tp_runner/plugin.py delete mode 100644 plugins/universal_search/__init__.py delete mode 100644 plugins/universal_search/plugin.py 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/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/auction_tracker/__init__.py b/plugins/auction_tracker/__init__.py deleted file mode 100644 index 2b39bff..0000000 --- a/plugins/auction_tracker/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Auction Tracker Plugin -""" - -from .plugin import AuctionTrackerPlugin - -__all__ = ["AuctionTrackerPlugin"] diff --git a/plugins/auction_tracker/plugin.py b/plugins/auction_tracker/plugin.py deleted file mode 100644 index 78b945d..0000000 --- a/plugins/auction_tracker/plugin.py +++ /dev/null @@ -1,261 +0,0 @@ -""" -EU-Utility - Auction Tracker Plugin - -Track auction prices and market trends. -""" - -import json -from datetime import datetime, timedelta -from pathlib import Path -from collections import defaultdict - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTableWidget, QTableWidgetItem, - QLineEdit, QComboBox, QFrame -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin -from core.icon_manager import get_icon_manager -from PyQt6.QtGui import QIcon - - -class AuctionTrackerPlugin(BasePlugin): - """Track auction prices and analyze market trends.""" - - name = "Auction Tracker" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Track prices, markups, and market trends" - hotkey = "ctrl+shift+a" - - def initialize(self): - """Setup auction tracker.""" - self.data_file = Path("data/auction_tracker.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.price_history = defaultdict(list) - self.watchlist = [] - - self._load_data() - - def _load_data(self): - """Load auction data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.price_history = defaultdict(list, data.get('prices', {})) - self.watchlist = data.get('watchlist', []) - except: - pass - - def _save_data(self): - """Save auction data.""" - with open(self.data_file, 'w') as f: - json.dump({ - 'prices': dict(self.price_history), - 'watchlist': self.watchlist - }, f, indent=2) - - def get_ui(self): - """Create auction tracker UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Get icon manager - icon_mgr = get_icon_manager() - - # Title with icon - title_layout = QHBoxLayout() - - title_icon = QLabel() - icon_pixmap = icon_mgr.get_pixmap('trending-up', size=20) - title_icon.setPixmap(icon_pixmap) - title_icon.setFixedSize(20, 20) - title_layout.addWidget(title_icon) - - title = QLabel("Auction Tracker") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - title_layout.addWidget(title) - title_layout.addStretch() - - layout.addLayout(title_layout) - - # Search - search_layout = QHBoxLayout() - - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Search item...") - 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_pixmap = icon_mgr.get_pixmap('search', size=16) - search_btn.setIcon(QIcon(search_pixmap)) - search_btn.setIconSize(Qt.QSize(16, 16)) - search_btn.setFixedSize(32, 32) - search_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - border: none; - border-radius: 4px; - } - QPushButton:hover { - background-color: #ffa060; - } - """) - search_btn.clicked.connect(self._search_item) - search_layout.addWidget(search_btn) - - layout.addLayout(search_layout) - - # Price table - self.price_table = QTableWidget() - self.price_table.setColumnCount(6) - self.price_table.setHorizontalHeaderLabels(["Item", "Bid", "Buyout", "Markup", "Trend", "Time"]) - self.price_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - QHeaderView::section { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,180); - padding: 8px; - font-weight: bold; - font-size: 11px; - } - """) - self.price_table.horizontalHeader().setStretchLastSection(True) - layout.addWidget(self.price_table) - - # Quick scan - scan_btn = QPushButton("Scan Auction Window") - scan_btn.setStyleSheet(""" - QPushButton { - background-color: #ffc107; - color: black; - padding: 12px; - border: none; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: #ffd54f; - } - """) - scan_btn.clicked.connect(self._scan_auction) - layout.addWidget(scan_btn) - - # Sample data - self._add_sample_data() - - layout.addStretch() - return widget - - def _add_sample_data(self): - """Add sample auction data.""" - sample_items = [ - {"name": "Nanocube", "bid": 304.00, "buyout": 304.00, "markup": 101.33}, - {"name": "Aakas Plating", "bid": 154.00, "buyout": 159.00, "markup": 102.67}, - {"name": "Wenrex Ingot", "bid": 111.00, "buyout": 118.00, "markup": 108.82}, - ] - - self.price_table.setRowCount(len(sample_items)) - for i, item in enumerate(sample_items): - self.price_table.setItem(i, 0, QTableWidgetItem(item['name'])) - self.price_table.setItem(i, 1, QTableWidgetItem(f"{item['bid']:.2f}")) - self.price_table.setItem(i, 2, QTableWidgetItem(f"{item['buyout']:.2f}")) - - markup_item = QTableWidgetItem(f"{item['markup']:.2f}%") - if item['markup'] > 105: - markup_item.setForeground(Qt.GlobalColor.red) - elif item['markup'] < 102: - markup_item.setForeground(Qt.GlobalColor.green) - self.price_table.setItem(i, 3, markup_item) - - self.price_table.setItem(i, 4, QTableWidgetItem("โ†’")) - self.price_table.setItem(i, 5, QTableWidgetItem("2m ago")) - - def _search_item(self): - """Search for item price history.""" - query = self.search_input.text().lower() - # TODO: Implement search - - def _scan_auction(self): - """Scan auction window with OCR.""" - # TODO: Implement OCR scanning - pass - - def record_price(self, item_name, bid, buyout, tt_value=None): - """Record a price observation.""" - entry = { - 'time': datetime.now().isoformat(), - 'bid': bid, - 'buyout': buyout, - 'tt': tt_value, - 'markup': (buyout / tt_value * 100) if tt_value else None - } - - self.price_history[item_name].append(entry) - - # Keep last 100 entries per item - if len(self.price_history[item_name]) > 100: - self.price_history[item_name] = self.price_history[item_name][-100:] - - self._save_data() - - def get_price_trend(self, item_name, days=7): - """Get price trend for an item.""" - history = self.price_history.get(item_name, []) - if not history: - return None - - cutoff = (datetime.now() - timedelta(days=days)).isoformat() - recent = [h for h in history if h['time'] > cutoff] - - if len(recent) < 2: - return None - - prices = [h['buyout'] for h in recent] - return { - 'current': prices[-1], - 'average': sum(prices) / len(prices), - 'min': min(prices), - 'max': max(prices), - 'trend': 'up' if prices[-1] > prices[0] else 'down' if prices[-1] < prices[0] else 'stable' - } - - def get_deals(self, max_markup=102.0): - """Find items below market price.""" - deals = [] - for item_name, history in self.price_history.items(): - if not history: - continue - - latest = history[-1] - markup = latest.get('markup') - - if markup and markup <= max_markup: - deals.append({ - 'item': item_name, - 'price': latest['buyout'], - 'markup': markup - }) - - return sorted(deals, key=lambda x: x['markup']) diff --git a/plugins/auto_screenshot/__init__.py b/plugins/auto_screenshot/__init__.py deleted file mode 100644 index ab058c2..0000000 --- a/plugins/auto_screenshot/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Auto-Screenshot Plugin for EU-Utility - -Automatically capture screenshots on Global, HOF, or other -significant game events. -""" - -from .plugin import AutoScreenshotPlugin - -__all__ = ['AutoScreenshotPlugin'] diff --git a/plugins/auto_screenshot/plugin.py b/plugins/auto_screenshot/plugin.py deleted file mode 100644 index 58974c5..0000000 --- a/plugins/auto_screenshot/plugin.py +++ /dev/null @@ -1,735 +0,0 @@ -""" -EU-Utility - Auto-Screenshot Plugin - -Automatically capture screenshots on Global, HOF, or other -significant game events. Perfect for sharing achievements! -""" - -import os -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Optional, Any -from dataclasses import dataclass, field - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, - QCheckBox, QSpinBox, QGroupBox, QFileDialog, QLineEdit, - QComboBox, QListWidget, QListWidgetItem, QTabWidget -) -from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QObject -from PyQt6.QtGui import QColor - -from plugins.base_plugin import BasePlugin -from core.event_bus import GlobalEvent, LootEvent, SkillGainEvent - - -@dataclass -class ScreenshotRule: - """Rule for when to take a screenshot.""" - event_type: str # 'global', 'hof', 'ath', 'skill', 'loot_value', 'custom' - min_value: float = 0.0 # For value-based triggers (PED, skill gain, etc.) - enabled: bool = True - play_sound: bool = True - show_notification: bool = True - custom_pattern: str = "" # For custom log patterns - - -class AutoScreenshotSignals(QObject): - """Signals for thread-safe UI updates.""" - screenshot_taken = pyqtSignal(str) # Path to screenshot - event_detected = pyqtSignal(str) # Event description - - -class AutoScreenshotPlugin(BasePlugin): - """ - Plugin for automatic screenshots on game events. - - Features: - - Screenshot on Global/HOF/ATH - - Screenshot on rare loot - - Screenshot on significant skill gains - - Configurable capture delay - - Custom filename patterns - - Notification and sound options - """ - - name = "Auto-Screenshot" - version = "1.0.0" - author = "EU-Utility" - description = "Capture screenshots on Global/HOF and other events" - icon = "๐Ÿ“ธ" - hotkey = "ctrl+shift+c" - - def __init__(self, overlay_window, config): - super().__init__(overlay_window, config) - - # Settings - self.enabled = self.get_config('enabled', True) - self.capture_delay_ms = self.get_config('capture_delay_ms', 500) - self.save_directory = self.get_config('save_directory', - str(Path.home() / "Documents" / "Entropia Universe" / "Screenshots" / "Globals")) - - # Filename pattern - self.filename_pattern = self.get_config('filename_pattern', - "{event_type}_{timestamp}_{player}") - - # Screenshot rules - self.rules: Dict[str, ScreenshotRule] = {} - self._load_rules() - - # Statistics - self.screenshots_taken = 0 - self.events_captured: List[Dict] = [] - - # UI references - self._ui = None - self.events_list = None - self.status_label = None - self.dir_label = None - - # Signals - self.signals = AutoScreenshotSignals() - self.signals.screenshot_taken.connect(self._on_screenshot_saved) - self.signals.event_detected.connect(self._on_event_logged) - - # Event subscriptions - self._subscriptions: List[str] = [] - - # Pending screenshot timer - self._pending_screenshot = None - self._pending_timer = None - - def initialize(self) -> None: - """Initialize plugin.""" - self.log_info("Initializing Auto-Screenshot") - - # Ensure save directory exists - Path(self.save_directory).mkdir(parents=True, exist_ok=True) - - # Subscribe to events - if self.enabled: - self._subscribe_to_events() - - self.log_info(f"Auto-Screenshot initialized (enabled={self.enabled})") - - def _subscribe_to_events(self) -> None: - """Subscribe to game events.""" - # Global/HOF events - self._subscriptions.append( - self.subscribe_typed(GlobalEvent, self._on_global_event) - ) - - # Loot events (for high-value loot) - self._subscriptions.append( - self.subscribe_typed(LootEvent, self._on_loot_event) - ) - - # Skill gain events - self._subscriptions.append( - self.subscribe_typed(SkillGainEvent, self._on_skill_event) - ) - - def get_ui(self) -> QWidget: - """Return the plugin's UI widget.""" - if self._ui is None: - self._ui = self._create_ui() - return self._ui - - def _create_ui(self) -> QWidget: - """Create the plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(12) - layout.setContentsMargins(16, 16, 16, 16) - - # Header - header = QLabel("๐Ÿ“ธ Auto-Screenshot") - header.setStyleSheet(""" - font-size: 18px; - font-weight: bold; - color: white; - padding-bottom: 8px; - """) - layout.addWidget(header) - - # Status - self.status_label = QLabel(f"Status: {'Enabled' if self.enabled else 'Disabled'} | Captured: {self.screenshots_taken}") - self.status_label.setStyleSheet("color: rgba(255, 255, 255, 150);") - layout.addWidget(self.status_label) - - # Create tabs - tabs = QTabWidget() - tabs.setStyleSheet(""" - QTabWidget::pane { - background-color: rgba(30, 35, 45, 150); - border: 1px solid rgba(100, 150, 200, 50); - border-radius: 8px; - } - QTabBar::tab { - background-color: rgba(50, 60, 75, 200); - color: white; - padding: 8px 16px; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - } - QTabBar::tab:selected { - background-color: rgba(100, 150, 200, 200); - } - """) - - # === Rules Tab === - rules_widget = QWidget() - rules_layout = QVBoxLayout(rules_widget) - rules_layout.setContentsMargins(12, 12, 12, 12) - - rules_header = QLabel("Capture Rules") - rules_header.setStyleSheet("font-weight: bold; color: white;") - rules_layout.addWidget(rules_header) - - # Global checkbox - self.global_checkbox = QCheckBox("Capture on Global (any value)") - self.global_checkbox.setChecked(self.rules.get('global', ScreenshotRule('global')).enabled) - self.global_checkbox.setStyleSheet("color: white;") - self.global_checkbox.stateChanged.connect(lambda: self._update_rule('global', self.global_checkbox.isChecked())) - rules_layout.addWidget(self.global_checkbox) - - # HOF checkbox - self.hof_checkbox = QCheckBox("Capture on HOF (50+ PED)") - self.hof_checkbox.setChecked(self.rules.get('hof', ScreenshotRule('hof', min_value=50)).enabled) - self.hof_checkbox.setStyleSheet("color: white;") - self.hof_checkbox.stateChanged.connect(lambda: self._update_rule('hof', self.hof_checkbox.isChecked())) - rules_layout.addWidget(self.hof_checkbox) - - # ATH checkbox - self.ath_checkbox = QCheckBox("Capture on ATH (All-Time High)") - self.ath_checkbox.setChecked(self.rules.get('ath', ScreenshotRule('ath')).enabled) - self.ath_checkbox.setStyleSheet("color: white;") - self.ath_checkbox.stateChanged.connect(lambda: self._update_rule('ath', self.ath_checkbox.isChecked())) - rules_layout.addWidget(self.ath_checkbox) - - # Discovery checkbox - self.discovery_checkbox = QCheckBox("Capture on Discovery") - self.discovery_checkbox.setChecked(self.rules.get('discovery', ScreenshotRule('discovery')).enabled) - self.discovery_checkbox.setStyleSheet("color: white;") - self.discovery_checkbox.stateChanged.connect(lambda: self._update_rule('discovery', self.discovery_checkbox.isChecked())) - rules_layout.addWidget(self.discovery_checkbox) - - # High value loot - loot_layout = QHBoxLayout() - self.loot_checkbox = QCheckBox("Capture on loot above") - self.loot_checkbox.setChecked(self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100)).enabled) - self.loot_checkbox.setStyleSheet("color: white;") - self.loot_checkbox.stateChanged.connect(lambda: self._update_rule('loot_value', self.loot_checkbox.isChecked())) - loot_layout.addWidget(self.loot_checkbox) - - self.loot_spin = QSpinBox() - self.loot_spin.setRange(1, 10000) - self.loot_spin.setValue(int(self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100)).min_value)) - self.loot_spin.setSuffix(" PED") - self.loot_spin.setStyleSheet(self._spinbox_style()) - loot_layout.addWidget(self.loot_spin) - loot_layout.addStretch() - rules_layout.addLayout(loot_layout) - - # Skill gain - skill_layout = QHBoxLayout() - self.skill_checkbox = QCheckBox("Capture on skill gain above") - self.skill_checkbox.setChecked(self.rules.get('skill', ScreenshotRule('skill', min_value=0.1)).enabled) - self.skill_checkbox.setStyleSheet("color: white;") - self.skill_checkbox.stateChanged.connect(lambda: self._update_rule('skill', self.skill_checkbox.isChecked())) - skill_layout.addWidget(self.skill_checkbox) - - self.skill_spin = QSpinBox() - self.skill_spin.setRange(1, 1000) - self.skill_spin.setValue(int(self.rules.get('skill', ScreenshotRule('skill', min_value=0.1)).min_value * 100)) - self.skill_spin.setSuffix(" points (x0.01)") - self.skill_spin.setStyleSheet(self._spinbox_style()) - skill_layout.addWidget(self.skill_spin) - skill_layout.addStretch() - rules_layout.addLayout(skill_layout) - - # Sound/notification options - options_group = QGroupBox("Notification Options") - options_layout = QVBoxLayout(options_group) - - self.sound_checkbox = QCheckBox("Play sound on capture") - self.sound_checkbox.setChecked(self.get_config('play_sound', True)) - self.sound_checkbox.setStyleSheet("color: white;") - options_layout.addWidget(self.sound_checkbox) - - self.notification_checkbox = QCheckBox("Show notification on capture") - self.notification_checkbox.setChecked(self.get_config('show_notification', True)) - self.notification_checkbox.setStyleSheet("color: white;") - options_layout.addWidget(self.notification_checkbox) - - rules_layout.addWidget(options_group) - rules_layout.addStretch() - - tabs.addTab(rules_widget, "๐Ÿ“‹ Rules") - - # === History Tab === - history_widget = QWidget() - history_layout = QVBoxLayout(history_widget) - history_layout.setContentsMargins(12, 12, 12, 12) - - history_header = QLabel("Recent Captures") - history_header.setStyleSheet("font-weight: bold; color: white;") - history_layout.addWidget(history_header) - - self.events_list = QListWidget() - self.events_list.setStyleSheet(""" - QListWidget { - background-color: rgba(30, 35, 45, 150); - border: 1px solid rgba(100, 150, 200, 50); - border-radius: 8px; - color: white; - } - QListWidget::item { - padding: 8px; - border-bottom: 1px solid rgba(100, 150, 200, 30); - } - QListWidget::item:selected { - background-color: rgba(100, 150, 200, 100); - } - """) - history_layout.addWidget(self.events_list) - - # History buttons - history_btn_layout = QHBoxLayout() - - open_folder_btn = QPushButton("๐Ÿ“ Open Folder") - open_folder_btn.setStyleSheet(self._button_style("#2196f3")) - open_folder_btn.clicked.connect(self._open_screenshots_folder) - history_btn_layout.addWidget(open_folder_btn) - - clear_history_btn = QPushButton("๐Ÿงน Clear History") - clear_history_btn.setStyleSheet(self._button_style()) - clear_history_btn.clicked.connect(self._clear_history) - history_btn_layout.addWidget(clear_history_btn) - - history_btn_layout.addStretch() - history_layout.addLayout(history_btn_layout) - - tabs.addTab(history_widget, "๐Ÿ“œ History") - - # === Settings Tab === - settings_widget = QWidget() - settings_layout = QVBoxLayout(settings_widget) - settings_layout.setContentsMargins(12, 12, 12, 12) - - settings_header = QLabel("Capture Settings") - settings_header.setStyleSheet("font-weight: bold; color: white;") - settings_layout.addWidget(settings_header) - - # Master enable - self.enable_checkbox = QCheckBox("Enable Auto-Screenshot") - self.enable_checkbox.setChecked(self.enabled) - self.enable_checkbox.setStyleSheet("color: #4caf50; font-weight: bold;") - self.enable_checkbox.stateChanged.connect(self._toggle_enabled) - settings_layout.addWidget(self.enable_checkbox) - - # Capture delay - delay_layout = QHBoxLayout() - delay_label = QLabel("Capture Delay:") - delay_label.setStyleSheet("color: white;") - delay_layout.addWidget(delay_label) - - self.delay_spin = QSpinBox() - self.delay_spin.setRange(0, 5000) - self.delay_spin.setValue(self.capture_delay_ms) - self.delay_spin.setSuffix(" ms") - self.delay_spin.setSingleStep(100) - self.delay_spin.setStyleSheet(self._spinbox_style()) - delay_layout.addWidget(self.delay_spin) - - delay_info = QLabel("(time to hide overlay)") - delay_info.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;") - delay_layout.addWidget(delay_info) - delay_layout.addStretch() - settings_layout.addLayout(delay_layout) - - # Save directory - dir_group = QGroupBox("Save Location") - dir_layout = QVBoxLayout(dir_group) - - dir_row = QHBoxLayout() - self.dir_label = QLabel(self.save_directory) - self.dir_label.setStyleSheet("color: white;") - self.dir_label.setWordWrap(True) - dir_row.addWidget(self.dir_label, 1) - - change_dir_btn = QPushButton("๐Ÿ“ Change") - change_dir_btn.setStyleSheet(self._button_style()) - change_dir_btn.clicked.connect(self._change_save_directory) - dir_row.addWidget(change_dir_btn) - - dir_layout.addLayout(dir_row) - settings_layout.addWidget(dir_group) - - # Filename pattern - pattern_group = QGroupBox("Filename Pattern") - pattern_layout = QVBoxLayout(pattern_group) - - pattern_info = QLabel("Available variables: {timestamp}, {event_type}, {player}, {value}") - pattern_info.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;") - pattern_layout.addWidget(pattern_info) - - self.pattern_input = QLineEdit(self.filename_pattern) - self.pattern_input.setStyleSheet(self._input_style()) - pattern_layout.addWidget(self.pattern_input) - - settings_layout.addWidget(pattern_group) - settings_layout.addStretch() - - tabs.addTab(settings_widget, "โš™๏ธ Settings") - - layout.addWidget(tabs) - - # Save button - save_btn = QPushButton("๐Ÿ’พ Save Settings") - save_btn.setStyleSheet(self._button_style("#4caf50")) - save_btn.clicked.connect(self._save_settings) - layout.addWidget(save_btn) - - # Test button - test_btn = QPushButton("๐Ÿ“ธ Test Screenshot") - test_btn.setStyleSheet(self._button_style("#ff9800")) - test_btn.clicked.connect(lambda: self._take_screenshot("test", "TestUser", 0)) - layout.addWidget(test_btn) - - return widget - - def _button_style(self, color: str = "#607d8b") -> str: - """Generate button stylesheet.""" - return f""" - QPushButton {{ - background-color: {color}; - color: white; - border: none; - border-radius: 8px; - padding: 10px 16px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: {color}dd; - }} - QPushButton:pressed {{ - background-color: {color}aa; - }} - """ - - def _input_style(self) -> str: - """Generate input stylesheet.""" - return """ - QLineEdit { - background-color: rgba(50, 60, 75, 200); - color: white; - border: 1px solid rgba(100, 150, 200, 100); - border-radius: 8px; - padding: 8px 12px; - } - """ - - def _spinbox_style(self) -> str: - """Generate spinbox stylesheet.""" - return """ - QSpinBox { - background-color: rgba(50, 60, 75, 200); - color: white; - border: 1px solid rgba(100, 150, 200, 100); - border-radius: 4px; - padding: 4px; - } - """ - - def _on_global_event(self, event: GlobalEvent) -> None: - """Handle global/HOF events.""" - if not self.enabled: - return - - event_type = event.achievement_type.lower() - - # Check if we should capture - if event_type == 'global' and self.rules.get('global', ScreenshotRule('global')).enabled: - self._schedule_screenshot('global', event.player_name, event.value) - - elif event_type in ['hof', 'hall of fame'] and self.rules.get('hof', ScreenshotRule('hof')).enabled: - if event.value >= self.rules.get('hof', ScreenshotRule('hof', min_value=50)).min_value: - self._schedule_screenshot('hof', event.player_name, event.value) - - elif event_type in ['ath', 'all time high'] and self.rules.get('ath', ScreenshotRule('ath')).enabled: - self._schedule_screenshot('ath', event.player_name, event.value) - - elif event_type == 'discovery' and self.rules.get('discovery', ScreenshotRule('discovery')).enabled: - self._schedule_screenshot('discovery', event.player_name, event.value) - - def _on_loot_event(self, event: LootEvent) -> None: - """Handle loot events.""" - if not self.enabled: - return - - rule = self.rules.get('loot_value', ScreenshotRule('loot_value', min_value=100)) - if rule.enabled and event.total_tt_value >= rule.min_value: - self._schedule_screenshot('loot', 'player', event.total_tt_value) - - def _on_skill_event(self, event: SkillGainEvent) -> None: - """Handle skill gain events.""" - if not self.enabled: - return - - rule = self.rules.get('skill', ScreenshotRule('skill', min_value=0.1)) - if rule.enabled and event.gain_amount >= rule.min_value: - self._schedule_screenshot('skill', 'player', event.gain_amount) - - def _schedule_screenshot(self, event_type: str, player_name: str, value: float) -> None: - """Schedule a screenshot with optional delay.""" - if self._pending_timer: - self._pending_timer.stop() - - self._pending_screenshot = (event_type, player_name, value) - - if self.capture_delay_ms > 0: - self._pending_timer = QTimer() - self._pending_timer.timeout.connect(lambda: self._execute_screenshot()) - self._pending_timer.setSingleShot(True) - self._pending_timer.start(self.capture_delay_ms) - else: - self._execute_screenshot() - - def _execute_screenshot(self) -> None: - """Execute the pending screenshot.""" - if not self._pending_screenshot: - return - - event_type, player_name, value = self._pending_screenshot - self._pending_screenshot = None - - self._take_screenshot(event_type, player_name, value) - - def _take_screenshot(self, event_type: str, player_name: str, value: float) -> None: - """Take and save a screenshot.""" - try: - # Generate filename - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = self.filename_pattern.format( - timestamp=timestamp, - event_type=event_type.upper(), - player=player_name, - value=f"{value:.0f}" - ) - filename = f"{filename}.png" - - # Clean filename - filename = "".join(c for c in filename if c.isalnum() or c in "._- ") - - filepath = Path(self.save_directory) / filename - - # Capture screenshot - screenshot = self.capture_screen(full_screen=True) - - # Save - screenshot.save(str(filepath), 'PNG') - - self.screenshots_taken += 1 - - # Log event - event_data = { - 'timestamp': datetime.now().isoformat(), - 'event_type': event_type, - 'player': player_name, - 'value': value, - 'filepath': str(filepath) - } - self.events_captured.append(event_data) - - # Trim history - if len(self.events_captured) > 100: - self.events_captured = self.events_captured[-100:] - - # Emit signals - self.signals.screenshot_taken.emit(str(filepath)) - self.signals.event_detected.emit(f"{event_type.upper()}: {player_name} - {value:.0f} PED") - - # Play sound if enabled - if self.sound_checkbox.isChecked(): - self.play_sound('global' if event_type in ['global', 'hof', 'ath'] else 'skill_gain') - - # Show notification if enabled - if self.notification_checkbox.isChecked(): - self.notify( - f"๐Ÿ“ธ {event_type.upper()} Captured!", - f"Saved to {filename}", - notification_type='success' - ) - - self.log_info(f"Screenshot saved: {filepath}") - - except Exception as e: - self.log_error(f"Screenshot failed: {e}") - self.notify_error("Screenshot Failed", str(e)) - - def _on_screenshot_saved(self, filepath: str) -> None: - """Handle screenshot saved event.""" - self.status_label.setText(f"Status: {'Enabled' if self.enabled else 'Disabled'} | Captured: {self.screenshots_taken}") - - def _on_event_logged(self, description: str) -> None: - """Handle event logged.""" - if self.events_list: - item = QListWidgetItem(f"[{datetime.now().strftime('%H:%M:%S')}] {description}") - item.setForeground(QColor("white")) - self.events_list.insertItem(0, item) - - # Trim list - while self.events_list.count() > 50: - self.events_list.takeItem(self.events_list.count() - 1) - - def _update_rule(self, rule_name: str, enabled: bool) -> None: - """Update a rule's enabled state.""" - if rule_name not in self.rules: - # Create default rule - defaults = { - 'global': ScreenshotRule('global'), - 'hof': ScreenshotRule('hof', min_value=50), - 'ath': ScreenshotRule('ath'), - 'discovery': ScreenshotRule('discovery'), - 'loot_value': ScreenshotRule('loot_value', min_value=100), - 'skill': ScreenshotRule('skill', min_value=0.1) - } - self.rules[rule_name] = defaults.get(rule_name, ScreenshotRule(rule_name)) - - self.rules[rule_name].enabled = enabled - self._save_rules() - - def _toggle_enabled(self, state) -> None: - """Toggle plugin enabled state.""" - self.enabled = state == Qt.CheckState.Checked.value - - if self.enabled: - self._subscribe_to_events() - self.status_label.setText(f"Status: Enabled | Captured: {self.screenshots_taken}") - self.status_label.setStyleSheet("color: #4caf50;") - else: - # Unsubscribe - for sub_id in self._subscriptions: - self.unsubscribe_typed(sub_id) - self._subscriptions.clear() - self.status_label.setText(f"Status: Disabled | Captured: {self.screenshots_taken}") - self.status_label.setStyleSheet("color: #f44336;") - - def _change_save_directory(self) -> None: - """Change the save directory.""" - new_dir = QFileDialog.getExistingDirectory( - self._ui, - "Select Screenshot Directory", - self.save_directory - ) - - if new_dir: - self.save_directory = new_dir - Path(self.save_directory).mkdir(parents=True, exist_ok=True) - if self.dir_label: - self.dir_label.setText(new_dir) - - def _open_screenshots_folder(self) -> None: - """Open the screenshots folder.""" - import subprocess - import platform - - try: - if platform.system() == 'Windows': - subprocess.run(['explorer', self.save_directory], check=True) - elif platform.system() == 'Darwin': - subprocess.run(['open', self.save_directory], check=True) - else: - subprocess.run(['xdg-open', self.save_directory], check=True) - except Exception as e: - self.log_error(f"Failed to open folder: {e}") - - def _clear_history(self) -> None: - """Clear the events history.""" - self.events_captured.clear() - if self.events_list: - self.events_list.clear() - - def _save_settings(self) -> None: - """Save all settings.""" - self.capture_delay_ms = self.delay_spin.value() - self.filename_pattern = self.pattern_input.text() - - # Update rules with values - if 'loot_value' in self.rules: - self.rules['loot_value'].min_value = self.loot_spin.value() - if 'skill' in self.rules: - self.rules['skill'].min_value = self.skill_spin.value() / 100 - - self.set_config('enabled', self.enabled) - self.set_config('capture_delay_ms', self.capture_delay_ms) - self.set_config('save_directory', self.save_directory) - self.set_config('filename_pattern', self.filename_pattern) - self.set_config('play_sound', self.sound_checkbox.isChecked()) - self.set_config('show_notification', self.notification_checkbox.isChecked()) - - self._save_rules() - - self.notify_success("Settings Saved", "Auto-screenshot settings updated") - - def _save_rules(self) -> None: - """Save rules to storage.""" - rules_data = { - name: { - 'event_type': rule.event_type, - 'min_value': rule.min_value, - 'enabled': rule.enabled, - 'play_sound': rule.play_sound, - 'show_notification': rule.show_notification - } - for name, rule in self.rules.items() - } - self.save_data('rules', rules_data) - self.save_data('events_captured', self.events_captured) - - def _load_rules(self) -> None: - """Load rules from storage.""" - rules_data = self.load_data('rules', {}) - for name, data in rules_data.items(): - self.rules[name] = ScreenshotRule( - event_type=data['event_type'], - min_value=data.get('min_value', 0), - enabled=data.get('enabled', True), - play_sound=data.get('play_sound', True), - show_notification=data.get('show_notification', True) - ) - - # Set defaults if not loaded - defaults = { - 'global': ScreenshotRule('global'), - 'hof': ScreenshotRule('hof', min_value=50), - 'ath': ScreenshotRule('ath'), - 'discovery': ScreenshotRule('discovery'), - 'loot_value': ScreenshotRule('loot_value', min_value=100), - 'skill': ScreenshotRule('skill', min_value=0.1) - } - - for name, rule in defaults.items(): - if name not in self.rules: - self.rules[name] = rule - - self.events_captured = self.load_data('events_captured', []) - self.screenshots_taken = len(self.events_captured) - - def on_hotkey(self) -> None: - """Handle hotkey press - take manual screenshot.""" - self._take_screenshot('manual', 'player', 0) - - def shutdown(self) -> None: - """Cleanup on shutdown.""" - self._save_settings() - - if self._pending_timer: - self._pending_timer.stop() - - # Unsubscribe - for sub_id in self._subscriptions: - self.unsubscribe_typed(sub_id) - - super().shutdown() 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/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/base_plugin.py b/plugins/base_plugin.py deleted file mode 100644 index 8934498..0000000 --- a/plugins/base_plugin.py +++ /dev/null @@ -1,1193 +0,0 @@ -""" -EU-Utility - Plugin Base Class - -Defines the interface that all plugins must implement. -Includes PluginAPI integration for cross-plugin communication. -""" - -from abc import ABC, abstractmethod -from typing import Optional, Dict, Any, TYPE_CHECKING, Callable, List, Type - -if TYPE_CHECKING: - from core.overlay_window import OverlayWindow - from core.plugin_api import PluginAPI, APIEndpoint, APIType - from core.event_bus import BaseEvent, EventCategory - - -class BasePlugin(ABC): - """Base class for all EU-Utility plugins. - - To define hotkeys for your plugin, use either: - - 1. Legacy single hotkey (simple toggle): - hotkey = "ctrl+shift+n" - - 2. New multi-hotkey format (recommended): - hotkeys = [ - { - 'action': 'toggle', # Unique action identifier - 'description': 'Toggle My Plugin', # Display name in settings - 'default': 'ctrl+shift+m', # Default hotkey combination - 'config_key': 'myplugin_toggle' # Settings key (optional) - }, - { - 'action': 'quick_action', - 'description': 'Quick Scan', - 'default': 'ctrl+shift+s', - } - ] - """ - - # Plugin metadata - override in subclass - name: str = "Unnamed Plugin" - version: str = "1.0.0" - author: str = "Unknown" - description: str = "No description provided" - icon: Optional[str] = None - - # Plugin settings - hotkey: Optional[str] = None # Legacy single hotkey (e.g., "ctrl+shift+n") - hotkeys: Optional[List[Dict[str, str]]] = None # New multi-hotkey format - enabled: bool = True - - # Dependencies - override in subclass - # Format: { - # 'pip': ['package1', 'package2>=1.0'], - # 'plugins': ['plugin_id1', 'plugin_id2'], # Other plugins this plugin requires - # 'optional': {'package3': 'description'} - # } - dependencies: Dict[str, Any] = {} - - def __init__(self, overlay_window: 'OverlayWindow', config: Dict[str, Any]): - self.overlay = overlay_window - self.config = config - self._ui = None - self._api_registered = False - self._plugin_id = f"{self.__class__.__module__}.{self.__class__.__name__}" - - # Track event subscriptions for cleanup - self._event_subscriptions: List[str] = [] - - # Get API instance - try: - from core.plugin_api import get_api - self.api = get_api() - except ImportError: - self.api = None - - @abstractmethod - def initialize(self) -> None: - """Called when plugin is loaded. Setup API connections, etc.""" - pass - - @abstractmethod - def get_ui(self) -> Any: - """Return the plugin's UI widget (QWidget).""" - return None - - def on_show(self) -> None: - """Called when overlay becomes visible.""" - pass - - def on_hide(self) -> None: - """Called when overlay is hidden.""" - pass - - def on_hotkey(self) -> None: - """Called when plugin's hotkey is pressed.""" - pass - - def shutdown(self) -> None: - """Called when app is closing. Cleanup resources.""" - # Unregister APIs - if self.api and self._api_registered: - self.api.unregister_api(self._plugin_id) - - # Unsubscribe from all typed events - self.unsubscribe_all_typed() - - # ========== Config Methods ========== - - def get_config(self, key: str, default: Any = None) -> Any: - """Get a config value with default.""" - return self.config.get(key, default) - - def set_config(self, key: str, value: Any) -> None: - """Set a config value.""" - self.config[key] = value - - # ========== API Methods ========== - - def register_api(self, name: str, handler: Callable, api_type: 'APIType' = None, description: str = "") -> bool: - """Register an API endpoint for other plugins to use. - - Example: - self.register_api( - "scan_window", - self.scan_window, - APIType.OCR, - "Scan game window and return text" - ) - """ - if not self.api: - print(f"[{self.name}] API not available") - return False - - try: - from core.plugin_api import APIEndpoint, APIType - - if api_type is None: - api_type = APIType.UTILITY - - endpoint = APIEndpoint( - name=name, - api_type=api_type, - description=description, - handler=handler, - plugin_id=self._plugin_id, - version=self.version - ) - - success = self.api.register_api(endpoint) - if success: - self._api_registered = True - return success - - except Exception as e: - print(f"[{self.name}] Failed to register API: {e}") - return False - - def call_api(self, plugin_id: str, api_name: str, *args, **kwargs) -> Any: - """Call another plugin's API. - - Example: - # Call Game Reader's OCR API - result = self.call_api("plugins.game_reader.plugin", "capture_screen") - """ - if not self.api: - raise RuntimeError("API not available") - - return self.api.call_api(plugin_id, api_name, *args, **kwargs) - - def find_apis(self, api_type: 'APIType' = None) -> list: - """Find available APIs from other plugins.""" - if not self.api: - return [] - - return self.api.find_apis(api_type) - - # ========== Shared Services ========== - - def ocr_capture(self, region: tuple = None) -> Dict[str, Any]: - """Capture screen and perform OCR. - - Returns: - {'text': str, 'confidence': float, 'raw_results': list} - """ - if not self.api: - return {"text": "", "confidence": 0, "error": "API not available"} - - return self.api.ocr_capture(region) - - # ========== Screenshot Service Methods ========== - - def capture_screen(self, full_screen: bool = True): - """Capture screenshot. - - Args: - full_screen: If True, capture entire screen - - Returns: - PIL Image object - - Example: - # Capture full screen - screenshot = self.capture_screen() - - # Capture specific region - region = self.capture_region(100, 100, 800, 600) - """ - if not self.api: - raise RuntimeError("API not available") - - return self.api.capture_screen(full_screen) - - def capture_region(self, x: int, y: int, width: int, height: int): - """Capture specific screen region. - - Args: - x: Left coordinate - y: Top coordinate - width: Region width - height: Region height - - Returns: - PIL Image object - - Example: - # Capture a 400x200 region starting at (100, 100) - image = self.capture_region(100, 100, 400, 200) - """ - if not self.api: - raise RuntimeError("API not available") - - return self.api.capture_region(x, y, width, height) - - def get_last_screenshot(self): - """Get the most recent screenshot. - - Returns: - PIL Image or None if no screenshots taken yet - """ - if not self.api: - return None - - return self.api.get_last_screenshot() - - def read_log(self, lines: int = 50, filter_text: str = None) -> list: - """Read recent game log lines.""" - if not self.api: - return [] - - return self.api.read_log(lines, filter_text) - - def get_shared_data(self, key: str, default=None): - """Get shared data from other plugins.""" - if not self.api: - return default - - return self.api.get_data(key, default) - - def set_shared_data(self, key: str, value: Any): - """Set shared data for other plugins.""" - if self.api: - self.api.set_data(key, value) - - # ========== Legacy Event System ========== - - def publish_event(self, event_type: str, data: Dict[str, Any]): - """Publish an event for other plugins to consume (legacy).""" - if self.api: - self.api.publish_event(event_type, data) - - def subscribe(self, event_type: str, callback: Callable): - """Subscribe to events from other plugins (legacy).""" - if self.api: - self.api.subscribe(event_type, callback) - - # ========== Enhanced Typed Event System ========== - - def publish_typed(self, event: 'BaseEvent') -> None: - """ - Publish a typed event to the Event Bus. - - Args: - event: A typed event instance (SkillGainEvent, LootEvent, etc.) - - Example: - from core.event_bus import LootEvent - - self.publish_typed(LootEvent( - mob_name="Daikiba", - items=[{"name": "Animal Oil", "value": 0.05}], - total_tt_value=0.05 - )) - """ - if self.api: - self.api.publish_typed(event) - - def subscribe_typed( - self, - event_class: Type['BaseEvent'], - callback: Callable, - **filter_kwargs - ) -> str: - """ - Subscribe to a specific event type with optional filtering. - - Args: - event_class: The event class to subscribe to - callback: Function to call when matching events occur - **filter_kwargs: Additional filter criteria - - min_damage: Minimum damage threshold - - max_damage: Maximum damage threshold - - mob_types: List of mob names to filter - - skill_names: List of skill names to filter - - sources: List of event sources to filter - - replay_last: Number of recent events to replay - - predicate: Custom filter function - - Returns: - Subscription ID (store this to unsubscribe later) - - Example: - from core.event_bus import DamageEvent - - # Subscribe to all damage events - self.sub_id = self.subscribe_typed(DamageEvent, self.on_damage) - - # Subscribe to high damage events only - self.sub_id = self.subscribe_typed( - DamageEvent, - self.on_big_hit, - min_damage=100 - ) - - # Subscribe with replay - self.sub_id = self.subscribe_typed( - SkillGainEvent, - self.on_skill_gain, - replay_last=10 - ) - """ - if not self.api: - print(f"[{self.name}] API not available for event subscription") - return "" - - sub_id = self.api.subscribe_typed(event_class, callback, **filter_kwargs) - if sub_id: - self._event_subscriptions.append(sub_id) - return sub_id - - def unsubscribe_typed(self, subscription_id: str) -> bool: - """ - Unsubscribe from a specific typed event subscription. - - Args: - subscription_id: The subscription ID returned by subscribe_typed - - Returns: - True if subscription was found and removed - """ - if not self.api: - return False - - result = self.api.unsubscribe_typed(subscription_id) - if result and subscription_id in self._event_subscriptions: - self._event_subscriptions.remove(subscription_id) - return result - - def unsubscribe_all_typed(self) -> None: - """Unsubscribe from all typed event subscriptions.""" - if not self.api: - return - - for sub_id in self._event_subscriptions[:]: # Copy list to avoid modification during iteration - self.api.unsubscribe_typed(sub_id) - self._event_subscriptions.clear() - - def get_recent_events( - self, - event_type: Type['BaseEvent'] = None, - count: int = 100, - category: 'EventCategory' = None - ) -> List['BaseEvent']: - """ - Get recent events from history. - - Args: - event_type: Filter by event class - count: Maximum number of events to return - category: Filter by event category - - Returns: - List of matching events - - Example: - from core.event_bus import LootEvent - - # Get last 20 loot events - recent_loot = self.get_recent_events(LootEvent, 20) - """ - if not self.api: - return [] - - return self.api.get_recent_events(event_type, count, category) - - def get_event_stats(self) -> Dict[str, Any]: - """ - Get Event Bus statistics. - - Returns: - Dict with event bus statistics - """ - if not self.api: - return {} - - return self.api.get_event_stats() - - # ========== Utility Methods ========== - - def format_ped(self, value: float) -> str: - """Format PED value.""" - if self.api: - return self.api.format_ped(value) - return f"{value:.2f} PED" - - def format_pec(self, value: float) -> str: - """Format PEC value.""" - if self.api: - return self.api.format_pec(value) - return f"{value:.0f} PEC" - - def calculate_dpp(self, damage: float, ammo: int, decay: float) -> float: - """Calculate Damage Per PEC.""" - if self.api: - return self.api.calculate_dpp(damage, ammo, decay) - - # Fallback calculation - if damage <= 0: - return 0.0 - ammo_cost = ammo * 0.01 - total_cost = ammo_cost + decay - if total_cost <= 0: - return 0.0 - return damage / (total_cost / 100) - - def calculate_markup(self, price: float, tt: float) -> float: - """Calculate markup percentage.""" - if self.api: - return self.api.calculate_markup(price, tt) - - if tt <= 0: - return 0.0 - return (price / tt) * 100 - - # ========== Audio Service Methods ========== - - def play_sound(self, filename_or_key: str, blocking: bool = False) -> bool: - """Play a sound by key or filename. - - Args: - filename_or_key: Sound key ('global', 'hof', 'skill_gain', 'alert', 'error') - or path to file - blocking: If True, wait for sound to complete (default: False) - - Returns: - True if sound was queued/played, False on error or if muted - - Examples: - # Play predefined sounds - self.play_sound('hof') - self.play_sound('skill_gain') - self.play_sound('alert') - - # Play custom sound file - self.play_sound('/path/to/custom.wav') - """ - if not self.api: - return False - - return self.api.play_sound(filename_or_key, blocking) - - def set_volume(self, volume: float) -> None: - """Set global audio volume. - - Args: - volume: Volume level from 0.0 (mute) to 1.0 (max) - """ - if self.api: - self.api.set_volume(volume) - - def get_volume(self) -> float: - """Get current audio volume. - - Returns: - Current volume level (0.0 to 1.0) - """ - if not self.api: - return 0.0 - - return self.api.get_volume() - - def mute(self) -> None: - """Mute all audio.""" - if self.api: - self.api.mute_audio() - - def unmute(self) -> None: - """Unmute audio.""" - if self.api: - self.api.unmute_audio() - - def toggle_mute(self) -> bool: - """Toggle audio mute state. - - Returns: - New muted state (True if now muted) - """ - if not self.api: - return False - - return self.api.toggle_mute_audio() - - def is_muted(self) -> bool: - """Check if audio is muted. - - Returns: - True if audio is muted - """ - if not self.api: - return False - - return self.api.is_audio_muted() - - def is_audio_available(self) -> bool: - """Check if audio service is available. - - Returns: - True if audio backend is initialized and working - """ - if not self.api: - return False - - return self.api.is_audio_available() - - # ========== Background Task Methods ========== - - def run_in_background(self, func: Callable, *args, - priority: str = 'normal', - on_complete: Callable = None, - on_error: Callable = None, - **kwargs) -> str: - """Run a function in a background thread. - - Use this instead of creating your own QThreads. - - Args: - func: Function to execute in background - *args: Positional arguments for the function - priority: 'high', 'normal', or 'low' (default: 'normal') - on_complete: Called with result when task completes successfully - on_error: Called with exception when task fails - **kwargs: Keyword arguments for the function - - Returns: - Task ID for tracking/cancellation - - Example: - def heavy_calculation(data): - return process(data) - - def on_done(result): - self.update_ui(result) - - def on_fail(error): - self.show_error(str(error)) - - task_id = self.run_in_background( - heavy_calculation, - large_dataset, - priority='high', - on_complete=on_done, - on_error=on_fail - ) - - # Or with decorator style: - @self.run_in_background - def fetch_remote_data(): - return requests.get(url).json() - """ - if not self.api: - raise RuntimeError("API not available") - - return self.api.run_in_background( - func, *args, - priority=priority, - on_complete=on_complete, - on_error=on_error, - **kwargs - ) - - def schedule_task(self, delay_ms: int, func: Callable, *args, - priority: str = 'normal', - on_complete: Callable = None, - on_error: Callable = None, - periodic: bool = False, - interval_ms: int = None, - **kwargs) -> str: - """Schedule a task for delayed or periodic execution. - - Args: - delay_ms: Milliseconds to wait before first execution - func: Function to execute - *args: Positional arguments - priority: 'high', 'normal', or 'low' - on_complete: Called with result after each execution - on_error: Called with exception if execution fails - periodic: If True, repeat execution at interval_ms - interval_ms: Milliseconds between periodic executions - **kwargs: Keyword arguments - - Returns: - Task ID for tracking/cancellation - - Example: - # One-time delayed execution - task_id = self.schedule_task( - 5000, # 5 seconds - lambda: print("Hello after delay!") - ) - - # Periodic data refresh (every 30 seconds) - self.schedule_task( - 0, # Start immediately - self.refresh_data, - periodic=True, - interval_ms=30000, - on_complete=lambda data: self.update_display(data) - ) - """ - if not self.api: - raise RuntimeError("API not available") - - return self.api.schedule_task( - delay_ms, func, *args, - priority=priority, - on_complete=on_complete, - on_error=on_error, - periodic=periodic, - interval_ms=interval_ms, - **kwargs - ) - - def cancel_task(self, task_id: str) -> bool: - """Cancel a pending or running task. - - Args: - task_id: Task ID returned by run_in_background or schedule_task - - Returns: - True if task was cancelled, False if not found or already done - """ - if not self.api: - return False - - return self.api.cancel_task(task_id) - - def connect_task_signals(self, - on_completed: Callable = None, - on_failed: Callable = None, - on_started: Callable = None, - on_cancelled: Callable = None) -> bool: - """Connect to task status signals for UI updates. - - Connects Qt signals so UI updates from background threads are thread-safe. - - Args: - on_completed: Called with (task_id, result) when tasks complete - on_failed: Called with (task_id, error_message) when tasks fail - on_started: Called with (task_id) when tasks start - on_cancelled: Called with (task_id) when tasks are cancelled - - Returns: - True if signals were connected - - Example: - class MyPlugin(BasePlugin): - def initialize(self): - # Connect task signals for UI updates - self.connect_task_signals( - on_completed=self._on_task_done, - on_failed=self._on_task_error - ) - - def _on_task_done(self, task_id, result): - self.status_label.setText(f"Task {task_id}: Done!") - - def _on_task_error(self, task_id, error): - self.status_label.setText(f"Task {task_id} failed: {error}") - """ - if not self.api: - return False - - connected = False - - if on_completed: - connected = self.api.connect_task_signal('completed', on_completed) or connected - if on_failed: - connected = self.api.connect_task_signal('failed', on_failed) or connected - if on_started: - connected = self.api.connect_task_signal('started', on_started) or connected - if on_cancelled: - connected = self.api.connect_task_signal('cancelled', on_cancelled) or connected - - return connected - - # ========== Nexus API Methods ========== - - def nexus_search(self, query: str, entity_type: str = "items", limit: int = 20) -> List[Dict[str, Any]]: - """Search for entities via Entropia Nexus API. - - Args: - query: Search query string - entity_type: Type of entity to search. Valid types: - - items, weapons, armors - - mobs, pets - - blueprints, materials - - locations, teleporters, shops, planets, areas - - skills - - enhancers, medicaltools, finders, excavators, refiners - - vehicles, decorations, furniture - - storagecontainers, strongboxes, vendors - limit: Maximum number of results (default: 20, max: 100) - - Returns: - List of search result dictionaries - - Example: - # Search for weapons - results = self.nexus_search("ArMatrix", entity_type="weapons") - - # Search for mobs - mobs = self.nexus_search("Atrox", entity_type="mobs") - - # Search for locations - locations = self.nexus_search("Fort", entity_type="locations") - - # Process results - for item in results: - print(f"{item['name']} ({item['type']})") - """ - if not self.api: - return [] - - return self.api.nexus_search(query, entity_type, limit) - - def nexus_get_item_details(self, item_id: str) -> Optional[Dict[str, Any]]: - """Get detailed information about a specific item. - - Args: - item_id: The item's unique identifier (e.g., "armatrix_lp-35") - - Returns: - Dictionary with item details, or None if not found - - Example: - details = self.nexus_get_item_details("armatrix_lp-35") - if details: - print(f"Name: {details['name']}") - print(f"TT Value: {details['tt_value']} PED") - print(f"Damage: {details.get('damage', 'N/A')}") - print(f"Range: {details.get('range', 'N/A')}m") - """ - if not self.api: - return None - - return self.api.nexus_get_item_details(item_id) - - def nexus_get_market_data(self, item_id: str) -> Optional[Dict[str, Any]]: - """Get market data for a specific item. - - Args: - item_id: The item's unique identifier - - Returns: - Dictionary with market data, or None if not found - - Example: - market = self.nexus_get_market_data("armatrix_lp-35") - if market: - print(f"Current markup: {market['current_markup']:.1f}%") - print(f"7-day avg: {market['avg_markup_7d']:.1f}%") - print(f"24h Volume: {market['volume_24h']}") - - # Check orders - for buy in market.get('buy_orders', [])[:5]: - print(f"Buy: {buy['price']} PED x {buy['quantity']}") - """ - if not self.api: - return None - - return self.api.nexus_get_market_data(item_id) - - def nexus_is_available(self) -> bool: - """Check if Nexus API is available. - - Returns: - True if Nexus API service is ready - """ - if not self.api: - return False - - return self.api.nexus_is_available() - - # ========== HTTP Client Methods ========== - - def http_get(self, url: str, cache_ttl: int = 300, headers: Dict[str, str] = None, **kwargs) -> Dict[str, Any]: - """Make an HTTP GET request with caching. - - Args: - url: The URL to fetch - cache_ttl: Cache TTL in seconds (default: 300 = 5 minutes) - headers: Additional headers - **kwargs: Additional arguments - - Returns: - Dict with 'status_code', 'headers', 'content', 'text', 'json', 'from_cache' - - Example: - response = self.http_get( - "https://api.example.com/data", - cache_ttl=600, - headers={'Accept': 'application/json'} - ) - if response['status_code'] == 200: - data = response['json'] - """ - if not self.api: - raise RuntimeError("API not available") - - # Get HTTP client from services - http_client = self.api.services.get('http') - if not http_client: - raise RuntimeError("HTTP client not available") - - return http_client.get(url, cache_ttl=cache_ttl, headers=headers, **kwargs) - - # ========== DataStore Methods ========== - - def save_data(self, key: str, data: Any) -> bool: - """Save data to persistent storage. - - Data is automatically scoped to this plugin and survives app restarts. - - Args: - key: Key to store data under - data: Data to store (must be JSON serializable) - - Returns: - True if saved successfully - - Example: - # Save plugin settings - self.save_data("settings", {"theme": "dark", "volume": 0.8}) - - # Save user progress - self.save_data("total_loot", {"ped": 150.50, "items": 42}) - """ - if not self.api: - return False - - data_store = self.api.services.get('data_store') - if not data_store: - print(f"[{self.name}] DataStore not available") - return False - - return data_store.save(self._plugin_id, key, data) - - def load_data(self, key: str, default: Any = None) -> Any: - """Load data from persistent storage. - - Args: - key: Key to load data from - default: Default value if key doesn't exist - - Returns: - Stored data or default value - - Example: - # Load settings with defaults - settings = self.load_data("settings", {"theme": "light", "volume": 1.0}) - - # Load progress - progress = self.load_data("total_loot", {"ped": 0, "items": 0}) - print(f"Total loot: {progress['ped']} PED") - """ - if not self.api: - return default - - data_store = self.api.services.get('data_store') - if not data_store: - return default - - return data_store.load(self._plugin_id, key, default) - - def delete_data(self, key: str) -> bool: - """Delete data from persistent storage. - - Args: - key: Key to delete - - Returns: - True if deleted (or didn't exist), False on error - """ - if not self.api: - return False - - data_store = self.api.services.get('data_store') - if not data_store: - return False - - return data_store.delete(self._plugin_id, key) - - def get_all_data_keys(self) -> List[str]: - """Get all data keys stored by this plugin. - - Returns: - List of key names - """ - if not self.api: - return [] - - data_store = self.api.services.get('data_store') - if not data_store: - return [] - - return data_store.get_all_keys(self._plugin_id) - - # ========== Window Manager Methods ========== - - def get_eu_window(self) -> Optional[Dict[str, Any]]: - """Get information about the Entropia Universe game window. - - Returns: - Dict with window info or None if not found: - - handle: Window handle (int) - - title: Window title (str) - - rect: (left, top, right, bottom) tuple - - width: Window width (int) - - height: Window height (int) - - visible: Whether window is visible (bool) - - Example: - window = self.get_eu_window() - if window: - print(f"EU window: {window['width']}x{window['height']}") - print(f"Position: {window['rect']}") - """ - if not self.api: - return None - - return self.api.get_eu_window() - - def is_eu_focused(self) -> bool: - """Check if Entropia Universe window is currently focused. - - Returns: - True if EU is the active window - - Example: - if self.is_eu_focused(): - # Safe to capture screenshot - screenshot = self.capture_screen() - """ - if not self.api: - return False - - return self.api.is_eu_focused() - - def is_eu_visible(self) -> bool: - """Check if Entropia Universe window is visible. - - Returns: - True if EU window is visible (not minimized) - """ - if not self.api: - return False - - return self.api.is_eu_visible() - - def bring_eu_to_front(self) -> bool: - """Bring Entropia Universe window to front and focus it. - - Returns: - True if successful - """ - if not self.api: - return False - - return self.api.bring_eu_to_front() - - # ========== Clipboard Methods ========== - - def copy_to_clipboard(self, text: str) -> bool: - """Copy text to system clipboard. - - Args: - text: Text to copy - - Returns: - True if successful - - Example: - # Copy coordinates - self.copy_to_clipboard("12345, 67890") - - # Copy calculation result - result = self.calculate_dpp(50, 100, 2.5) - self.copy_to_clipboard(f"DPP: {result:.2f}") - """ - if not self.api: - return False - - return self.api.copy_to_clipboard(text) - - def paste_from_clipboard(self) -> str: - """Paste text from system clipboard. - - Returns: - Clipboard content or empty string - - Example: - # Get pasted coordinates - coords = self.paste_from_clipboard() - if coords: - x, y = map(int, coords.split(",")) - """ - if not self.api: - return "" - - return self.api.paste_from_clipboard() - - def get_clipboard_history(self, limit: int = 10) -> List[Dict[str, str]]: - """Get recent clipboard history. - - Args: - limit: Maximum number of entries to return - - Returns: - List of clipboard entries with 'text', 'timestamp', 'source' - """ - if not self.api: - return [] - - return self.api.get_clipboard_history(limit) - - # ========== Notification Methods ========== - - def notify(self, title: str, message: str, notification_type: str = 'info', sound: bool = False, duration_ms: int = 5000) -> str: - """Show a toast notification. - - Args: - title: Notification title - message: Notification message - notification_type: 'info', 'warning', 'error', or 'success' - sound: Play notification sound - duration_ms: How long to show notification (default: 5000ms) - - Returns: - Notification ID - - Example: - # Info notification - self.notify("Session Started", "Tracking loot...") - - # Success with sound - self.notify("Global!", "You received 150 PED", notification_type='success', sound=True) - - # Warning - self.notify("Low Ammo", "Only 100 shots remaining", notification_type='warning') - - # Error - self.notify("Connection Failed", "Check your internet", notification_type='error', sound=True) - """ - if not self.api: - return "" - - return self.api.notify(title, message, notification_type, sound, duration_ms) - - def notify_info(self, title: str, message: str, sound: bool = False) -> str: - """Show info notification (convenience method).""" - return self.notify(title, message, 'info', sound) - - def notify_success(self, title: str, message: str, sound: bool = False) -> str: - """Show success notification (convenience method).""" - return self.notify(title, message, 'success', sound) - - def notify_warning(self, title: str, message: str, sound: bool = False) -> str: - """Show warning notification (convenience method).""" - return self.notify(title, message, 'warning', sound) - - def notify_error(self, title: str, message: str, sound: bool = True) -> str: - """Show error notification (convenience method).""" - return self.notify(title, message, 'error', sound) - - def close_notification(self, notification_id: str) -> bool: - """Close a specific notification. - - Args: - notification_id: ID returned by notify() - - Returns: - True if closed - """ - if not self.api: - return False - - return self.api.close_notification(notification_id) - - def close_all_notifications(self) -> None: - """Close all visible notifications.""" - if not self.api: - return - - self.api.close_all_notifications() - - # ========== Settings Methods ========== - - def get_setting(self, key: str, default: Any = None) -> Any: - """Get a global EU-Utility setting. - - These are user preferences that apply across all plugins. - - Args: - key: Setting key - default: Default value if not set - - Returns: - Setting value - - Available settings: - - theme: 'dark', 'light', or 'auto' - - overlay_opacity: float 0.0-1.0 - - icon_size: 'small', 'medium', 'large' - - minimize_to_tray: bool - - show_tooltips: bool - - global_hotkeys: Dict of hotkey mappings - """ - if not self.api: - return default - - settings = self.api.services.get('settings') - if not settings: - return default - - return settings.get(key, default) - - def set_setting(self, key: str, value: Any) -> bool: - """Set a global EU-Utility setting. - - Args: - key: Setting key - value: Value to set - - Returns: - True if saved - """ - if not self.api: - return False - - settings = self.api.services.get('settings') - if not settings: - return False - - return settings.set(key, value) - - # ========== Logging Methods ========== - - def log_debug(self, message: str) -> None: - """Log debug message (development only).""" - print(f"[DEBUG][{self.name}] {message}") - - def log_info(self, message: str) -> None: - """Log info message.""" - print(f"[INFO][{self.name}] {message}") - - def log_warning(self, message: str) -> None: - """Log warning message.""" - print(f"[WARNING][{self.name}] {message}") - - def log_error(self, message: str) -> None: - """Log error message.""" - print(f"[ERROR][{self.name}] {message}") diff --git a/plugins/calculator/__init__.py b/plugins/calculator/__init__.py deleted file mode 100644 index b5ba8bf..0000000 --- a/plugins/calculator/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Calculator Plugin for EU-Utility -""" - -from .plugin import CalculatorPlugin - -__all__ = ["CalculatorPlugin"] diff --git a/plugins/calculator/plugin.py b/plugins/calculator/plugin.py deleted file mode 100644 index 2514914..0000000 --- a/plugins/calculator/plugin.py +++ /dev/null @@ -1,386 +0,0 @@ -""" -EU-Utility - Calculator Plugin - -Standard calculator with Windows-style layout. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, - QLineEdit, QPushButton, QLabel, QGridLayout -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin - - -class CalculatorPlugin(BasePlugin): - """Standard calculator with Windows-style layout.""" - - name = "Calculator" - version = "1.1.0" - author = "ImpulsiveFPS" - description = "Standard calculator" - hotkey = "ctrl+shift+c" - - def initialize(self): - """Setup calculator.""" - self.current_value = "0" - self.stored_value = None - self.pending_op = None - self.memory = 0 - self.start_new = True - - def get_ui(self): - """Create calculator UI with Windows layout.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(2) - - # Title - title = QLabel("๐Ÿงฎ Calculator") - title.setStyleSheet("color: #4a9eff; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Display - self.display = QLineEdit("0") - self.display.setAlignment(Qt.AlignmentFlag.AlignRight) - self.display.setStyleSheet(""" - QLineEdit { - background-color: #1a1a1a; - color: white; - font-size: 32px; - font-family: 'Segoe UI', Arial; - padding: 15px; - border: none; - border-radius: 4px; - } - """) - self.display.setReadOnly(True) - layout.addWidget(self.display) - - # Button grid - Windows Calculator Layout - grid = QGridLayout() - grid.setSpacing(2) - - # Row 1: Memory buttons - mem_buttons = ['MC', 'MR', 'M+', 'M-', 'MS', 'M~'] - for i, btn_text in enumerate(mem_buttons): - btn = self._create_button(btn_text, "#3a3a3a") - btn.clicked.connect(lambda checked, t=btn_text: self._on_memory(t)) - grid.addWidget(btn, 0, i) - - # Row 2: %, CE, C, โŒซ - row2 = ['%', 'CE', 'C', 'โŒซ'] - for i, btn_text in enumerate(row2): - btn = self._create_button(btn_text, "#3a3a3a") - btn.clicked.connect(lambda checked, t=btn_text: self._on_special(t)) - grid.addWidget(btn, 1, i) - - # Row 3: ยน/โ‚“, xยฒ, ยฒโˆšx, รท - row3 = [('ยน/โ‚“', '1/x'), ('xยฒ', 'sq'), ('ยฒโˆšx', 'sqrt'), 'รท'] - for i, item in enumerate(row3): - if isinstance(item, tuple): - text, op = item - else: - text = op = item - btn = self._create_button(text, "#3a3a3a") - btn.clicked.connect(lambda checked, o=op: self._on_operator(o)) - grid.addWidget(btn, 2, i) - - # Row 4: 7, 8, 9, ร— - row4 = ['7', '8', '9', 'ร—'] - for i, btn_text in enumerate(row4): - btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['ร—']) - if btn_text == 'ร—': - btn.clicked.connect(lambda checked: self._on_operator('*')) - else: - btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t)) - grid.addWidget(btn, 3, i) - - # Row 5: 4, 5, 6, - - row5 = ['4', '5', '6', '-'] - for i, btn_text in enumerate(row5): - btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['-']) - if btn_text == '-': - btn.clicked.connect(lambda checked: self._on_operator('-')) - else: - btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t)) - grid.addWidget(btn, 4, i) - - # Row 6: 1, 2, 3, + - row6 = ['1', '2', '3', '+'] - for i, btn_text in enumerate(row6): - btn = self._create_button(btn_text, "#2a2a2a", is_number=btn_text not in ['+']) - if btn_text == '+': - btn.clicked.connect(lambda checked: self._on_operator('+')) - else: - btn.clicked.connect(lambda checked, t=btn_text: self._on_number(t)) - grid.addWidget(btn, 5, i) - - # Row 7: +/-, 0, ., = - row7 = [('ยฑ', '+/-'), '0', '.', '='] - for i, item in enumerate(row7): - if isinstance(item, tuple): - text, val = item - else: - text = val = item - - if val == '=': - btn = self._create_button(text, "#0078d4", text_color="white") # Blue equals - else: - btn = self._create_button(text, "#2a2a2a", is_number=True) - - if val == '+/-': - btn.clicked.connect(self._on_negate) - elif val == '.': - btn.clicked.connect(self._on_decimal) - elif val == '=': - btn.clicked.connect(self._on_equals) - else: - btn.clicked.connect(lambda checked, t=val: self._on_number(t)) - - grid.addWidget(btn, 6, i) - - # Set column stretch - for i in range(4): - grid.setColumnStretch(i, 1) - - # Set row stretch - for i in range(7): - grid.setRowStretch(i, 1) - - layout.addLayout(grid) - layout.addStretch() - - return widget - - def _create_button(self, text, bg_color, is_number=False, text_color="#ffffff"): - """Create a calculator button.""" - btn = QPushButton(text) - btn.setMinimumSize(60, 45) - - if is_number: - font_size = "18px" - font_weight = "normal" - else: - font_size = "14px" - font_weight = "normal" - - btn.setStyleSheet(f""" - QPushButton {{ - background-color: {bg_color}; - color: {text_color}; - font-size: {font_size}; - font-weight: {font_weight}; - border: none; - border-radius: 4px; - }} - QPushButton:hover {{ - background-color: {self._lighten(bg_color)}; - }} - QPushButton:pressed {{ - background-color: {self._darken(bg_color)}; - }} - """) - - return btn - - def _lighten(self, color): - """Lighten a hex color slightly.""" - # Simple approximation - increase each component - if color == "#2a2a2a": - return "#3a3a3a" - elif color == "#3a3a3a": - return "#4a4a4a" - elif color == "#0078d4": - return "#1084e0" - return color - - def _darken(self, color): - """Darken a hex color slightly.""" - if color == "#2a2a2a": - return "#1a1a1a" - elif color == "#3a3a3a": - return "#2a2a2a" - elif color == "#0078d4": - return "#006cbd" - return color - - def _on_number(self, num): - """Handle number button press.""" - if self.start_new: - self.current_value = num - self.start_new = False - else: - if self.current_value == "0": - self.current_value = num - else: - self.current_value += num - self._update_display() - - def _on_decimal(self): - """Handle decimal point.""" - if self.start_new: - self.current_value = "0." - self.start_new = False - elif "." not in self.current_value: - self.current_value += "." - self._update_display() - - def _on_operator(self, op): - """Handle operator button.""" - try: - current = float(self.current_value) - - if op == "1/x": - result = 1 / current - self.current_value = self._format_result(result) - self.start_new = True - elif op == "sq": - result = current ** 2 - self.current_value = self._format_result(result) - self.start_new = True - elif op == "sqrt": - import math - result = math.sqrt(current) - self.current_value = self._format_result(result) - self.start_new = True - else: - # Binary operators - if self.pending_op and not self.start_new: - self._calculate() - - self.stored_value = float(self.current_value) - self.pending_op = op - self.start_new = True - - self._update_display() - except Exception: - self.current_value = "Error" - self._update_display() - self.start_new = True - - def _on_special(self, op): - """Handle special buttons (%, CE, C, backspace).""" - if op == 'C': - # Clear all - self.current_value = "0" - self.stored_value = None - self.pending_op = None - self.start_new = True - elif op == 'CE': - # Clear entry - self.current_value = "0" - self.start_new = True - elif op == 'โŒซ': - # Backspace - if len(self.current_value) > 1: - self.current_value = self.current_value[:-1] - else: - self.current_value = "0" - elif op == '%': - # Percent - try: - result = float(self.current_value) / 100 - self.current_value = self._format_result(result) - self.start_new = True - except: - self.current_value = "Error" - self.start_new = True - - self._update_display() - - def _on_memory(self, op): - """Handle memory operations.""" - try: - current = float(self.current_value) - - if op == 'MC': - self.memory = 0 - elif op == 'MR': - self.current_value = self._format_result(self.memory) - self.start_new = True - elif op == 'M+': - self.memory += current - elif op == 'M-': - self.memory -= current - elif op == 'MS': - self.memory = current - elif op == 'M~': - # Memory clear (same as MC) - self.memory = 0 - - self._update_display() - except: - pass - - def _on_negate(self): - """Toggle sign.""" - try: - current = float(self.current_value) - result = -current - self.current_value = self._format_result(result) - self._update_display() - except: - pass - - def _on_equals(self): - """Calculate result.""" - self._calculate() - self.pending_op = None - self.stored_value = None - self.start_new = True - - def _calculate(self): - """Perform pending calculation.""" - if self.pending_op and self.stored_value is not None: - try: - current = float(self.current_value) - - if self.pending_op == '+': - result = self.stored_value + current - elif self.pending_op == '-': - result = self.stored_value - current - elif self.pending_op == '*': - result = self.stored_value * current - elif self.pending_op == 'รท': - if current != 0: - result = self.stored_value / current - else: - result = "Error" - else: - return - - self.current_value = self._format_result(result) - self._update_display() - except: - self.current_value = "Error" - self._update_display() - - def _format_result(self, result): - """Format calculation result.""" - if isinstance(result, str): - return result - - # Check if it's essentially an integer - if result == int(result): - return str(int(result)) - - # Format with reasonable precision - formatted = f"{result:.10f}" - # Remove trailing zeros - formatted = formatted.rstrip('0').rstrip('.') - - # Limit length - if len(formatted) > 12: - formatted = f"{result:.6e}" - - return formatted - - def _update_display(self): - """Update the display.""" - self.display.setText(self.current_value) - - def on_hotkey(self): - """Focus calculator when hotkey pressed.""" - pass diff --git a/plugins/chat_logger/__init__.py b/plugins/chat_logger/__init__.py deleted file mode 100644 index 5e50812..0000000 --- a/plugins/chat_logger/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Chat Logger Plugin -""" - -from .plugin import ChatLoggerPlugin - -__all__ = ["ChatLoggerPlugin"] diff --git a/plugins/chat_logger/plugin.py b/plugins/chat_logger/plugin.py deleted file mode 100644 index 27278f6..0000000 --- a/plugins/chat_logger/plugin.py +++ /dev/null @@ -1,285 +0,0 @@ -""" -EU-Utility - Chat Logger Plugin - -Log and search chat messages with filters. -""" - -import re -import json -from datetime import datetime, timedelta -from pathlib import Path -from collections import deque - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTextEdit, QLineEdit, QComboBox, - QCheckBox, QFrame -) -from PyQt6.QtCore import Qt, QTimer - -from plugins.base_plugin import BasePlugin -from core.icon_manager import get_icon_manager - - -class ChatLoggerPlugin(BasePlugin): - """Log and search chat messages.""" - - name = "Chat Logger" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Log, search, and filter chat messages" - hotkey = "ctrl+shift+t" # T for chaT - - # Chat channels - CHANNELS = { - 'main': 'Main', - 'society': 'Society', - 'team': 'Team', - 'local': 'Local', - 'global': 'Global', - 'trade': 'Trade', - 'private': 'Private', - } - - def initialize(self): - """Setup chat logger.""" - self.data_file = Path("data/chat_log.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - # Keep last 10000 messages in memory - self.messages = deque(maxlen=10000) - self.filters = { - 'show_main': True, - 'show_society': True, - 'show_team': True, - 'show_local': True, - 'show_global': True, - 'show_trade': True, - 'show_private': True, - 'search_text': '', - 'show_globals_only': False, - 'show_loot': False, - } - - self._load_recent() - - def _load_recent(self): - """Load recent messages.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.messages.extend(data.get('messages', [])[-1000:]) - except: - pass - - def _save_messages(self): - """Save messages to file.""" - # Keep last 24 hours - cutoff = (datetime.now() - timedelta(hours=24)).isoformat() - recent = [m for m in self.messages if m['time'] > cutoff] - - with open(self.data_file, 'w') as f: - json.dump({'messages': recent}, f) - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(10) - layout.setContentsMargins(0, 0, 0, 0) - - # Get icon manager - icon_mgr = get_icon_manager() - - # Title with icon - title_layout = QHBoxLayout() - - title_icon = QLabel() - icon_pixmap = icon_mgr.get_pixmap('message-square', size=20) - title_icon.setPixmap(icon_pixmap) - title_icon.setFixedSize(20, 20) - title_layout.addWidget(title_icon) - - title = QLabel("Chat Logger") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - title_layout.addWidget(title) - title_layout.addStretch() - - layout.addLayout(title_layout) - - # Search bar - search_layout = QHBoxLayout() - - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Search messages...") - self.search_input.setStyleSheet(""" - QLineEdit { - background-color: rgba(255, 255, 255, 15); - color: white; - border: 1px solid rgba(255, 255, 255, 30); - border-radius: 6px; - padding: 8px; - } - """) - self.search_input.textChanged.connect(self._update_filter) - search_layout.addWidget(self.search_input) - - search_btn = QPushButton("๐Ÿ”") - search_btn.setFixedSize(32, 32) - search_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255, 255, 255, 15); - border: none; - border-radius: 6px; - font-size: 14px; - } - QPushButton:hover { - background-color: rgba(255, 255, 255, 30); - } - """) - search_layout.addWidget(search_btn) - - layout.addLayout(search_layout) - - # Filters - filters_frame = QFrame() - filters_frame.setStyleSheet(""" - QFrame { - background-color: rgba(0, 0, 0, 50); - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 20); - } - """) - filters_layout = QHBoxLayout(filters_frame) - filters_layout.setContentsMargins(10, 6, 10, 6) - - # Channel filters - self.filter_checks = {} - for channel_id, channel_name in self.CHANNELS.items(): - cb = QCheckBox(channel_name) - cb.setChecked(True) - cb.setStyleSheet("color: rgba(255, 255, 255, 180); font-size: 10px;") - cb.stateChanged.connect(self._update_filter) - self.filter_checks[channel_id] = cb - filters_layout.addWidget(cb) - - filters_layout.addStretch() - layout.addWidget(filters_frame) - - # Chat display - self.chat_display = QTextEdit() - self.chat_display.setReadOnly(True) - self.chat_display.setStyleSheet(""" - QTextEdit { - background-color: rgba(20, 25, 35, 150); - color: white; - border: 1px solid rgba(255, 255, 255, 20); - border-radius: 8px; - padding: 10px; - font-family: Consolas, monospace; - font-size: 11px; - } - """) - layout.addWidget(self.chat_display) - - # Stats - self.stats_label = QLabel("Messages: 0") - self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 10px;") - layout.addWidget(self.stats_label) - - # Refresh display - self._refresh_display() - - return widget - - def _update_filter(self): - """Update filter settings.""" - self.filters['search_text'] = self.search_input.text().lower() - - for channel_id, cb in self.filter_checks.items(): - self.filters[f'show_{channel_id}'] = cb.isChecked() - - self._refresh_display() - - def _refresh_display(self): - """Refresh chat display.""" - html = [] - - for msg in reversed(self.messages): - # Apply filters - channel = msg.get('channel', 'main') - if not self.filters.get(f'show_{channel}', True): - continue - - text = msg.get('text', '') - if self.filters['search_text']: - if self.filters['search_text'] not in text.lower(): - continue - - # Format message - time_str = msg['time'][11:16] if msg['time'] else '--:--' - author = msg.get('author', 'Unknown') - - # Color by channel - colors = { - 'main': '#ffffff', - 'society': '#9c27b0', - 'team': '#4caf50', - 'local': '#ffc107', - 'global': '#f44336', - 'trade': '#ff9800', - 'private': '#00bcd4', - } - color = colors.get(channel, '#ffffff') - - html.append(f''' -
- [{time_str}] - {author}: - {text} -
- ''') - - self.chat_display.setHtml(''.join(html[:100])) # Show last 100 - self.stats_label.setText(f"Messages: {len(self.messages)}") - - def parse_chat_message(self, message, channel='main', author='Unknown'): - """Parse and log chat message.""" - entry = { - 'time': datetime.now().isoformat(), - 'channel': channel, - 'author': author, - 'text': message, - } - - self.messages.append(entry) - self._refresh_display() - - # Auto-save periodically - if len(self.messages) % 100 == 0: - self._save_messages() - - def search(self, query): - """Search chat history.""" - results = [] - query_lower = query.lower() - - for msg in self.messages: - if query_lower in msg.get('text', '').lower(): - results.append(msg) - - return results - - def get_globals(self): - """Get global messages.""" - return [m for m in self.messages if m.get('channel') == 'global'] - - def get_loot_messages(self): - """Get loot-related messages.""" - loot_keywords = ['received', 'loot', 'item', 'ped'] - return [ - m for m in self.messages - if any(kw in m.get('text', '').lower() for kw in loot_keywords) - ] diff --git a/plugins/codex_tracker/__init__.py b/plugins/codex_tracker/__init__.py deleted file mode 100644 index dcba8fd..0000000 --- a/plugins/codex_tracker/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Codex Tracker Plugin -""" - -from .plugin import CodexTrackerPlugin - -__all__ = ["CodexTrackerPlugin"] diff --git a/plugins/codex_tracker/plugin.py b/plugins/codex_tracker/plugin.py deleted file mode 100644 index e36aed2..0000000 --- a/plugins/codex_tracker/plugin.py +++ /dev/null @@ -1,218 +0,0 @@ -""" -EU-Utility - Codex Tracker Plugin - -Track creature challenge progress from Codex. -""" - -import json -from pathlib import Path -from datetime import datetime - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QProgressBar, QTableWidget, QTableWidgetItem, - QComboBox, QFrame -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin - - -class CodexTrackerPlugin(BasePlugin): - """Track creature codex progress.""" - - name = "Codex Tracker" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Track creature challenges and codex progress" - hotkey = "ctrl+shift+x" - - # Arkadia creatures from screenshot - CREATURES = [ - {"name": "Bokol", "rank": 22, "progress": 49.5}, - {"name": "Nusul", "rank": 15, "progress": 24.8}, - {"name": "Wombana", "rank": 10, "progress": 45.2}, - {"name": "Arkadian Hornet", "rank": 1, "progress": 15.0}, - {"name": "Feran", "rank": 4, "progress": 86.0}, - {"name": "Hadraada", "rank": 6, "progress": 0.4}, - {"name": "Halix", "rank": 14, "progress": 0.2}, - {"name": "Huon", "rank": 1, "progress": 45.8}, - {"name": "Kadra", "rank": 2, "progress": 0.7}, - {"name": "Magurg", "rank": 1, "progress": 8.7}, - {"name": "Monura", "rank": 1, "progress": 5.2}, - ] - - def initialize(self): - """Setup codex tracker.""" - self.data_file = Path("data/codex.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.creatures = self.CREATURES.copy() - self.scanned_data = {} - - self._load_data() - - def _load_data(self): - """Load codex data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.creatures = data.get('creatures', self.CREATURES) - except: - pass - - def _save_data(self): - """Save codex data.""" - with open(self.data_file, 'w') as f: - json.dump({'creatures': self.creatures}, f, indent=2) - - def get_ui(self): - """Create codex tracker UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("๐Ÿ“– Codex Tracker") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Summary - summary = QHBoxLayout() - - total_creatures = len(self.creatures) - completed = sum(1 for c in self.creatures if c.get('progress', 0) >= 100) - - self.total_label = QLabel(f"Creatures: {total_creatures}") - self.total_label.setStyleSheet("color: #4a9eff; font-size: 13px;") - summary.addWidget(self.total_label) - - self.completed_label = QLabel(f"Completed: {completed}") - self.completed_label.setStyleSheet("color: #4caf50; font-size: 13px;") - summary.addWidget(self.completed_label) - - summary.addStretch() - layout.addLayout(summary) - - # Scan button - scan_btn = QPushButton("Scan Codex Window") - scan_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 12px; - border: none; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: #ffa060; - } - """) - scan_btn.clicked.connect(self._scan_codex) - layout.addWidget(scan_btn) - - # Creatures list - self.creatures_table = QTableWidget() - self.creatures_table.setColumnCount(4) - self.creatures_table.setHorizontalHeaderLabels(["Creature", "Rank", "Progress", "Next Rank"]) - self.creatures_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - QHeaderView::section { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,180); - padding: 8px; - border: none; - font-weight: bold; - font-size: 11px; - } - """) - self.creatures_table.horizontalHeader().setStretchLastSection(True) - layout.addWidget(self.creatures_table) - - self._refresh_table() - - layout.addStretch() - return widget - - def _refresh_table(self): - """Refresh creatures table.""" - self.creatures_table.setRowCount(len(self.creatures)) - - for i, creature in enumerate(sorted(self.creatures, key=lambda x: -x.get('progress', 0))): - # Name - name_item = QTableWidgetItem(creature.get('name', 'Unknown')) - name_item.setFlags(name_item.flags() & ~Qt.ItemFlag.ItemIsEditable) - self.creatures_table.setItem(i, 0, name_item) - - # Rank - rank = creature.get('rank', 0) - rank_item = QTableWidgetItem(str(rank)) - rank_item.setFlags(rank_item.flags() & ~Qt.ItemFlag.ItemIsEditable) - rank_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) - self.creatures_table.setItem(i, 1, rank_item) - - # Progress bar - progress = creature.get('progress', 0) - progress_widget = QWidget() - progress_layout = QHBoxLayout(progress_widget) - progress_layout.setContentsMargins(5, 2, 5, 2) - - bar = QProgressBar() - bar.setValue(int(progress)) - bar.setTextVisible(True) - bar.setFormat(f"{progress:.1f}%") - bar.setStyleSheet(""" - QProgressBar { - background-color: rgba(60, 70, 90, 150); - border: none; - border-radius: 3px; - text-align: center; - color: white; - font-size: 10px; - } - QProgressBar::chunk { - background-color: #ff8c42; - border-radius: 3px; - } - """) - progress_layout.addWidget(bar) - - self.creatures_table.setCellWidget(i, 2, progress_widget) - - # Next rank (estimated kills needed) - progress_item = QTableWidgetItem(f"~{int((100-progress) * 120)} kills") - progress_item.setFlags(progress_item.flags() & ~Qt.ItemFlag.ItemIsEditable) - progress_item.setForeground(Qt.GlobalColor.gray) - self.creatures_table.setItem(i, 3, progress_item) - - def _scan_codex(self): - """Scan codex window with OCR.""" - # TODO: Implement OCR scanning - # For now, simulate update - for creature in self.creatures: - creature['progress'] = min(100, creature.get('progress', 0) + 0.5) - - self._save_data() - self._refresh_table() - - def get_closest_to_rankup(self, count=3): - """Get creatures closest to ranking up.""" - sorted_creatures = sorted( - self.creatures, - key=lambda x: -(x.get('progress', 0)) - ) - return [c for c in sorted_creatures[:count] if c.get('progress', 0) < 100] - - def get_recommended_hunt(self): - """Get recommended creature to hunt.""" - close = self.get_closest_to_rankup(1) - return close[0] if close else None diff --git a/plugins/crafting_calc/__init__.py b/plugins/crafting_calc/__init__.py deleted file mode 100644 index 371956a..0000000 --- a/plugins/crafting_calc/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Crafting Calculator Plugin -""" - -from .plugin import CraftingCalculatorPlugin - -__all__ = ["CraftingCalculatorPlugin"] diff --git a/plugins/crafting_calc/plugin.py b/plugins/crafting_calc/plugin.py deleted file mode 100644 index bd79a00..0000000 --- a/plugins/crafting_calc/plugin.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -EU-Utility - Crafting Calculator Plugin - -Calculate crafting success rates and material costs. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QComboBox, QLineEdit, QTableWidget, - QTableWidgetItem, QFrame, QGroupBox -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin - - -class CraftingCalculatorPlugin(BasePlugin): - """Calculate crafting costs and success rates.""" - - name = "Crafting Calc" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Crafting success rates and material costs" - hotkey = "ctrl+shift+b" # B for Blueprint - - # Sample blueprints data - BLUEPRINTS = { - 'Weapon': [ - 'ArMatrix LP-35 (L)', - 'ArMatrix BP-25 (L)', - 'Omegaton M83 Predator', - ], - 'Armor': [ - 'Vigiator Harness (M)', - 'Vigiator Thighs (M)', - ], - 'Tool': [ - 'Ziplex Z1 Seeker', - 'Ziplex Z3 Seeker', - ], - 'Material': [ - 'Metal Residue', - 'Energy Matter Residue', - ], - } - - def initialize(self): - """Setup crafting calculator.""" - self.saved_recipes = [] - - def get_ui(self): - """Create crafting calculator UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("๐Ÿ”จ Crafting Calculator") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Blueprint selector - bp_group = QGroupBox("Blueprint") - bp_group.setStyleSheet(self._group_style()) - bp_layout = QVBoxLayout(bp_group) - - # Category - cat_layout = QHBoxLayout() - cat_layout.addWidget(QLabel("Category:")) - self.cat_combo = QComboBox() - self.cat_combo.addItems(list(self.BLUEPRINTS.keys())) - self.cat_combo.currentTextChanged.connect(self._update_blueprints) - cat_layout.addWidget(self.cat_combo) - bp_layout.addLayout(cat_layout) - - # Blueprint - bp_layout2 = QHBoxLayout() - bp_layout2.addWidget(QLabel("Blueprint:")) - self.bp_combo = QComboBox() - self._update_blueprints(self.cat_combo.currentText()) - bp_layout2.addWidget(self.bp_combo) - bp_layout.addLayout(bp_layout2) - - # QR - qr_layout = QHBoxLayout() - qr_layout.addWidget(QLabel("QR:")) - self.qr_input = QLineEdit() - self.qr_input.setPlaceholderText("1.0") - self.qr_input.setText("1.0") - qr_layout.addWidget(self.qr_input) - bp_layout.addLayout(qr_layout) - - layout.addWidget(bp_group) - - # Materials - mat_group = QGroupBox("Materials") - mat_group.setStyleSheet(self._group_style()) - mat_layout = QVBoxLayout(mat_group) - - self.mat_table = QTableWidget() - self.mat_table.setColumnCount(4) - self.mat_table.setHorizontalHeaderLabels(["Material", "Needed", "Have", "Buy"]) - self.mat_table.setRowCount(3) - - sample_mats = [ - ("Lysterium Ingot", 50, 0), - ("Oil", 30, 10), - ("Meldar Paper", 10, 5), - ] - - for i, (mat, needed, have) in enumerate(sample_mats): - self.mat_table.setItem(i, 0, QTableWidgetItem(mat)) - self.mat_table.setItem(i, 1, QTableWidgetItem(str(needed))) - self.mat_table.setItem(i, 2, QTableWidgetItem(str(have))) - buy = needed - have if needed > have else 0 - self.mat_table.setItem(i, 3, QTableWidgetItem(str(buy))) - - self.mat_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: none; - } - QHeaderView::section { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,180); - padding: 6px; - font-size: 10px; - } - """) - mat_layout.addWidget(self.mat_table) - - layout.addWidget(mat_group) - - # Calculator - calc_group = QGroupBox("Calculator") - calc_group.setStyleSheet(self._group_style()) - calc_layout = QVBoxLayout(calc_group) - - # Click calculator - click_layout = QHBoxLayout() - click_layout.addWidget(QLabel("Clicks:")) - self.clicks_input = QLineEdit() - self.clicks_input.setPlaceholderText("10") - self.clicks_input.setText("10") - click_layout.addWidget(self.clicks_input) - calc_layout.addLayout(click_layout) - - calc_btn = QPushButton("Calculate") - calc_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 10px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - calc_btn.clicked.connect(self._calculate) - calc_layout.addWidget(calc_btn) - - # Results - self.result_label = QLabel("Success Rate: ~45%") - self.result_label.setStyleSheet("color: #4caf50; font-weight: bold;") - calc_layout.addWidget(self.result_label) - - self.cost_label = QLabel("Estimated Cost: 15.50 PED") - self.cost_label.setStyleSheet("color: #ffc107;") - calc_layout.addWidget(self.cost_label) - - layout.addWidget(calc_group) - layout.addStretch() - - return widget - - def _group_style(self): - 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; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px; - } - """ - - def _update_blueprints(self, category): - """Update blueprint list.""" - self.bp_combo.clear() - self.bp_combo.addItems(self.BLUEPRINTS.get(category, [])) - - def _calculate(self): - """Calculate crafting results.""" - try: - qr = float(self.qr_input.text() or 1.0) - clicks = int(self.clicks_input.text() or 10) - - # Simple formula (real one is more complex) - base_rate = 0.45 - qr_bonus = (qr - 1.0) * 0.05 - success_rate = min(0.95, base_rate + qr_bonus) - - expected_success = int(clicks * success_rate) - - self.result_label.setText( - f"Success Rate: ~{success_rate*100:.1f}% | " - f"Expected: {expected_success}/{clicks}" - ) - - except Exception as e: - self.result_label.setText(f"Error: {e}") 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/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/discord_presence/__init__.py b/plugins/discord_presence/__init__.py deleted file mode 100644 index 977afb9..0000000 --- a/plugins/discord_presence/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .plugin import DiscordPresencePlugin - -__all__ = ['DiscordPresencePlugin'] diff --git a/plugins/discord_presence/plugin.py b/plugins/discord_presence/plugin.py deleted file mode 100644 index b834e66..0000000 --- a/plugins/discord_presence/plugin.py +++ /dev/null @@ -1,217 +0,0 @@ -# Description: Discord Rich Presence plugin for EU-Utility -# Author: LemonNexus -# Version: 1.0.0 - -""" -Discord Rich Presence plugin for EU-Utility. -Shows current Entropia Universe activity in Discord. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QLineEdit, QCheckBox, QComboBox -) -from PyQt6.QtCore import QTimer -from plugins.base_plugin import BasePlugin -import time -import threading - - -class DiscordPresencePlugin(BasePlugin): - """Discord Rich Presence integration for EU-Utility.""" - - name = "Discord Presence" - version = "1.0.0" - author = "LemonNexus" - description = "Show EU activity in Discord status" - icon = "message-square" - - def initialize(self): - """Initialize Discord RPC.""" - self.enabled = self.load_data("enabled", False) - self.client_id = self.load_data("client_id", "") - self.show_details = self.load_data("show_details", True) - self.rpc = None - self.connected = False - - # Try to import pypresence - try: - from pypresence import Presence - self.Presence = Presence - self.available = True - except ImportError: - self.log_warning("pypresence not installed. Run: pip install pypresence") - self.available = False - return - - # Auto-connect if enabled - if self.enabled and self.client_id: - self._connect() - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(15) - - # Title - title = QLabel("Discord Rich Presence") - title.setStyleSheet("font-size: 18px; font-weight: bold; color: #5865F2;") - layout.addWidget(title) - - # Status - self.status_label = QLabel("Status: " + ("Connected" if self.connected else "Disconnected")) - self.status_label.setStyleSheet( - f"color: {'#43b581' if self.connected else '#f04747'}; font-weight: bold;" - ) - layout.addWidget(self.status_label) - - # Enable checkbox - self.enable_checkbox = QCheckBox("Enable Discord Presence") - self.enable_checkbox.setChecked(self.enabled) - self.enable_checkbox.toggled.connect(self._on_enable_toggled) - layout.addWidget(self.enable_checkbox) - - # Client ID input - id_layout = QHBoxLayout() - id_layout.addWidget(QLabel("Client ID:")) - self.id_input = QLineEdit(self.client_id) - self.id_input.setPlaceholderText("Your Discord Application Client ID") - id_layout.addWidget(self.id_input) - layout.addLayout(id_layout) - - # Show details - self.details_checkbox = QCheckBox("Show detailed activity") - self.details_checkbox.setChecked(self.show_details) - self.details_checkbox.toggled.connect(self._on_details_toggled) - layout.addWidget(self.details_checkbox) - - # Activity type - layout.addWidget(QLabel("Activity Type:")) - self.activity_combo = QComboBox() - self.activity_combo.addItems(["Playing", "Hunting", "Mining", "Crafting", "Trading"]) - self.activity_combo.currentTextChanged.connect(self._update_presence) - layout.addWidget(self.activity_combo) - - # Buttons - btn_layout = QHBoxLayout() - - connect_btn = QPushButton("Connect") - connect_btn.clicked.connect(self._connect) - btn_layout.addWidget(connect_btn) - - disconnect_btn = QPushButton("Disconnect") - disconnect_btn.clicked.connect(self._disconnect) - btn_layout.addWidget(disconnect_btn) - - update_btn = QPushButton("Update Presence") - update_btn.clicked.connect(self._update_presence) - btn_layout.addWidget(update_btn) - - layout.addLayout(btn_layout) - - # Info - info = QLabel( - "To use this feature:\n" - "1. Create a Discord app at discord.com/developers\n" - "2. Copy the Client ID\n" - "3. Paste it above and click Connect" - ) - info.setStyleSheet("color: rgba(255,255,255,150); font-size: 11px;") - layout.addWidget(info) - - layout.addStretch() - return widget - - def _on_enable_toggled(self, checked): - """Handle enable toggle.""" - self.enabled = checked - self.save_data("enabled", checked) - if checked: - self._connect() - else: - self._disconnect() - - def _on_details_toggled(self, checked): - """Handle details toggle.""" - self.show_details = checked - self.save_data("show_details", checked) - self._update_presence() - - def _connect(self): - """Connect to Discord.""" - if not self.available: - self.log_error("pypresence not installed") - return - - client_id = self.id_input.text().strip() - if not client_id: - self.log_warning("No Client ID provided") - return - - self.client_id = client_id - self.save_data("client_id", client_id) - - try: - self.rpc = self.Presence(client_id) - self.rpc.connect() - self.connected = True - self.status_label.setText("Status: Connected") - self.status_label.setStyleSheet("color: #43b581; font-weight: bold;") - self._update_presence() - self.log_info("Discord RPC connected") - except Exception as e: - self.log_error(f"Failed to connect: {e}") - self.connected = False - - def _disconnect(self): - """Disconnect from Discord.""" - if self.rpc: - try: - self.rpc.close() - except: - pass - self.rpc = None - self.connected = False - self.status_label.setText("Status: Disconnected") - self.status_label.setStyleSheet("color: #f04747; font-weight: bold;") - self.log_info("Discord RPC disconnected") - - def _update_presence(self): - """Update Discord presence.""" - if not self.connected or not self.rpc: - return - - activity = self.activity_combo.currentText() - - try: - if self.show_details: - self.rpc.update( - state=f"In Entropia Universe", - details=f"Currently {activity}", - large_image="eu_logo", - large_text="Entropia Universe", - small_image="playing", - small_text=activity, - start=time.time() - ) - else: - self.rpc.update( - state="Playing Entropia Universe", - large_image="eu_logo", - large_text="Entropia Universe" - ) - self.log_info("Presence updated") - except Exception as e: - self.log_error(f"Failed to update presence: {e}") - - def on_show(self): - """Update UI when shown.""" - self.status_label.setText("Status: " + ("Connected" if self.connected else "Disconnected")) - self.status_label.setStyleSheet( - f"color: {'#43b581' if self.connected else '#f04747'}; font-weight: bold;" - ) - - def shutdown(self): - """Cleanup on shutdown.""" - self._disconnect() diff --git a/plugins/dpp_calculator/__init__.py b/plugins/dpp_calculator/__init__.py deleted file mode 100644 index 1130b97..0000000 --- a/plugins/dpp_calculator/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -DPP Calculator Plugin -""" - -from .plugin import DPPCalculatorPlugin - -__all__ = ["DPPCalculatorPlugin"] diff --git a/plugins/dpp_calculator/plugin.py b/plugins/dpp_calculator/plugin.py deleted file mode 100644 index 2d1bfb2..0000000 --- a/plugins/dpp_calculator/plugin.py +++ /dev/null @@ -1,230 +0,0 @@ -""" -EU-Utility - DPP Calculator Plugin - -Calculate Damage Per PEC for weapons and setups. -""" - -from decimal import Decimal, ROUND_HALF_UP -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QLineEdit, QPushButton, QComboBox, QTableWidget, - QTableWidgetItem, QFrame, QGroupBox -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin -from core.icon_manager import get_icon_manager - - -class DPPCalculatorPlugin(BasePlugin): - """Calculate weapon efficiency and DPP.""" - - name = "DPP Calculator" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Calculate Damage Per PEC and weapon efficiency" - hotkey = "ctrl+shift+d" - - def initialize(self): - """Setup DPP calculator.""" - self.saved_setups = [] - - def get_ui(self): - """Create DPP calculator UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Get icon manager - icon_mgr = get_icon_manager() - - # Title with icon - title_layout = QHBoxLayout() - - title_icon = QLabel() - icon_pixmap = icon_mgr.get_pixmap('target', size=20) - title_icon.setPixmap(icon_pixmap) - title_icon.setFixedSize(20, 20) - title_layout.addWidget(title_icon) - - title = QLabel("DPP Calculator") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - title_layout.addWidget(title) - title_layout.addStretch() - - layout.addLayout(title_layout) - - # Calculator section - calc_group = QGroupBox("Weapon Setup") - calc_group.setStyleSheet(""" - QGroupBox { - color: rgba(255,255,255,200); - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - margin-top: 10px; - font-weight: bold; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px; - } - """) - calc_layout = QVBoxLayout(calc_group) - - # Weapon damage - dmg_layout = QHBoxLayout() - dmg_layout.addWidget(QLabel("Damage:")) - self.dmg_input = QLineEdit() - self.dmg_input.setPlaceholderText("e.g., 45.2") - dmg_layout.addWidget(self.dmg_input) - calc_layout.addLayout(dmg_layout) - - # Ammo cost - ammo_layout = QHBoxLayout() - ammo_layout.addWidget(QLabel("Ammo per shot:")) - self.ammo_input = QLineEdit() - self.ammo_input.setPlaceholderText("e.g., 100") - ammo_layout.addWidget(self.ammo_input) - calc_layout.addLayout(ammo_layout) - - # Weapon decay - decay_layout = QHBoxLayout() - decay_layout.addWidget(QLabel("Decay (PEC):")) - self.decay_input = QLineEdit() - self.decay_input.setPlaceholderText("e.g., 2.5") - decay_layout.addWidget(self.decay_input) - calc_layout.addLayout(decay_layout) - - # Amp/scope - amp_layout = QHBoxLayout() - amp_layout.addWidget(QLabel("Amp decay:")) - self.amp_input = QLineEdit() - self.amp_input.setPlaceholderText("0") - self.amp_input.setText("0") - amp_layout.addWidget(self.amp_input) - calc_layout.addLayout(amp_layout) - - # Calculate button - calc_btn = QPushButton("Calculate DPP") - calc_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 10px; - border: none; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: #ffa060; - } - """) - calc_btn.clicked.connect(self._calculate) - calc_layout.addWidget(calc_btn) - - # Results - self.result_label = QLabel("DPP: -") - self.result_label.setStyleSheet(""" - color: #4caf50; - font-size: 20px; - font-weight: bold; - """) - calc_layout.addWidget(self.result_label) - - self.cost_label = QLabel("Cost per shot: -") - self.cost_label.setStyleSheet("color: rgba(255,255,255,150);") - calc_layout.addWidget(self.cost_label) - - layout.addWidget(calc_group) - - # Reference table - ref_group = QGroupBox("DPP Reference") - ref_group.setStyleSheet(calc_group.styleSheet()) - ref_layout = QVBoxLayout(ref_group) - - ref_table = QTableWidget() - ref_table.setColumnCount(3) - ref_table.setHorizontalHeaderLabels(["Rating", "DPP Range", "Efficiency"]) - ref_table.setRowCount(5) - - ratings = [ - ("Excellent", "4.0+", "95-100%"), - ("Very Good", "3.5-4.0", "85-95%"), - ("Good", "3.0-3.5", "75-85%"), - ("Average", "2.5-3.0", "60-75%"), - ("Poor", "< 2.5", "< 60%"), - ] - - for i, (rating, dpp, eff) in enumerate(ratings): - ref_table.setItem(i, 0, QTableWidgetItem(rating)) - ref_table.setItem(i, 1, QTableWidgetItem(dpp)) - ref_table.setItem(i, 2, QTableWidgetItem(eff)) - - ref_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: none; - } - QHeaderView::section { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,180); - padding: 6px; - font-size: 10px; - } - """) - ref_layout.addWidget(ref_table) - - layout.addWidget(ref_group) - layout.addStretch() - - return widget - - def _calculate(self): - """Calculate DPP.""" - try: - damage = Decimal(self.dmg_input.text() or 0) - ammo = Decimal(self.ammo_input.text() or 0) - decay = Decimal(self.decay_input.text() or 0) - amp = Decimal(self.amp_input.text() or 0) - - # Convert ammo to PEC (1 ammo = 0.0001 PED = 0.01 PEC) - ammo_cost = ammo * Decimal('0.01') - - # Total cost in PEC - total_cost = ammo_cost + decay + amp - - if total_cost > 0: - dpp = damage / (total_cost / Decimal('100')) # Convert to PED for DPP - - self.result_label.setText(f"DPP: {dpp:.3f}") - self.result_label.setStyleSheet(f""" - color: {'#4caf50' if dpp >= Decimal('3.5') else '#ffc107' if dpp >= Decimal('2.5') else '#f44336'}; - font-size: 20px; - font-weight: bold; - """) - - cost_ped = total_cost / Decimal('100') - self.cost_label.setText(f"Cost per shot: {cost_ped:.4f} PED ({total_cost:.2f} PEC)") - else: - self.result_label.setText("DPP: Enter values") - - except Exception as e: - self.result_label.setText(f"Error: {e}") - - def calculate_from_api(self, weapon_data): - """Calculate DPP from Nexus API weapon data.""" - damage = Decimal(str(weapon_data.get('damage', 0))) - decay = Decimal(str(weapon_data.get('decay', 0))) - ammo = Decimal(str(weapon_data.get('ammo', 0))) - - # Calculate - ammo_cost = ammo * Decimal('0.01') - total_cost = ammo_cost + decay - - if total_cost > 0: - return damage / (total_cost / Decimal('100')) - return Decimal('0') diff --git a/plugins/enhancer_calc/__init__.py b/plugins/enhancer_calc/__init__.py deleted file mode 100644 index 8758bb8..0000000 --- a/plugins/enhancer_calc/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Enhancer Calculator Plugin -""" - -from .plugin import EnhancerCalculatorPlugin - -__all__ = ["EnhancerCalculatorPlugin"] diff --git a/plugins/enhancer_calc/plugin.py b/plugins/enhancer_calc/plugin.py deleted file mode 100644 index a5cc975..0000000 --- a/plugins/enhancer_calc/plugin.py +++ /dev/null @@ -1,160 +0,0 @@ -""" -EU-Utility - Enhancer Calculator Plugin - -Calculate enhancer break rates and costs. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QLineEdit, QPushButton, QComboBox, QFrame -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin - - -class EnhancerCalculatorPlugin(BasePlugin): - """Calculate enhancer usage and costs.""" - - name = "Enhancer Calc" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Calculate enhancer break rates and costs" - hotkey = "ctrl+shift+e" - - # Break rates (approximate) - BREAK_RATES = { - 'Accuracy': 0.0012, # 0.12% - 'Damage': 0.0015, # 0.15% - 'Economy': 0.0010, # 0.10% - 'Range': 0.0013, # 0.13% - } - - def initialize(self): - """Setup enhancer calculator.""" - self.saved_calculations = [] - - def get_ui(self): - """Create enhancer calculator UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel(" Enhancer Calculator") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Calculator - calc_frame = QFrame() - calc_frame.setStyleSheet(""" - QFrame { - background-color: rgba(30, 35, 45, 200); - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - """) - calc_layout = QVBoxLayout(calc_frame) - calc_layout.setSpacing(10) - - # Enhancer type - type_layout = QHBoxLayout() - type_layout.addWidget(QLabel("Type:")) - self.type_combo = QComboBox() - self.type_combo.addItems(list(self.BREAK_RATES.keys())) - self.type_combo.setStyleSheet(""" - QComboBox { - background-color: rgba(20, 25, 35, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - padding: 5px; - } - """) - type_layout.addWidget(self.type_combo) - calc_layout.addLayout(type_layout) - - # TT value - tt_layout = QHBoxLayout() - tt_layout.addWidget(QLabel("TT Value:")) - self.tt_input = QLineEdit() - self.tt_input.setPlaceholderText("e.g., 40.00") - tt_layout.addWidget(self.tt_input) - calc_layout.addLayout(tt_layout) - - # Number of shots - shots_layout = QHBoxLayout() - shots_layout.addWidget(QLabel("Shots/hour:")) - self.shots_input = QLineEdit() - self.shots_input.setPlaceholderText("e.g., 3600") - self.shots_input.setText("3600") - shots_layout.addWidget(self.shots_input) - calc_layout.addLayout(shots_layout) - - # Calculate button - calc_btn = QPushButton("Calculate") - calc_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 10px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - calc_btn.clicked.connect(self._calculate) - calc_layout.addWidget(calc_btn) - - # Results - self.break_rate_label = QLabel("Break rate: -") - self.break_rate_label.setStyleSheet("color: rgba(255,255,255,180);") - calc_layout.addWidget(self.break_rate_label) - - self.breaks_label = QLabel("Expected breaks/hour: -") - self.breaks_label.setStyleSheet("color: #f44336;") - calc_layout.addWidget(self.breaks_label) - - self.cost_label = QLabel("Cost/hour: -") - self.cost_label.setStyleSheet("color: #ffc107;") - calc_layout.addWidget(self.cost_label) - - layout.addWidget(calc_frame) - - # Info - info = QLabel(""" - Typical break rates: - โ€ข Damage: ~0.15% per shot - โ€ข Accuracy: ~0.12% per shot - โ€ข Economy: ~0.10% per shot - โ€ข Range: ~0.13% per shot - """) - info.setStyleSheet("color: rgba(255,255,255,120); font-size: 10px;") - info.setWordWrap(True) - layout.addWidget(info) - - layout.addStretch() - return widget - - def _calculate(self): - """Calculate enhancer costs.""" - try: - enhancer_type = self.type_combo.currentText() - tt_value = float(self.tt_input.text() or 0) - shots = int(self.shots_input.text() or 3600) - - break_rate = self.BREAK_RATES.get(enhancer_type, 0.001) - - # Expected breaks - expected_breaks = shots * break_rate - - # Cost - hourly_cost = expected_breaks * tt_value - - self.break_rate_label.setText(f"Break rate: {break_rate*100:.3f}% per shot") - self.breaks_label.setText(f"Expected breaks/hour: {expected_breaks:.2f}") - self.cost_label.setText(f"Cost/hour: {hourly_cost:.2f} PED") - - except Exception as e: - self.break_rate_label.setText(f"Error: {e}") diff --git a/plugins/event_bus_example/__init__.py b/plugins/event_bus_example/__init__.py deleted file mode 100644 index 887ba92..0000000 --- a/plugins/event_bus_example/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -"""Event Bus Example Plugin.""" -from .plugin import EventBusExamplePlugin - -__all__ = ['EventBusExamplePlugin'] diff --git a/plugins/event_bus_example/plugin.py b/plugins/event_bus_example/plugin.py deleted file mode 100644 index 1b3081e..0000000 --- a/plugins/event_bus_example/plugin.py +++ /dev/null @@ -1,211 +0,0 @@ -""" -Example plugin demonstrating Enhanced Event Bus usage. - -This plugin shows how to use: -- publish_typed() - Publish typed events -- subscribe_typed() - Subscribe with filtering -- get_recent_events() - Retrieve event history -- Event filtering (min_damage, mob_types, etc.) -- Event replay for new subscribers -""" - -from plugins.base_plugin import BasePlugin -from core.event_bus import ( - SkillGainEvent, LootEvent, DamageEvent, GlobalEvent, - EventCategory -) - - -class EventBusExamplePlugin(BasePlugin): - """Example plugin showing Enhanced Event Bus usage.""" - - name = "Event Bus Example" - version = "1.0.0" - author = "EU-Utility" - description = "Demonstrates Enhanced Event Bus features" - - def __init__(self, overlay_window, config): - super().__init__(overlay_window, config) - self.big_hits = [] - self.skill_gains = [] - self.dragon_loot = [] - self._subscriptions = [] - - def initialize(self) -> None: - """Setup event subscriptions.""" - print(f"[{self.name}] Initializing...") - - # 1. Subscribe to ALL damage events - sub_id = self.subscribe_typed( - DamageEvent, - self.on_any_damage, - replay_last=5 # Replay last 5 damage events on subscribe - ) - self._subscriptions.append(sub_id) - print(f"[{self.name}] Subscribed to all damage events") - - # 2. Subscribe to HIGH damage events only (filtering) - sub_id = self.subscribe_typed( - DamageEvent, - self.on_big_damage, - min_damage=100, # Only events with damage >= 100 - replay_last=3 - ) - self._subscriptions.append(sub_id) - print(f"[{self.name}] Subscribed to high damage events (โ‰ฅ100)") - - # 3. Subscribe to skill gains for specific skills - sub_id = self.subscribe_typed( - SkillGainEvent, - self.on_combat_skill_gain, - skill_names=["Rifle", "Handgun", "Sword", "Knife"], - replay_last=10 - ) - self._subscriptions.append(sub_id) - print(f"[{self.name}] Subscribed to combat skill gains") - - # 4. Subscribe to loot from specific mobs - sub_id = self.subscribe_typed( - LootEvent, - self.on_dragon_loot, - mob_types=["Dragon", "Drake", "Dragon Old"], - replay_last=5 - ) - self._subscriptions.append(sub_id) - print(f"[{self.name}] Subscribed to Dragon/Drake loot") - - # 5. Subscribe to ALL globals - sub_id = self.subscribe_typed( - GlobalEvent, - self.on_global_announcement - ) - self._subscriptions.append(sub_id) - print(f"[{self.name}] Subscribed to global announcements") - - # 6. Demonstrate publishing events - self._publish_example_events() - - # 7. Show event stats - stats = self.get_event_stats() - print(f"[{self.name}] Event Bus Stats:") - print(f" - Total published: {stats.get('total_published', 0)}") - print(f" - Active subs: {stats.get('active_subscriptions', 0)}") - - def _publish_example_events(self): - """Publish some example events to demonstrate.""" - # Publish a skill gain - self.publish_typed(SkillGainEvent( - skill_name="Rifle", - skill_value=25.5, - gain_amount=0.01, - source="example_plugin" - )) - - # Publish some damage events - self.publish_typed(DamageEvent( - damage_amount=50.5, - damage_type="impact", - is_outgoing=True, - target_name="Berycled Young", - source="example_plugin" - )) - - self.publish_typed(DamageEvent( - damage_amount=150.0, - damage_type="penetration", - is_critical=True, - is_outgoing=True, - target_name="Daikiba", - source="example_plugin" - )) - - # Publish loot - self.publish_typed(LootEvent( - mob_name="Dragon", - items=[ - {"name": "Dragon Scale", "value": 15.0}, - {"name": "Animal Oil", "value": 0.05} - ], - total_tt_value=15.05, - source="example_plugin" - )) - - print(f"[{self.name}] Published example events") - - # ========== Event Handlers ========== - - def on_any_damage(self, event: DamageEvent): - """Handle all damage events.""" - direction = "dealt" if event.is_outgoing else "received" - crit = " CRITICAL" if event.is_critical else "" - print(f"[{self.name}] Damage {direction}: {event.damage_amount:.1f}{crit} to {event.target_name}") - - def on_big_damage(self, event: DamageEvent): - """Handle only high damage events (filtered).""" - self.big_hits.append(event) - print(f"[{self.name}] ๐Ÿ’ฅ BIG HIT! {event.damage_amount:.1f} damage to {event.target_name}") - print(f"[{self.name}] Total big hits recorded: {len(self.big_hits)}") - - def on_combat_skill_gain(self, event: SkillGainEvent): - """Handle combat skill gains.""" - self.skill_gains.append(event) - print(f"[{self.name}] โš”๏ธ Skill up: {event.skill_name} +{event.gain_amount:.4f} = {event.skill_value:.4f}") - - def on_dragon_loot(self, event: LootEvent): - """Handle Dragon/Drake loot.""" - self.dragon_loot.append(event) - items_str = ", ".join(event.get_item_names()) - print(f"[{self.name}] ๐Ÿ‰ Dragon loot from {event.mob_name}: {items_str} (TT: {event.total_tt_value:.2f} PED)") - - def on_global_announcement(self, event: GlobalEvent): - """Handle global announcements.""" - item_str = f" with {event.item_name}" if event.item_name else "" - print(f"[{self.name}] ๐ŸŒ GLOBAL: {event.player_name} - {event.achievement_type.upper()}{item_str} ({event.value:.2f} PED)") - - def get_ui(self): - """Return simple info panel.""" - from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QTextEdit - - widget = QWidget() - layout = QVBoxLayout() - - # Title - title = QLabel(f"

{self.name}

") - layout.addWidget(title) - - # Stats - stats_text = f""" - Recorded Events:
- โ€ข Big Hits (โ‰ฅ100 dmg): {len(self.big_hits)}
- โ€ข Combat Skill Gains: {len(self.skill_gains)}
- โ€ข Dragon Loot: {len(self.dragon_loot)}
-
- Active Subscriptions: {len(self._subscriptions)} - """ - stats_label = QLabel(stats_text) - stats_label.setWordWrap(True) - layout.addWidget(stats_label) - - # Recent events - layout.addWidget(QLabel("Recent Combat Events:")) - text_area = QTextEdit() - text_area.setReadOnly(True) - text_area.setMaximumHeight(200) - - # Get recent damage events from event bus - recent = self.get_recent_events(DamageEvent, count=10) - events_text = "\\n".join([ - f"โ€ข {e.damage_amount:.1f} dmg to {e.target_name}" - for e in reversed(recent) - ]) or "No recent damage events" - text_area.setText(events_text) - layout.addWidget(text_area) - - widget.setLayout(layout) - return widget - - def shutdown(self) -> None: - """Cleanup.""" - print(f"[{self.name}] Shutting down...") - # Unsubscribe from all typed events (handled by base class) - super().shutdown() diff --git a/plugins/game_reader/__init__.py b/plugins/game_reader/__init__.py deleted file mode 100644 index 0d896f8..0000000 --- a/plugins/game_reader/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Game Reader Plugin for EU-Utility -""" - -from .plugin import GameReaderPlugin - -__all__ = ["GameReaderPlugin"] diff --git a/plugins/game_reader/plugin.py b/plugins/game_reader/plugin.py deleted file mode 100644 index 093fd10..0000000 --- a/plugins/game_reader/plugin.py +++ /dev/null @@ -1,261 +0,0 @@ -""" -EU-Utility - OCR Scanner Plugin - -Reads text from in-game menus using OCR. -""" - -import subprocess -import platform -import tempfile -from pathlib import Path -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, - QLabel, QPushButton, QTextEdit, QComboBox, - QFrame, QScrollArea, QGroupBox -) -from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer -from PyQt6.QtGui import QPixmap, QImage - -from plugins.base_plugin import BasePlugin - - -class OCRScannerThread(QThread): - """Background thread for OCR scanning.""" - result_ready = pyqtSignal(str) - error_occurred = pyqtSignal(str) - - def __init__(self, region=None): - super().__init__() - self.region = region # (x, y, width, height) - - def run(self): - """Capture screen and perform OCR.""" - try: - system = platform.system() - - # Create temp file for screenshot - with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp: - screenshot_path = tmp.name - - # Capture screenshot - if system == "Windows": - # Use PowerShell to capture screen - ps_cmd = f''' - Add-Type -AssemblyName System.Windows.Forms - Add-Type -AssemblyName System.Drawing - $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds - $bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height) - $graphics = [System.Drawing.Graphics]::FromImage($bitmap) - $graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size) - $bitmap.Save("{screenshot_path}") - $graphics.Dispose() - $bitmap.Dispose() - ''' - subprocess.run(['powershell', '-Command', ps_cmd], capture_output=True, timeout=10) - - elif system == "Linux": - # Use gnome-screenshot or import - try: - subprocess.run(['gnome-screenshot', '-f', screenshot_path], - capture_output=True, timeout=10) - except: - subprocess.run(['import', '-window', 'root', screenshot_path], - capture_output=True, timeout=10) - - # Perform OCR - text = self._perform_ocr(screenshot_path) - - # Clean up - Path(screenshot_path).unlink(missing_ok=True) - - self.result_ready.emit(text) - - except Exception as e: - self.error_occurred.emit(str(e)) - - def _perform_ocr(self, image_path): - """Perform OCR on image.""" - try: - # Try easyocr first - import easyocr - reader = easyocr.Reader(['en']) - results = reader.readtext(image_path) - text = '\n'.join([result[1] for result in results]) - return text if text else "No text detected" - except: - pass - - try: - # Try pytesseract - import pytesseract - from PIL import Image - image = Image.open(image_path) - text = pytesseract.image_to_string(image) - return text if text.strip() else "No text detected" - except: - pass - - return "OCR not available. Install: pip install easyocr or pytesseract" - - -class GameReaderPlugin(BasePlugin): - """Read in-game menus and text using OCR.""" - - name = "Game Reader" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "OCR scanner for in-game menus and text" - hotkey = "ctrl+shift+r" # R for Read - - def initialize(self): - """Setup game reader.""" - self.scan_thread = None - self.last_result = "" - - def get_ui(self): - """Create game reader UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("๐Ÿ“ท Game Reader (OCR)") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Info - info = QLabel("Capture in-game menus and read the text") - info.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;") - layout.addWidget(info) - - # Scan button - scan_btn = QPushButton("Capture Screen") - scan_btn.setStyleSheet(""" - QPushButton { - background-color: #4a9eff; - color: white; - padding: 15px; - border: none; - border-radius: 10px; - font-size: 14px; - font-weight: bold; - } - QPushButton:hover { - background-color: #5aafff; - } - QPushButton:pressed { - background-color: #3a8eef; - } - """) - scan_btn.clicked.connect(self._capture_screen) - layout.addWidget(scan_btn) - - # Status - self.status_label = QLabel("Ready to capture") - self.status_label.setStyleSheet("color: #666; font-size: 11px;") - self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(self.status_label) - - # Results area - results_frame = QFrame() - results_frame.setStyleSheet(""" - QFrame { - background-color: rgba(0, 0, 0, 50); - border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 20); - } - """) - results_layout = QVBoxLayout(results_frame) - results_layout.setContentsMargins(10, 10, 10, 10) - - results_label = QLabel("Captured Text:") - results_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;") - results_layout.addWidget(results_label) - - self.result_text = QTextEdit() - self.result_text.setPlaceholderText("Captured text will appear here...") - self.result_text.setStyleSheet(""" - QTextEdit { - background-color: rgba(30, 30, 30, 100); - color: white; - border: none; - border-radius: 6px; - padding: 8px; - font-size: 13px; - } - """) - self.result_text.setMaximumHeight(150) - results_layout.addWidget(self.result_text) - - # Copy button - copy_btn = QPushButton("Copy Text") - copy_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255, 255, 255, 20); - color: white; - padding: 8px; - border: none; - border-radius: 6px; - } - QPushButton:hover { - background-color: rgba(255, 255, 255, 30); - } - """) - copy_btn.clicked.connect(self._copy_text) - results_layout.addWidget(copy_btn) - - layout.addWidget(results_frame) - - # Common uses - uses_label = QLabel("Common Uses:") - uses_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 11px; margin-top: 10px;") - layout.addWidget(uses_label) - - uses_text = QLabel( - "โ€ข Read NPC dialogue\n" - "โ€ข Capture mission text\n" - "โ€ข Extract item stats\n" - "โ€ข Read shop prices" - ) - uses_text.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;") - layout.addWidget(uses_text) - - layout.addStretch() - - return widget - - def _capture_screen(self): - """Capture screen and perform OCR.""" - self.status_label.setText("Capturing...") - self.status_label.setStyleSheet("color: #4a9eff;") - - # Start scan thread - self.scan_thread = OCRScannerThread() - self.scan_thread.result_ready.connect(self._on_result) - self.scan_thread.error_occurred.connect(self._on_error) - self.scan_thread.start() - - def _on_result(self, text): - """Handle OCR result.""" - self.result_text.setText(text) - self.last_result = text - self.status_label.setText(f"โœ… Captured {len(text)} characters") - self.status_label.setStyleSheet("color: #4caf50;") - - def _on_error(self, error): - """Handle OCR error.""" - self.status_label.setText(f"โŒ Error: {error}") - self.status_label.setStyleSheet("color: #f44336;") - - def _copy_text(self): - """Copy text to clipboard.""" - from PyQt6.QtWidgets import QApplication - clipboard = QApplication.clipboard() - clipboard.setText(self.result_text.toPlainText()) - self.status_label.setText("๐Ÿ“‹ Copied to clipboard!") - - def on_hotkey(self): - """Capture on hotkey.""" - self._capture_screen() diff --git a/plugins/game_reader_test/__init__.py b/plugins/game_reader_test/__init__.py deleted file mode 100644 index 401c3cf..0000000 --- a/plugins/game_reader_test/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Game Reader Test Plugin.""" -from .plugin import GameReaderTestPlugin diff --git a/plugins/game_reader_test/plugin.py b/plugins/game_reader_test/plugin.py deleted file mode 100644 index c47532f..0000000 --- a/plugins/game_reader_test/plugin.py +++ /dev/null @@ -1,1197 +0,0 @@ -""" -EU-Utility - Game Reader Test Plugin - -Debug and test tool for OCR and game reading functionality. -Tests screen capture, OCR accuracy, and text extraction. -""" - -import re -from pathlib import Path -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, - QLabel, QPushButton, QComboBox, QCheckBox, - QSpinBox, QGroupBox, QSplitter, QFrame, - QTabWidget, QLineEdit, QProgressBar, - QFileDialog, QMessageBox, QTableWidget, - QTableWidgetItem, QHeaderView -) -from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal -from PyQt6.QtGui import QPixmap, QImage, QColor - -from plugins.base_plugin import BasePlugin - - -class OCRTestThread(QThread): - """Background thread for OCR testing.""" - result_ready = pyqtSignal(dict) - progress_update = pyqtSignal(int, str) - - def __init__(self, region=None, backend='auto'): - super().__init__() - self.region = region - self.backend = backend - - def run(self): - """Run OCR test.""" - import time - results = { - 'success': False, - 'text': '', - 'backend_used': '', - 'processing_time': 0, - 'error': None - } - - try: - start_time = time.time() - self.progress_update.emit(10, "Capturing screen...") - - # Capture screen - from PIL import Image, ImageGrab - if self.region: - screenshot = ImageGrab.grab(bbox=self.region) - else: - screenshot = ImageGrab.grab() - - self.progress_update.emit(30, "Running OCR...") - - # Try OCR backends - text = "" - backend_used = "none" - - if self.backend in ('auto', 'easyocr'): - try: - import easyocr - import numpy as np - self.progress_update.emit(50, "Loading EasyOCR...") - reader = easyocr.Reader(['en'], gpu=False, verbose=False) - self.progress_update.emit(70, "Processing with EasyOCR...") - # Convert PIL Image to numpy array - screenshot_np = np.array(screenshot) - ocr_result = reader.readtext( - screenshot_np, - detail=0, - paragraph=True - ) - text = '\n'.join(ocr_result) - backend_used = "easyocr" - except Exception as e: - if self.backend == 'easyocr': - raise e - - if not text and self.backend in ('auto', 'tesseract'): - try: - import pytesseract - self.progress_update.emit(70, "Processing with Tesseract...") - text = pytesseract.image_to_string(screenshot) - backend_used = "tesseract" - except Exception as e: - if self.backend == 'tesseract': - raise e - - if not text and self.backend in ('auto', 'paddle'): - try: - from paddleocr import PaddleOCR - import numpy as np - self.progress_update.emit(70, "Processing with PaddleOCR...") - # Try with show_log, fall back without for compatibility - try: - ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False) - except TypeError: - ocr = PaddleOCR(use_angle_cls=True, lang='en') - # Convert PIL to numpy - screenshot_np = np.array(screenshot) - result = ocr.ocr(screenshot_np, cls=True) - if result and result[0]: - texts = [line[1][0] for line in result[0]] - text = '\n'.join(texts) - backend_used = "paddleocr" - except Exception as e: - if self.backend == 'paddle': - raise e - - processing_time = time.time() - start_time - - results.update({ - 'success': True, - 'text': text or "No text detected", - 'backend_used': backend_used, - 'processing_time': processing_time - }) - - self.progress_update.emit(100, "Complete!") - - except Exception as e: - results['error'] = str(e) - self.progress_update.emit(100, f"Error: {e}") - - self.result_ready.emit(results) - - -class GameReaderTestPlugin(BasePlugin): - """Test and debug tool for game reading/OCR functionality.""" - - name = "Game Reader Test" - version = "1.0.0" - author = "EU-Utility" - description = "Debug tool for testing OCR and screen reading" - - # Dependencies for OCR functionality - dependencies = { - 'pip': ['pillow', 'numpy'], - 'optional': { - 'easyocr': 'Best OCR accuracy, auto-downloads models', - 'pytesseract': 'Alternative OCR engine', - 'paddleocr': 'Advanced OCR with layout detection' - } - } - - def __init__(self, overlay_window, config): - super().__init__(overlay_window, config) - self.test_history = [] - self.max_history = 50 - self.ocr_thread = None - - def initialize(self): - """Initialize plugin.""" - self.log_info("Game Reader Test initialized") - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(12) - - # Header - header = QLabel("๐Ÿ“ท Game Reader Test & Debug Tool") - header.setStyleSheet("font-size: 16px; font-weight: bold; color: #ff8c42;") - layout.addWidget(header) - - # Create tabs - tabs = QTabWidget() - - # Tab 1: Quick Test - tabs.addTab(self._create_quick_test_tab(), "Quick Test") - - # Tab 2: File Test (NEW) - tabs.addTab(self._create_file_test_tab(), "File Test") - - # Tab 3: Region Test - tabs.addTab(self._create_region_test_tab(), "Region Test") - - # Tab 3: History - tabs.addTab(self._create_history_tab(), "History") - - # Tab 5: Skills Parser (NEW) - tabs.addTab(self._create_skills_parser_tab(), "Skills Parser") - - # Tab 6: Calibration - tabs.addTab(self._create_calibration_tab(), "Calibration") - - layout.addWidget(tabs, 1) - - # Status bar - status_frame = QFrame() - status_frame.setStyleSheet("background-color: #1a1f2e; border-radius: 6px; padding: 8px;") - status_layout = QHBoxLayout(status_frame) - - self.status_label = QLabel("Ready - Select a tab to begin testing") - self.status_label.setStyleSheet("color: #4ecdc4;") - status_layout.addWidget(self.status_label) - - layout.addWidget(status_frame) - - return widget - - def _create_quick_test_tab(self): - """Create quick test tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Info - info = QLabel("Quick OCR Test - Captures full screen and extracts text") - info.setStyleSheet("color: #888;") - layout.addWidget(info) - - # Backend selection - backend_layout = QHBoxLayout() - backend_layout.addWidget(QLabel("OCR Backend:")) - self.backend_combo = QComboBox() - self.backend_combo.addItems(["Auto (try all)", "EasyOCR", "Tesseract", "PaddleOCR"]) - backend_layout.addWidget(self.backend_combo) - backend_layout.addStretch() - layout.addLayout(backend_layout) - - # Progress bar - self.progress_bar = QProgressBar() - self.progress_bar.setRange(0, 100) - self.progress_bar.setValue(0) - self.progress_bar.setStyleSheet(""" - QProgressBar { - border: 1px solid #333; - border-radius: 4px; - text-align: center; - } - QProgressBar::chunk { - background-color: #ff8c42; - } - """) - layout.addWidget(self.progress_bar) - - # Test button - self.test_btn = QPushButton("โ–ถ Run OCR Test") - self.test_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: #141f23; - font-weight: bold; - padding: 12px; - font-size: 14px; - } - QPushButton:hover { - background-color: #ffa05c; - } - """) - self.test_btn.clicked.connect(self._run_quick_test) - layout.addWidget(self.test_btn) - - # Results - results_group = QGroupBox("OCR Results") - results_layout = QVBoxLayout(results_group) - - self.results_text = QTextEdit() - self.results_text.setReadOnly(True) - self.results_text.setPlaceholderText("OCR results will appear here...") - self.results_text.setStyleSheet(""" - QTextEdit { - background-color: #0d1117; - color: #c9d1d9; - font-family: Consolas, monospace; - font-size: 12px; - } - """) - results_layout.addWidget(self.results_text) - - # Stats - self.stats_label = QLabel("Backend: - | Time: - | Status: Waiting") - self.stats_label.setStyleSheet("color: #888;") - results_layout.addWidget(self.stats_label) - - layout.addWidget(results_group) - - # Save buttons - btn_layout = QHBoxLayout() - - save_text_btn = QPushButton("๐Ÿ’พ Save Text") - save_text_btn.clicked.connect(self._save_text) - btn_layout.addWidget(save_text_btn) - - copy_btn = QPushButton("๐Ÿ“‹ Copy to Clipboard") - copy_btn.clicked.connect(self._copy_to_clipboard) - btn_layout.addWidget(copy_btn) - - clear_btn = QPushButton("๐Ÿ—‘ Clear") - clear_btn.clicked.connect(self._clear_results) - btn_layout.addWidget(clear_btn) - - btn_layout.addStretch() - layout.addLayout(btn_layout) - - layout.addStretch() - return tab - - def _create_file_test_tab(self): - """Create file-based OCR test tab for testing with saved screenshots.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - # Info - info = QLabel("Test OCR on an image file (PNG, JPG, BMP)") - info.setStyleSheet("color: #888;") - layout.addWidget(info) - - # File selection - file_layout = QHBoxLayout() - - self.file_path_label = QLabel("No file selected") - self.file_path_label.setStyleSheet("color: #aaa; padding: 8px; background-color: #1a1f2e; border-radius: 4px;") - file_layout.addWidget(self.file_path_label, 1) - - browse_btn = QPushButton("Browse...") - browse_btn.setStyleSheet(""" - QPushButton { - background-color: #4ecdc4; - color: #141f23; - font-weight: bold; - padding: 8px 16px; - } - """) - browse_btn.clicked.connect(self._browse_image_file) - file_layout.addWidget(browse_btn) - - layout.addLayout(file_layout) - - # Backend selection - backend_layout = QHBoxLayout() - backend_layout.addWidget(QLabel("OCR Backend:")) - self.file_backend_combo = QComboBox() - self.file_backend_combo.addItems(["Auto (try all)", "EasyOCR", "Tesseract", "PaddleOCR"]) - backend_layout.addWidget(self.file_backend_combo) - backend_layout.addStretch() - layout.addLayout(backend_layout) - - # Test button - file_test_btn = QPushButton("โ–ถ Run OCR on File") - file_test_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: #141f23; - font-weight: bold; - padding: 12px; - font-size: 14px; - } - """) - file_test_btn.clicked.connect(self._run_file_test) - layout.addWidget(file_test_btn) - - # Results - results_group = QGroupBox("OCR Results") - results_layout = QVBoxLayout(results_group) - - self.file_results_text = QTextEdit() - self.file_results_text.setReadOnly(True) - self.file_results_text.setPlaceholderText("OCR results will appear here...") - self.file_results_text.setStyleSheet(""" - QTextEdit { - background-color: #0d1117; - color: #c9d1d9; - font-family: Consolas, monospace; - font-size: 12px; - } - """) - results_layout.addWidget(self.file_results_text) - - # Stats - self.file_stats_label = QLabel("File: - | Backend: - | Status: Waiting") - self.file_stats_label.setStyleSheet("color: #888;") - results_layout.addWidget(self.file_stats_label) - - layout.addWidget(results_group, 1) - - layout.addStretch() - return tab - - def _create_region_test_tab(self): - """Create region test tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - info = QLabel("Test OCR on specific screen region") - info.setStyleSheet("color: #888;") - layout.addWidget(info) - - # Region input - region_group = QGroupBox("Screen Region (pixels)") - region_layout = QHBoxLayout(region_group) - - self.x_input = QSpinBox() - self.x_input.setRange(0, 10000) - self.x_input.setValue(100) - region_layout.addWidget(QLabel("X:")) - region_layout.addWidget(self.x_input) - - self.y_input = QSpinBox() - self.y_input.setRange(0, 10000) - self.y_input.setValue(100) - region_layout.addWidget(QLabel("Y:")) - region_layout.addWidget(self.y_input) - - self.w_input = QSpinBox() - self.w_input.setRange(100, 10000) - self.w_input.setValue(400) - region_layout.addWidget(QLabel("Width:")) - region_layout.addWidget(self.w_input) - - self.h_input = QSpinBox() - self.h_input.setRange(100, 10000) - self.h_input.setValue(300) - region_layout.addWidget(QLabel("Height:")) - region_layout.addWidget(self.h_input) - - layout.addWidget(region_group) - - # Presets - preset_layout = QHBoxLayout() - preset_layout.addWidget(QLabel("Quick Presets:")) - - presets = [ - ("Chat Window", 10, 800, 600, 200), - ("Skills Window", 100, 100, 500, 400), - ("Inventory", 1200, 200, 600, 500), - ("Mission Tracker", 1600, 100, 300, 600), - ] - - for name, x, y, w, h in presets: - btn = QPushButton(name) - btn.clicked.connect(lambda checked, px=x, py=y, pw=w, ph=h: self._set_region(px, py, pw, ph)) - preset_layout.addWidget(btn) - - preset_layout.addStretch() - layout.addLayout(preset_layout) - - # Test button - region_test_btn = QPushButton("โ–ถ Test Region OCR") - region_test_btn.setStyleSheet(""" - QPushButton { - background-color: #4ecdc4; - color: #141f23; - font-weight: bold; - padding: 10px; - } - """) - region_test_btn.clicked.connect(self._run_region_test) - layout.addWidget(region_test_btn) - - # Results - self.region_results = QTextEdit() - self.region_results.setReadOnly(True) - self.region_results.setPlaceholderText("Region OCR results will appear here...") - self.region_results.setStyleSheet(""" - QTextEdit { - background-color: #0d1117; - color: #c9d1d9; - font-family: Consolas, monospace; - } - """) - layout.addWidget(self.region_results) - - layout.addStretch() - return tab - - def _create_history_tab(self): - """Create history tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - info = QLabel("History of OCR tests") - info.setStyleSheet("color: #888;") - layout.addWidget(info) - - self.history_text = QTextEdit() - self.history_text.setReadOnly(True) - self.history_text.setStyleSheet(""" - QTextEdit { - background-color: #0d1117; - color: #c9d1d9; - } - """) - layout.addWidget(self.history_text) - - # Controls - btn_layout = QHBoxLayout() - - refresh_btn = QPushButton("๐Ÿ”„ Refresh") - refresh_btn.clicked.connect(self._update_history) - btn_layout.addWidget(refresh_btn) - - clear_hist_btn = QPushButton("๐Ÿ—‘ Clear History") - clear_hist_btn.clicked.connect(self._clear_history) - btn_layout.addWidget(clear_hist_btn) - - btn_layout.addStretch() - layout.addLayout(btn_layout) - - return tab - - def _create_skills_parser_tab(self): - """Create skills parser tab for testing skill window OCR.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - info = QLabel("๐Ÿ“Š Skills Window Parser - Extract skills from the EU Skills window") - info.setStyleSheet("color: #888;") - layout.addWidget(info) - - # Instructions - instructions = QLabel( - "1. Open your Skills window in Entropia Universe\n" - "2. Click 'Capture Skills Window' below\n" - "3. View parsed skills in the table" - ) - instructions.setStyleSheet("color: #666; font-size: 11px;") - layout.addWidget(instructions) - - # Capture button - capture_btn = QPushButton("๐Ÿ“ท Capture Skills Window") - capture_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: #141f23; - font-weight: bold; - padding: 12px; - font-size: 14px; - } - """) - capture_btn.clicked.connect(self._capture_and_parse_skills) - layout.addWidget(capture_btn) - - # Results table - from PyQt6.QtWidgets import QTableWidget, QTableWidgetItem, QHeaderView - - self.skills_table = QTableWidget() - self.skills_table.setColumnCount(3) - self.skills_table.setHorizontalHeaderLabels(["Skill Name", "Rank", "Points"]) - self.skills_table.horizontalHeader().setStretchLastSection(True) - self.skills_table.setStyleSheet(""" - QTableWidget { - background-color: #0d1117; - border: 1px solid #333; - } - QTableWidget::item { - padding: 6px; - color: #c9d1d9; - } - QHeaderView::section { - background-color: #1a1f2e; - color: #ff8c42; - padding: 8px; - font-weight: bold; - } - """) - layout.addWidget(self.skills_table, 1) - - # Stats label - self.skills_stats_label = QLabel("No skills captured yet") - self.skills_stats_label.setStyleSheet("color: #888;") - layout.addWidget(self.skills_stats_label) - - # Raw text view - raw_group = QGroupBox("Raw OCR Text (for debugging)") - raw_layout = QVBoxLayout(raw_group) - - self.skills_raw_text = QTextEdit() - self.skills_raw_text.setReadOnly(True) - self.skills_raw_text.setMaximumHeight(150) - self.skills_raw_text.setStyleSheet(""" - QTextEdit { - background-color: #0d1117; - color: #666; - font-family: Consolas, monospace; - font-size: 10px; - } - """) - raw_layout.addWidget(self.skills_raw_text) - layout.addWidget(raw_group) - - layout.addStretch() - return tab - - def _capture_and_parse_skills(self): - """Capture screen and parse skills.""" - from PyQt6.QtCore import Qt - - self.skills_stats_label.setText("Capturing...") - self.skills_stats_label.setStyleSheet("color: #4ecdc4;") - - # Run in thread to not block UI - from threading import Thread - - def capture_and_parse(): - try: - from PIL import ImageGrab - import re - from datetime import datetime - - # Capture screen - screenshot = ImageGrab.grab() - - # Run OCR - text = "" - try: - import easyocr - reader = easyocr.Reader(['en'], gpu=False, verbose=False) - import numpy as np - result = reader.readtext(np.array(screenshot), detail=0, paragraph=False) - text = '\n'.join(result) - except Exception as e: - # Fallback to raw OCR - text = str(e) - - # Parse skills - skills = self._parse_skills_from_text(text) - - # Update UI - from PyQt6.QtCore import QMetaObject, Qt, Q_ARG - QMetaObject.invokeMethod( - self.skills_raw_text, - "setPlainText", - Qt.ConnectionType.QueuedConnection, - Q_ARG(str, text) - ) - - # Update table - QMetaObject.invokeMethod( - self, "_update_skills_table", - Qt.ConnectionType.QueuedConnection, - Q_ARG(object, skills) - ) - - # Update stats - stats_text = f"Found {len(skills)} skills" - QMetaObject.invokeMethod( - self.skills_stats_label, - "setText", - Qt.ConnectionType.QueuedConnection, - Q_ARG(str, stats_text) - ) - QMetaObject.invokeMethod( - self.skills_stats_label, - "setStyleSheet", - Qt.ConnectionType.QueuedConnection, - Q_ARG(str, "color: #4ecdc4;") - ) - - except Exception as e: - from PyQt6.QtCore import QMetaObject, Qt, Q_ARG - QMetaObject.invokeMethod( - self.skills_stats_label, - "setText", - Qt.ConnectionType.QueuedConnection, - Q_ARG(str, f"Error: {str(e)}") - ) - QMetaObject.invokeMethod( - self.skills_stats_label, - "setStyleSheet", - Qt.ConnectionType.QueuedConnection, - Q_ARG(str, "color: #ff6b6b;") - ) - - thread = Thread(target=capture_and_parse) - thread.daemon = True - thread.start() - - def _parse_skills_from_text(self, text): - """Parse skills from OCR text.""" - skills = {} - - # Ranks in Entropia Universe - multi-word first for proper matching - SINGLE_RANKS = [ - 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average', - 'Skilled', 'Expert', 'Professional', 'Master', - 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable', - 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome' - ] - MULTI_RANKS = ['Arch Master', 'Grand Master'] - ALL_RANKS = MULTI_RANKS + SINGLE_RANKS - rank_pattern = '|'.join(ALL_RANKS) - - # Clean text - text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '') - text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '') - - lines = text.split('\n') - - for line in lines: - line = line.strip() - if not line or len(line) < 10: - continue - - # Pattern: SkillName Rank Points - match = re.search( - rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)', - line, re.IGNORECASE - ) - - if match: - skill_name = match.group(1).strip() - rank = match.group(2) - points = int(match.group(3)) - - # Clean skill name - remove "Skill" prefix - skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE) - - if points > 0 and skill_name: - skills[skill_name] = {'rank': rank, 'points': points} - - return skills - - def _update_skills_table(self, skills): - """Update the skills table with parsed data.""" - self.skills_table.setRowCount(len(skills)) - - for i, (skill_name, data) in enumerate(sorted(skills.items())): - self.skills_table.setItem(i, 0, QTableWidgetItem(skill_name)) - self.skills_table.setItem(i, 1, QTableWidgetItem(data['rank'])) - self.skills_table.setItem(i, 2, QTableWidgetItem(str(data['points']))) - - self.skills_table.resizeColumnsToContents() - - def _create_calibration_tab(self): - """Create calibration tab.""" - tab = QWidget() - layout = QVBoxLayout(tab) - - info = QLabel("Calibration tools for optimizing OCR accuracy") - info.setStyleSheet("color: #888;") - layout.addWidget(info) - - # DPI awareness - dpi_group = QGroupBox("Display Settings") - dpi_layout = QVBoxLayout(dpi_group) - - self.dpi_label = QLabel("Detecting display DPI...") - dpi_layout.addWidget(self.dpi_label) - - detect_dpi_btn = QPushButton("Detect Display Settings") - detect_dpi_btn.clicked.connect(self._detect_display_settings) - dpi_layout.addWidget(detect_dpi_btn) - - layout.addWidget(dpi_group) - - # Backend status - backend_group = QGroupBox("OCR Backend Status") - backend_layout = QVBoxLayout(backend_group) - - self.backend_status = QTextEdit() - self.backend_status.setReadOnly(True) - self.backend_status.setMaximumHeight(200) - self._check_backends() - backend_layout.addWidget(self.backend_status) - - # Install buttons - install_layout = QHBoxLayout() - - install_easyocr_btn = QPushButton("๐Ÿ“ฆ Install EasyOCR") - install_easyocr_btn.setStyleSheet(""" - QPushButton { - background-color: #4ecdc4; - color: #141f23; - font-weight: bold; - padding: 8px; - } - """) - install_easyocr_btn.clicked.connect(self._install_easyocr) - install_layout.addWidget(install_easyocr_btn) - - install_tesseract_btn = QPushButton("๐Ÿ“ฆ Install Tesseract Package") - install_tesseract_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: #141f23; - font-weight: bold; - padding: 8px; - } - """) - install_tesseract_btn.clicked.connect(self._install_pytesseract) - install_layout.addWidget(install_tesseract_btn) - - detect_tesseract_btn = QPushButton("๐Ÿ” Auto-Detect Tesseract") - detect_tesseract_btn.setStyleSheet(""" - QPushButton { - background-color: #a0aec0; - color: #141f23; - font-weight: bold; - padding: 8px; - } - """) - detect_tesseract_btn.clicked.connect(self._auto_detect_tesseract) - install_layout.addWidget(detect_tesseract_btn) - - install_paddle_btn = QPushButton("๐Ÿ“ฆ Install PaddleOCR") - install_paddle_btn.setStyleSheet(""" - QPushButton { - background-color: #4a5568; - color: white; - font-weight: bold; - padding: 8px; - } - """) - install_paddle_btn.clicked.connect(self._install_paddleocr) - install_layout.addWidget(install_paddle_btn) - - backend_layout.addLayout(install_layout) - layout.addWidget(backend_group) - - # Tips - tips_group = QGroupBox("Tips for Best Results") - tips_layout = QVBoxLayout(tips_group) - - tips_text = QLabel(""" - โ€ข Make sure text is clearly visible and not blurry - โ€ข Use region selection to focus on specific text areas - โ€ข Higher resolution screens work better for OCR - โ€ข Close other windows to reduce background noise - โ€ข Ensure good contrast between text and background - """) - tips_text.setWordWrap(True) - tips_text.setStyleSheet("color: #aaa;") - tips_layout.addWidget(tips_text) - - layout.addWidget(tips_group) - - layout.addStretch() - return tab - - def _set_region(self, x, y, w, h): - """Set region values.""" - self.x_input.setValue(x) - self.y_input.setValue(y) - self.w_input.setValue(w) - self.h_input.setValue(h) - - def _browse_image_file(self): - """Browse for an image file to test OCR on.""" - file_path, _ = QFileDialog.getOpenFileName( - None, - "Select Image File", - "", - "Images (*.png *.jpg *.jpeg *.bmp *.tiff);;All Files (*)" - ) - if file_path: - self.selected_file_path = file_path - self.file_path_label.setText(Path(file_path).name) - self.file_path_label.setStyleSheet("color: #4ecdc4; padding: 8px; background-color: #1a1f2e; border-radius: 4px;") - - def _run_file_test(self): - """Run OCR on the selected image file.""" - if not hasattr(self, 'selected_file_path') or not self.selected_file_path: - QMessageBox.warning(None, "No File", "Please select an image file first!") - return - - backend_map = { - 0: 'auto', - 1: 'easyocr', - 2: 'tesseract', - 3: 'paddle' - } - backend = backend_map.get(self.file_backend_combo.currentIndex(), 'auto') - - self.file_results_text.setPlainText("Processing...") - self.file_stats_label.setText("Processing...") - - # Run OCR in a thread - from threading import Thread - - def process_file(): - try: - from PIL import Image - import time - - start_time = time.time() - - # Load image - image = Image.open(self.selected_file_path) - - # Try OCR backends - text = "" - backend_used = "none" - - if backend in ('auto', 'easyocr'): - try: - import easyocr - import numpy as np - reader = easyocr.Reader(['en'], gpu=False, verbose=False) - # Convert PIL Image to numpy array for EasyOCR - image_np = np.array(image) - ocr_result = reader.readtext( - image_np, - detail=0, - paragraph=True - ) - text = '\n'.join(ocr_result) - backend_used = "easyocr" - except Exception as e: - if backend == 'easyocr': - raise e - - if not text and backend in ('auto', 'tesseract'): - try: - import pytesseract - text = pytesseract.image_to_string(image) - backend_used = "tesseract" - except Exception as e: - if backend == 'tesseract': - error_msg = str(e) - if "tesseract is not installed" in error_msg.lower() or "not in your path" in error_msg.lower(): - raise Exception( - "Tesseract is not installed.\n\n" - "To use Tesseract OCR:\n" - "1. Download from: https://github.com/UB-Mannheim/tesseract/wiki\n" - "2. Install to C:\\Program Files\\Tesseract-OCR\\\n" - "3. Add to PATH or restart EU-Utility\n\n" - "Alternatively, use EasyOCR (auto-installs): pip install easyocr" - ) - raise e - - if not text and backend in ('auto', 'paddle'): - try: - from paddleocr import PaddleOCR - # Try without show_log argument for compatibility - try: - ocr = PaddleOCR(use_angle_cls=True, lang='en', show_log=False) - except TypeError: - # Older version without show_log - ocr = PaddleOCR(use_angle_cls=True, lang='en') - # Convert PIL to numpy for PaddleOCR - import numpy as np - image_np = np.array(image) - result = ocr.ocr(image_np, cls=True) - if result and result[0]: - texts = [line[1][0] for line in result[0]] - text = '\n'.join(texts) - backend_used = "paddleocr" - except Exception as e: - if backend == 'paddle': - raise e - - processing_time = time.time() - start_time - - # Update UI (thread-safe via Qt signals would be better, but this works for simple case) - from PyQt6.QtCore import QMetaObject, Qt, Q_ARG - QMetaObject.invokeMethod( - self.file_results_text, - "setPlainText", - Qt.ConnectionType.QueuedConnection, - Q_ARG(str, text if text else "No text detected") - ) - QMetaObject.invokeMethod( - self.file_stats_label, - "setText", - Qt.ConnectionType.QueuedConnection, - Q_ARG(str, f"File: {Path(self.selected_file_path).name} | Backend: {backend_used} | Time: {processing_time:.2f}s") - ) - - except Exception as e: - from PyQt6.QtCore import QMetaObject, Qt, Q_ARG - QMetaObject.invokeMethod( - self.file_results_text, - "setPlainText", - Qt.ConnectionType.QueuedConnection, - Q_ARG(str, f"Error: {str(e)}") - ) - - thread = Thread(target=process_file) - thread.daemon = True - thread.start() - - def _run_quick_test(self): - """Run quick OCR test.""" - if self.ocr_thread and self.ocr_thread.isRunning(): - return - - backend_map = { - 0: 'auto', - 1: 'easyocr', - 2: 'tesseract', - 3: 'paddle' - } - backend = backend_map.get(self.backend_combo.currentIndex(), 'auto') - - self.test_btn.setEnabled(False) - self.test_btn.setText("โณ Running...") - self.progress_bar.setValue(0) - - self.ocr_thread = OCRTestThread(backend=backend) - self.ocr_thread.progress_update.connect(self._update_progress) - self.ocr_thread.result_ready.connect(self._on_ocr_complete) - self.ocr_thread.start() - - def _update_progress(self, value, message): - """Update progress bar.""" - self.progress_bar.setValue(value) - self.status_label.setText(message) - - def _on_ocr_complete(self, results): - """Handle OCR completion.""" - self.test_btn.setEnabled(True) - self.test_btn.setText("โ–ถ Run OCR Test") - - if results['success']: - self.results_text.setPlainText(results['text']) - self.stats_label.setText( - f"Backend: {results['backend_used']} | " - f"Time: {results['processing_time']:.2f}s | " - f"Status: โœ… Success" - ) - - # Add to history - self._add_to_history(results) - else: - self.results_text.setPlainText(f"Error: {results.get('error', 'Unknown error')}") - self.stats_label.setText(f"Backend: {results.get('backend_used', '-')} | Status: โŒ Failed") - - def _run_region_test(self): - """Run region OCR test.""" - region = ( - self.x_input.value(), - self.y_input.value(), - self.w_input.value(), - self.h_input.value() - ) - - self.region_results.setPlainText("Running OCR on region...") - - # Use api's ocr_capture if available - try: - result = self.ocr_capture(region=region) - self.region_results.setPlainText(result.get('text', 'No text detected')) - except Exception as e: - self.region_results.setPlainText(f"Error: {e}") - - def _save_text(self): - """Save OCR text to file.""" - text = self.results_text.toPlainText() - if not text: - QMessageBox.warning(None, "Save Error", "No text to save!") - return - - file_path, _ = QFileDialog.getSaveFileName( - None, "Save OCR Text", "ocr_result.txt", "Text Files (*.txt)" - ) - if file_path: - with open(file_path, 'w', encoding='utf-8') as f: - f.write(text) - self.status_label.setText(f"Saved to {file_path}") - - def _copy_to_clipboard(self): - """Copy text to clipboard.""" - text = self.results_text.toPlainText() - if text: - self.copy_to_clipboard(text) - self.status_label.setText("Copied to clipboard!") - - def _clear_results(self): - """Clear results.""" - self.results_text.clear() - self.stats_label.setText("Backend: - | Time: - | Status: Waiting") - self.progress_bar.setValue(0) - - def _add_to_history(self, results): - """Add result to history.""" - from datetime import datetime - entry = { - 'time': datetime.now().strftime("%H:%M:%S"), - 'backend': results['backend_used'], - 'time_taken': results['processing_time'], - 'text_preview': results['text'][:100] + "..." if len(results['text']) > 100 else results['text'] - } - self.test_history.insert(0, entry) - if len(self.test_history) > self.max_history: - self.test_history = self.test_history[:self.max_history] - self._update_history() - - def _update_history(self): - """Update history display.""" - lines = [] - for entry in self.test_history: - lines.append(f"[{entry['time']}] {entry['backend']} ({entry['time_taken']:.2f}s)") - lines.append(f" {entry['text_preview']}") - lines.append("") - self.history_text.setPlainText('\n'.join(lines) if lines else "No history yet") - - def _clear_history(self): - """Clear history.""" - self.test_history.clear() - self._update_history() - - def _detect_display_settings(self): - """Detect display settings.""" - try: - from PyQt6.QtWidgets import QApplication - from PyQt6.QtGui import QScreen - - app = QApplication.instance() - if app: - screens = app.screens() - info = [] - for i, screen in enumerate(screens): - geo = screen.geometry() - dpi = screen.logicalDotsPerInch() - info.append(f"Screen {i+1}: {geo.width()}x{geo.height()} @ {dpi:.0f} DPI") - self.dpi_label.setText('\n'.join(info)) - except Exception as e: - self.dpi_label.setText(f"Error: {e}") - - def _install_easyocr(self): - """Install EasyOCR backend.""" - from core.ocr_backend_manager import get_ocr_backend_manager - manager = get_ocr_backend_manager() - success, message = manager.install_backend('easyocr') - if success: - self.notify_success("Installation Complete", message) - else: - self.notify_error("Installation Failed", message) - self._check_backends() - - def _install_pytesseract(self): - """Install pytesseract Python package.""" - from core.ocr_backend_manager import get_ocr_backend_manager - manager = get_ocr_backend_manager() - success, message = manager.install_backend('tesseract') - if success: - self.notify_success("Installation Complete", message + "\n\nNote: You also need to install the Tesseract binary from:\nhttps://github.com/UB-Mannheim/tesseract/wiki") - else: - self.notify_error("Installation Failed", message) - self._check_backends() - - def _install_paddleocr(self): - """Install PaddleOCR backend.""" - from core.ocr_backend_manager import get_ocr_backend_manager - manager = get_ocr_backend_manager() - success, message = manager.install_backend('paddleocr') - if success: - self.notify_success("Installation Complete", message) - else: - self.notify_error("Installation Failed", message) - self._check_backends() - - def _auto_detect_tesseract(self): - """Auto-detect Tesseract from registry/paths.""" - from core.ocr_backend_manager import get_ocr_backend_manager - manager = get_ocr_backend_manager() - if manager.auto_configure_tesseract(): - self.notify_success("Tesseract Found", f"Auto-configured Tesseract at:\n{manager.backends['tesseract']['path']}") - else: - self.notify_warning("Not Found", "Could not find Tesseract installation.\n\nPlease install from:\nhttps://github.com/UB-Mannheim/tesseract/wiki") - self._check_backends() - - def _check_backends(self): - """Check OCR backend availability with install buttons.""" - from core.ocr_backend_manager import get_ocr_backend_manager - - manager = get_ocr_backend_manager() - statuses = [] - - # EasyOCR - easyocr_status = manager.get_backend_status('easyocr') - if easyocr_status['available']: - statuses.append("โœ… EasyOCR - Available (recommended)") - else: - statuses.append("โŒ EasyOCR - Not installed\n Click 'Install EasyOCR' button below") - - # Tesseract - tesseract_status = manager.get_backend_status('tesseract') - if tesseract_status['available']: - path_info = f" at {tesseract_status['path']}" if tesseract_status['path'] else "" - statuses.append(f"โœ… Tesseract - Available{path_info}") - elif tesseract_status['installed']: - statuses.append("โš ๏ธ Tesseract - Python package installed but binary not found\n Click 'Auto-Detect Tesseract' or install binary from:\n https://github.com/UB-Mannheim/tesseract/wiki") - else: - statuses.append("โŒ Tesseract - Not installed\n Click 'Install Tesseract Package' then install binary") - - # PaddleOCR - paddle_status = manager.get_backend_status('paddleocr') - if paddle_status['available']: - statuses.append("โœ… PaddleOCR - Available") - else: - statuses.append("โŒ PaddleOCR - Not installed\n Click 'Install PaddleOCR' button below") - - statuses.append("\n๐Ÿ’ก Recommendation: EasyOCR is easiest (auto-downloads models)") - - self.backend_status.setPlainText('\n'.join(statuses)) - - def shutdown(self): - """Clean up.""" - if self.ocr_thread and self.ocr_thread.isRunning(): - self.ocr_thread.wait(1000) - super().shutdown() diff --git a/plugins/global_tracker/__init__.py b/plugins/global_tracker/__init__.py deleted file mode 100644 index 4d0572d..0000000 --- a/plugins/global_tracker/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Global Tracker Plugin -""" - -from .plugin import GlobalTrackerPlugin - -__all__ = ["GlobalTrackerPlugin"] diff --git a/plugins/global_tracker/plugin.py b/plugins/global_tracker/plugin.py deleted file mode 100644 index 935d09a..0000000 --- a/plugins/global_tracker/plugin.py +++ /dev/null @@ -1,257 +0,0 @@ -""" -EU-Utility - Global Tracker Plugin - -Track globals, HOFs, with notifications. -""" - -import json -from datetime import datetime, timedelta -from pathlib import Path -from collections import deque - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTableWidget, QTableWidgetItem, - QCheckBox, QFrame -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin -from core.icon_manager import get_icon_manager -from PyQt6.QtGui import QPixmap - - -class GlobalTrackerPlugin(BasePlugin): - """Track globals and HOFs with stats.""" - - name = "Global Tracker" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Track globals, HOFs, and ATHs" - hotkey = "ctrl+shift+g" - - def initialize(self): - """Setup global tracker.""" - self.data_file = Path("data/globals.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.globals = deque(maxlen=1000) - self.my_globals = [] - self.notifications_enabled = True - - self._load_data() - - def _load_data(self): - """Load global data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.globals.extend(data.get('globals', [])) - self.my_globals = data.get('my_globals', []) - except: - pass - - def _save_data(self): - """Save global data.""" - with open(self.data_file, 'w') as f: - json.dump({ - 'globals': list(self.globals), - 'my_globals': self.my_globals - }, f, indent=2) - - def get_ui(self): - """Create global tracker UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Get icon manager - icon_mgr = get_icon_manager() - - # Title with icon - title_layout = QHBoxLayout() - - title_icon = QLabel() - icon_pixmap = icon_mgr.get_pixmap('dollar-sign', size=20) - title_icon.setPixmap(icon_pixmap) - title_icon.setFixedSize(20, 20) - title_layout.addWidget(title_icon) - - title = QLabel("Global Tracker") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - title_layout.addWidget(title) - title_layout.addStretch() - - layout.addLayout(title_layout) - - # Stats - stats_frame = QFrame() - stats_frame.setStyleSheet(""" - QFrame { - background-color: rgba(30, 35, 45, 200); - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - """) - stats_layout = QHBoxLayout(stats_frame) - - total = len(self.globals) - hofs = sum(1 for g in self.globals if g.get('value', 0) >= 1000) - - self.total_label = QLabel(f"Globals: {total}") - self.total_label.setStyleSheet("color: #4caf50; font-weight: bold;") - stats_layout.addWidget(self.total_label) - - self.hof_label = QLabel(f"HOFs: {hofs}") - self.hof_label.setStyleSheet("color: #ffc107; font-weight: bold;") - stats_layout.addWidget(self.hof_label) - - self.my_label = QLabel(f"My Globals: {len(self.my_globals)}") - self.my_label.setStyleSheet("color: #ff8c42; font-weight: bold;") - stats_layout.addWidget(self.my_label) - - stats_layout.addStretch() - layout.addWidget(stats_frame) - - # Filters - filters = QHBoxLayout() - self.show_mine_cb = QCheckBox("Show only my globals") - self.show_mine_cb.setStyleSheet("color: white;") - filters.addWidget(self.show_mine_cb) - - self.show_hof_cb = QCheckBox("HOFs only") - self.show_hof_cb.setStyleSheet("color: white;") - filters.addWidget(self.show_hof_cb) - - filters.addStretch() - layout.addLayout(filters) - - # Globals table - self.globals_table = QTableWidget() - self.globals_table.setColumnCount(5) - self.globals_table.setHorizontalHeaderLabels(["Time", "Player", "Mob/Item", "Value", "Type"]) - self.globals_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - QHeaderView::section { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,180); - padding: 8px; - font-weight: bold; - font-size: 11px; - } - """) - self.globals_table.horizontalHeader().setStretchLastSection(True) - layout.addWidget(self.globals_table) - - self._refresh_globals() - - # Test buttons - test_layout = QHBoxLayout() - - test_global = QPushButton("Test Global") - test_global.setStyleSheet(""" - QPushButton { - background-color: #4caf50; - color: white; - padding: 8px; - border: none; - border-radius: 4px; - } - """) - test_global.clicked.connect(self._test_global) - test_layout.addWidget(test_global) - - test_hof = QPushButton("Test HOF") - test_hof.setStyleSheet(""" - QPushButton { - background-color: #ffc107; - color: black; - padding: 8px; - border: none; - border-radius: 4px; - } - """) - test_hof.clicked.connect(self._test_hof) - test_layout.addWidget(test_hof) - - test_layout.addStretch() - layout.addLayout(test_layout) - - layout.addStretch() - return widget - - def _refresh_globals(self): - """Refresh globals table.""" - recent = list(self.globals)[-50:] # Last 50 - - self.globals_table.setRowCount(len(recent)) - for i, g in enumerate(reversed(recent)): - self.globals_table.setItem(i, 0, QTableWidgetItem(g.get('time', '-')[-8:])) - self.globals_table.setItem(i, 1, QTableWidgetItem(g.get('player', 'Unknown'))) - self.globals_table.setItem(i, 2, QTableWidgetItem(g.get('target', 'Unknown'))) - - value_item = QTableWidgetItem(f"{g.get('value', 0):.2f} PED") - value = g.get('value', 0) - if value >= 10000: - value_item.setForeground(Qt.GlobalColor.magenta) - elif value >= 1000: - value_item.setForeground(Qt.GlobalColor.yellow) - elif value >= 50: - value_item.setForeground(Qt.GlobalColor.green) - self.globals_table.setItem(i, 3, value_item) - - g_type = "ATH" if value >= 10000 else "HOF" if value >= 1000 else "Global" - self.globals_table.setItem(i, 4, QTableWidgetItem(g_type)) - - def _test_global(self): - """Add test global.""" - self.add_global("TestPlayer", "Argo Scout", 45.23, is_mine=True) - self._refresh_globals() - - def _test_hof(self): - """Add test HOF.""" - self.add_global("TestPlayer", "Oratan Miner", 1250.00, is_mine=True) - self._refresh_globals() - - def add_global(self, player, target, value, is_mine=False): - """Add a global.""" - entry = { - 'time': datetime.now().isoformat(), - 'player': player, - 'target': target, - 'value': value, - 'is_mine': is_mine - } - - self.globals.append(entry) - - if is_mine: - self.my_globals.append(entry) - - self._save_data() - self._refresh_globals() - - def parse_chat_message(self, message): - """Parse global from chat.""" - # Look for global patterns - import re - - # Example: "Player killed Argo Scout worth 45.23 PED!" - pattern = r'(\w+)\s+(?:killed|mined|crafted)\s+(.+?)\s+worth\s+([\d.]+)\s+PED' - match = re.search(pattern, message, re.IGNORECASE) - - if match: - player = match.group(1) - target = match.group(2) - value = float(match.group(3)) - - is_mine = player == "You" or player == "Your avatar" - self.add_global(player, target, value, is_mine) 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/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/inventory_manager/__init__.py b/plugins/inventory_manager/__init__.py deleted file mode 100644 index 64fccac..0000000 --- a/plugins/inventory_manager/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Inventory Manager Plugin -""" - -from .plugin import InventoryManagerPlugin - -__all__ = ["InventoryManagerPlugin"] diff --git a/plugins/inventory_manager/plugin.py b/plugins/inventory_manager/plugin.py deleted file mode 100644 index 656aa90..0000000 --- a/plugins/inventory_manager/plugin.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -EU-Utility - Inventory Manager Plugin - -Track inventory items, value, and weight. -""" - -import json -from pathlib import Path -from collections import defaultdict - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTableWidget, QTableWidgetItem, - QLineEdit, QProgressBar, QFrame -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin - - -class InventoryManagerPlugin(BasePlugin): - """Track and manage inventory.""" - - name = "Inventory" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Track items, TT value, and weight" - hotkey = "ctrl+shift+i" - - def initialize(self): - """Setup inventory manager.""" - self.data_file = Path("data/inventory.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.items = [] - self.max_slots = 200 - self.max_weight = 355.0 - - self._load_data() - - def _load_data(self): - """Load inventory data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.items = data.get('items', []) - except: - pass - - def _save_data(self): - """Save inventory data.""" - with open(self.data_file, 'w') as f: - json.dump({'items': self.items}, f, indent=2) - - def get_ui(self): - """Create inventory UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("๐ŸŽ’ Inventory Manager") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Stats - stats_frame = QFrame() - stats_frame.setStyleSheet(""" - QFrame { - background-color: rgba(30, 35, 45, 200); - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - """) - stats_layout = QVBoxLayout(stats_frame) - - # Slots - slots_used = len(self.items) - slots_layout = QHBoxLayout() - slots_layout.addWidget(QLabel("Slots:")) - self.slots_progress = QProgressBar() - self.slots_progress.setMaximum(self.max_slots) - self.slots_progress.setValue(slots_used) - self.slots_progress.setStyleSheet(""" - QProgressBar { - background-color: rgba(60, 70, 90, 150); - border: none; - border-radius: 3px; - text-align: center; - color: white; - } - QProgressBar::chunk { - background-color: #4a9eff; - border-radius: 3px; - } - """) - slots_layout.addWidget(self.slots_progress) - self.slots_label = QLabel(f"{slots_used}/{self.max_slots}") - self.slots_label.setStyleSheet("color: white;") - slots_layout.addWidget(self.slots_label) - stats_layout.addLayout(slots_layout) - - # Weight - total_weight = sum(item.get('weight', 0) for item in self.items) - weight_layout = QHBoxLayout() - weight_layout.addWidget(QLabel("Weight:")) - self.weight_progress = QProgressBar() - self.weight_progress.setMaximum(int(self.max_weight)) - self.weight_progress.setValue(int(total_weight)) - self.weight_progress.setStyleSheet(""" - QProgressBar { - background-color: rgba(60, 70, 90, 150); - border: none; - border-radius: 3px; - text-align: center; - color: white; - } - QProgressBar::chunk { - background-color: #ffc107; - border-radius: 3px; - } - """) - weight_layout.addWidget(self.weight_progress) - self.weight_label = QLabel(f"{total_weight:.1f}/{self.max_weight} kg") - self.weight_label.setStyleSheet("color: white;") - weight_layout.addWidget(self.weight_label) - stats_layout.addLayout(weight_layout) - - # PED value - total_tt = sum(item.get('tt', 0) for item in self.items) - ped_label = QLabel(f"๐Ÿ’ฐ Total TT: {total_tt:.2f} PED") - ped_label.setStyleSheet("color: #ffc107; font-weight: bold;") - stats_layout.addWidget(ped_label) - - layout.addWidget(stats_frame) - - # Items table - self.items_table = QTableWidget() - self.items_table.setColumnCount(5) - self.items_table.setHorizontalHeaderLabels(["Item", "Qty", "TT", "Weight", "Container"]) - self.items_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - QHeaderView::section { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,180); - padding: 8px; - font-weight: bold; - font-size: 11px; - } - """) - self.items_table.horizontalHeader().setStretchLastSection(True) - layout.addWidget(self.items_table) - - self._refresh_items() - - # Scan button - scan_btn = QPushButton("Scan Inventory") - scan_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 12px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - scan_btn.clicked.connect(self._scan_inventory) - layout.addWidget(scan_btn) - - layout.addStretch() - return widget - - def _refresh_items(self): - """Refresh items table.""" - self.items_table.setRowCount(len(self.items)) - - for i, item in enumerate(self.items): - self.items_table.setItem(i, 0, QTableWidgetItem(item.get('name', 'Unknown'))) - self.items_table.setItem(i, 1, QTableWidgetItem(str(item.get('quantity', 1)))) - self.items_table.setItem(i, 2, QTableWidgetItem(f"{item.get('tt', 0):.2f}")) - self.items_table.setItem(i, 3, QTableWidgetItem(f"{item.get('weight', 0):.2f}")) - self.items_table.setItem(i, 4, QTableWidgetItem(item.get('container', 'Main'))) - - def _scan_inventory(self): - """Scan inventory with OCR.""" - # TODO: Implement OCR - pass - - def add_item(self, name, quantity=1, tt=0.0, weight=0.0, container='Main'): - """Add item to inventory.""" - # Check if exists - for item in self.items: - if item['name'] == name and item['container'] == container: - item['quantity'] += quantity - item['tt'] += tt - item['weight'] += weight - self._save_data() - self._refresh_items() - return - - # New item - self.items.append({ - 'name': name, - 'quantity': quantity, - 'tt': tt, - 'weight': weight, - 'container': container - }) - self._save_data() - self._refresh_items() diff --git a/plugins/log_parser_test/__init__.py b/plugins/log_parser_test/__init__.py deleted file mode 100644 index eeaca4c..0000000 --- a/plugins/log_parser_test/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -"""Log Parser Test Plugin.""" -from .plugin import LogParserTestPlugin diff --git a/plugins/log_parser_test/plugin.py b/plugins/log_parser_test/plugin.py deleted file mode 100644 index bbfffe8..0000000 --- a/plugins/log_parser_test/plugin.py +++ /dev/null @@ -1,384 +0,0 @@ -""" -EU-Utility - Log Parser Test Plugin - -Debug and test tool for the Log Reader core service. -Monitors log parsing in real-time with detailed output. -""" - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, - QLabel, QPushButton, QComboBox, QCheckBox, - QSpinBox, QGroupBox, QSplitter, QFrame, - QTableWidget, QTableWidgetItem, QHeaderView -) -from PyQt6.QtCore import Qt, QTimer -from PyQt6.QtGui import QColor, QFont - -from plugins.base_plugin import BasePlugin -from core.event_bus import SkillGainEvent, LootEvent, DamageEvent, GlobalEvent - - -class LogParserTestPlugin(BasePlugin): - """Test and debug tool for log parsing functionality.""" - - name = "Log Parser Test" - version = "1.0.0" - author = "EU-Utility" - description = "Debug tool for testing log parsing and event detection" - - # Example: This plugin could depend on Game Reader Test for shared OCR utilities - # dependencies = { - # 'plugins': ['plugins.game_reader_test.plugin.GameReaderTestPlugin'] - # } - - def __init__(self, overlay_window, config): - super().__init__(overlay_window, config) - self.event_counts = { - 'skill_gain': 0, - 'loot': 0, - 'global': 0, - 'damage': 0, - 'damage_taken': 0, - 'heal': 0, - 'mission_complete': 0, - 'tier_increase': 0, - 'enhancer_break': 0, - 'unknown': 0 - } - self.recent_events = [] - self.max_events = 100 - self.auto_scroll = True - - def initialize(self): - """Initialize plugin and subscribe to events.""" - # Subscribe to all event types for testing - self.subscribe_typed(SkillGainEvent, self._on_skill_gain) - self.subscribe_typed(LootEvent, self._on_loot) - self.subscribe_typed(DamageEvent, self._on_damage) - self.subscribe_typed(GlobalEvent, self._on_global) - - # Also subscribe to raw log events via API - api = self._get_api() - if api: - api.subscribe('log_event', self._on_raw_log_event) - - self.log_info("Log Parser Test initialized - monitoring events...") - - def _get_api(self): - """Get PluginAPI instance.""" - try: - from core.plugin_api import get_api - return get_api() - except: - return None - - def _on_skill_gain(self, event: SkillGainEvent): - """Handle skill gain events.""" - self.event_counts['skill_gain'] += 1 - self._add_event("Skill Gain", f"{event.skill_name}: +{event.gain_amount} pts", "#4ecdc4") - - def _on_loot(self, event: LootEvent): - """Handle loot events.""" - self.event_counts['loot'] += 1 - # LootEvent has items list, extract first item for display - if event.items: - item = event.items[0] - item_name = item.get('name', 'Unknown') - quantity = item.get('quantity', 1) - value = item.get('value', 0) - value_str = f" ({value:.2f} PED)" if value else "" - self._add_event("Loot", f"{item_name} x{quantity}{value_str} from {event.mob_name}", "#ff8c42") - else: - self._add_event("Loot", f"Loot from {event.mob_name}", "#ff8c42") - - def _on_damage(self, event: DamageEvent): - """Handle damage events.""" - if event.is_outgoing: - self.event_counts['damage'] += 1 - self._add_event("Damage", f"{event.damage_amount} damage ({event.damage_type})", "#ff6b6b") - else: - self.event_counts['damage_taken'] += 1 - self._add_event("Damage Taken", f"{event.damage_amount} damage from {event.attacker_name}", "#ff4757") - - def _on_global(self, event: GlobalEvent): - """Handle global events.""" - self.event_counts['global'] += 1 - item_str = f" with {event.item_name}" if event.item_name else "" - self._add_event("Global", f"{event.player_name} got {event.achievement_type}{item_str} worth {event.value} PED!", "#ffd93d") - - def _on_raw_log_event(self, event_data): - """Handle raw log events.""" - event_type = event_data.get('event_type', 'unknown') - self.event_counts[event_type] = self.event_counts.get(event_type, 0) + 1 - - def _add_event(self, event_type: str, details: str, color: str): - """Add event to recent events list.""" - from datetime import datetime - timestamp = datetime.now().strftime("%H:%M:%S") - - self.recent_events.insert(0, { - 'time': timestamp, - 'type': event_type, - 'details': details, - 'color': color - }) - - # Trim to max - if len(self.recent_events) > self.max_events: - self.recent_events = self.recent_events[:self.max_events] - - # Update UI if available - if hasattr(self, 'events_table'): - self._update_events_table() - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(12) - - # Header - header = QLabel("๐Ÿ“Š Log Parser Test & Debug Tool") - header.setStyleSheet("font-size: 16px; font-weight: bold; color: #ff8c42;") - layout.addWidget(header) - - # Status bar - status_frame = QFrame() - status_frame.setStyleSheet("background-color: #1a1f2e; border-radius: 6px; padding: 8px;") - status_layout = QHBoxLayout(status_frame) - - self.status_label = QLabel("Status: Monitoring...") - self.status_label.setStyleSheet("color: #4ecdc4;") - status_layout.addWidget(self.status_label) - status_layout.addStretch() - - self.log_path_label = QLabel() - self._update_log_path() - self.log_path_label.setStyleSheet("color: #666;") - status_layout.addWidget(self.log_path_label) - - layout.addWidget(status_frame) - - # Event counters - counters_group = QGroupBox("Event Counters") - counters_layout = QHBoxLayout(counters_group) - - self.counter_labels = {} - for event_type, color in [ - ('skill_gain', '#4ecdc4'), - ('loot', '#ff8c42'), - ('global', '#ffd93d'), - ('damage', '#ff6b6b'), - ('damage_taken', '#ff4757') - ]: - frame = QFrame() - frame.setStyleSheet(f"background-color: #1a1f2e; border-left: 3px solid {color}; padding: 8px;") - frame_layout = QVBoxLayout(frame) - - name_label = QLabel(event_type.replace('_', ' ').title()) - name_label.setStyleSheet("color: #888; font-size: 10px;") - frame_layout.addWidget(name_label) - - count_label = QLabel("0") - count_label.setStyleSheet(f"color: {color}; font-size: 20px; font-weight: bold;") - frame_layout.addWidget(count_label) - - self.counter_labels[event_type] = count_label - counters_layout.addWidget(frame) - - layout.addWidget(counters_group) - - # Recent events table - events_group = QGroupBox("Recent Events (last 100)") - events_layout = QVBoxLayout(events_group) - - self.events_table = QTableWidget() - self.events_table.setColumnCount(3) - self.events_table.setHorizontalHeaderLabels(["Time", "Type", "Details"]) - self.events_table.horizontalHeader().setStretchLastSection(True) - self.events_table.setStyleSheet(""" - QTableWidget { - background-color: #141f23; - border: 1px solid #333; - } - QTableWidget::item { - padding: 6px; - } - """) - events_layout.addWidget(self.events_table) - - # Auto-scroll checkbox - self.auto_scroll_cb = QCheckBox("Auto-scroll to new events") - self.auto_scroll_cb.setChecked(True) - events_layout.addWidget(self.auto_scroll_cb) - - layout.addWidget(events_group, 1) # Stretch factor 1 - - # Test controls - controls_group = QGroupBox("Test Controls") - controls_layout = QHBoxLayout(controls_group) - - # Simulate event buttons - btn_layout = QHBoxLayout() - - test_skill_btn = QPushButton("Test: Skill Gain") - test_skill_btn.clicked.connect(self._simulate_skill_gain) - btn_layout.addWidget(test_skill_btn) - - test_loot_btn = QPushButton("Test: Loot") - test_loot_btn.clicked.connect(self._simulate_loot) - btn_layout.addWidget(test_loot_btn) - - test_damage_btn = QPushButton("Test: Damage") - test_damage_btn.clicked.connect(self._simulate_damage) - btn_layout.addWidget(test_damage_btn) - - clear_btn = QPushButton("Clear Events") - clear_btn.clicked.connect(self._clear_events) - btn_layout.addWidget(clear_btn) - - controls_layout.addLayout(btn_layout) - controls_layout.addStretch() - - layout.addWidget(controls_group) - - # Raw log view - raw_group = QGroupBox("Raw Log Lines (last 50)") - raw_layout = QVBoxLayout(raw_group) - - self.raw_log_text = QTextEdit() - self.raw_log_text.setReadOnly(True) - self.raw_log_text.setStyleSheet(""" - QTextEdit { - background-color: #0d1117; - color: #c9d1d9; - font-family: Consolas, monospace; - font-size: 11px; - } - """) - raw_layout.addWidget(self.raw_log_text) - - layout.addWidget(raw_group) - - # Start update timer - self.update_timer = QTimer() - self.update_timer.timeout.connect(self._update_counters) - self.update_timer.start(1000) # Update every second - - # Subscribe to raw log lines - self.read_log(lines=50) # Pre-fill with recent log - - return widget - - def _update_log_path(self): - """Update log path display.""" - try: - from core.log_reader import LogReader - reader = LogReader() - if reader.log_path: - self.log_path_label.setText(f"Log: {reader.log_path}") - else: - self.log_path_label.setText("Log: Not found") - except Exception as e: - self.log_path_label.setText(f"Log: Error - {e}") - - def _update_counters(self): - """Update counter displays.""" - for event_type, label in self.counter_labels.items(): - count = self.event_counts.get(event_type, 0) - label.setText(str(count)) - - def _update_events_table(self): - """Update events table with recent events.""" - self.events_table.setRowCount(len(self.recent_events)) - - for i, event in enumerate(self.recent_events): - # Time - time_item = QTableWidgetItem(event['time']) - time_item.setForeground(QColor("#888")) - self.events_table.setItem(i, 0, time_item) - - # Type - type_item = QTableWidgetItem(event['type']) - type_item.setForeground(QColor(event['color'])) - type_item.setFont(QFont("Segoe UI", weight=QFont.Weight.Bold)) - self.events_table.setItem(i, 1, type_item) - - # Details - details_item = QTableWidgetItem(event['details']) - details_item.setForeground(QColor("#c9d1d9")) - self.events_table.setItem(i, 2, details_item) - - # Auto-scroll - if self.auto_scroll_cb.isChecked() and self.recent_events: - self.events_table.scrollToTop() - - def _simulate_skill_gain(self): - """Simulate a skill gain event for testing.""" - from datetime import datetime - event = SkillGainEvent( - timestamp=datetime.now(), - skill_name="Rifle", - gain_amount=0.1234, - skill_value=1234.56 - ) - self._on_skill_gain(event) - self.log_info("Simulated skill gain event") - - def _simulate_loot(self): - """Simulate a loot event for testing.""" - from datetime import datetime - event = LootEvent( - timestamp=datetime.now(), - mob_name="Atrox", - items=[{'name': 'Shrapnel', 'quantity': 100, 'value': 1.0}], - total_tt_value=1.0, - position=None - ) - self._on_loot(event) - self.log_info("Simulated loot event") - - def _simulate_damage(self): - """Simulate a damage event for testing.""" - from datetime import datetime - event = DamageEvent( - timestamp=datetime.now(), - damage_amount=45, - damage_type="Impact", - is_critical=True, - target_name="Atrox", - attacker_name="Player", - is_outgoing=True - ) - self._on_damage(event) - self.log_info("Simulated damage event") - - def _clear_events(self): - """Clear all events.""" - self.recent_events.clear() - for key in self.event_counts: - self.event_counts[key] = 0 - self._update_events_table() - self._update_counters() - self.log_info("Cleared all events") - - def read_log(self, lines=50): - """Read recent log lines.""" - try: - # Call parent class method (BasePlugin.read_log) - log_lines = super().read_log(lines=lines) - if log_lines: - self.raw_log_text.setPlainText('\n'.join(log_lines)) - except Exception as e: - self.raw_log_text.setPlainText(f"Error reading log: {e}") - - def on_show(self): - """Called when plugin is shown.""" - self._update_events_table() - self._update_counters() - - def shutdown(self): - """Clean up.""" - if hasattr(self, 'update_timer'): - self.update_timer.stop() - super().shutdown() diff --git a/plugins/loot_tracker/__init__.py b/plugins/loot_tracker/__init__.py deleted file mode 100644 index b7024fd..0000000 --- a/plugins/loot_tracker/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Loot Tracker Plugin -""" - -from .plugin import LootTrackerPlugin - -__all__ = ["LootTrackerPlugin"] diff --git a/plugins/loot_tracker/plugin.py b/plugins/loot_tracker/plugin.py deleted file mode 100644 index 695350b..0000000 --- a/plugins/loot_tracker/plugin.py +++ /dev/null @@ -1,224 +0,0 @@ -""" -EU-Utility - Loot Tracker Plugin - -Track and analyze hunting loot with statistics and ROI. -""" - -import re -from datetime import datetime -from collections import defaultdict -from pathlib import Path -import json - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTableWidget, QTableWidgetItem, - QComboBox, QLineEdit, QTabWidget, QFrame -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin - - -class LootTrackerPlugin(BasePlugin): - """Track hunting loot and calculate ROI.""" - - name = "Loot Tracker" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Track hunting loot with stats and ROI analysis" - hotkey = "ctrl+shift+l" # L for Loot - - def initialize(self): - """Setup loot tracker.""" - self.data_file = Path("data/loot_tracker.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.sessions = [] - self.current_session = { - 'start_time': None, - 'kills': 0, - 'loot_items': [], - 'total_tt': 0.0, - 'ammo_cost': 0.0, - 'weapon_decay': 0.0, - } - - self._load_data() - - def _load_data(self): - """Load historical data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - self.sessions = json.load(f) - except: - self.sessions = [] - - def _save_data(self): - """Save data to file.""" - with open(self.data_file, 'w') as f: - json.dump(self.sessions, f, indent=2) - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("Loot Tracker") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Stats summary - stats_frame = QFrame() - stats_frame.setStyleSheet(""" - QFrame { - background-color: rgba(0, 0, 0, 50); - border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 20); - } - """) - stats_layout = QHBoxLayout(stats_frame) - - self.kills_label = QLabel("Kills: 0") - self.kills_label.setStyleSheet("color: #4caf50; font-size: 14px; font-weight: bold;") - stats_layout.addWidget(self.kills_label) - - self.tt_label = QLabel("TT: 0.00 PED") - self.tt_label.setStyleSheet("color: #ffc107; font-size: 14px; font-weight: bold;") - stats_layout.addWidget(self.tt_label) - - self.roi_label = QLabel("ROI: 0%") - self.roi_label.setStyleSheet("color: #4a9eff; font-size: 14px; font-weight: bold;") - stats_layout.addWidget(self.roi_label) - - layout.addWidget(stats_frame) - - # Session controls - controls = QHBoxLayout() - - self.start_btn = QPushButton("โ–ถ Start Session") - self.start_btn.setStyleSheet(""" - QPushButton { - background-color: #4caf50; - color: white; - padding: 10px 20px; - border: none; - border-radius: 8px; - font-weight: bold; - } - QPushButton:hover { - background-color: #5cbf60; - } - """) - self.start_btn.clicked.connect(self._start_session) - controls.addWidget(self.start_btn) - - self.stop_btn = QPushButton("โน Stop Session") - self.stop_btn.setStyleSheet(""" - QPushButton { - background-color: #f44336; - color: white; - padding: 10px 20px; - border: none; - border-radius: 8px; - font-weight: bold; - } - QPushButton:hover { - background-color: #f55a4e; - } - """) - self.stop_btn.clicked.connect(self._stop_session) - self.stop_btn.setEnabled(False) - controls.addWidget(self.stop_btn) - - layout.addLayout(controls) - - # Loot table - self.loot_table = QTableWidget() - self.loot_table.setColumnCount(4) - self.loot_table.setHorizontalHeaderLabels(["Item", "Qty", "TT Value", "Time"]) - self.loot_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 30, 30, 100); - color: white; - border: none; - border-radius: 6px; - } - QHeaderView::section { - background-color: rgba(74, 158, 255, 100); - color: white; - padding: 6px; - } - """) - self.loot_table.horizontalHeader().setStretchLastSection(True) - layout.addWidget(self.loot_table) - - layout.addStretch() - return widget - - def _start_session(self): - """Start new hunting session.""" - self.current_session = { - 'start_time': datetime.now().isoformat(), - 'kills': 0, - 'loot_items': [], - 'total_tt': 0.0, - 'ammo_cost': 0.0, - 'weapon_decay': 0.0, - } - self.start_btn.setEnabled(False) - self.stop_btn.setEnabled(True) - - def _stop_session(self): - """Stop current session and save.""" - self.current_session['end_time'] = datetime.now().isoformat() - self.sessions.append(self.current_session) - self._save_data() - - self.start_btn.setEnabled(True) - self.stop_btn.setEnabled(False) - self._update_stats() - - def _update_stats(self): - """Update statistics display.""" - kills = self.current_session.get('kills', 0) - tt = self.current_session.get('total_tt', 0.0) - - self.kills_label.setText(f"Kills: {kills}") - self.tt_label.setText(f"TT: {tt:.2f} PED") - - def parse_chat_message(self, message): - """Parse loot from chat.""" - # Look for loot patterns - # Example: "You received Animal Hide (0.03 PED)" - loot_pattern = r'You received (.+?) \(([\d.]+) PED\)' - match = re.search(loot_pattern, message) - - if match: - item_name = match.group(1) - tt_value = float(match.group(2)) - - self.current_session['loot_items'].append({ - 'item': item_name, - 'tt': tt_value, - 'time': datetime.now().isoformat() - }) - self.current_session['total_tt'] += tt_value - self._update_stats() - - # Check for new kill - # Multiple items in quick succession = one kill - # Longer gap = new kill - self.current_session['kills'] += 1 - - def on_hotkey(self): - """Toggle session on hotkey.""" - if self.start_btn.isEnabled(): - self._start_session() - else: - self._stop_session() diff --git a/plugins/mining_helper/__init__.py b/plugins/mining_helper/__init__.py deleted file mode 100644 index ac46eea..0000000 --- a/plugins/mining_helper/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Mining Helper Plugin -""" - -from .plugin import MiningHelperPlugin - -__all__ = ["MiningHelperPlugin"] diff --git a/plugins/mining_helper/plugin.py b/plugins/mining_helper/plugin.py deleted file mode 100644 index 13004e8..0000000 --- a/plugins/mining_helper/plugin.py +++ /dev/null @@ -1,273 +0,0 @@ -""" -EU-Utility - Mining Helper Plugin - -Track mining finds, claims, and locations. -""" - -import json -from datetime import datetime -from pathlib import Path -from collections import defaultdict - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTableWidget, QTableWidgetItem, - QComboBox, QTextEdit -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin - - -class MiningHelperPlugin(BasePlugin): - """Track mining activities and claims.""" - - name = "Mining Helper" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Track mining finds, claims, and hotspot locations" - hotkey = "ctrl+shift+n" # N for miNiNg - - # Resource types - RESOURCES = [ - "Alicenies Liquid", - "Ares Powder", - "Blausariam", - "Caldorite", - "Cobalt", - "Copper", - "Dianthus", - "Erdorium", - "Frigulite", - "Ganganite", - "Himi", - "Ignisium", - "Iron", - "Kaz Ingot", - "Lysterium", - "Maganite", - "Niksarium", - "Oil", - "Platinum", - "Redulite", - "Rubio", - "Sopur", - "Titan", - "Typonolic Steam", - "Uranium", - "Veldspar", - "Xantium", - "Zinc", - ] - - def initialize(self): - """Setup mining helper.""" - self.data_file = Path("data/mining_helper.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.claims = [] - self.current_run = { - 'start_time': None, - 'drops': 0, - 'finds': [], - 'total_tt': 0.0, - } - - self._load_data() - - def _load_data(self): - """Load historical data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.claims = data.get('claims', []) - except: - self.claims = [] - - def _save_data(self): - """Save data.""" - with open(self.data_file, 'w') as f: - json.dump({'claims': self.claims}, f, indent=2) - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("โ›๏ธ Mining Helper") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Stats - stats = QHBoxLayout() - - self.drops_label = QLabel("Drops: 0") - self.drops_label.setStyleSheet("color: #9c27b0; font-size: 14px; font-weight: bold;") - stats.addWidget(self.drops_label) - - self.finds_label = QLabel("Finds: 0") - self.finds_label.setStyleSheet("color: #4caf50; font-size: 14px; font-weight: bold;") - stats.addWidget(self.finds_label) - - self.hit_rate_label = QLabel("Hit Rate: 0%") - self.hit_rate_label.setStyleSheet("color: #ffc107; font-size: 14px; font-weight: bold;") - stats.addWidget(self.hit_rate_label) - - layout.addLayout(stats) - - # Quick add claim - add_frame = QWidget() - add_frame.setStyleSheet(""" - QWidget { - background-color: rgba(0, 0, 0, 50); - border-radius: 8px; - border: 1px solid rgba(255, 255, 255, 20); - } - """) - add_layout = QHBoxLayout(add_frame) - add_layout.setContentsMargins(10, 10, 10, 10) - - self.resource_combo = QComboBox() - self.resource_combo.addItems(self.RESOURCES) - self.resource_combo.setStyleSheet(""" - QComboBox { - background-color: rgba(255, 255, 255, 15); - color: white; - border: none; - padding: 5px; - border-radius: 4px; - } - """) - add_layout.addWidget(self.resource_combo) - - self.size_combo = QComboBox() - self.size_combo.addItems(["Tiny", "Small", "Medium", "Large", "Huge", "Massive"]) - self.size_combo.setStyleSheet(self.resource_combo.styleSheet()) - add_layout.addWidget(self.size_combo) - - add_btn = QPushButton("+ Add Claim") - add_btn.setStyleSheet(""" - QPushButton { - background-color: #9c27b0; - color: white; - padding: 8px 15px; - border: none; - border-radius: 6px; - font-weight: bold; - } - QPushButton:hover { - background-color: #ab47bc; - } - """) - add_btn.clicked.connect(self._add_claim) - add_layout.addWidget(add_btn) - - layout.addWidget(add_frame) - - # Claims table - self.claims_table = QTableWidget() - self.claims_table.setColumnCount(4) - self.claims_table.setHorizontalHeaderLabels(["Resource", "Size", "TT", "Time"]) - self.claims_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 30, 30, 100); - color: white; - border: none; - border-radius: 6px; - } - QHeaderView::section { - background-color: rgba(156, 39, 176, 100); - color: white; - padding: 6px; - } - """) - self.claims_table.horizontalHeader().setStretchLastSection(True) - layout.addWidget(self.claims_table) - - layout.addStretch() - return widget - - def _add_claim(self): - """Add a claim manually.""" - resource = self.resource_combo.currentText() - size = self.size_combo.currentText() - - claim = { - 'resource': resource, - 'size': size, - 'tt_value': self._estimate_tt(size), - 'time': datetime.now().isoformat(), - 'location': None, # Could get from game - } - - self.claims.append(claim) - self._save_data() - self._update_table() - self._update_stats() - - def _estimate_tt(self, size): - """Estimate TT value based on claim size.""" - estimates = { - 'Tiny': 0.05, - 'Small': 0.25, - 'Medium': 1.00, - 'Large': 5.00, - 'Huge': 25.00, - 'Massive': 100.00, - } - return estimates.get(size, 0.05) - - def _update_table(self): - """Update claims table.""" - recent = self.claims[-20:] # Show last 20 - self.claims_table.setRowCount(len(recent)) - - for i, claim in enumerate(recent): - self.claims_table.setItem(i, 0, QTableWidgetItem(claim['resource'])) - self.claims_table.setItem(i, 1, QTableWidgetItem(claim['size'])) - self.claims_table.setItem(i, 2, QTableWidgetItem(f"{claim['tt_value']:.2f}")) - time_str = claim['time'][11:16] if claim['time'] else '-' - self.claims_table.setItem(i, 3, QTableWidgetItem(time_str)) - - def _update_stats(self): - """Update statistics.""" - drops = len(self.claims) + 10 # Estimate - finds = len(self.claims) - hit_rate = (finds / drops * 100) if drops > 0 else 0 - - self.drops_label.setText(f"Drops: ~{drops}") - self.finds_label.setText(f"Finds: {finds}") - self.hit_rate_label.setText(f"Hit Rate: {hit_rate:.1f}%") - - def parse_chat_message(self, message): - """Parse mining claims from chat.""" - # Look for claim patterns - # Example: "You found a Tiny Lysterium claim" - for resource in self.RESOURCES: - if resource in message and "claim" in message.lower(): - # Extract size - sizes = ["Tiny", "Small", "Medium", "Large", "Huge", "Massive"] - size = "Unknown" - for s in sizes: - if s in message: - size = s - break - - claim = { - 'resource': resource, - 'size': size, - 'tt_value': self._estimate_tt(size), - 'time': datetime.now().isoformat(), - 'location': None, - } - - self.claims.append(claim) - self._save_data() - self._update_table() - self._update_stats() - break diff --git a/plugins/mission_tracker/__init__.py b/plugins/mission_tracker/__init__.py deleted file mode 100644 index de1d978..0000000 --- a/plugins/mission_tracker/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Mission Tracker Plugin -""" - -from .plugin import MissionTrackerPlugin - -__all__ = ["MissionTrackerPlugin"] diff --git a/plugins/mission_tracker/plugin.py b/plugins/mission_tracker/plugin.py deleted file mode 100644 index bf2c878..0000000 --- a/plugins/mission_tracker/plugin.py +++ /dev/null @@ -1,315 +0,0 @@ -""" -EU-Utility - Mission Tracker Plugin - -Track active missions and progress. -""" - -import json -from datetime import datetime -from pathlib import Path - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QProgressBar, QFrame, QScrollArea -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin -from core.icon_manager import get_icon_manager - - -class MissionTrackerPlugin(BasePlugin): - """Track active missions and daily challenges.""" - - name = "Mission Tracker" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Track missions, challenges, and objectives" - hotkey = "ctrl+shift+m" - - def initialize(self): - """Setup mission tracker.""" - self.data_file = Path("data/missions.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.missions = [] - self.daily_challenges = [] - - self._load_data() - - def _load_data(self): - """Load mission data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.missions = data.get('missions', []) - self.daily_challenges = data.get('daily', []) - except: - pass - - def _save_data(self): - """Save mission data.""" - with open(self.data_file, 'w') as f: - json.dump({ - 'missions': self.missions, - 'daily': self.daily_challenges - }, f, indent=2) - - def get_ui(self): - """Create mission tracker UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("๐Ÿ“œ Mission Tracker") - title.setStyleSheet(""" - color: white; - font-size: 16px; - font-weight: bold; - """) - layout.addWidget(title) - - # Active missions section - active_label = QLabel("Active Missions") - active_label.setStyleSheet("color: rgba(255,255,255,200); font-size: 12px;") - layout.addWidget(active_label) - - # Mission cards - self.missions_container = QWidget() - self.missions_layout = QVBoxLayout(self.missions_container) - self.missions_layout.setSpacing(10) - self.missions_layout.setContentsMargins(0, 0, 0, 0) - - self._refresh_missions() - layout.addWidget(self.missions_container) - - # Daily challenges - daily_label = QLabel("Daily Challenges") - daily_label.setStyleSheet("color: rgba(255,255,255,200); font-size: 12px; margin-top: 10px;") - layout.addWidget(daily_label) - - self.daily_container = QWidget() - self.daily_layout = QVBoxLayout(self.daily_container) - self.daily_layout.setSpacing(8) - self.daily_layout.setContentsMargins(0, 0, 0, 0) - - self._refresh_daily() - layout.addWidget(self.daily_container) - - # Add mission button - add_btn = QPushButton("+ Add Mission") - add_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(255, 140, 66, 200); - color: white; - padding: 10px; - border: none; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: rgba(255, 160, 80, 230); - } - """) - add_btn.clicked.connect(self._add_mission) - layout.addWidget(add_btn) - - layout.addStretch() - return widget - - def _create_mission_card(self, mission): - """Create a mission card widget.""" - card = QFrame() - card.setStyleSheet(""" - QFrame { - background-color: rgba(30, 35, 45, 200); - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - """) - layout = QVBoxLayout(card) - layout.setContentsMargins(12, 10, 12, 10) - layout.setSpacing(8) - - # Header - header = QHBoxLayout() - - name = QLabel(mission.get('name', 'Unknown Mission')) - name.setStyleSheet("color: #ff8c42; font-weight: bold; font-size: 12px;") - header.addWidget(name) - - header.addStretch() - - # Complete button - complete_btn = QPushButton("โœ“") - complete_btn.setFixedSize(24, 24) - complete_btn.setStyleSheet(""" - QPushButton { - background-color: rgba(76, 175, 80, 150); - color: white; - border: none; - border-radius: 3px; - font-weight: bold; - } - QPushButton:hover { - background-color: rgba(76, 175, 80, 200); - } - """) - complete_btn.clicked.connect(lambda: self._complete_mission(mission)) - header.addWidget(complete_btn) - - layout.addLayout(header) - - # Progress - current = mission.get('current', 0) - total = mission.get('total', 1) - pct = min(100, int(current / total * 100)) - - progress_layout = QHBoxLayout() - - progress = QProgressBar() - progress.setValue(pct) - progress.setTextVisible(False) - progress.setFixedHeight(6) - progress.setStyleSheet(""" - QProgressBar { - background-color: rgba(60, 70, 90, 150); - border: none; - border-radius: 3px; - } - QProgressBar::chunk { - background-color: #ff8c42; - border-radius: 3px; - } - """) - progress_layout.addWidget(progress, 1) - - progress_text = QLabel(f"{current}/{total}") - progress_text.setStyleSheet("color: rgba(255,255,255,150); font-size: 10px;") - progress_layout.addWidget(progress_text) - - layout.addLayout(progress_layout) - - return card - - def _create_challenge_card(self, challenge): - """Create a daily challenge card.""" - card = QFrame() - card.setStyleSheet(""" - QFrame { - background-color: rgba(25, 30, 40, 180); - border: 1px solid rgba(80, 90, 110, 60); - border-radius: 4px; - } - """) - layout = QHBoxLayout(card) - layout.setContentsMargins(10, 8, 10, 8) - layout.setSpacing(10) - - # Icon based on type - icon_mgr = get_icon_manager() - icon_label = QLabel() - icon_pixmap = icon_mgr.get_pixmap('sword', size=16) - icon_label.setPixmap(icon_pixmap) - icon_label.setFixedSize(16, 16) - layout.addWidget(icon_label) - - # Name - name = QLabel(challenge.get('name', 'Challenge')) - name.setStyleSheet("color: white; font-size: 11px;") - layout.addWidget(name) - - layout.addStretch() - - # Progress - current = challenge.get('current', 0) - total = challenge.get('total', 1) - pct = min(100, int(current / total * 100)) - - progress = QProgressBar() - progress.setValue(pct) - progress.setTextVisible(False) - progress.setFixedSize(60, 4) - progress.setStyleSheet(""" - QProgressBar { - background-color: rgba(60, 70, 90, 150); - border: none; - border-radius: 2px; - } - QProgressBar::chunk { - background-color: #ffc107; - border-radius: 2px; - } - """) - layout.addWidget(progress) - - text = QLabel(f"{current}/{total}") - text.setStyleSheet("color: rgba(255,255,255,120); font-size: 10px;") - layout.addWidget(text) - - return card - - def _refresh_missions(self): - """Refresh mission display.""" - # Clear existing - while self.missions_layout.count(): - item = self.missions_layout.takeAt(0) - if item.widget(): - item.widget().deleteLater() - - # Add missions - for mission in self.missions: - card = self._create_mission_card(mission) - self.missions_layout.addWidget(card) - - def _refresh_daily(self): - """Refresh daily challenges.""" - while self.daily_layout.count(): - item = self.daily_layout.takeAt(0) - if item.widget(): - item.widget().deleteLater() - - for challenge in self.daily_challenges: - card = self._create_challenge_card(challenge) - self.daily_layout.addWidget(card) - - def _add_mission(self): - """Add a new mission.""" - # Add sample mission - self.missions.append({ - 'name': 'Oratan Payback Mission III', - 'current': 0, - 'total': 100, - 'type': 'kill', - 'target': 'Oratan Prospector Bandits', - 'added': datetime.now().isoformat() - }) - self._save_data() - self._refresh_missions() - - def _complete_mission(self, mission): - """Mark mission as complete.""" - if mission in self.missions: - self.missions.remove(mission) - self._save_data() - self._refresh_missions() - - def parse_chat_message(self, message): - """Parse mission progress from chat.""" - # Look for mission progress - # Example: "Mission updated: 12/100 Oratan Prospector Bandits" - for mission in self.missions: - target = mission.get('target', '') - if target and target in message: - # Extract progress - import re - match = re.search(r'(\d+)/(\d+)', message) - if match: - mission['current'] = int(match.group(1)) - mission['total'] = int(match.group(2)) - self._save_data() - self._refresh_missions() diff --git a/plugins/nexus_search/__init__.py b/plugins/nexus_search/__init__.py deleted file mode 100644 index fb6a458..0000000 --- a/plugins/nexus_search/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Nexus Search Plugin for EU-Utility -""" - -from .plugin import NexusSearchPlugin - -__all__ = ["NexusSearchPlugin"] diff --git a/plugins/nexus_search/plugin.py b/plugins/nexus_search/plugin.py deleted file mode 100644 index 6dea9f3..0000000 --- a/plugins/nexus_search/plugin.py +++ /dev/null @@ -1,442 +0,0 @@ -""" -EU-Utility - Nexus Search Plugin - -Built-in plugin for searching EntropiaNexus via API. -Uses official Nexus API endpoints. -""" - -import json -import webbrowser -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, - QLineEdit, QPushButton, QLabel, QComboBox, - QListWidget, QListWidgetItem, QTabWidget, - QTableWidget, QTableWidgetItem, QHeaderView -) -from PyQt6.QtCore import Qt, QThread, pyqtSignal - -from plugins.base_plugin import BasePlugin - - -class NexusAPIClient: - """Client for EntropiaNexus API.""" - - BASE_URL = "https://www.entropianexus.com" - - @classmethod - def fetch_exchange_items(cls, search_query=None, http_get_func=None): - """Fetch exchange items from Nexus API.""" - try: - url = f"{cls.BASE_URL}/api/market/exchange" - - if http_get_func: - response = http_get_func( - url, - cache_ttl=60, # 1 minute cache for market data - headers={'Accept': 'application/json', 'Accept-Encoding': 'gzip'} - ) - data = response.get('json') if response else None - else: - # Fallback for standalone usage - import urllib.request - req = urllib.request.Request( - url, - headers={'Accept': 'application/json', 'Accept-Encoding': 'gzip'} - ) - with urllib.request.urlopen(req, timeout=10) as resp: - data = json.loads(resp.read().decode('utf-8')) - - # Filter by search query if provided - if search_query and data: - search_lower = search_query.lower() - filtered = [] - for category in data: - if 'items' in category: - for item in category['items']: - if search_lower in item.get('name', '').lower(): - filtered.append(item) - return filtered - - return data - - except Exception as e: - print(f"API Error: {e}") - return None - - @classmethod - def fetch_item_prices(cls, item_ids, http_get_func=None): - """Fetch latest prices for items.""" - try: - if not item_ids: - return {} - - ids_str = ','.join(str(id) for id in item_ids[:100]) # Max 100 - url = f"{cls.BASE_URL}/api/market/prices/latest?items={ids_str}" - - if http_get_func: - response = http_get_func( - url, - cache_ttl=60, # 1 minute cache - headers={'Accept': 'application/json'} - ) - return response.get('json') if response else {} - else: - # Fallback for standalone usage - import urllib.request - req = urllib.request.Request( - url, - headers={'Accept': 'application/json'} - ) - with urllib.request.urlopen(req, timeout=10) as resp: - return json.loads(resp.read().decode('utf-8')) - - except Exception as e: - print(f"Price API Error: {e}") - return {} - - @classmethod - def search_users(cls, query, http_get_func=None): - """Search for verified users.""" - try: - params = {'q': query, 'limit': 10} - query_string = '&'.join(f"{k}={v}" for k, v in params.items()) - url = f"{cls.BASE_URL}/api/users/search?{query_string}" - - if http_get_func: - response = http_get_func( - url, - cache_ttl=300, # 5 minute cache for user search - headers={'Accept': 'application/json'} - ) - return response.get('json') if response else None - else: - # Fallback for standalone usage - import urllib.request - req = urllib.request.Request( - url, - headers={'Accept': 'application/json'} - ) - with urllib.request.urlopen(req, timeout=10) as resp: - return json.loads(resp.read().decode('utf-8')) - - except Exception as e: - print(f"User Search Error: {e}") - return None - - -class NexusSearchThread(QThread): - """Background thread for API searches.""" - results_ready = pyqtSignal(list, str) # results, search_type - error_occurred = pyqtSignal(str) - - def __init__(self, query, search_type, http_get_func=None): - super().__init__() - self.query = query - self.search_type = search_type - self.http_get_func = http_get_func - - def run(self): - """Perform API search.""" - try: - results = [] - - if self.search_type == "Items": - # Search exchange items - data = NexusAPIClient.fetch_exchange_items(self.query, http_get_func=self.http_get_func) - if data: - if isinstance(data, list) and len(data) > 0 and 'name' in data[0]: - # Already filtered items - results = data[:20] # Limit to 20 - else: - # Full category structure - for category in data: - if 'items' in category: - for item in category['items']: - if self.query.lower() in item.get('name', '').lower(): - results.append(item) - if len(results) >= 20: - break - if len(results) >= 20: - break - - elif self.search_type == "Users": - # Search users - data = NexusAPIClient.search_users(self.query, http_get_func=self.http_get_func) - if data: - results = data[:10] - - self.results_ready.emit(results, self.search_type) - - except Exception as e: - self.error_occurred.emit(str(e)) - - -class NexusSearchPlugin(BasePlugin): - """Search EntropiaNexus via API.""" - - name = "EntropiaNexus" - version = "1.1.0" - author = "ImpulsiveFPS" - description = "Search items, users, and market data via Nexus API" - hotkey = "ctrl+shift+n" - - def initialize(self): - """Setup the plugin.""" - self.base_url = "https://www.entropianexus.com" - self.search_thread = None - self.current_results = [] - - def get_ui(self): - """Create plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - - # Title - title = QLabel("EntropiaNexus") - title.setStyleSheet("color: #4a9eff; font-size: 18px; font-weight: bold;") - layout.addWidget(title) - - # Search type - type_layout = QHBoxLayout() - type_layout.addWidget(QLabel("Search:")) - - self.search_type = QComboBox() - self.search_type.addItems([ - "Items", - "Users", - ]) - self.search_type.setStyleSheet(""" - QComboBox { - background-color: #444; - color: white; - padding: 5px; - border-radius: 4px; - min-width: 100px; - } - """) - type_layout.addWidget(self.search_type) - - # Search input - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Enter search term...") - self.search_input.setStyleSheet(""" - QLineEdit { - background-color: #333; - color: white; - padding: 8px; - border: 2px solid #555; - border-radius: 4px; - font-size: 13px; - } - QLineEdit:focus { - border-color: #4a9eff; - } - """) - self.search_input.returnPressed.connect(self._do_search) - type_layout.addWidget(self.search_input, 1) - - # Search button - search_btn = QPushButton("๐Ÿ”") - search_btn.setFixedWidth(40) - search_btn.setStyleSheet(""" - QPushButton { - background-color: #4a9eff; - color: white; - border: none; - border-radius: 4px; - font-size: 14px; - } - QPushButton:hover { - background-color: #5aafff; - } - """) - search_btn.clicked.connect(self._do_search) - type_layout.addWidget(search_btn) - - layout.addLayout(type_layout) - - # Status - self.status_label = QLabel("Ready") - self.status_label.setStyleSheet("color: #666; font-size: 11px;") - layout.addWidget(self.status_label) - - # Results table - self.results_table = QTableWidget() - self.results_table.setColumnCount(3) - self.results_table.setHorizontalHeaderLabels(["Name", "Type", "Price"]) - self.results_table.horizontalHeader().setStretchLastSection(True) - self.results_table.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) - self.results_table.setStyleSheet(""" - QTableWidget { - background-color: #2a2a2a; - color: white; - border: 1px solid #444; - border-radius: 4px; - gridline-color: #444; - } - QTableWidget::item { - padding: 8px; - border-bottom: 1px solid #333; - } - QTableWidget::item:selected { - background-color: #4a9eff; - } - QHeaderView::section { - background-color: #333; - color: #aaa; - padding: 8px; - border: none; - font-weight: bold; - } - """) - self.results_table.cellClicked.connect(self._on_item_clicked) - self.results_table.setMaximumHeight(300) - layout.addWidget(self.results_table) - - # Action buttons - btn_layout = QHBoxLayout() - - open_btn = QPushButton("Open on Nexus") - open_btn.setStyleSheet(""" - QPushButton { - background-color: #333; - color: white; - padding: 8px 16px; - border: none; - border-radius: 4px; - } - QPushButton:hover { - background-color: #444; - } - """) - open_btn.clicked.connect(self._open_selected) - btn_layout.addWidget(open_btn) - - btn_layout.addStretch() - - # Quick links - links_label = QLabel("Quick:") - links_label.setStyleSheet("color: #666;") - btn_layout.addWidget(links_label) - - for name, url in [ - ("Market", "/market/exchange"), - ("Items", "/items"), - ("Mobs", "/mobs"), - ]: - btn = QPushButton(name) - btn.setStyleSheet(""" - QPushButton { - background-color: transparent; - color: #4a9eff; - border: none; - padding: 5px; - } - QPushButton:hover { - color: #5aafff; - text-decoration: underline; - } - """) - btn.clicked.connect(lambda checked, u=self.base_url + url: webbrowser.open(u)) - btn_layout.addWidget(btn) - - layout.addLayout(btn_layout) - layout.addStretch() - - return widget - - def _do_search(self): - """Perform API search.""" - query = self.search_input.text().strip() - if not query or len(query) < 2: - self.status_label.setText("Enter at least 2 characters") - return - - search_type = self.search_type.currentText() - - # Clear previous results - self.results_table.setRowCount(0) - self.current_results = [] - self.status_label.setText("Searching...") - - # Start search thread with http_get function - self.search_thread = NexusSearchThread(query, search_type, 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 _on_results(self, results, search_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): - if search_type == "Items": - name = item.get('name', 'Unknown') - item_type = item.get('type', 'Item') - - # Price info - buy_price = item.get('buy', []) - sell_price = item.get('sell', []) - - if buy_price: - price_text = f"Buy: {buy_price[0].get('price', 'N/A')}" - elif sell_price: - price_text = f"Sell: {sell_price[0].get('price', 'N/A')}" - else: - price_text = "No orders" - - self.results_table.setItem(row, 0, QTableWidgetItem(name)) - self.results_table.setItem(row, 1, QTableWidgetItem(item_type)) - self.results_table.setItem(row, 2, QTableWidgetItem(price_text)) - - elif search_type == "Users": - name = item.get('name', 'Unknown') - eu_name = item.get('euName', '') - - self.results_table.setItem(row, 0, QTableWidgetItem(name)) - self.results_table.setItem(row, 1, QTableWidgetItem("User")) - self.results_table.setItem(row, 2, QTableWidgetItem(eu_name or '')) - - self.status_label.setText(f"Found {len(results)} results") - - def _on_error(self, error): - """Handle search error.""" - self.status_label.setText(f"Error: {error}") - - def _on_item_clicked(self, row, column): - """Handle item click.""" - if row < len(self.current_results): - item = self.current_results[row] - search_type = self.search_type.currentText() - - if search_type == "Items": - item_id = item.get('id') - if item_id: - url = f"{self.base_url}/items/{item_id}" - webbrowser.open(url) - - elif search_type == "Users": - user_id = item.get('id') - if user_id: - url = f"{self.base_url}/users/{user_id}" - webbrowser.open(url) - - def _open_selected(self): - """Open selected item in browser.""" - selected = self.results_table.selectedItems() - if selected: - row = selected[0].row() - self._on_item_clicked(row, 0) - - def on_hotkey(self): - """Focus search when hotkey pressed.""" - if hasattr(self, 'search_input'): - self.search_input.setFocus() - self.search_input.selectAll() diff --git a/plugins/price_alerts/__init__.py b/plugins/price_alerts/__init__.py deleted file mode 100644 index e70c463..0000000 --- a/plugins/price_alerts/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -""" -Price Alerts Plugin for EU-Utility - -Monitor Entropia Nexus prices and get alerts when items -reach target prices. -""" - -from .plugin import PriceAlertPlugin - -__all__ = ['PriceAlertPlugin'] diff --git a/plugins/price_alerts/plugin.py b/plugins/price_alerts/plugin.py deleted file mode 100644 index d2d2648..0000000 --- a/plugins/price_alerts/plugin.py +++ /dev/null @@ -1,693 +0,0 @@ -""" -EU-Utility - Price Alert Plugin - -Monitor Entropia Nexus API prices and get alerts when items -reach target prices (good for buying low/selling high). -""" - -import json -from datetime import datetime, timedelta -from typing import Dict, List, Optional, Any -from dataclasses import dataclass, field, asdict - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, - QTableWidget, QTableWidgetItem, QHeaderView, QLineEdit, - QDoubleSpinBox, QComboBox, QMessageBox, QGroupBox, - QCheckBox, QSpinBox, QSplitter, QTextEdit -) -from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QObject -from PyQt6.QtGui import QColor - -from plugins.base_plugin import BasePlugin -from core.nexus_api import get_nexus_api, MarketData - - -@dataclass -class PriceAlert: - """A price alert configuration.""" - id: str - item_id: str - item_name: str - alert_type: str # 'below' or 'above' - target_price: float - current_price: Optional[float] = None - last_checked: Optional[datetime] = None - triggered: bool = False - trigger_count: int = 0 - enabled: bool = True - created_at: datetime = field(default_factory=datetime.now) - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary for serialization.""" - return { - 'id': self.id, - 'item_id': self.item_id, - 'item_name': self.item_name, - 'alert_type': self.alert_type, - 'target_price': self.target_price, - 'current_price': self.current_price, - 'last_checked': self.last_checked.isoformat() if self.last_checked else None, - 'triggered': self.triggered, - 'trigger_count': self.trigger_count, - 'enabled': self.enabled, - 'created_at': self.created_at.isoformat() - } - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> 'PriceAlert': - """Create from dictionary.""" - alert = cls( - id=data['id'], - item_id=data['item_id'], - item_name=data['item_name'], - alert_type=data['alert_type'], - target_price=data['target_price'], - current_price=data.get('current_price'), - triggered=data.get('triggered', False), - trigger_count=data.get('trigger_count', 0), - enabled=data.get('enabled', True) - ) - if data.get('last_checked'): - alert.last_checked = datetime.fromisoformat(data['last_checked']) - if data.get('created_at'): - alert.created_at = datetime.fromisoformat(data['created_at']) - return alert - - def check_condition(self, current_price: float) -> bool: - """Check if the alert condition is met.""" - self.current_price = current_price - self.last_checked = datetime.now() - - if self.alert_type == 'below': - return current_price <= self.target_price - elif self.alert_type == 'above': - return current_price >= self.target_price - return False - - -class PriceAlertSignals(QObject): - """Signals for thread-safe UI updates.""" - alert_triggered = pyqtSignal(object) # PriceAlert - prices_updated = pyqtSignal() - - -class PriceAlertPlugin(BasePlugin): - """ - Plugin for monitoring Entropia Nexus prices. - - Features: - - Monitor any item's market price - - Set alerts for price thresholds (buy low, sell high) - - Auto-refresh at configurable intervals - - Notification when alerts trigger - - Price history tracking - """ - - name = "Price Alerts" - version = "1.0.0" - author = "EU-Utility" - description = "Monitor Nexus prices and get alerts" - icon = "๐Ÿ””" - hotkey = "ctrl+shift+p" - - def __init__(self, overlay_window, config): - super().__init__(overlay_window, config) - - # Alert storage - self.alerts: Dict[str, PriceAlert] = {} - self.price_history: Dict[str, List[Dict]] = {} # item_id -> price history - - # Services - self.nexus = get_nexus_api() - self.signals = PriceAlertSignals() - - # Settings - self.check_interval = self.get_config('check_interval', 300) # 5 minutes - self.notifications_enabled = self.get_config('notifications_enabled', True) - self.sound_enabled = self.get_config('sound_enabled', True) - self.history_days = self.get_config('history_days', 7) - - # UI references - self._ui = None - self.alerts_table = None - self.search_results_table = None - self.search_input = None - self.status_label = None - - # Timer - self._check_timer = None - - # Connect signals - self.signals.alert_triggered.connect(self._on_alert_triggered) - self.signals.prices_updated.connect(self._update_alerts_table) - - # Load saved alerts - self._load_alerts() - - def initialize(self) -> None: - """Initialize plugin.""" - self.log_info("Initializing Price Alerts") - - # Start price check timer - self._check_timer = QTimer() - self._check_timer.timeout.connect(self._check_all_prices) - self._check_timer.start(self.check_interval * 1000) - - # Initial price check - self._check_all_prices() - - self.log_info(f"Price Alerts initialized with {len(self.alerts)} alerts") - - def get_ui(self) -> QWidget: - """Return the plugin's UI widget.""" - if self._ui is None: - self._ui = self._create_ui() - return self._ui - - def _create_ui(self) -> QWidget: - """Create the plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(12) - layout.setContentsMargins(16, 16, 16, 16) - - # Header - header = QLabel("๐Ÿ”” Price Alerts") - header.setStyleSheet(""" - font-size: 18px; - font-weight: bold; - color: white; - padding-bottom: 8px; - """) - layout.addWidget(header) - - # Status - self.status_label = QLabel(f"Active Alerts: {len(self.alerts)} | Next check: --") - self.status_label.setStyleSheet("color: rgba(255, 255, 255, 150);") - layout.addWidget(self.status_label) - - # Create tabs - tabs = QSplitter(Qt.Orientation.Vertical) - - # === Alerts Tab === - alerts_widget = QWidget() - alerts_layout = QVBoxLayout(alerts_widget) - alerts_layout.setContentsMargins(0, 0, 0, 0) - - alerts_header = QLabel("Your Alerts") - alerts_header.setStyleSheet("font-weight: bold; color: white;") - alerts_layout.addWidget(alerts_header) - - self.alerts_table = QTableWidget() - self.alerts_table.setColumnCount(6) - self.alerts_table.setHorizontalHeaderLabels([ - "Item", "Condition", "Target", "Current", "Status", "Actions" - ]) - self.alerts_table.horizontalHeader().setStretchLastSection(True) - self.alerts_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) - self.alerts_table.setStyleSheet(self._table_style()) - alerts_layout.addWidget(self.alerts_table) - - # Alert buttons - alerts_btn_layout = QHBoxLayout() - - refresh_btn = QPushButton("๐Ÿ”„ Check Now") - refresh_btn.setStyleSheet(self._button_style("#2196f3")) - refresh_btn.clicked.connect(self._check_all_prices) - alerts_btn_layout.addWidget(refresh_btn) - - clear_triggered_btn = QPushButton("๐Ÿงน Clear Triggered") - clear_triggered_btn.setStyleSheet(self._button_style()) - clear_triggered_btn.clicked.connect(self._clear_triggered) - alerts_btn_layout.addWidget(clear_triggered_btn) - - alerts_btn_layout.addStretch() - alerts_layout.addLayout(alerts_btn_layout) - - tabs.addWidget(alerts_widget) - - # === Search Tab === - search_widget = QWidget() - search_layout = QVBoxLayout(search_widget) - search_layout.setContentsMargins(0, 0, 0, 0) - - search_header = QLabel("Search Items to Monitor") - search_header.setStyleSheet("font-weight: bold; color: white;") - search_layout.addWidget(search_header) - - # Search input - search_input_layout = QHBoxLayout() - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Search for items...") - self.search_input.setStyleSheet(self._input_style()) - self.search_input.returnPressed.connect(self._search_items) - search_input_layout.addWidget(self.search_input) - - search_btn = QPushButton("๐Ÿ” Search") - search_btn.setStyleSheet(self._button_style("#4caf50")) - search_btn.clicked.connect(self._search_items) - search_input_layout.addWidget(search_btn) - - search_layout.addLayout(search_input_layout) - - # Search results - self.search_results_table = QTableWidget() - self.search_results_table.setColumnCount(4) - self.search_results_table.setHorizontalHeaderLabels(["Name", "Type", "Current Markup", "Action"]) - self.search_results_table.horizontalHeader().setStretchLastSection(True) - self.search_results_table.setStyleSheet(self._table_style()) - search_layout.addWidget(self.search_results_table) - - tabs.addWidget(search_widget) - - layout.addWidget(tabs) - - # Settings - settings_group = QGroupBox("Settings") - settings_layout = QVBoxLayout(settings_group) - - # Check interval - interval_layout = QHBoxLayout() - interval_label = QLabel("Check Interval:") - interval_label.setStyleSheet("color: white;") - interval_layout.addWidget(interval_label) - - self.interval_spin = QSpinBox() - self.interval_spin.setRange(1, 60) - self.interval_spin.setValue(self.check_interval // 60) - self.interval_spin.setSuffix(" min") - self.interval_spin.setStyleSheet(self._spinbox_style()) - interval_layout.addWidget(self.interval_spin) - - interval_layout.addStretch() - settings_layout.addLayout(interval_layout) - - # Notification settings - notif_layout = QHBoxLayout() - - self.notif_checkbox = QCheckBox("Show Notifications") - self.notif_checkbox.setChecked(self.notifications_enabled) - self.notif_checkbox.setStyleSheet("color: white;") - notif_layout.addWidget(self.notif_checkbox) - - self.sound_checkbox = QCheckBox("Play Sound") - self.sound_checkbox.setChecked(self.sound_enabled) - self.sound_checkbox.setStyleSheet("color: white;") - notif_layout.addWidget(self.sound_checkbox) - - notif_layout.addStretch() - settings_layout.addLayout(notif_layout) - - layout.addWidget(settings_group) - - # Apply settings button - apply_btn = QPushButton("๐Ÿ’พ Apply Settings") - apply_btn.setStyleSheet(self._button_style("#4caf50")) - apply_btn.clicked.connect(self._apply_settings) - layout.addWidget(apply_btn) - - # Initial table population - self._update_alerts_table() - - return widget - - def _table_style(self) -> str: - """Generate table stylesheet.""" - return """ - QTableWidget { - background-color: rgba(30, 35, 45, 150); - border: 1px solid rgba(100, 150, 200, 50); - border-radius: 8px; - color: white; - gridline-color: rgba(100, 150, 200, 30); - } - QHeaderView::section { - background-color: rgba(50, 60, 75, 200); - color: white; - padding: 8px; - border: none; - } - QTableWidget::item { - padding: 6px; - } - QTableWidget::item:selected { - background-color: rgba(100, 150, 200, 100); - } - """ - - def _button_style(self, color: str = "#607d8b") -> str: - """Generate button stylesheet.""" - return f""" - QPushButton {{ - background-color: {color}; - color: white; - border: none; - border-radius: 8px; - padding: 8px 14px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: {color}dd; - }} - QPushButton:pressed {{ - background-color: {color}aa; - }} - """ - - def _input_style(self) -> str: - """Generate input stylesheet.""" - return """ - QLineEdit { - background-color: rgba(50, 60, 75, 200); - color: white; - border: 1px solid rgba(100, 150, 200, 100); - border-radius: 8px; - padding: 8px 12px; - } - QLineEdit:focus { - border: 1px solid rgba(100, 180, 255, 150); - } - """ - - def _spinbox_style(self) -> str: - """Generate spinbox stylesheet.""" - return """ - QSpinBox { - background-color: rgba(50, 60, 75, 200); - color: white; - border: 1px solid rgba(100, 150, 200, 100); - border-radius: 4px; - padding: 4px; - } - """ - - def _search_items(self) -> None: - """Search for items via Nexus API.""" - query = self.search_input.text().strip() - if not query: - return - - self.status_label.setText(f"Searching for '{query}'...") - - # Run search in background - self.run_in_background( - self.nexus.search_items, - query, - limit=20, - priority='normal', - on_complete=self._on_search_complete, - on_error=self._on_search_error - ) - - def _on_search_complete(self, results: List[Any]) -> None: - """Handle search completion.""" - self.search_results_table.setRowCount(len(results)) - - for row, item in enumerate(results): - # Name - name_item = QTableWidgetItem(item.name) - name_item.setForeground(QColor("white")) - self.search_results_table.setItem(row, 0, name_item) - - # Type - type_item = QTableWidgetItem(item.type) - type_item.setForeground(QColor("#2196f3")) - self.search_results_table.setItem(row, 1, type_item) - - # Current markup (will be fetched) - markup_item = QTableWidgetItem("Click to check") - markup_item.setForeground(QColor("rgba(255, 255, 255, 100)")) - self.search_results_table.setItem(row, 2, markup_item) - - # Action button - add_btn = QPushButton("+ Alert") - add_btn.setStyleSheet(self._button_style("#4caf50")) - add_btn.clicked.connect(lambda checked, i=item: self._show_add_alert_dialog(i)) - self.search_results_table.setCellWidget(row, 3, add_btn) - - self.status_label.setText(f"Found {len(results)} items") - - def _on_search_error(self, error: Exception) -> None: - """Handle search error.""" - self.status_label.setText(f"Search failed: {str(error)}") - self.notify_error("Search Failed", str(error)) - - def _show_add_alert_dialog(self, item: Any) -> None: - """Show dialog to add a new alert.""" - from PyQt6.QtWidgets import QDialog, QFormLayout, QDialogButtonBox - - dialog = QDialog(self._ui) - dialog.setWindowTitle(f"Add Alert: {item.name}") - dialog.setStyleSheet(""" - QDialog { - background-color: #2a3040; - } - QLabel { - color: white; - } - """) - - layout = QFormLayout(dialog) - - # Alert type - type_combo = QComboBox() - type_combo.addItems(["Price Below", "Price Above"]) - type_combo.setStyleSheet(self._input_style()) - layout.addRow("Alert When:", type_combo) - - # Target price - price_spin = QDoubleSpinBox() - price_spin.setRange(0.01, 1000000) - price_spin.setDecimals(2) - price_spin.setValue(100.0) - price_spin.setSuffix(" %") - price_spin.setStyleSheet(self._spinbox_style()) - layout.addRow("Target Price:", price_spin) - - # Buttons - buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel - ) - buttons.accepted.connect(dialog.accept) - buttons.rejected.connect(dialog.reject) - layout.addRow(buttons) - - if dialog.exec() == QDialog.DialogCode.Accepted: - alert_type = 'below' if type_combo.currentIndex() == 0 else 'above' - self._add_alert(item.id, item.name, alert_type, price_spin.value()) - - def _add_alert(self, item_id: str, item_name: str, alert_type: str, target_price: float) -> None: - """Add a new price alert.""" - import uuid - - alert_id = str(uuid.uuid4())[:8] - alert = PriceAlert( - id=alert_id, - item_id=item_id, - item_name=item_name, - alert_type=alert_type, - target_price=target_price - ) - - self.alerts[alert_id] = alert - self._save_alerts() - self._update_alerts_table() - - # Check price immediately - self._check_alert_price(alert) - - self.notify_success("Alert Added", f"Monitoring {item_name}") - self.log_info(f"Added price alert for {item_name}: {alert_type} {target_price}%") - - def _remove_alert(self, alert_id: str) -> None: - """Remove an alert.""" - if alert_id in self.alerts: - del self.alerts[alert_id] - self._save_alerts() - self._update_alerts_table() - self.log_info(f"Removed alert {alert_id}") - - def _check_all_prices(self) -> None: - """Check prices for all enabled alerts.""" - if not self.alerts: - return - - self.status_label.setText("Checking prices...") - - # Check each alert's price - for alert in self.alerts.values(): - if alert.enabled: - self._check_alert_price(alert) - - # Update UI - self._update_alerts_table() - - # Schedule next check - next_check = datetime.now() + timedelta(seconds=self.check_interval) - self.status_label.setText(f"Active Alerts: {len(self.alerts)} | Next check: {next_check.strftime('%H:%M')}") - - def _check_alert_price(self, alert: PriceAlert) -> None: - """Check price for a single alert.""" - try: - market_data = self.nexus.get_market_data(alert.item_id) - if market_data and market_data.current_markup is not None: - current_price = market_data.current_markup - - # Update price history - if alert.item_id not in self.price_history: - self.price_history[alert.item_id] = [] - - self.price_history[alert.item_id].append({ - 'timestamp': datetime.now().isoformat(), - 'price': current_price - }) - - # Trim history - cutoff = datetime.now() - timedelta(days=self.history_days) - self.price_history[alert.item_id] = [ - h for h in self.price_history[alert.item_id] - if datetime.fromisoformat(h['timestamp']) > cutoff - ] - - # Check condition - if alert.check_condition(current_price): - if not alert.triggered: - alert.triggered = True - alert.trigger_count += 1 - self.signals.alert_triggered.emit(alert) - else: - alert.triggered = False - - alert.last_checked = datetime.now() - except Exception as e: - self.log_error(f"Error checking price for {alert.item_name}: {e}") - - def _on_alert_triggered(self, alert: PriceAlert) -> None: - """Handle triggered alert.""" - condition = "dropped below" if alert.alert_type == 'below' else "rose above" - message = f"{alert.item_name} {condition} {alert.target_price:.1f}% (current: {alert.current_price:.1f}%)" - - if self.notifications_enabled: - self.notify("๐Ÿšจ Price Alert", message, sound=self.sound_enabled) - - if self.sound_enabled: - self.play_sound('alert') - - self.log_info(f"Price alert triggered: {message}") - self._save_alerts() - - def _update_alerts_table(self) -> None: - """Update the alerts table.""" - if not self.alerts_table: - return - - enabled_alerts = [a for a in self.alerts.values() if a.enabled] - self.alerts_table.setRowCount(len(enabled_alerts)) - - for row, alert in enumerate(enabled_alerts): - # Item name - name_item = QTableWidgetItem(alert.item_name) - name_item.setForeground(QColor("white")) - self.alerts_table.setItem(row, 0, name_item) - - # Condition - condition_text = f"Alert when {'below' if alert.alert_type == 'below' else 'above'}" - condition_item = QTableWidgetItem(condition_text) - condition_item.setForeground(QColor("#2196f3")) - self.alerts_table.setItem(row, 1, condition_item) - - # Target price - target_item = QTableWidgetItem(f"{alert.target_price:.1f}%") - target_item.setForeground(QColor("#ffc107")) - self.alerts_table.setItem(row, 2, target_item) - - # Current price - current_text = f"{alert.current_price:.1f}%" if alert.current_price else "--" - current_item = QTableWidgetItem(current_text) - current_item.setForeground(QColor("#4caf50" if alert.current_price else "rgba(255,255,255,100)")) - self.alerts_table.setItem(row, 3, current_item) - - # Status - status_text = "๐Ÿšจ TRIGGERED" if alert.triggered else ("โœ… OK" if alert.last_checked else "โณ Pending") - status_item = QTableWidgetItem(status_text) - status_color = "#f44336" if alert.triggered else ("#4caf50" if alert.last_checked else "rgba(255,255,255,100)") - status_item.setForeground(QColor(status_color)) - self.alerts_table.setItem(row, 4, status_item) - - # Actions - actions_widget = QWidget() - actions_layout = QHBoxLayout(actions_widget) - actions_layout.setContentsMargins(4, 2, 4, 2) - - remove_btn = QPushButton("๐Ÿ—‘๏ธ") - remove_btn.setFixedSize(28, 28) - remove_btn.setStyleSheet(""" - QPushButton { - background-color: #f44336; - color: white; - border: none; - border-radius: 4px; - } - """) - remove_btn.clicked.connect(lambda checked, aid=alert.id: self._remove_alert(aid)) - actions_layout.addWidget(remove_btn) - actions_layout.addStretch() - - self.alerts_table.setCellWidget(row, 5, actions_widget) - - self.status_label.setText(f"Active Alerts: {len(self.alerts)}") - - def _clear_triggered(self) -> None: - """Clear all triggered alerts.""" - for alert in self.alerts.values(): - alert.triggered = False - self._save_alerts() - self._update_alerts_table() - - def _apply_settings(self) -> None: - """Apply and save settings.""" - self.check_interval = self.interval_spin.value() * 60 - self.notifications_enabled = self.notif_checkbox.isChecked() - self.sound_enabled = self.sound_checkbox.isChecked() - - self.set_config('check_interval', self.check_interval) - self.set_config('notifications_enabled', self.notifications_enabled) - self.set_config('sound_enabled', self.sound_enabled) - - # Update timer - if self._check_timer: - self._check_timer.setInterval(self.check_interval * 1000) - - self.notify_success("Settings Saved", "Price alert settings updated") - - def _save_alerts(self) -> None: - """Save alerts to storage.""" - alerts_data = {aid: alert.to_dict() for aid, alert in self.alerts.items()} - self.save_data('alerts', alerts_data) - self.save_data('price_history', self.price_history) - - def _load_alerts(self) -> None: - """Load alerts from storage.""" - alerts_data = self.load_data('alerts', {}) - for aid, data in alerts_data.items(): - try: - self.alerts[aid] = PriceAlert.from_dict(data) - except Exception as e: - self.log_error(f"Failed to load alert {aid}: {e}") - - self.price_history = self.load_data('price_history', {}) - - def on_hotkey(self) -> None: - """Handle hotkey press.""" - self._check_all_prices() - self.notify("Price Check", f"Checked {len(self.alerts)} items") - - def shutdown(self) -> None: - """Cleanup on shutdown.""" - self._save_alerts() - - if self._check_timer: - self._check_timer.stop() - - super().shutdown() diff --git a/plugins/profession_scanner/__init__.py b/plugins/profession_scanner/__init__.py deleted file mode 100644 index 7248c5f..0000000 --- a/plugins/profession_scanner/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Profession Scanner Plugin -""" - -from .plugin import ProfessionScannerPlugin - -__all__ = ["ProfessionScannerPlugin"] diff --git a/plugins/profession_scanner/plugin.py b/plugins/profession_scanner/plugin.py deleted file mode 100644 index c35ad5c..0000000 --- a/plugins/profession_scanner/plugin.py +++ /dev/null @@ -1,247 +0,0 @@ -""" -EU-Utility - Profession Scanner Plugin - -Scan and track profession progress with OCR. -""" - -import re -import json -from datetime import datetime -from pathlib import Path -from decimal import Decimal - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTableWidget, QTableWidgetItem, QProgressBar, - QFrame, QGroupBox, QComboBox -) -from PyQt6.QtCore import Qt, QThread, pyqtSignal - -from plugins.base_plugin import BasePlugin - - -class ProfessionOCRThread(QThread): - """OCR scan for professions window.""" - scan_complete = pyqtSignal(dict) - scan_error = pyqtSignal(str) - progress_update = pyqtSignal(str) - - def run(self): - """Perform OCR scan.""" - try: - self.progress_update.emit("Capturing screen...") - - import pyautogui - screenshot = pyautogui.screenshot() - - self.progress_update.emit("Running OCR...") - - # OCR - try: - import easyocr - reader = easyocr.Reader(['en'], verbose=False) - results = reader.readtext(screenshot) - text = '\n'.join([r[1] for r in results]) - except: - import pytesseract - from PIL import Image - text = pytesseract.image_to_string(screenshot) - - # Parse professions - professions = self._parse_professions(text) - self.scan_complete.emit(professions) - - except Exception as e: - self.scan_error.emit(str(e)) - - def _parse_professions(self, text): - """Parse profession data from OCR text.""" - professions = {} - - # Pattern: ProfessionName Rank %Progress - # Example: "Laser Pistoleer (Hit) Elite, 72 68.3%" - lines = text.split('\n') - - for line in lines: - # Match profession with rank and percentage - match = re.search(r'(\w+(?:\s+\w+)*)\s+\(?(\w+)?\)?\s+(Elite|Champion|Astonishing|Remarkable|Outstanding|Marvelous|Prodigious|Amazing|Incredible|Awesome),?\s+(\d+)[,\s]+(\d+\.?\d*)%?', line) - if match: - prof_name = match.group(1).strip() - spec = match.group(2) or "" - rank_name = match.group(3) - rank_num = match.group(4) - progress = float(match.group(5)) - - full_name = f"{prof_name} ({spec})" if spec else prof_name - - professions[full_name] = { - 'rank_name': rank_name, - 'rank_num': int(rank_num), - 'progress': progress, - 'scanned_at': datetime.now().isoformat() - } - - return professions - - -class ProfessionScannerPlugin(BasePlugin): - """Scan and track profession progress.""" - - name = "Profession Scanner" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Track profession ranks and progress" - hotkey = "ctrl+shift+p" - - def initialize(self): - """Setup profession scanner.""" - self.data_file = Path("data/professions.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.professions = {} - self._load_data() - - def _load_data(self): - """Load saved data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.professions = data.get('professions', {}) - except: - pass - - def _save_data(self): - """Save data.""" - with open(self.data_file, 'w') as f: - json.dump({'professions': self.professions}, f, indent=2) - - def get_ui(self): - """Create profession scanner UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Header - header = QLabel("Profession Tracker") - header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;") - layout.addWidget(header) - - # Summary - summary = QHBoxLayout() - self.total_label = QLabel(f"Professions: {len(self.professions)}") - self.total_label.setStyleSheet("color: #4ecdc4; font-weight: bold;") - summary.addWidget(self.total_label) - - summary.addStretch() - layout.addLayout(summary) - - # Scan button - scan_btn = QPushButton("Scan Professions Window") - scan_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 12px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - scan_btn.clicked.connect(self._scan_professions) - layout.addWidget(scan_btn) - - # Progress - self.progress_label = QLabel("Ready to scan") - self.progress_label.setStyleSheet("color: rgba(255,255,255,150);") - layout.addWidget(self.progress_label) - - # Professions table - self.prof_table = QTableWidget() - self.prof_table.setColumnCount(4) - self.prof_table.setHorizontalHeaderLabels(["Profession", "Rank", "Level", "Progress"]) - self.prof_table.horizontalHeader().setStretchLastSection(True) - - # Style table - self.prof_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - QHeaderView::section { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,180); - padding: 8px; - font-weight: bold; - } - """) - - layout.addWidget(self.prof_table) - - # Refresh table - self._refresh_table() - - return widget - - def _scan_professions(self): - """Start OCR scan.""" - self.scanner = ProfessionOCRThread() - self.scanner.scan_complete.connect(self._on_scan_complete) - self.scanner.scan_error.connect(self._on_scan_error) - self.scanner.progress_update.connect(self._on_progress) - self.scanner.start() - - def _on_progress(self, message): - """Update progress.""" - self.progress_label.setText(message) - - def _on_scan_complete(self, professions): - """Handle scan completion.""" - self.professions.update(professions) - self._save_data() - self._refresh_table() - self.progress_label.setText(f"Found {len(professions)} professions") - self.total_label.setText(f"Professions: {len(self.professions)}") - - def _on_scan_error(self, error): - """Handle error.""" - self.progress_label.setText(f"Error: {error}") - - def _refresh_table(self): - """Refresh professions table.""" - self.prof_table.setRowCount(len(self.professions)) - - for i, (name, data) in enumerate(sorted(self.professions.items())): - self.prof_table.setItem(i, 0, QTableWidgetItem(name)) - self.prof_table.setItem(i, 1, QTableWidgetItem(data.get('rank_name', '-'))) - self.prof_table.setItem(i, 2, QTableWidgetItem(str(data.get('rank_num', 0)))) - - # Progress with bar - progress = data.get('progress', 0) - progress_widget = QWidget() - progress_layout = QHBoxLayout(progress_widget) - progress_layout.setContentsMargins(5, 2, 5, 2) - - bar = QProgressBar() - bar.setValue(int(progress)) - bar.setTextVisible(True) - bar.setFormat(f"{progress:.1f}%") - bar.setStyleSheet(""" - QProgressBar { - background-color: rgba(60, 70, 90, 150); - border: none; - border-radius: 3px; - text-align: center; - color: white; - } - QProgressBar::chunk { - background-color: #ff8c42; - border-radius: 3px; - } - """) - progress_layout.addWidget(bar) - - self.prof_table.setCellWidget(i, 3, progress_widget) diff --git a/plugins/session_exporter/__init__.py b/plugins/session_exporter/__init__.py deleted file mode 100644 index 68eccfd..0000000 --- a/plugins/session_exporter/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -Session Exporter Plugin for EU-Utility - -Export hunting/mining/crafting sessions to CSV/JSON formats. -""" - -from .plugin import SessionExporterPlugin - -__all__ = ['SessionExporterPlugin'] diff --git a/plugins/session_exporter/plugin.py b/plugins/session_exporter/plugin.py deleted file mode 100644 index f001c5f..0000000 --- a/plugins/session_exporter/plugin.py +++ /dev/null @@ -1,643 +0,0 @@ -""" -EU-Utility - Session Exporter Plugin - -Export hunting/mining/crafting sessions to CSV/JSON formats. -Tracks loot, skills gained, globals, and other session data. -""" - -import json -import csv -from datetime import datetime -from pathlib import Path -from typing import Dict, List, Any, Optional -from dataclasses import dataclass, field, asdict - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, - QTableWidget, QTableWidgetItem, QHeaderView, QComboBox, - QFileDialog, QMessageBox, QGroupBox, QSpinBox, QCheckBox, - QTabWidget, QTextEdit, QSplitter -) -from PyQt6.QtCore import Qt, QTimer -from PyQt6.QtGui import QColor - -from plugins.base_plugin import BasePlugin -from core.event_bus import ( - LootEvent, SkillGainEvent, GlobalEvent, DamageEvent, - EventCategory, get_event_bus -) - - -@dataclass -class SessionEntry: - """Single session entry representing an event.""" - timestamp: datetime - event_type: str - description: str - value: float = 0.0 - details: Dict[str, Any] = field(default_factory=dict) - - -@dataclass -class SessionSummary: - """Summary statistics for a session.""" - start_time: datetime - end_time: Optional[datetime] = None - total_loot_tt: float = 0.0 - total_loot_count: int = 0 - globals_count: int = 0 - hofs_count: int = 0 - skill_gains: int = 0 - total_damage_dealt: float = 0.0 - total_damage_taken: float = 0.0 - unique_items: List[str] = field(default_factory=list) - - -class SessionExporterPlugin(BasePlugin): - """ - Plugin for exporting hunting/mining sessions to various formats. - - Features: - - Automatic session tracking from event bus - - Export to CSV or JSON - - Session statistics and summaries - - Configurable auto-export - """ - - name = "Session Exporter" - version = "1.0.0" - author = "EU-Utility" - description = "Export hunting/mining sessions to CSV/JSON" - icon = "๐Ÿ“Š" - hotkey = "ctrl+shift+e" - - def __init__(self, overlay_window, config): - super().__init__(overlay_window, config) - - # Session state - self.session_active = False - self.session_start_time: Optional[datetime] = None - self.session_entries: List[SessionEntry] = [] - self.session_summary = None - - # Event subscriptions - self._subscriptions: List[str] = [] - - # Auto-export settings - self.auto_export_enabled = self.get_config('auto_export', False) - self.auto_export_interval = self.get_config('auto_export_interval', 300) # 5 minutes - self.auto_export_format = self.get_config('auto_export_format', 'json') - - # Export settings - self.export_directory = self.get_config('export_directory', str(Path.home() / "Documents" / "EU-Sessions")) - Path(self.export_directory).mkdir(parents=True, exist_ok=True) - - # UI references - self._ui = None - self.session_table = None - self.status_label = None - self.stats_label = None - - # Auto-export timer - self._export_timer = None - - def initialize(self) -> None: - """Initialize plugin and subscribe to events.""" - self.log_info("Initializing Session Exporter") - - # Subscribe to events - self._subscriptions.append( - self.subscribe_typed(LootEvent, self._on_loot) - ) - self._subscriptions.append( - self.subscribe_typed(SkillGainEvent, self._on_skill_gain) - ) - self._subscriptions.append( - self.subscribe_typed(GlobalEvent, self._on_global) - ) - self._subscriptions.append( - self.subscribe_typed(DamageEvent, self._on_damage) - ) - - # Start session automatically - self.start_session() - - # Setup auto-export timer if enabled - if self.auto_export_enabled: - self._setup_auto_export() - - self.log_info("Session Exporter initialized") - - def get_ui(self) -> QWidget: - """Return the plugin's UI widget.""" - if self._ui is None: - self._ui = self._create_ui() - return self._ui - - def _create_ui(self) -> QWidget: - """Create the plugin UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(12) - layout.setContentsMargins(16, 16, 16, 16) - - # Header - header = QLabel("๐Ÿ“Š Session Exporter") - header.setStyleSheet(""" - font-size: 18px; - font-weight: bold; - color: white; - padding-bottom: 8px; - """) - layout.addWidget(header) - - # Status section - status_group = QGroupBox("Session Status") - status_layout = QVBoxLayout(status_group) - - self.status_label = QLabel("Session: Active") - self.status_label.setStyleSheet("color: #4caf50; font-weight: bold;") - status_layout.addWidget(self.status_label) - - self.stats_label = QLabel(self._get_stats_text()) - self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 150);") - status_layout.addWidget(self.stats_label) - - layout.addWidget(status_group) - - # Control buttons - button_layout = QHBoxLayout() - - self.start_btn = QPushButton("โ–ถ๏ธ Start New") - self.start_btn.setStyleSheet(self._button_style()) - self.start_btn.clicked.connect(self.start_session) - button_layout.addWidget(self.start_btn) - - self.stop_btn = QPushButton("โน๏ธ Stop") - self.stop_btn.setStyleSheet(self._button_style()) - self.stop_btn.clicked.connect(self.stop_session) - button_layout.addWidget(self.stop_btn) - - self.export_csv_btn = QPushButton("๐Ÿ“„ Export CSV") - self.export_csv_btn.setStyleSheet(self._button_style("#2196f3")) - self.export_csv_btn.clicked.connect(lambda: self.export_session('csv')) - button_layout.addWidget(self.export_csv_btn) - - self.export_json_btn = QPushButton("๐Ÿ“„ Export JSON") - self.export_json_btn.setStyleSheet(self._button_style("#2196f3")) - self.export_json_btn.clicked.connect(lambda: self.export_session('json')) - button_layout.addWidget(self.export_json_btn) - - layout.addLayout(button_layout) - - # Session entries table - table_group = QGroupBox("Session Entries") - table_layout = QVBoxLayout(table_group) - - self.session_table = QTableWidget() - self.session_table.setColumnCount(4) - self.session_table.setHorizontalHeaderLabels(["Time", "Type", "Description", "Value"]) - self.session_table.horizontalHeader().setStretchLastSection(True) - self.session_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) - self.session_table.setStyleSheet(""" - QTableWidget { - background-color: rgba(30, 35, 45, 150); - border: 1px solid rgba(100, 150, 200, 50); - border-radius: 8px; - color: white; - } - QHeaderView::section { - background-color: rgba(50, 60, 75, 200); - color: white; - padding: 8px; - border: none; - } - QTableWidget::item { - padding: 6px; - } - """) - table_layout.addWidget(self.session_table) - - layout.addWidget(table_group) - - # Settings - settings_group = QGroupBox("Settings") - settings_layout = QVBoxLayout(settings_group) - - # Auto-export - auto_export_layout = QHBoxLayout() - self.auto_export_checkbox = QCheckBox("Auto-export every") - self.auto_export_checkbox.setChecked(self.auto_export_enabled) - self.auto_export_checkbox.setStyleSheet("color: white;") - auto_export_layout.addWidget(self.auto_export_checkbox) - - self.auto_export_spin = QSpinBox() - self.auto_export_spin.setRange(1, 60) - self.auto_export_spin.setValue(self.auto_export_interval // 60) - self.auto_export_spin.setSuffix(" min") - self.auto_export_spin.setStyleSheet(""" - QSpinBox { - background-color: rgba(50, 60, 75, 200); - color: white; - border: 1px solid rgba(100, 150, 200, 100); - border-radius: 4px; - padding: 4px; - } - """) - auto_export_layout.addWidget(self.auto_export_spin) - - auto_export_layout.addStretch() - settings_layout.addLayout(auto_export_layout) - - # Export directory - dir_layout = QHBoxLayout() - dir_label = QLabel("Export Directory:") - dir_label.setStyleSheet("color: rgba(255, 255, 255, 150);") - dir_layout.addWidget(dir_label) - - self.dir_display = QLabel(self.export_directory) - self.dir_display.setStyleSheet("color: white;") - self.dir_display.setWordWrap(True) - dir_layout.addWidget(self.dir_display, 1) - - change_dir_btn = QPushButton("๐Ÿ“ Change") - change_dir_btn.setStyleSheet(self._button_style()) - change_dir_btn.clicked.connect(self._change_export_directory) - dir_layout.addWidget(change_dir_btn) - - settings_layout.addLayout(dir_layout) - - layout.addWidget(settings_group) - - # Apply settings button - apply_btn = QPushButton("๐Ÿ’พ Apply Settings") - apply_btn.setStyleSheet(self._button_style("#4caf50")) - apply_btn.clicked.connect(self._apply_settings) - layout.addWidget(apply_btn) - - layout.addStretch() - - return widget - - def _button_style(self, color: str = "#607d8b") -> str: - """Generate button stylesheet.""" - return f""" - QPushButton {{ - background-color: {color}; - color: white; - border: none; - border-radius: 8px; - padding: 10px 16px; - font-weight: bold; - }} - QPushButton:hover {{ - background-color: {color}dd; - }} - QPushButton:pressed {{ - background-color: {color}aa; - }} - """ - - def start_session(self) -> None: - """Start a new tracking session.""" - self.session_active = True - self.session_start_time = datetime.now() - self.session_entries = [] - self.session_summary = SessionSummary(start_time=self.session_start_time) - - self.log_info(f"Session started at {self.session_start_time}") - - if self.status_label: - self.status_label.setText(f"Session: Active (started {self.session_start_time.strftime('%H:%M:%S')})") - self.status_label.setStyleSheet("color: #4caf50; font-weight: bold;") - - self.notify("Session Started", "Tracking loot, skills, and globals") - - def stop_session(self) -> None: - """Stop the current tracking session.""" - if not self.session_active: - return - - self.session_active = False - if self.session_summary: - self.session_summary.end_time = datetime.now() - - self.log_info("Session stopped") - - if self.status_label: - duration = "" - if self.session_summary and self.session_summary.end_time: - duration_secs = (self.session_summary.end_time - self.session_start_time).total_seconds() - mins, secs = divmod(int(duration_secs), 60) - hours, mins = divmod(mins, 60) - duration = f"Duration: {hours:02d}:{mins:02d}:{secs:02d}" - - self.status_label.setText(f"Session: Stopped ({duration})") - self.status_label.setStyleSheet("color: #ff9800; font-weight: bold;") - - self.notify("Session Stopped", f"Total entries: {len(self.session_entries)}") - - def _on_loot(self, event: LootEvent) -> None: - """Handle loot events.""" - if not self.session_active: - return - - item_names = ", ".join(event.get_item_names()) - entry = SessionEntry( - timestamp=event.timestamp, - event_type="Loot", - description=f"From {event.mob_name}: {item_names}", - value=event.total_tt_value, - details={ - 'mob_name': event.mob_name, - 'items': event.items, - 'position': event.position - } - ) - - self.session_entries.append(entry) - - if self.session_summary: - self.session_summary.total_loot_tt += event.total_tt_value - self.session_summary.total_loot_count += 1 - for item in event.get_item_names(): - if item not in self.session_summary.unique_items: - self.session_summary.unique_items.append(item) - - self._update_ui() - - def _on_skill_gain(self, event: SkillGainEvent) -> None: - """Handle skill gain events.""" - if not self.session_active: - return - - entry = SessionEntry( - timestamp=event.timestamp, - event_type="Skill", - description=f"{event.skill_name} +{event.gain_amount:.4f}", - value=event.gain_amount, - details={ - 'skill_name': event.skill_name, - 'skill_value': event.skill_value, - 'gain_amount': event.gain_amount - } - ) - - self.session_entries.append(entry) - - if self.session_summary: - self.session_summary.skill_gains += 1 - - self._update_ui() - - def _on_global(self, event: GlobalEvent) -> None: - """Handle global/HOF events.""" - if not self.session_active: - return - - is_hof = event.achievement_type.lower() in ['hof', 'ath', 'discovery'] - - entry = SessionEntry( - timestamp=event.timestamp, - event_type="HOF" if is_hof else "Global", - description=f"{event.player_name}: {event.item_name or 'Value'} worth {event.value:.0f} PED", - value=event.value, - details={ - 'player_name': event.player_name, - 'achievement_type': event.achievement_type, - 'item_name': event.item_name - } - ) - - self.session_entries.append(entry) - - if self.session_summary: - if is_hof: - self.session_summary.hofs_count += 1 - else: - self.session_summary.globals_count += 1 - - self._update_ui() - - def _on_damage(self, event: DamageEvent) -> None: - """Handle damage events.""" - if not self.session_active: - return - - if event.is_outgoing: - if self.session_summary: - self.session_summary.total_damage_dealt += event.damage_amount - else: - if self.session_summary: - self.session_summary.total_damage_taken += event.damage_amount - - self._update_ui() - - def _update_ui(self) -> None: - """Update the UI with current session data.""" - if not self._ui or not self.session_table: - return - - # Update stats label - if self.stats_label: - self.stats_label.setText(self._get_stats_text()) - - # Update table (show last 50 entries) - recent_entries = self.session_entries[-50:] - self.session_table.setRowCount(len(recent_entries)) - - type_colors = { - 'Loot': '#4caf50', - 'Skill': '#2196f3', - 'Global': '#ff9800', - 'HOF': '#f44336' - } - - for row, entry in enumerate(recent_entries): - # Time - time_item = QTableWidgetItem(entry.timestamp.strftime("%H:%M:%S")) - time_item.setForeground(QColor("white")) - self.session_table.setItem(row, 0, time_item) - - # Type - type_item = QTableWidgetItem(entry.event_type) - type_item.setForeground(QColor(type_colors.get(entry.event_type, 'white'))) - self.session_table.setItem(row, 1, type_item) - - # Description - desc_item = QTableWidgetItem(entry.description) - desc_item.setForeground(QColor("white")) - self.session_table.setItem(row, 2, desc_item) - - # Value - value_item = QTableWidgetItem(f"{entry.value:.2f}") - value_item.setForeground(QColor("#ffc107")) - self.session_table.setItem(row, 3, value_item) - - # Scroll to bottom - self.session_table.scrollToBottom() - - def _get_stats_text(self) -> str: - """Get formatted statistics text.""" - if not self.session_summary: - return "No active session" - - lines = [ - f"Entries: {len(self.session_entries)}", - f"Loot: {self.session_summary.total_loot_count} items, {self.session_summary.total_loot_tt:.2f} PED TT", - f"Globals: {self.session_summary.globals_count} | HOFs: {self.session_summary.hofs_count}", - f"Skills: {self.session_summary.skill_gains}", - ] - - if self.session_summary.total_damage_dealt > 0: - lines.append(f"Damage Dealt: {self.session_summary.total_damage_dealt:,.0f}") - - return " | ".join(lines) - - def export_session(self, format_type: str = 'json') -> Optional[Path]: - """ - Export the current session to a file. - - Args: - format_type: 'json' or 'csv' - - Returns: - Path to exported file or None if failed - """ - if not self.session_entries: - self.notify_warning("Export Failed", "No session data to export") - return None - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"session_{timestamp}.{format_type}" - filepath = Path(self.export_directory) / filename - - try: - if format_type == 'json': - self._export_json(filepath) - elif format_type == 'csv': - self._export_csv(filepath) - else: - raise ValueError(f"Unknown format: {format_type}") - - self.notify_success("Export Complete", f"Saved to {filename}") - self.log_info(f"Session exported to {filepath}") - return filepath - - except Exception as e: - self.notify_error("Export Failed", str(e)) - self.log_error(f"Export failed: {e}") - return None - - def _export_json(self, filepath: Path) -> None: - """Export session to JSON format.""" - data = { - 'export_info': { - 'version': '1.0.0', - 'exported_at': datetime.now().isoformat(), - 'plugin': 'Session Exporter' - }, - 'session': { - 'start_time': self.session_start_time.isoformat() if self.session_start_time else None, - 'end_time': self.session_summary.end_time.isoformat() if self.session_summary and self.session_summary.end_time else None, - 'summary': asdict(self.session_summary) if self.session_summary else {}, - 'entries': [ - { - 'timestamp': e.timestamp.isoformat(), - 'event_type': e.event_type, - 'description': e.description, - 'value': e.value, - 'details': e.details - } - for e in self.session_entries - ] - } - } - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, ensure_ascii=False) - - def _export_csv(self, filepath: Path) -> None: - """Export session to CSV format.""" - with open(filepath, 'w', newline='', encoding='utf-8') as f: - writer = csv.writer(f) - - # Write header - writer.writerow(['timestamp', 'event_type', 'description', 'value', 'details']) - - # Write entries - for entry in self.session_entries: - writer.writerow([ - entry.timestamp.isoformat(), - entry.event_type, - entry.description, - entry.value, - json.dumps(entry.details) - ]) - - def _change_export_directory(self) -> None: - """Change the export directory.""" - new_dir = QFileDialog.getExistingDirectory( - self._ui, - "Select Export Directory", - self.export_directory - ) - - if new_dir: - self.export_directory = new_dir - if self.dir_display: - self.dir_display.setText(new_dir) - - def _apply_settings(self) -> None: - """Apply and save settings.""" - self.auto_export_enabled = self.auto_export_checkbox.isChecked() - self.auto_export_interval = self.auto_export_spin.value() * 60 - - self.set_config('auto_export', self.auto_export_enabled) - self.set_config('auto_export_interval', self.auto_export_interval) - self.set_config('export_directory', self.export_directory) - - # Update auto-export timer - if self.auto_export_enabled: - self._setup_auto_export() - elif self._export_timer: - self._export_timer.stop() - - self.notify_success("Settings Saved", "Session exporter settings updated") - - def _setup_auto_export(self) -> None: - """Setup auto-export timer.""" - if self._export_timer: - self._export_timer.stop() - - self._export_timer = QTimer() - self._export_timer.timeout.connect(lambda: self.export_session(self.auto_export_format)) - self._export_timer.start(self.auto_export_interval * 1000) - - self.log_info(f"Auto-export enabled every {self.auto_export_interval // 60} minutes") - - def on_hotkey(self) -> None: - """Handle hotkey press.""" - if self.session_active: - self.stop_session() - else: - self.start_session() - - def shutdown(self) -> None: - """Cleanup on shutdown.""" - # Auto-export on shutdown if enabled - if self.auto_export_enabled and self.session_entries: - self.export_session(self.auto_export_format) - - # Stop session - if self.session_active: - self.stop_session() - - # Unsubscribe from events - for sub_id in self._subscriptions: - self.unsubscribe_typed(sub_id) - - if self._export_timer: - self._export_timer.stop() - - super().shutdown() diff --git a/plugins/skill_scanner/__init__.py b/plugins/skill_scanner/__init__.py deleted file mode 100644 index bf789dd..0000000 --- a/plugins/skill_scanner/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Skill Scanner Plugin for EU-Utility -""" - -from .plugin import SkillScannerPlugin - -__all__ = ["SkillScannerPlugin"] diff --git a/plugins/skill_scanner/plugin.py b/plugins/skill_scanner/plugin.py deleted file mode 100644 index 0ba4b62..0000000 --- a/plugins/skill_scanner/plugin.py +++ /dev/null @@ -1,1240 +0,0 @@ -""" -EU-Utility - Skill Scanner Plugin - -Uses core OCR and Log services via PluginAPI. -""" - -import re -import json -from datetime import datetime -from pathlib import Path - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QTableWidget, QTableWidgetItem, QProgressBar, - QFrame, QGroupBox, QTextEdit, QSplitter, QComboBox -) -from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QObject - -from plugins.base_plugin import BasePlugin - - -def find_entropia_window(): - """ - Find the Entropia Universe game window. - Returns (left, top, width, height) or None if not found. - """ - try: - import platform - - if platform.system() == 'Windows': - import win32gui - import win32process - import psutil - - def callback(hwnd, windows): - if not win32gui.IsWindowVisible(hwnd): - return True - - # Get window title - title = win32gui.GetWindowText(hwnd) - - # Check if it's an Entropia window (title contains "Entropia Universe") - if 'Entropia Universe' in title: - try: - # Get process ID - _, pid = win32process.GetWindowThreadProcessId(hwnd) - process = psutil.Process(pid) - - # Verify process name contains "Entropia" - if 'entropia' in process.name().lower(): - rect = win32gui.GetWindowRect(hwnd) - left, top, right, bottom = rect - windows.append((left, top, right - left, bottom - top, title)) - except: - pass - - return True - - windows = [] - win32gui.EnumWindows(callback, windows) - - if windows: - # Return the largest window (most likely the main game) - windows.sort(key=lambda w: w[2] * w[3], reverse=True) - left, top, width, height, title = windows[0] - print(f"[SkillScanner] Found Entropia window: '{title}' at ({left}, {top}, {width}, {height})") - return (left, top, width, height) - - elif platform.system() == 'Linux': - # Try using xdotool or wmctrl - try: - import subprocess - result = subprocess.run( - ['xdotool', 'search', '--name', 'Entropia Universe'], - capture_output=True, text=True - ) - if result.returncode == 0 and result.stdout.strip(): - window_id = result.stdout.strip().split('\n')[0] - # Get window geometry - geo_result = subprocess.run( - ['xdotool', 'getwindowgeometry', window_id], - capture_output=True, text=True - ) - if geo_result.returncode == 0: - # Parse: "Position: 100,200 (screen: 0)" and "Geometry: 1920x1080" - pos_match = re.search(r'Position: (\d+),(\d+)', geo_result.stdout) - geo_match = re.search(r'Geometry: (\d+)x(\d+)', geo_result.stdout) - if pos_match and geo_match: - left = int(pos_match.group(1)) - top = int(pos_match.group(2)) - width = int(geo_match.group(1)) - height = int(geo_match.group(2)) - print(f"[SkillScanner] Found Entropia window at ({left}, {top}, {width}, {height})") - return (left, top, width, height) - except Exception as e: - print(f"[SkillScanner] Linux window detection failed: {e}") - - except Exception as e: - print(f"[SkillScanner] Window detection error: {e}") - - print("[SkillScanner] Could not find Entropia window - will use full screen") - return None - - -def capture_entropia_region(region=None): - """ - Capture screen region of Entropia window. - If region is None, tries to find the window automatically. - Returns PIL Image or None. - """ - try: - from PIL import ImageGrab - - if region is None: - region = find_entropia_window() - - if region: - left, top, width, height = region - # Add some padding to ensure we capture everything - # Don't go below 0 - left = max(0, left) - top = max(0, top) - screenshot = ImageGrab.grab(bbox=(left, top, left + width, top + height)) - print(f"[SkillScanner] Captured Entropia window region: {width}x{height}") - return screenshot - else: - # Fallback to full screen - screenshot = ImageGrab.grab() - print("[SkillScanner] Captured full screen (window not found)") - return screenshot - - except Exception as e: - print(f"[SkillScanner] Capture error: {e}") - return None - - -def is_valid_skill_text(text): - """ - Filter out non-game text from OCR results. - Returns True if text looks like it could be from the game. - """ - # List of patterns that indicate NON-game text (UI, Discord, etc.) - invalid_patterns = [ - # App/UI elements - 'Discord', 'Presence', 'Event Bus', 'Example', 'Game Reader', - 'Test', 'Page Scanner', 'HOTKEY MODE', 'Skill Tracker', - 'Navigate', 'window', 'UI', 'Plugin', 'Settings', - 'Click', 'Button', 'Menu', 'Panel', 'Tab', 'Loading...', - 'Calculator', 'Nexus Search', 'Dashboard', 'Analytics', - 'Multi-Page', 'Scanner', 'Auto-detect', 'F12', - 'Cleared', 'Parsed:', '[SkillScanner]', 'INFO', 'DEBUG', - 'Loading', 'Initializing', 'Connecting', 'Error:', 'Warning:', - 'Entropia.exe', 'Client (64 bit)', 'Arkadia', 'Calypso', - # Instructions from our own UI - 'Position Skills', 'Position Skills window', 'Start Smart Scan', - 'Scan Current Page', 'Save All', 'Clear Session', - 'Select Area', 'Drag over', 'Navigate pages', - # Column headers that might be picked up - 'Skill', 'Skills', 'Rank', 'Points', 'Name', - # Category names with extra text - 'Combat Wounding', 'Combat Serendipity', 'Combat Reflexes', - 'Scan Serendipity', 'Scan Wounding', 'Scan Reflexes', - 'Position Wounding', 'Position Serendipity', 'Position Reflexes', - 'Current Page', 'Smart Scan', 'All Scanned', - ] - - # Check for invalid patterns - text_upper = text.upper() - for pattern in invalid_patterns: - if pattern.upper() in text_upper: - return False - - # Check for reasonable skill name length (not too long, not too short) - words = text.split() - if len(words) > 7: # Skills rarely have 7+ words (reduced from 10) - return False - - # Check if text contains button/action words combined with skill-like text - action_words = ['Click', 'Scan', 'Position', 'Select', 'Navigate', 'Start', 'Save', 'Clear'] - text_lower = text.lower() - for word in action_words: - if word.lower() in text_lower: - # If it has action words, it's probably UI text - return False - - return True - - -class SkillOCRThread(QThread): - """OCR scan using core service.""" - scan_complete = pyqtSignal(dict) - scan_error = pyqtSignal(str) - progress_update = pyqtSignal(str) - - def __init__(self, ocr_service, scan_area=None): - super().__init__() - self.ocr_service = ocr_service - self.scan_area = scan_area # (x, y, width, height) or None - - def run(self): - """Perform OCR using core service - only on selected area or Entropia window.""" - try: - if self.scan_area: - # Use user-selected area - x, y, w, h = self.scan_area - self.progress_update.emit(f"Capturing selected area ({w}x{h})...") - - from PIL import ImageGrab - screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h)) - else: - # Capture Entropia game window - self.progress_update.emit("Finding Entropia window...") - screenshot = capture_entropia_region() - - if screenshot is None: - self.scan_error.emit("Could not capture screen") - return - - self.progress_update.emit("Running OCR...") - - # Use core OCR service with the captured image - result = self.ocr_service.recognize_image(screenshot) - - if 'error' in result and result['error']: - self.scan_error.emit(result['error']) - return - - self.progress_update.emit("Parsing skills...") - - # Parse skills from text - raw_text = result.get('text', '') - skills_data = self._parse_skills_filtered(raw_text) - - if not skills_data: - self.progress_update.emit("No skills found. Make sure Skills window is visible.") - else: - self.progress_update.emit(f"Found {len(skills_data)} skills") - - self.scan_complete.emit(skills_data) - - except Exception as e: - self.scan_error.emit(str(e)) - - if not skills_data: - self.progress_update.emit("No valid skills found. Make sure Skills window is open.") - else: - self.progress_update.emit(f"Found {len(skills_data)} skills") - - self.scan_complete.emit(skills_data) - - except Exception as e: - self.scan_error.emit(str(e)) - - def _parse_skills(self, text): - """Parse skill data from OCR text with improved handling for 3-column layout.""" - skills = {} - - # Ranks in Entropia Universe (including multi-word ranks) - # Single word ranks - SINGLE_RANKS = [ - 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average', - 'Skilled', 'Expert', 'Professional', 'Master', - 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable', - 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome' - ] - # Multi-word ranks (must be checked first - longer matches first) - MULTI_RANKS = [ - 'Arch Master', 'Grand Master' - ] - - # Combine: multi-word first (so they match before single word), then single - ALL_RANKS = MULTI_RANKS + SINGLE_RANKS - rank_pattern = '|'.join(ALL_RANKS) - - # Clean up the text - remove common headers and junk - text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '') - text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '') - - # Remove category names that appear as standalone words - for category in ['Attributes', 'COMBAT', 'Combat', 'Design', 'Construction', - 'Defense', 'General', 'Handgun', 'Heavy Melee Weapons', - 'Heavy Weapons', 'Information', 'Inflict Melee Damage', - 'Inflict Ranged Damage', 'Light Melee Weapons', 'Longblades', - 'Medical', 'Mining', 'Science', 'Social', 'Beauty', 'Mindforce']: - text = text.replace(category, ' ') - - # Remove extra whitespace - text = ' '.join(text.split()) - - # Find all skills in the text using finditer - for match in re.finditer( - rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)', - text, re.IGNORECASE - ): - skill_name = match.group(1).strip() - rank = match.group(2) - points = int(match.group(3)) - - # Clean up skill name - remove common words that might be prepended - skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE) - skill_name = skill_name.strip() - - # Validate skill name - filter out UI text - if not is_valid_skill_text(skill_name): - print(f"[SkillScanner] Filtered invalid skill name: '{skill_name}'") - continue - - # Validate - points should be reasonable (not too small) - if points > 0 and skill_name and len(skill_name) > 2: - skills[skill_name] = { - 'rank': rank, - 'points': points, - 'scanned_at': datetime.now().isoformat() - } - print(f"[SkillScanner] Parsed: {skill_name} = {rank} ({points})") - - # If no skills found with primary method, try alternative - if not skills: - skills = self._parse_skills_alternative(text, ALL_RANKS) - - return skills - - def _parse_skills_filtered(self, text): - """ - Parse skills with filtering to remove non-game text. - Only returns skills that pass validity checks. - """ - # First, split text into lines and filter each line - lines = text.split('\n') - valid_lines = [] - - for line in lines: - line = line.strip() - if not line: - continue - - # Check if line contains a rank (required for skill lines) - has_rank = any(rank in line for rank in [ - 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average', - 'Skilled', 'Expert', 'Professional', 'Master', - 'Arch Master', 'Grand Master', # Multi-word first - 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable', - 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome' - ]) - - if not has_rank: - continue # Skip lines without ranks - - # Check for invalid patterns - if not is_valid_skill_text(line): - print(f"[SkillScanner] Filtered out: '{line[:50]}...'") - continue - - valid_lines.append(line) - - # Join valid lines and parse - filtered_text = '\n'.join(valid_lines) - - if not filtered_text.strip(): - print("[SkillScanner] No valid game text found after filtering") - return {} - - print(f"[SkillScanner] Filtered {len(lines)} lines to {len(valid_lines)} valid lines") - - return self._parse_skills(filtered_text) - - def _parse_skills_alternative(self, text, ranks): - """Alternative parser for when text is heavily merged.""" - skills = {} - - # Find all rank positions in the text - for rank in ranks: - # Look for pattern: [text] [Rank] [number] - pattern = rf'([A-Z][a-z]{{2,}}(?:\s+[A-Z][a-z]{{2,}}){{0,3}})\s+{re.escape(rank)}\s+(\d{{1,6}})' - matches = re.finditer(pattern, text, re.IGNORECASE) - - for match in matches: - skill_name = match.group(1).strip() - points = int(match.group(2)) - - # Clean skill name - skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE) - - if points > 0 and len(skill_name) > 2: - skills[skill_name] = { - 'rank': rank, - 'points': points, - 'scanned_at': datetime.now().isoformat() - } - - return skills - - -class SnippingWidget(QWidget): - """Fullscreen overlay for snipping tool-style area selection.""" - - area_selected = pyqtSignal(int, int, int, int) # x, y, width, height - cancelled = pyqtSignal() - - def __init__(self): - super().__init__() - self.setWindowFlags( - Qt.WindowType.FramelessWindowHint | - Qt.WindowType.WindowStaysOnTopHint | - Qt.WindowType.Tool - ) - self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - - # Get screen geometry - from PyQt6.QtWidgets import QApplication - screen = QApplication.primaryScreen().geometry() - self.setGeometry(screen) - - self.begin = None - self.end = None - self.drawing = False - - # Semi-transparent dark overlay - self.overlay_color = Qt.GlobalColor.black - self.overlay_opacity = 160 # 0-255 - - def paintEvent(self, event): - from PyQt6.QtGui import QPainter, QPen, QColor, QBrush - - painter = QPainter(self) - - # Draw semi-transparent overlay - overlay = QColor(0, 0, 0, self.overlay_opacity) - painter.fillRect(self.rect(), overlay) - - if self.begin and self.end: - # Clear overlay in selected area (make it transparent) - x = min(self.begin.x(), self.end.x()) - y = min(self.begin.y(), self.end.y()) - w = abs(self.end.x() - self.begin.x()) - h = abs(self.end.y() - self.begin.y()) - - # Draw the clear rectangle - painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Clear) - painter.fillRect(x, y, w, h, Qt.GlobalColor.transparent) - painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceOver) - - # Draw border around selection - pen = QPen(Qt.GlobalColor.white, 2, Qt.PenStyle.SolidLine) - painter.setPen(pen) - painter.drawRect(x, y, w, h) - - # Draw dimensions text - painter.setPen(Qt.GlobalColor.white) - from PyQt6.QtGui import QFont - font = QFont("Arial", 10) - painter.setFont(font) - painter.drawText(x, y - 5, f"{w} x {h}") - - def mousePressEvent(self, event): - if event.button() == Qt.MouseButton.LeftButton: - self.begin = event.pos() - self.end = event.pos() - self.drawing = True - self.update() - - def mouseMoveEvent(self, event): - if self.drawing: - self.end = event.pos() - self.update() - - def mouseReleaseEvent(self, event): - if event.button() == Qt.MouseButton.LeftButton and self.drawing: - self.drawing = False - self.end = event.pos() - - # Calculate selection rectangle - x = min(self.begin.x(), self.end.x()) - y = min(self.begin.y(), self.end.y()) - w = abs(self.end.x() - self.begin.x()) - h = abs(self.end.y() - self.begin.y()) - - # Minimum size check - if w > 50 and h > 50: - self.area_selected.emit(x, y, w, h) - else: - self.cancelled.emit() - - self.close() - elif event.button() == Qt.MouseButton.RightButton: - # Right click to cancel - self.cancelled.emit() - self.close() - - def keyPressEvent(self, event): - if event.key() == Qt.Key.Key_Escape: - self.cancelled.emit() - self.close() - - -class SignalHelper(QObject): - """Helper QObject to hold signals since BasePlugin doesn't inherit from QObject.""" - hotkey_triggered = pyqtSignal() - update_status_signal = pyqtSignal(str, bool, bool) # message, success, error - update_session_table_signal = pyqtSignal(object) # skills dict - update_counters_signal = pyqtSignal() - enable_scan_button_signal = pyqtSignal(bool) - - -class SkillScannerPlugin(BasePlugin): - """Scan skills using core OCR and track gains via core Log service.""" - - name = "Skill Scanner" - version = "2.1.0" - author = "ImpulsiveFPS" - description = "Uses core OCR and Log services" - hotkey = "ctrl+shift+s" - - def initialize(self): - """Setup skill scanner.""" - # Create signal helper (QObject) for thread-safe UI updates - self._signals = SignalHelper() - - self.data_file = Path("data/skill_tracker.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - # Load saved data - self.skills_data = {} - self.skill_gains = [] - self._load_data() - - # Multi-page scanning state - self.current_scan_session = {} # Skills collected in current multi-page scan - self.pages_scanned = 0 - - # Scan area selection (x, y, width, height) - None means auto-detect game window - self.scan_area = None - - # Connect signals (using signal helper QObject) - self._signals.hotkey_triggered.connect(self._scan_page_for_multi) - self._signals.update_status_signal.connect(self._update_multi_page_status_slot) - self._signals.update_session_table_signal.connect(self._update_session_table) - self._signals.update_counters_signal.connect(self._update_counters_slot) - # Note: enable_scan_button_signal connected in get_ui() after button created - - # Subscribe to skill gain events from core Log service - try: - from core.plugin_api import get_api - api = get_api() - - # Check if log service available - log_service = api.services.get('log') - if log_service: - print(f"[SkillScanner] Connected to core Log service") - - except Exception as e: - print(f"[SkillScanner] Could not connect to Log service: {e}") - - def _load_data(self): - """Load saved skill data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.skills_data = data.get('skills', {}) - self.skill_gains = data.get('gains', []) - except: - pass - - def _save_data(self): - """Save skill data.""" - with open(self.data_file, 'w') as f: - json.dump({ - 'skills': self.skills_data, - 'gains': self.skill_gains - }, f, indent=2) - - def get_ui(self): - """Create skill scanner UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Header - header = QLabel("Skill Tracker") - header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;") - layout.addWidget(header) - - # Info about core services - info = self._get_service_status() - info_label = QLabel(info) - info_label.setStyleSheet("color: rgba(255,255,255,100); font-size: 11px;") - layout.addWidget(info_label) - - # Splitter - splitter = QSplitter(Qt.Orientation.Vertical) - - # Scan section - scan_group = QGroupBox("OCR Scan (Core Service)") - scan_layout = QVBoxLayout(scan_group) - - # Area selection row - area_layout = QHBoxLayout() - - self.select_area_btn = QPushButton("๐Ÿ“ Select Area") - self.select_area_btn.setStyleSheet(""" - QPushButton { - background-color: #4a9eff; - color: white; - padding: 10px 20px; - border: none; - border-radius: 4px; - font-weight: bold; - } - QPushButton:hover { - background-color: #3a8eef; - } - """) - self.select_area_btn.clicked.connect(self._start_area_selection) - area_layout.addWidget(self.select_area_btn) - - self.area_label = QLabel("Area: Not selected (will scan full game window)") - self.area_label.setStyleSheet("color: #888; font-size: 11px;") - area_layout.addWidget(self.area_label) - area_layout.addStretch() - - scan_layout.addLayout(area_layout) - - # Buttons row - buttons_layout = QHBoxLayout() - - scan_btn = QPushButton("Scan Skills Window") - scan_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 12px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - scan_btn.clicked.connect(self._scan_skills) - buttons_layout.addWidget(scan_btn) - - reset_btn = QPushButton("Reset Data") - reset_btn.setStyleSheet(""" - QPushButton { - background-color: #ff4757; - color: white; - padding: 12px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - reset_btn.clicked.connect(self._reset_data) - buttons_layout.addWidget(reset_btn) - - scan_layout.addLayout(buttons_layout) - - self.scan_progress = QLabel("Ready to scan") - self.scan_progress.setStyleSheet("color: rgba(255,255,255,150);") - scan_layout.addWidget(self.scan_progress) - - self.skills_table = QTableWidget() - self.skills_table.setColumnCount(3) - self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"]) - self.skills_table.horizontalHeader().setStretchLastSection(True) - scan_layout.addWidget(self.skills_table) - - splitter.addWidget(scan_group) - - # Multi-Page Scanning section - multi_page_group = QGroupBox("Multi-Page Scanner") - multi_page_layout = QVBoxLayout(multi_page_group) - - # Mode selection - mode_layout = QHBoxLayout() - mode_layout.addWidget(QLabel("Mode:")) - - self.scan_mode_combo = QComboBox() - self.scan_mode_combo.addItems(["Smart Auto + Hotkey Fallback", "Manual Hotkey Only", "Manual Click Only"]) - self.scan_mode_combo.currentIndexChanged.connect(self._on_scan_mode_changed) - mode_layout.addWidget(self.scan_mode_combo) - mode_layout.addStretch() - multi_page_layout.addLayout(mode_layout) - - # Instructions - self.instructions_label = QLabel( - "๐Ÿค– SMART MODE:\n" - "1. Click 'Select Area' above and drag over your Skills window\n" - "2. Click 'Start Smart Scan'\n" - "3. Navigate pages in EU - auto-detect will scan for you\n" - "4. If auto fails, use hotkey F12 to scan manually\n" - "5. Click 'Save All' when done" - ) - self.instructions_label.setStyleSheet("color: #888; font-size: 11px;") - multi_page_layout.addWidget(self.instructions_label) - - # Hotkey info - self.hotkey_info = QLabel("Hotkey: F12 = Scan Current Page") - self.hotkey_info.setStyleSheet("color: #4ecdc4; font-weight: bold;") - multi_page_layout.addWidget(self.hotkey_info) - - # Status row - status_layout = QHBoxLayout() - - self.multi_page_status = QLabel("โณ Ready to scan page 1") - self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;") - status_layout.addWidget(self.multi_page_status) - - self.pages_scanned_label = QLabel("Pages: 0") - self.pages_scanned_label.setStyleSheet("color: #4ecdc4;") - status_layout.addWidget(self.pages_scanned_label) - - self.total_skills_label = QLabel("Skills: 0") - self.total_skills_label.setStyleSheet("color: #4ecdc4;") - status_layout.addWidget(self.total_skills_label) - - status_layout.addStretch() - multi_page_layout.addLayout(status_layout) - - # Buttons - mp_buttons_layout = QHBoxLayout() - - self.scan_page_btn = QPushButton("โ–ถ๏ธ Start Smart Scan") - self.scan_page_btn.setStyleSheet(""" - QPushButton { - background-color: #4ecdc4; - color: #141f23; - padding: 12px; - border: none; - border-radius: 4px; - font-weight: bold; - font-size: 13px; - } - QPushButton:hover { - background-color: #3dbdb4; - } - """) - self.scan_page_btn.clicked.connect(self._start_smart_scan) - mp_buttons_layout.addWidget(self.scan_page_btn) - - # Connect signal helper for button enabling (now that button exists) - self._signals.enable_scan_button_signal.connect(self.scan_page_btn.setEnabled) - - save_all_btn = QPushButton("๐Ÿ’พ Save All Scanned") - save_all_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 12px; - border: none; - border-radius: 4px; - font-weight: bold; - font-size: 13px; - } - """) - save_all_btn.clicked.connect(self._save_multi_page_scan) - mp_buttons_layout.addWidget(save_all_btn) - - clear_session_btn = QPushButton("๐Ÿ—‘ Clear Session") - clear_session_btn.setStyleSheet(""" - QPushButton { - background-color: #666; - color: white; - padding: 12px; - border: none; - border-radius: 4px; - } - """) - clear_session_btn.clicked.connect(self._clear_multi_page_session) - mp_buttons_layout.addWidget(clear_session_btn) - - multi_page_layout.addLayout(mp_buttons_layout) - - # Current session table - self.session_table = QTableWidget() - self.session_table.setColumnCount(3) - self.session_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"]) - self.session_table.horizontalHeader().setStretchLastSection(True) - self.session_table.setMaximumHeight(200) - self.session_table.setStyleSheet(""" - QTableWidget { - background-color: #0d1117; - border: 1px solid #333; - } - QTableWidget::item { - padding: 4px; - color: #c9d1d9; - } - """) - multi_page_layout.addWidget(self.session_table) - - splitter.addWidget(multi_page_group) - - # Log section - log_group = QGroupBox("Log Tracking (Core Service)") - log_layout = QVBoxLayout(log_group) - - log_status = QLabel("Skill gains tracked from chat log") - log_status.setStyleSheet("color: #4ecdc4;") - log_layout.addWidget(log_status) - - self.gains_text = QTextEdit() - self.gains_text.setReadOnly(True) - self.gains_text.setMaximumHeight(150) - self.gains_text.setPlaceholderText("Recent skill gains from core Log service...") - log_layout.addWidget(self.gains_text) - - self.total_gains_label = QLabel(f"Total gains: {len(self.skill_gains)}") - self.total_gains_label.setStyleSheet("color: rgba(255,255,255,150);") - log_layout.addWidget(self.total_gains_label) - - splitter.addWidget(log_group) - - layout.addWidget(splitter) - - self._refresh_skills_table() - - return widget - - def _get_service_status(self) -> str: - """Get status of core services.""" - try: - from core.ocr_service import get_ocr_service - from core.log_reader import get_log_reader - - ocr = get_ocr_service() - log = get_log_reader() - - ocr_status = "โœ“" if ocr.is_available() else "โœ—" - log_status = "โœ“" if log.is_available() else "โœ—" - - return f"Core Services - OCR: {ocr_status} Log: {log_status}" - except: - return "Core Services - status unknown" - - def _scan_skills(self): - """Start OCR scan using core service.""" - try: - from core.ocr_service import get_ocr_service - ocr_service = get_ocr_service() - - if not ocr_service.is_available(): - self.scan_progress.setText("Error: OCR service not available") - return - - # Pass scan_area if user has selected one - self.scanner = SkillOCRThread(ocr_service, scan_area=self.scan_area) - self.scanner.scan_complete.connect(self._on_scan_complete) - self.scanner.scan_error.connect(self._on_scan_error) - self.scanner.progress_update.connect(self._on_scan_progress) - self.scanner.start() - - except Exception as e: - self.scan_progress.setText(f"Error: {e}") - - def _on_scan_progress(self, message): - self.scan_progress.setText(message) - - def _on_scan_complete(self, skills_data): - self.skills_data.update(skills_data) - self._save_data() - self._refresh_skills_table() - self.scan_progress.setText(f"Found {len(skills_data)} skills") - - def _reset_data(self): - """Reset all skill data.""" - from PyQt6.QtWidgets import QMessageBox - - reply = QMessageBox.question( - None, - "Reset Skill Data", - "Are you sure you want to clear all scanned skill data?\n\nThis cannot be undone.", - QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, - QMessageBox.StandardButton.No - ) - - if reply == QMessageBox.StandardButton.Yes: - self.skills_data = {} - self.skill_gains = [] - self._save_data() - self._refresh_skills_table() - self.gains_text.clear() - self.total_gains_label.setText("Total gains: 0") - self.scan_progress.setText("Data cleared") - - def _on_scan_error(self, error): - self.scan_progress.setText(f"Error: {error}") - self.scan_progress.setText(f"Error: {error}") - - def _refresh_skills_table(self): - self.skills_table.setRowCount(len(self.skills_data)) - for i, (name, data) in enumerate(sorted(self.skills_data.items())): - self.skills_table.setItem(i, 0, QTableWidgetItem(name)) - self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-'))) - self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0)))) - - def _scan_page_for_multi(self): - """Scan current page and add to multi-page session.""" - from PyQt6.QtCore import QTimer - - self.multi_page_status.setText("๐Ÿ“ท Scanning...") - self.multi_page_status.setStyleSheet("color: #ffd93d;") - self.scan_page_btn.setEnabled(False) - - # Run scan in thread - from threading import Thread - - def do_scan(): - try: - from core.ocr_service import get_ocr_service - from PIL import ImageGrab - import re - from datetime import datetime - - ocr_service = get_ocr_service() - if not ocr_service.is_available(): - self._signals.update_status_signal.emit("Error: OCR not available", False, True) - return - - # Capture based on scan_area setting - if self.scan_area: - x, y, w, h = self.scan_area - screenshot = ImageGrab.grab(bbox=(x, y, x + w, y + h)) - else: - screenshot = capture_entropia_region() - - if screenshot is None: - self._signals.update_status_signal.emit("Error: Could not capture screen", False, True) - return - - # OCR - result = ocr_service.recognize_image(screenshot) - text = result.get('text', '') - - # Parse skills - skills = self._parse_skills_from_text(text) - - # Add to session - for skill_name, data in skills.items(): - self.current_scan_session[skill_name] = data - - self.pages_scanned += 1 - - # Update UI via signals (thread-safe) - self._signals.update_session_table_signal.emit(self.current_scan_session) - - # Show success with checkmark and beep - self._signals.update_status_signal.emit( - f"โœ… Page {self.pages_scanned} scanned! {len(skills)} skills found. Click Next Page in game โ†’", - True, False - ) - - # Play beep sound - self._play_beep() - - except Exception as e: - self._signals.update_status_signal.emit(f"Error: {str(e)}", False, True) - finally: - self._signals.enable_scan_button_signal.emit(True) - - thread = Thread(target=do_scan) - thread.daemon = True - thread.start() - - def _start_area_selection(self): - """Open snipping tool for user to select scan area.""" - self.select_area_btn.setEnabled(False) - self.select_area_btn.setText("๐Ÿ“ Selecting...") - - # Create and show snipping widget - self.snipping_widget = SnippingWidget() - self.snipping_widget.area_selected.connect(self._on_area_selected) - self.snipping_widget.cancelled.connect(self._on_area_cancelled) - self.snipping_widget.show() - - def _on_area_selected(self, x, y, w, h): - """Handle area selection from snipping tool.""" - self.scan_area = (x, y, w, h) - self.area_label.setText(f"Area: {w}x{h} at ({x}, {y})") - self.area_label.setStyleSheet("color: #4ecdc4; font-size: 11px;") - self.select_area_btn.setEnabled(True) - self.select_area_btn.setText("๐Ÿ“ Select Area") - self._signals.update_status_signal.emit(f"โœ… Scan area selected: {w}x{h}", True, False) - - def _on_area_cancelled(self): - """Handle cancelled area selection.""" - self.select_area_btn.setEnabled(True) - self.select_area_btn.setText("๐Ÿ“ Select Area") - self._signals.update_status_signal.emit("Area selection cancelled", False, False) - - def _parse_skills_from_text(self, text): - """Parse skills from OCR text.""" - skills = {} - - # Ranks in Entropia Universe - multi-word first for proper matching - SINGLE_RANKS = [ - 'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average', - 'Skilled', 'Expert', 'Professional', 'Master', - 'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable', - 'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome' - ] - MULTI_RANKS = ['Arch Master', 'Grand Master'] - ALL_RANKS = MULTI_RANKS + SINGLE_RANKS - rank_pattern = '|'.join(ALL_RANKS) - - # Clean text - text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '') - text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '') - - # Remove category names - for category in ['Attributes', 'COMBAT', 'Combat', 'Design', 'Construction', - 'Defense', 'General', 'Handgun', 'Heavy Melee Weapons', - 'Heavy Weapons', 'Information', 'Inflict Melee Damage', - 'Inflict Ranged Damage', 'Light Melee Weapons', 'Longblades', - 'Medical', 'Mining', 'Science', 'Social', 'Beauty', 'Mindforce']: - text = text.replace(category, ' ') - - text = ' '.join(text.split()) - - # Find all skills - import re - for match in re.finditer( - rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)', - text, re.IGNORECASE - ): - skill_name = match.group(1).strip() - rank = match.group(2) - points = int(match.group(3)) - - skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE) - skill_name = skill_name.strip() - - # Validate skill name - filter out UI text - if not is_valid_skill_text(skill_name): - print(f"[SkillScanner] Filtered invalid skill name: '{skill_name}'") - continue - - if points > 0 and skill_name and len(skill_name) > 2: - skills[skill_name] = {'rank': rank, 'points': points} - - return skills - - def _update_multi_page_status_slot(self, message, success=False, error=False): - """Slot for updating multi-page status (called via signal).""" - color = "#4ecdc4" if success else "#ff4757" if error else "#ff8c42" - - self.multi_page_status.setText(message) - self.multi_page_status.setStyleSheet(f"color: {color}; font-size: 14px;") - self._update_counters_slot() - - def _update_counters_slot(self): - """Slot for updating counters (called via signal).""" - self.pages_scanned_label.setText(f"Pages: {self.pages_scanned}") - self.total_skills_label.setText(f"Skills: {len(self.current_scan_session)}") - - def _play_beep(self): - """Play a beep sound to notify user.""" - try: - import winsound - winsound.MessageBeep(winsound.MB_OK) - except: - # Fallback - try to use system beep - try: - print('\a') # ASCII bell character - except: - pass - - def _update_session_table(self, skills): - """Update the session table with current scan data.""" - self.session_table.setRowCount(len(skills)) - for i, (name, data) in enumerate(sorted(skills.items())): - self.session_table.setItem(i, 0, QTableWidgetItem(name)) - self.session_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-'))) - self.session_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0)))) - - def _save_multi_page_scan(self): - """Save all scanned skills from multi-page session.""" - if not self.current_scan_session: - from PyQt6.QtWidgets import QMessageBox - QMessageBox.information(None, "No Data", "No skills scanned yet. Scan some pages first!") - return - - # Merge with existing data - self.skills_data.update(self.current_scan_session) - self._save_data() - self._refresh_skills_table() - - from PyQt6.QtWidgets import QMessageBox - QMessageBox.information( - None, - "Scan Complete", - f"Saved {len(self.current_scan_session)} skills from {self.pages_scanned} pages!" - ) - - # Clear session after saving - self._clear_multi_page_session() - - def _clear_multi_page_session(self): - """Clear the current multi-page scanning session.""" - self.current_scan_session = {} - self.pages_scanned = 0 - self.auto_scan_active = False - self.session_table.setRowCount(0) - self.multi_page_status.setText("โณ Ready to scan page 1") - self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;") - self.pages_scanned_label.setText("Pages: 0") - self.total_skills_label.setText("Skills: 0") - - # Unregister hotkey if active - self._unregister_hotkey() - - def _on_scan_mode_changed(self, index): - """Handle scan mode change.""" - modes = [ - "๐Ÿค– SMART MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Click 'Start Smart Scan'\n3. Navigate pages - auto-detect will scan\n4. If auto fails, press F12\n5. Click 'Save All' when done", - "โŒจ๏ธ HOTKEY MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Navigate to page 1 in EU\n3. Press F12 to scan each page\n4. Click Next Page in EU\n5. Repeat F12 for each page", - "๐Ÿ–ฑ๏ธ MANUAL MODE:\n1. Click 'Select Area' and drag over Skills window\n2. Click 'Scan Current Page'\n3. Wait for beep\n4. Click Next Page in EU\n5. Repeat" - ] - self.instructions_label.setText(modes[index]) - - def _start_smart_scan(self): - """Start smart auto-scan with hotkey fallback.""" - mode = self.scan_mode_combo.currentIndex() - - if mode == 0: # Smart Auto + Hotkey - self._start_auto_scan_with_hotkey() - elif mode == 1: # Hotkey only - self._register_hotkey() - self._signals.update_status_signal.emit("Hotkey F12 ready! Navigate to first page and press F12", True, False) - else: # Manual click - self._scan_page_for_multi() - - def _start_auto_scan_with_hotkey(self): - """Start auto-detection with fallback to hotkey.""" - self.auto_scan_active = True - self.auto_scan_failures = 0 - self.last_page_number = None - - # Register F12 hotkey as fallback - self._register_hotkey() - - # Start monitoring - self._signals.update_status_signal.emit("๐Ÿค– Auto-detect started! Navigate to page 1...", True, False) - - # Start auto-detection timer - self.auto_scan_timer = QTimer() - self.auto_scan_timer.timeout.connect(self._check_for_page_change) - self.auto_scan_timer.start(500) # Check every 500ms - - def _register_hotkey(self): - """Register F12 hotkey for manual scan.""" - try: - import keyboard - keyboard.on_press_key('f12', lambda e: self._hotkey_scan()) - self.hotkey_registered = True - except Exception as e: - print(f"[SkillScanner] Could not register hotkey: {e}") - self.hotkey_registered = False - - def _unregister_hotkey(self): - """Unregister hotkey.""" - try: - if hasattr(self, 'hotkey_registered') and self.hotkey_registered: - import keyboard - keyboard.unhook_all() - self.hotkey_registered = False - except: - pass - - # Stop auto-scan timer - if hasattr(self, 'auto_scan_timer') and self.auto_scan_timer: - self.auto_scan_timer.stop() - self.auto_scan_active = False - - def _hotkey_scan(self): - """Scan triggered by F12 hotkey - thread safe via signal.""" - # Emit signal to safely call from hotkey thread - self._signals.hotkey_triggered.emit() - - def _check_for_page_change(self): - """Auto-detect page changes by monitoring page number area.""" - if not self.auto_scan_active: - return - - try: - from PIL import ImageGrab - import pytesseract - - # Capture page number area (bottom center of skills window) - # This is approximate - may need adjustment - screen = ImageGrab.grab() - width, height = screen.size - - # Try to capture the page number area (bottom center, small region) - # EU skills window shows page like "1/12" at bottom - page_area = (width // 2 - 50, height - 100, width // 2 + 50, height - 50) - page_img = ImageGrab.grab(bbox=page_area) - - # OCR just the page number - page_text = pytesseract.image_to_string(page_img, config='--psm 7 -c tessedit_char_whitelist=0123456789/') - - # Extract current page number - import re - match = re.search(r'(\d+)/(\d+)', page_text) - if match: - current_page = int(match.group(1)) - total_pages = int(match.group(2)) - - # If page changed, trigger scan - if self.last_page_number is not None and current_page != self.last_page_number: - self._signals.update_status_signal.emit(f"๐Ÿ“„ Page change detected: {current_page}/{total_pages}", True, False) - self._scan_page_for_multi() - - self.last_page_number = current_page - else: - # Failed to detect page number - self.auto_scan_failures += 1 - if self.auto_scan_failures >= 10: # After 5 seconds of failures - self._fallback_to_hotkey() - - except Exception as e: - self.auto_scan_failures += 1 - if self.auto_scan_failures >= 10: - self._fallback_to_hotkey() - - def _fallback_to_hotkey(self): - """Fallback to hotkey mode when auto-detection fails.""" - if hasattr(self, 'auto_scan_timer') and self.auto_scan_timer: - self.auto_scan_timer.stop() - - self.auto_scan_active = False - - # Keep hotkey registered - self._signals.update_status_signal.emit( - "โš ๏ธ Auto-detect unreliable. Use F12 hotkey to scan each page manually!", - False, True - ) - - # Play alert sound - self._play_beep() diff --git a/plugins/spotify_controller/__init__.py b/plugins/spotify_controller/__init__.py deleted file mode 100644 index d283b86..0000000 --- a/plugins/spotify_controller/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Spotify Controller Plugin for EU-Utility -""" - -from .plugin import SpotifyControllerPlugin - -__all__ = ["SpotifyControllerPlugin"] diff --git a/plugins/spotify_controller/plugin.py b/plugins/spotify_controller/plugin.py deleted file mode 100644 index d935400..0000000 --- a/plugins/spotify_controller/plugin.py +++ /dev/null @@ -1,473 +0,0 @@ -""" -EU-Utility - Spotify Controller Plugin - -Control Spotify playback and display current track info. -""" - -import subprocess -import platform -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, - QLabel, QPushButton, QSlider, QProgressBar -) -from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal - -from plugins.base_plugin import BasePlugin - - -class SpotifyInfoThread(QThread): - """Background thread to fetch Spotify info.""" - info_ready = pyqtSignal(dict) - error = pyqtSignal(str) - - def __init__(self, system): - super().__init__() - self.system = system - - def run(self): - """Fetch Spotify info.""" - try: - if self.system == "Linux": - result = subprocess.run( - ['playerctl', '--player=spotify', 'metadata', '--format', - '{{title}}|{{artist}}|{{album}}|{{position}}|{{mpris:length}}|{{status}}'], - capture_output=True, text=True, timeout=5 - ) - if result.returncode == 0: - parts = result.stdout.strip().split('|') - if len(parts) >= 6: - self.info_ready.emit({ - 'title': parts[0] or 'Unknown', - 'artist': parts[1] or 'Unknown Artist', - 'album': parts[2] or '', - 'position': self._parse_time(parts[3]), - 'duration': self._parse_time(parts[4]), - 'is_playing': parts[5] == 'Playing' - }) - return - - elif self.system == "Darwin": - script = ''' - tell application "Spotify" - if player state is playing then - return (name of current track) & "|" & (artist of current track) & "|" & (album of current track) & "|" & (player position) & "|" & (duration of current track / 1000) & "|Playing" - else - return (name of current track) & "|" & (artist of current track) & "|" & (album of current track) & "|" & (player position) & "|" & (duration of current track / 1000) & "|Paused" - end if - end tell - ''' - result = subprocess.run(['osascript', '-e', script], capture_output=True, text=True, timeout=5) - if result.returncode == 0: - parts = result.stdout.strip().split('|') - if len(parts) >= 6: - self.info_ready.emit({ - 'title': parts[0] or 'Unknown', - 'artist': parts[1] or 'Unknown Artist', - 'album': parts[2] or '', - 'position': float(parts[3]) if parts[3] else 0, - 'duration': float(parts[4]) if parts[4] else 0, - 'is_playing': parts[5] == 'Playing' - }) - return - - # Default/empty response - self.info_ready.emit({ - 'title': 'Not playing', - 'artist': '', - 'album': '', - 'position': 0, - 'duration': 0, - 'is_playing': False - }) - - except Exception as e: - self.error.emit(str(e)) - self.info_ready.emit({ - 'title': 'Not playing', - 'artist': '', - 'album': '', - 'position': 0, - 'duration': 0, - 'is_playing': False - }) - - def _parse_time(self, time_str): - """Parse time string to seconds.""" - try: - return int(time_str) / 1000000 - except: - return 0 - - -class SpotifyControllerPlugin(BasePlugin): - """Control Spotify playback and display current track.""" - - name = "Spotify" - version = "1.1.0" - author = "ImpulsiveFPS" - description = "Control Spotify and view current track info" - hotkey = "ctrl+shift+m" - - def initialize(self): - """Setup Spotify controller.""" - self.system = platform.system() - self.update_timer = None - self.info_thread = None - self.current_info = { - 'title': 'Not playing', - 'artist': '', - 'album': '', - 'position': 0, - 'duration': 0, - 'is_playing': False - } - - def get_ui(self): - """Create Spotify controller UI.""" - widget = QWidget() - layout = QVBoxLayout(widget) - layout.setSpacing(12) - - # Title - title = QLabel("Spotify") - title.setStyleSheet("color: #1DB954; font-size: 18px; font-weight: bold;") - layout.addWidget(title) - - # Album Art Placeholder - self.album_art = QLabel("๐Ÿ’ฟ") - self.album_art.setAlignment(Qt.AlignmentFlag.AlignCenter) - self.album_art.setStyleSheet(""" - QLabel { - background-color: #282828; - border-radius: 8px; - font-size: 64px; - padding: 20px; - min-height: 120px; - } - """) - layout.addWidget(self.album_art) - - # Track info container - info_container = QWidget() - info_container.setStyleSheet(""" - QWidget { - background-color: #1a1a1a; - border-radius: 8px; - padding: 12px; - } - """) - info_layout = QVBoxLayout(info_container) - info_layout.setSpacing(4) - - # Track title - self.track_label = QLabel("Not playing") - self.track_label.setStyleSheet(""" - color: white; - font-size: 16px; - font-weight: bold; - """) - self.track_label.setWordWrap(True) - info_layout.addWidget(self.track_label) - - # Artist - self.artist_label = QLabel("") - self.artist_label.setStyleSheet(""" - color: #b3b3b3; - font-size: 13px; - """) - info_layout.addWidget(self.artist_label) - - # Album - self.album_label = QLabel("") - self.album_label.setStyleSheet(""" - color: #666; - font-size: 11px; - """) - info_layout.addWidget(self.album_label) - - layout.addWidget(info_container) - - # Time info - time_layout = QHBoxLayout() - self.position_label = QLabel("0:00") - self.position_label.setStyleSheet("color: #888; font-size: 11px;") - time_layout.addWidget(self.position_label) - - time_layout.addStretch() - - self.duration_label = QLabel("0:00") - self.duration_label.setStyleSheet("color: #888; font-size: 11px;") - time_layout.addWidget(self.duration_label) - - layout.addLayout(time_layout) - - # Progress bar - self.progress = QProgressBar() - self.progress.setRange(0, 100) - self.progress.setValue(0) - self.progress.setTextVisible(False) - self.progress.setStyleSheet(""" - QProgressBar { - background-color: #404040; - border: none; - height: 4px; - border-radius: 2px; - } - QProgressBar::chunk { - background-color: #1DB954; - border-radius: 2px; - } - """) - layout.addWidget(self.progress) - - # Control buttons - btn_layout = QHBoxLayout() - btn_layout.setSpacing(15) - btn_layout.addStretch() - - # Previous - prev_btn = QPushButton("โฎ") - prev_btn.setFixedSize(50, 50) - prev_btn.setStyleSheet(self._get_button_style("#404040")) - prev_btn.clicked.connect(self._previous_track) - btn_layout.addWidget(prev_btn) - - # Play/Pause - self.play_btn = QPushButton("โ–ถ") - self.play_btn.setFixedSize(60, 60) - self.play_btn.setStyleSheet(self._get_play_button_style()) - self.play_btn.clicked.connect(self._toggle_playback) - btn_layout.addWidget(self.play_btn) - - # Next - next_btn = QPushButton("โญ") - next_btn.setFixedSize(50, 50) - next_btn.setStyleSheet(self._get_button_style("#404040")) - next_btn.clicked.connect(self._next_track) - btn_layout.addWidget(next_btn) - - btn_layout.addStretch() - layout.addLayout(btn_layout) - - # Volume - volume_layout = QHBoxLayout() - volume_layout.addWidget(QLabel("๐Ÿ”ˆ")) - - self.volume_slider = QSlider(Qt.Orientation.Horizontal) - self.volume_slider.setRange(0, 100) - self.volume_slider.setValue(50) - self.volume_slider.setStyleSheet(""" - QSlider::groove:horizontal { - background: #404040; - height: 4px; - border-radius: 2px; - } - QSlider::handle:horizontal { - background: #fff; - width: 12px; - margin: -4px 0; - border-radius: 6px; - } - QSlider::sub-page:horizontal { - background: #1DB954; - border-radius: 2px; - } - """) - self.volume_slider.valueChanged.connect(self._set_volume) - volume_layout.addWidget(self.volume_slider) - - volume_layout.addWidget(QLabel("๐Ÿ”Š")) - layout.addLayout(volume_layout) - - # Status - self.status_label = QLabel("Click play to control Spotify") - self.status_label.setStyleSheet("color: #666; font-size: 10px;") - self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) - layout.addWidget(self.status_label) - - layout.addStretch() - - # Start update timer - self._start_timer() - - return widget - - def _get_button_style(self, color): - """Get button stylesheet.""" - return f""" - QPushButton {{ - background-color: {color}; - color: white; - font-size: 18px; - border: none; - border-radius: 25px; - }} - QPushButton:hover {{ - background-color: #505050; - }} - QPushButton:pressed {{ - background-color: #303030; - }} - """ - - def _get_play_button_style(self): - """Get play button style (green).""" - return """ - QPushButton { - background-color: #1DB954; - color: white; - font-size: 22px; - border: none; - border-radius: 30px; - } - QPushButton:hover { - background-color: #1ed760; - } - QPushButton:pressed { - background-color: #1aa34a; - } - """ - - def _start_timer(self): - """Start status update timer.""" - self.update_timer = QTimer() - self.update_timer.timeout.connect(self._fetch_spotify_info) - self.update_timer.start(1000) - - def _fetch_spotify_info(self): - """Fetch Spotify info in background.""" - if self.info_thread and self.info_thread.isRunning(): - return - - self.info_thread = SpotifyInfoThread(self.system) - self.info_thread.info_ready.connect(self._update_ui) - self.info_thread.start() - - def _update_ui(self, info): - """Update UI with Spotify info.""" - self.current_info = info - - # Update track info - self.track_label.setText(info.get('title', 'Unknown')) - self.artist_label.setText(info.get('artist', '')) - self.album_label.setText(info.get('album', '')) - - # Update play button - is_playing = info.get('is_playing', False) - self.play_btn.setText("โธ" if is_playing else "โ–ถ") - - # Update time - position = info.get('position', 0) - duration = info.get('duration', 0) - - self.position_label.setText(self._format_time(position)) - self.duration_label.setText(self._format_time(duration)) - - # Update progress bar - if duration > 0: - progress = int((position / duration) * 100) - self.progress.setValue(progress) - else: - self.progress.setValue(0) - - def _format_time(self, seconds): - """Format seconds to mm:ss.""" - try: - minutes = int(seconds) // 60 - secs = int(seconds) % 60 - return f"{minutes}:{secs:02d}" - except: - return "0:00" - - def _send_media_key(self, key): - """Send media key press to system.""" - try: - if self.system == "Windows": - import ctypes - key_codes = { - 'play': 0xB3, - 'next': 0xB0, - 'prev': 0xB1, - } - if key in key_codes: - ctypes.windll.user32.keybd_event(key_codes[key], 0, 0, 0) - ctypes.windll.user32.keybd_event(key_codes[key], 0, 2, 0) - return True - - elif self.system == "Linux": - cmd_map = { - 'play': ['playerctl', '--player=spotify', 'play-pause'], - 'next': ['playerctl', '--player=spotify', 'next'], - 'prev': ['playerctl', '--player=spotify', 'previous'], - } - if key in cmd_map: - subprocess.run(cmd_map[key], capture_output=True) - return True - - elif self.system == "Darwin": - cmd_map = { - 'play': ['osascript', '-e', 'tell application "Spotify" to playpause'], - 'next': ['osascript', '-e', 'tell application "Spotify" to next track'], - 'prev': ['osascript', '-e', 'tell application "Spotify" to previous track'], - } - if key in cmd_map: - subprocess.run(cmd_map[key], capture_output=True) - return True - - except Exception as e: - print(f"Error sending media key: {e}") - - return False - - def _toggle_playback(self): - """Toggle play/pause.""" - if self._send_media_key('play'): - self.current_info['is_playing'] = not self.current_info.get('is_playing', False) - self.play_btn.setText("โธ" if self.current_info['is_playing'] else "โ–ถ") - self.status_label.setText("Command sent to Spotify") - else: - self.status_label.setText("โŒ Could not control Spotify") - - def _next_track(self): - """Next track.""" - if self._send_media_key('next'): - self.status_label.setText("โญ Next track") - else: - self.status_label.setText("โŒ Could not skip") - - def _previous_track(self): - """Previous track.""" - if self._send_media_key('prev'): - self.status_label.setText("โฎ Previous track") - else: - self.status_label.setText("โŒ Could not go back") - - def _set_volume(self, value): - """Set volume (0-100).""" - try: - if self.system == "Linux": - subprocess.run(['playerctl', '--player=spotify', 'volume', str(value / 100)], capture_output=True) - except: - pass - - def on_hotkey(self): - """Toggle play/pause with hotkey.""" - self._toggle_playback() - - def on_hide(self): - """Stop timer when overlay hidden.""" - if self.update_timer: - self.update_timer.stop() - - def on_show(self): - """Restart timer when overlay shown.""" - if self.update_timer: - self.update_timer.start() - self._fetch_spotify_info() - - def shutdown(self): - """Cleanup.""" - if self.update_timer: - self.update_timer.stop() - if self.info_thread and self.info_thread.isRunning(): - self.info_thread.wait() diff --git a/plugins/tp_runner/__init__.py b/plugins/tp_runner/__init__.py deleted file mode 100644 index 25d81e4..0000000 --- a/plugins/tp_runner/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -TP Runner Plugin -""" - -from .plugin import TPRunnerPlugin - -__all__ = ["TPRunnerPlugin"] diff --git a/plugins/tp_runner/plugin.py b/plugins/tp_runner/plugin.py deleted file mode 100644 index 1df3333..0000000 --- a/plugins/tp_runner/plugin.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -EU-Utility - TP Runner Plugin - -Track teleporter locations and plan routes. -""" - -import json -from pathlib import Path - -from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QPushButton, QComboBox, QTreeWidget, QTreeWidgetItem, - QLineEdit, QFrame -) -from PyQt6.QtCore import Qt - -from plugins.base_plugin import BasePlugin - - -class TPRunnerPlugin(BasePlugin): - """Track TP locations and plan routes.""" - - name = "TP Runner" - version = "1.0.0" - author = "ImpulsiveFPS" - description = "Teleporter locations and route planner" - hotkey = "ctrl+shift+p" # P for Port - - # Arkadia TPs - ARKADIA_TPS = [ - "Arkadia City", "Arkadia City Outskirts", "8 Coins Creek", - "Celeste Harbour", "Celeste Outpost North", "Celeste Outpost South", - "Celeste Quarry", "Cycadia", "Dauntless Dock", "East Scythe", - "Genesis", "Hadesheim", "Hadesheim Ashland", "Hadesheim Pass", - "Hadesheim Valley", "Hellfire Hills", "Hero's Landing", - "Horror-Filled Hallows", "Jagged Coast", "Jungle Camp", - "Khorum Coast", "Khorum Highway", "Kronus", "Lava Camp", - "Lighthouse", "Living Graveyard", "Mountaintop", "Neo-Shanghai", - "North Scythe", "Nusul Fields", "Oily Business", "Perseus", - "Pilgrim's Landing", "Poseidon West", "Red Sands", - "Releks Hills", "Rest Stop", "Ripper Snapper", "Sacred Cove", - "Sentinel Hill", "Shady Ridge", "Sisyphus", "South Scythe", - "Spiral Mountain", "Stormbird Landing", "Sundari", "Tides", - "Traveller's Landing", "Victorious", "Vikings", "West Scythe", - "Wild Banks", "Wolf's Ridge", - ] - - def initialize(self): - """Setup TP runner.""" - self.data_file = Path("data/tp_runner.json") - self.data_file.parent.mkdir(parents=True, exist_ok=True) - - self.unlocked_tps = set() - self.favorite_routes = [] - - self._load_data() - - def _load_data(self): - """Load TP data.""" - if self.data_file.exists(): - try: - with open(self.data_file, 'r') as f: - data = json.load(f) - self.unlocked_tps = set(data.get('unlocked', [])) - self.favorite_routes = data.get('routes', []) - except: - pass - - def _save_data(self): - """Save TP data.""" - with open(self.data_file, 'w') as f: - json.dump({ - 'unlocked': list(self.unlocked_tps), - 'routes': self.favorite_routes - }, f, indent=2) - - def get_ui(self): - """Create TP runner UI.""" - widget = QWidget() - widget.setStyleSheet("background: transparent;") - layout = QVBoxLayout(widget) - layout.setSpacing(15) - layout.setContentsMargins(0, 0, 0, 0) - - # Title - title = QLabel("๐Ÿš€ TP Runner") - title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;") - layout.addWidget(title) - - # Progress - unlocked = len(self.unlocked_tps) - total = len(self.ARKADIA_TPS) - progress = QLabel(f"Unlocked: {unlocked}/{total} ({unlocked/total*100:.1f}%)") - progress.setStyleSheet("color: #4caf50;") - layout.addWidget(progress) - - # Route planner - planner = QFrame() - planner.setStyleSheet(""" - QFrame { - background-color: rgba(30, 35, 45, 200); - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - """) - planner_layout = QVBoxLayout(planner) - - # From/To - fromto = QHBoxLayout() - - self.from_combo = QComboBox() - self.from_combo.addItems(self.ARKADIA_TPS) - self.from_combo.setStyleSheet(""" - QComboBox { - background-color: rgba(20, 25, 35, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - padding: 5px; - } - """) - fromto.addWidget(QLabel("From:")) - fromto.addWidget(self.from_combo) - - self.to_combo = QComboBox() - self.to_combo.addItems(self.ARKADIA_TPS) - self.to_combo.setStyleSheet(self.from_combo.styleSheet()) - fromto.addWidget(QLabel("To:")) - fromto.addWidget(self.to_combo) - - planner_layout.addLayout(fromto) - - # Route button - route_btn = QPushButton("Find Route") - route_btn.setStyleSheet(""" - QPushButton { - background-color: #ff8c42; - color: white; - padding: 10px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - route_btn.clicked.connect(self._find_route) - planner_layout.addWidget(route_btn) - - layout.addWidget(planner) - - # TP List - self.tp_tree = QTreeWidget() - self.tp_tree.setHeaderLabels(["Teleporter", "Status"]) - self.tp_tree.setStyleSheet(""" - QTreeWidget { - background-color: rgba(30, 35, 45, 200); - color: white; - border: 1px solid rgba(100, 110, 130, 80); - border-radius: 6px; - } - QHeaderView::section { - background-color: rgba(35, 40, 55, 200); - color: rgba(255,255,255,180); - padding: 8px; - font-weight: bold; - font-size: 11px; - } - """) - - # Populate list - for tp in sorted(self.ARKADIA_TPS): - item = QTreeWidgetItem() - item.setText(0, tp) - if tp in self.unlocked_tps: - item.setText(1, "Unlocked") - item.setForeground(1, Qt.GlobalColor.green) - else: - item.setText(1, "Locked") - item.setForeground(1, Qt.GlobalColor.gray) - self.tp_tree.addTopLevelItem(item) - - layout.addWidget(self.tp_tree) - - # Mark as unlocked button - unlock_btn = QPushButton("Mark Unlocked") - unlock_btn.setStyleSheet(""" - QPushButton { - background-color: #4caf50; - color: white; - padding: 10px; - border: none; - border-radius: 4px; - font-weight: bold; - } - """) - unlock_btn.clicked.connect(self._mark_unlocked) - layout.addWidget(unlock_btn) - - layout.addStretch() - return widget - - def _find_route(self): - """Find route between TPs.""" - from_tp = self.from_combo.currentText() - to_tp = self.to_combo.currentText() - - if from_tp == to_tp: - return - - # Simple distance estimation (would use actual coordinates) - # For now, just show direct route - - def _mark_unlocked(self): - """Mark selected TP as unlocked.""" - item = self.tp_tree.currentItem() - if item: - tp_name = item.text(0) - self.unlocked_tps.add(tp_name) - item.setText(1, "Unlocked") - item.setForeground(1, Qt.GlobalColor.green) - self._save_data() 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/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()