Initial commit: Plugin repository with 22 plugins

Includes:
- All current EU-Utility plugins
- manifest.json with plugin metadata
- README.md with developer documentation
- Plugins organized by category (Tools, Tracking, Information, etc.)

Categories:
- Tools: Calculator, Crafting Calc, Enhancer Calc, etc.
- Tracking: Skill Scanner, Loot Tracker, Mining Helper, etc.
- Information: Nexus Search, Dashboard, Chat Logger
- Market: Auction Tracker, Inventory Manager
- Analytics, Media, Social, Navigation, Data

Ready for EU-Utility Plugin Store integration.
This commit is contained in:
LemonNexus 2026-02-15 01:40:08 +00:00
commit 2f94cf85fc
131 changed files with 15607 additions and 0 deletions

146
README.md Normal file
View File

@ -0,0 +1,146 @@
# EU-Utility Plugins Repository
Official plugin repository for EU-Utility - the Entropia Universe addon framework.
## About
This repository contains plugins that extend EU-Utility's functionality. The core EU-Utility application is a framework-only addon loader - all user-facing features come from plugins.
## Plugin Categories
### Tools
- **Calculator** - PED/PEC calculations, DPP, markup
- **Crafting Calculator** - Crafting success rates and profit
- **Enhancer Calculator** - Break rate and cost analysis
- **DPP Calculator** - Advanced weapon efficiency
- **Universal Search** - Quick search across all plugins
### Tracking
- **Skill Scanner** - OCR-based skill tracking
- **Loot Tracker** - Real-time loot tracking
- **Mining Helper** - Mining claim tracking
- **Mission Tracker** - Mission progress
- **Codex Tracker** - Mob codex progress
- **Global Tracker** - Globals and HOFs
### Information
- **Nexus Search** - Entropia Nexus database
- **Dashboard** - Overview and stats
- **Chat Logger** - Advanced chat logging
### Market
- **Auction Tracker** - Price tracking and alerts
- **Inventory Manager** - Item management
### Analytics
- **Analytics** - Charts and visualizations
### Media
- **Spotify Controller** - Music control
### Social
- **Discord Presence** - Rich Discord status
### Navigation
- **TP Runner** - Teleport helper
### Data
- **Import/Export** - Backup and restore
## For Developers
### Plugin Structure
```
plugins/your_plugin/
├── __init__.py
└── plugin.py
```
### Minimum Requirements
```python
from plugins.base_plugin import BasePlugin
class YourPlugin(BasePlugin):
name = "Your Plugin"
version = "1.0.0"
author = "Your Name"
description = "What your plugin does"
def initialize(self):
# Setup code
pass
def get_ui(self):
# Return QWidget for UI
return QWidget()
```
### Adding to Repository
1. Create your plugin folder in `plugins/`
2. Add entry to `manifest.json`
3. Submit pull request
### Manifest Format
```json
{
"id": "your_plugin",
"name": "Your Plugin",
"version": "1.0.0",
"author": "Your Name",
"description": "Description",
"folder": "plugins/your_plugin/",
"icon": "icon_name",
"tags": ["tag1", "tag2"],
"dependencies": {
"core": ["ocr", "log"],
"plugins": ["other_plugin"]
},
"min_core_version": "2.0.0",
"category": "Tools"
}
```
## Installation
Plugins are installed through the EU-Utility Plugin Store:
1. Open EU-Utility Settings
2. Go to Plugin Store tab
3. Browse and install plugins
Or manually:
1. Clone this repository
2. Copy plugin folder to EU-Utility's `plugins/` directory
3. Restart EU-Utility
## Core Services Available
Plugins can access these core services via PluginAPI:
- **OCR** - Screen text recognition
- **Log Reader** - chat.log parsing
- **Nexus API** - Entropia Nexus database
- **Data Store** - Persistent storage
- **HTTP Client** - Network requests
- **Window Manager** - Game window detection
- **Screenshot** - Screen capture
- **Audio** - Sound playback
- **Notifications** - Toast notifications
- **Clipboard** - Copy/paste
## License
All plugins in this repository are released under MIT License.
## Contributing
1. Fork this repository
2. Create your plugin
3. Test thoroughly
4. Submit pull request
## Support
- Issues: Open issue on Gitea
- Discord: EU-Utility Discord server
- Documentation: See EU-Utility core docs

344
manifest.json Normal file
View File

@ -0,0 +1,344 @@
{
"manifest_version": "1.0.0",
"repository_name": "EU-Utility Official Plugins",
"repository_url": "https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo",
"last_updated": "2026-02-15T00:00:00Z",
"plugins": [
{
"id": "skill_scanner",
"name": "Skill Scanner",
"version": "2.1.0",
"author": "ImpulsiveFPS",
"description": "Scan and track your skills using OCR. Features multi-page scanning, progress tracking, and automatic skill gain detection from logs.",
"folder": "plugins/skill_scanner/",
"icon": "skills",
"tags": ["skills", "ocr", "tracking", "progress"],
"dependencies": {
"core": ["ocr", "log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tools"
},
{
"id": "calculator",
"name": "Calculator",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "PED/PEC calculator with common EU formulas. Calculate DPP, markup, enhancer break rates, and more.",
"folder": "plugins/calculator/",
"icon": "calculator",
"tags": ["calculator", "math", "ped", "dpp"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tools"
},
{
"id": "nexus_search",
"name": "Nexus Search",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Search Entropia Nexus database for items, mobs, blueprints, and locations. View market data and item details.",
"folder": "plugins/nexus_search/",
"icon": "search",
"tags": ["nexus", "search", "items", "market"],
"dependencies": {
"core": ["http"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Information"
},
{
"id": "dashboard",
"name": "Dashboard",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Overview dashboard showing session stats, recent globals, skill gains, and quick access to all plugins.",
"folder": "plugins/dashboard/",
"icon": "grid",
"tags": ["dashboard", "overview", "stats"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Information"
},
{
"id": "loot_tracker",
"name": "Loot Tracker",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Track hunting loot in real-time from game logs. Calculate TT value, markup, and profit/loss per session.",
"folder": "plugins/loot_tracker/",
"icon": "package",
"tags": ["loot", "hunting", "tracking", "profit"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tracking"
},
{
"id": "mining_helper",
"name": "Mining Helper",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Mining assistant with claim tracking, resource mapping, and extraction calculator.",
"folder": "plugins/mining_helper/",
"icon": "pickaxe",
"tags": ["mining", "tracking", "resources"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tracking"
},
{
"id": "crafting_calc",
"name": "Crafting Calculator",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Calculate crafting success rates, material costs, and profit margins. Track crafting skill gains.",
"folder": "plugins/crafting_calc/",
"icon": "tool",
"tags": ["crafting", "calculator", "profit"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tools"
},
{
"id": "inventory_manager",
"name": "Inventory Manager",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Manage your inventory with item tracking, value calculation, and storage organization.",
"folder": "plugins/inventory_manager/",
"icon": "box",
"tags": ["inventory", "items", "storage"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Management"
},
{
"id": "mission_tracker",
"name": "Mission Tracker",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Track mission progress, objectives, and rewards. Get notifications when missions complete.",
"folder": "plugins/mission_tracker/",
"icon": "check",
"tags": ["missions", "tracking", "quests"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tracking"
},
{
"id": "enhancer_calc",
"name": "Enhancer Calculator",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Calculate enhancer break rates, costs, and optimal enhancer combinations for your gear.",
"folder": "plugins/enhancer_calc/",
"icon": "zap",
"tags": ["enhancers", "calculator", "gear"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tools"
},
{
"id": "codex_tracker",
"name": "Codex Tracker",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Track Codex progress for all mobs. See rank rewards and completion status at a glance.",
"folder": "plugins/codex_tracker/",
"icon": "book",
"tags": ["codex", "tracking", "mobs", "rewards"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tracking"
},
{
"id": "dpp_calculator",
"name": "DPP Calculator",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Advanced Damage Per PEC calculator with weapon comparison and efficiency analysis.",
"folder": "plugins/dpp_calculator/",
"icon": "trending-up",
"tags": ["dpp", "calculator", "weapons", "efficiency"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tools"
},
{
"id": "tp_runner",
"name": "TP Runner",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Teleportation helper with location bookmarks and quick TP navigation.",
"folder": "plugins/tp_runner/",
"icon": "navigation",
"tags": ["teleport", "locations", "navigation"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Navigation"
},
{
"id": "global_tracker",
"name": "Global Tracker",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Track globals and HOFs in real-time. View personal and planetary statistics.",
"folder": "plugins/global_tracker/",
"icon": "award",
"tags": ["globals", "hof", "tracking", "statistics"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tracking"
},
{
"id": "auction_tracker",
"name": "Auction Tracker",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Track auction prices, bid history, and market trends. Set price alerts.",
"folder": "plugins/auction_tracker/",
"icon": "shopping-bag",
"tags": ["auction", "market", "prices", "alerts"],
"dependencies": {
"core": ["http"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Market"
},
{
"id": "analytics",
"name": "Analytics",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Advanced analytics and visualizations for your Entropia data. Charts, graphs, and trends.",
"folder": "plugins/analytics/",
"icon": "trending-up",
"tags": ["analytics", "charts", "data", "visualization"],
"dependencies": {
"core": ["log", "data_store"],
"plugins": ["loot_tracker"]
},
"min_core_version": "2.0.0",
"category": "Analytics"
},
{
"id": "spotify_controller",
"name": "Spotify Controller",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Control Spotify playback from within EU-Utility. In-game overlay widget included.",
"folder": "plugins/spotify_controller/",
"icon": "music",
"tags": ["spotify", "music", "media", "overlay"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Media"
},
{
"id": "discord_presence",
"name": "Discord Presence",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Rich Discord presence showing your current EU activity.",
"folder": "plugins/discord_presence/",
"icon": "message-square",
"tags": ["discord", "presence", "social"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Social"
},
{
"id": "chat_logger",
"name": "Chat Logger",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Advanced chat logging with filtering, search, and export functionality.",
"folder": "plugins/chat_logger/",
"icon": "file",
"tags": ["chat", "logs", "history"],
"dependencies": {
"core": ["log"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tools"
},
{
"id": "universal_search",
"name": "Universal Search",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Quick search across all plugins. Find items, skills, locations instantly.",
"folder": "plugins/universal_search/",
"icon": "search",
"tags": ["search", "quick", "universal"],
"dependencies": {
"core": [],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Tools"
},
{
"id": "import_export",
"name": "Import/Export",
"version": "1.0.0",
"author": "ImpulsiveFPS",
"description": "Import and export your EU-Utility data. Backup and restore functionality.",
"folder": "plugins/import_export/",
"icon": "external",
"tags": ["import", "export", "backup", "data"],
"dependencies": {
"core": ["data_store"],
"plugins": []
},
"min_core_version": "2.0.0",
"category": "Data"
}
]
}

0
plugins/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,3 @@
from .plugin import AnalyticsPlugin
__all__ = ['AnalyticsPlugin']

Binary file not shown.

Binary file not shown.

525
plugins/analytics/plugin.py Normal file
View File

@ -0,0 +1,525 @@
# 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()
})

View File

@ -0,0 +1,7 @@
"""
Auction Tracker Plugin
"""
from .plugin import AuctionTrackerPlugin
__all__ = ["AuctionTrackerPlugin"]

View File

@ -0,0 +1,261 @@
"""
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'])

View File

@ -0,0 +1,10 @@
"""
Auto-Screenshot Plugin for EU-Utility
Automatically capture screenshots on Global, HOF, or other
significant game events.
"""
from .plugin import AutoScreenshotPlugin
__all__ = ['AutoScreenshotPlugin']

View File

@ -0,0 +1,735 @@
"""
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()

View File

@ -0,0 +1,3 @@
from .plugin import AutoUpdaterPlugin
__all__ = ['AutoUpdaterPlugin']

View File

@ -0,0 +1,481 @@
# 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)

1193
plugins/base_plugin.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
"""
Calculator Plugin for EU-Utility
"""
from .plugin import CalculatorPlugin
__all__ = ["CalculatorPlugin"]

Binary file not shown.

View File

@ -0,0 +1,386 @@
"""
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'), ('', '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

View File

@ -0,0 +1,7 @@
"""
Chat Logger Plugin
"""
from .plugin import ChatLoggerPlugin
__all__ = ["ChatLoggerPlugin"]

Binary file not shown.

View File

@ -0,0 +1,285 @@
"""
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'''
<div style="margin: 2px 0;">
<span style="color: #666;">[{time_str}]</span>
<span style="color: {color}; font-weight: bold;">{author}:</span>
<span style="color: #ccc;">{text}</span>
</div>
''')
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)
]

View File

@ -0,0 +1,7 @@
"""
Codex Tracker Plugin
"""
from .plugin import CodexTrackerPlugin
__all__ = ["CodexTrackerPlugin"]

View File

@ -0,0 +1,218 @@
"""
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

View File

@ -0,0 +1,7 @@
"""
Crafting Calculator Plugin
"""
from .plugin import CraftingCalculatorPlugin
__all__ = ["CraftingCalculatorPlugin"]

View File

@ -0,0 +1,219 @@
"""
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}")

View File

@ -0,0 +1,7 @@
"""
Dashboard Plugin
"""
from .plugin import DashboardPlugin
__all__ = ["DashboardPlugin"]

Binary file not shown.

Binary file not shown.

326
plugins/dashboard/plugin.py Normal file
View File

@ -0,0 +1,326 @@
"""
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()

View File

@ -0,0 +1,3 @@
from .plugin import DiscordPresencePlugin
__all__ = ['DiscordPresencePlugin']

View File

@ -0,0 +1,217 @@
# 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()

View File

@ -0,0 +1,7 @@
"""
DPP Calculator Plugin
"""
from .plugin import DPPCalculatorPlugin
__all__ = ["DPPCalculatorPlugin"]

View File

@ -0,0 +1,230 @@
"""
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')

View File

@ -0,0 +1,7 @@
"""
Enhancer Calculator Plugin
"""
from .plugin import EnhancerCalculatorPlugin
__all__ = ["EnhancerCalculatorPlugin"]

View File

@ -0,0 +1,160 @@
"""
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}")

View File

@ -0,0 +1,4 @@
"""Event Bus Example Plugin."""
from .plugin import EventBusExamplePlugin
__all__ = ['EventBusExamplePlugin']

View File

@ -0,0 +1,211 @@
"""
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"<h2>{self.name}</h2>")
layout.addWidget(title)
# Stats
stats_text = f"""
<b>Recorded Events:</b><br>
Big Hits (100 dmg): {len(self.big_hits)}<br>
Combat Skill Gains: {len(self.skill_gains)}<br>
Dragon Loot: {len(self.dragon_loot)}<br>
<br>
<b>Active Subscriptions:</b> {len(self._subscriptions)}
"""
stats_label = QLabel(stats_text)
stats_label.setWordWrap(True)
layout.addWidget(stats_label)
# Recent events
layout.addWidget(QLabel("<b>Recent Combat Events:</b>"))
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()

View File

@ -0,0 +1,7 @@
"""
Game Reader Plugin for EU-Utility
"""
from .plugin import GameReaderPlugin
__all__ = ["GameReaderPlugin"]

Binary file not shown.

View File

@ -0,0 +1,261 @@
"""
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()

View File

@ -0,0 +1,2 @@
"""Game Reader Test Plugin."""
from .plugin import GameReaderTestPlugin

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
"""
Global Tracker Plugin
"""
from .plugin import GlobalTrackerPlugin
__all__ = ["GlobalTrackerPlugin"]

View File

@ -0,0 +1,257 @@
"""
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)

View File

@ -0,0 +1,3 @@
from .plugin import ImportExportPlugin
__all__ = ['ImportExportPlugin']

View File

@ -0,0 +1,333 @@
# 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)

View File

@ -0,0 +1,7 @@
"""
Inventory Manager Plugin
"""
from .plugin import InventoryManagerPlugin
__all__ = ["InventoryManagerPlugin"]

View File

@ -0,0 +1,219 @@
"""
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()

View File

@ -0,0 +1,2 @@
"""Log Parser Test Plugin."""
from .plugin import LogParserTestPlugin

View File

@ -0,0 +1,384 @@
"""
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()

View File

@ -0,0 +1,7 @@
"""
Loot Tracker Plugin
"""
from .plugin import LootTrackerPlugin
__all__ = ["LootTrackerPlugin"]

View File

@ -0,0 +1,224 @@
"""
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()

View File

@ -0,0 +1,7 @@
"""
Mining Helper Plugin
"""
from .plugin import MiningHelperPlugin
__all__ = ["MiningHelperPlugin"]

View File

@ -0,0 +1,273 @@
"""
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

View File

@ -0,0 +1,7 @@
"""
Mission Tracker Plugin
"""
from .plugin import MissionTrackerPlugin
__all__ = ["MissionTrackerPlugin"]

View File

@ -0,0 +1,315 @@
"""
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()

View File

@ -0,0 +1,7 @@
"""
Nexus Search Plugin for EU-Utility
"""
from .plugin import NexusSearchPlugin
__all__ = ["NexusSearchPlugin"]

View File

@ -0,0 +1,442 @@
"""
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()

View File

@ -0,0 +1,7 @@
"""
Plugin Store UI Plugin
"""
from .plugin import PluginStoreUIPlugin
__all__ = ["PluginStoreUIPlugin"]

View File

@ -0,0 +1,273 @@
"""
EU-Utility - Plugin Store UI Plugin
Browse and install community plugins.
"""
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QLineEdit, QListWidget, QListWidgetItem,
QProgressBar, QFrame, QTextEdit
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from plugins.base_plugin import BasePlugin
class PluginStoreUIPlugin(BasePlugin):
"""Browse and install community plugins."""
name = "Plugin Store"
version = "1.0.0"
author = "ImpulsiveFPS"
description = "Community plugin marketplace"
hotkey = "ctrl+shift+slash"
def initialize(self):
"""Setup plugin store."""
self.available_plugins = []
self.installed_plugins = []
self.is_loading = False
def get_ui(self):
"""Create plugin store UI."""
widget = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Title
title = QLabel("🛒 Plugin Store")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Search
search_layout = QHBoxLayout()
self.search_input = QLineEdit()
self.search_input.setPlaceholderText("Search plugins...")
self.search_input.setStyleSheet("""
QLineEdit {
background-color: rgba(30, 35, 45, 200);
color: white;
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 4px;
padding: 8px;
}
""")
search_layout.addWidget(self.search_input)
search_btn = QPushButton("🔍")
search_btn.setFixedSize(32, 32)
search_btn.setStyleSheet("""
QPushButton {
background-color: #ff8c42;
border: none;
border-radius: 4px;
}
""")
search_btn.clicked.connect(self._search_plugins)
search_layout.addWidget(search_btn)
layout.addLayout(search_layout)
# Categories
cats_layout = QHBoxLayout()
for cat in ["All", "Hunting", "Mining", "Crafting", "Tools", "Social"]:
btn = QPushButton(cat)
btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,15);
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
}
QPushButton:hover {
background-color: rgba(255,255,255,30);
}
""")
cats_layout.addWidget(btn)
cats_layout.addStretch()
layout.addLayout(cats_layout)
# Plugins list
self.plugins_list = QListWidget()
self.plugins_list.setStyleSheet("""
QListWidget {
background-color: rgba(30, 35, 45, 200);
color: white;
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 6px;
}
QListWidget::item {
padding: 12px;
border-bottom: 1px solid rgba(100, 110, 130, 40);
}
QListWidget::item:hover {
background-color: rgba(255, 255, 255, 10);
}
""")
self.plugins_list.itemClicked.connect(self._show_plugin_details)
layout.addWidget(self.plugins_list)
# Sample plugins
self._load_sample_plugins()
# Details panel
self.details_panel = QFrame()
self.details_panel.setStyleSheet("""
QFrame {
background-color: rgba(30, 35, 45, 200);
border: 1px solid rgba(100, 110, 130, 80);
border-radius: 6px;
}
""")
details_layout = QVBoxLayout(self.details_panel)
self.detail_name = QLabel("Select a plugin")
self.detail_name.setStyleSheet("color: #ff8c42; font-size: 14px; font-weight: bold;")
details_layout.addWidget(self.detail_name)
self.detail_desc = QLabel("")
self.detail_desc.setStyleSheet("color: rgba(255,255,255,150);")
self.detail_desc.setWordWrap(True)
details_layout.addWidget(self.detail_desc)
self.detail_author = QLabel("")
self.detail_author.setStyleSheet("color: rgba(255,255,255,100); font-size: 10px;")
details_layout.addWidget(self.detail_author)
self.install_btn = QPushButton("Install")
self.install_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
padding: 10px;
border: none;
border-radius: 4px;
font-weight: bold;
}
""")
self.install_btn.clicked.connect(self._install_plugin)
self.install_btn.setEnabled(False)
details_layout.addWidget(self.install_btn)
layout.addWidget(self.details_panel)
# Refresh button
refresh_btn = QPushButton("🔄 Refresh")
refresh_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255,255,255,20);
color: white;
padding: 8px;
border: none;
border-radius: 4px;
}
""")
refresh_btn.clicked.connect(self._refresh_store)
layout.addWidget(refresh_btn)
layout.addStretch()
return widget
def _load_sample_plugins(self):
"""Load sample plugin list."""
sample = [
{
'name': 'Crafting Calculator',
'description': 'Calculate crafting success rates and costs',
'author': 'EU Community',
'version': '1.0.0',
'downloads': 542,
'rating': 4.5,
},
{
'name': 'Global Tracker',
'description': 'Track globals and HOFs with notifications',
'author': 'ImpulsiveFPS',
'version': '1.2.0',
'downloads': 1203,
'rating': 4.8,
},
{
'name': 'Bank Manager',
'description': 'Manage storage and bank items across planets',
'author': 'StorageMaster',
'version': '0.9.0',
'downloads': 328,
'rating': 4.2,
},
{
'name': 'Society Tools',
'description': 'Society management and member tracking',
'author': 'SocietyDev',
'version': '1.0.0',
'downloads': 215,
'rating': 4.0,
},
{
'name': 'Team Helper',
'description': 'Team coordination and loot sharing',
'author': 'TeamPlayer',
'version': '1.1.0',
'downloads': 876,
'rating': 4.6,
},
]
self.available_plugins = sample
self._update_list()
def _update_list(self):
"""Update plugins list."""
self.plugins_list.clear()
for plugin in self.available_plugins:
item = QListWidgetItem(
f"{plugin['name']} v{plugin['version']}\n"
f"{plugin['rating']} | ⬇ {plugin['downloads']}"
)
item.setData(Qt.ItemDataRole.UserRole, plugin)
self.plugins_list.addItem(item)
def _show_plugin_details(self, item):
"""Show plugin details."""
plugin = item.data(Qt.ItemDataRole.UserRole)
if plugin:
self.detail_name.setText(f"{plugin['name']} v{plugin['version']}")
self.detail_desc.setText(plugin['description'])
self.detail_author.setText(f"By {plugin['author']} | ⭐ {plugin['rating']}")
self.install_btn.setEnabled(True)
self.selected_plugin = plugin
def _install_plugin(self):
"""Install selected plugin."""
if hasattr(self, 'selected_plugin'):
print(f"Installing {self.selected_plugin['name']}...")
self.install_btn.setText("Installing...")
self.install_btn.setEnabled(False)
# TODO: Actual install
def _search_plugins(self):
"""Search plugins."""
query = self.search_input.text().lower()
filtered = [
p for p in self.available_plugins
if query in p['name'].lower() or query in p['description'].lower()
]
self.plugins_list.clear()
for plugin in filtered:
item = QListWidgetItem(
f"{plugin['name']} v{plugin['version']}\n"
f"{plugin['rating']} | ⬇ {plugin['downloads']}"
)
item.setData(Qt.ItemDataRole.UserRole, plugin)
self.plugins_list.addItem(item)
def _refresh_store(self):
"""Refresh plugin list."""
self._load_sample_plugins()

View File

@ -0,0 +1,10 @@
"""
Price Alerts Plugin for EU-Utility
Monitor Entropia Nexus prices and get alerts when items
reach target prices.
"""
from .plugin import PriceAlertPlugin
__all__ = ['PriceAlertPlugin']

Some files were not shown because too many files have changed in this diff Show More