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:
commit
2f94cf85fc
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,3 @@
|
|||
from .plugin import AnalyticsPlugin
|
||||
|
||||
__all__ = ['AnalyticsPlugin']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
})
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Auction Tracker Plugin
|
||||
"""
|
||||
|
||||
from .plugin import AuctionTrackerPlugin
|
||||
|
||||
__all__ = ["AuctionTrackerPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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'])
|
||||
|
|
@ -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']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .plugin import AutoUpdaterPlugin
|
||||
|
||||
__all__ = ['AutoUpdaterPlugin']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Calculator Plugin for EU-Utility
|
||||
"""
|
||||
|
||||
from .plugin import CalculatorPlugin
|
||||
|
||||
__all__ = ["CalculatorPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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'), ('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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Chat Logger Plugin
|
||||
"""
|
||||
|
||||
from .plugin import ChatLoggerPlugin
|
||||
|
||||
__all__ = ["ChatLoggerPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
]
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Codex Tracker Plugin
|
||||
"""
|
||||
|
||||
from .plugin import CodexTrackerPlugin
|
||||
|
||||
__all__ = ["CodexTrackerPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Crafting Calculator Plugin
|
||||
"""
|
||||
|
||||
from .plugin import CraftingCalculatorPlugin
|
||||
|
||||
__all__ = ["CraftingCalculatorPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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}")
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Dashboard Plugin
|
||||
"""
|
||||
|
||||
from .plugin import DashboardPlugin
|
||||
|
||||
__all__ = ["DashboardPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .plugin import DiscordPresencePlugin
|
||||
|
||||
__all__ = ['DiscordPresencePlugin']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
DPP Calculator Plugin
|
||||
"""
|
||||
|
||||
from .plugin import DPPCalculatorPlugin
|
||||
|
||||
__all__ = ["DPPCalculatorPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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')
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Enhancer Calculator Plugin
|
||||
"""
|
||||
|
||||
from .plugin import EnhancerCalculatorPlugin
|
||||
|
||||
__all__ = ["EnhancerCalculatorPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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}")
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
"""Event Bus Example Plugin."""
|
||||
from .plugin import EventBusExamplePlugin
|
||||
|
||||
__all__ = ['EventBusExamplePlugin']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Game Reader Plugin for EU-Utility
|
||||
"""
|
||||
|
||||
from .plugin import GameReaderPlugin
|
||||
|
||||
__all__ = ["GameReaderPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
"""Game Reader Test Plugin."""
|
||||
from .plugin import GameReaderTestPlugin
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Global Tracker Plugin
|
||||
"""
|
||||
|
||||
from .plugin import GlobalTrackerPlugin
|
||||
|
||||
__all__ = ["GlobalTrackerPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .plugin import ImportExportPlugin
|
||||
|
||||
__all__ = ['ImportExportPlugin']
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Inventory Manager Plugin
|
||||
"""
|
||||
|
||||
from .plugin import InventoryManagerPlugin
|
||||
|
||||
__all__ = ["InventoryManagerPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
"""Log Parser Test Plugin."""
|
||||
from .plugin import LogParserTestPlugin
|
||||
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Loot Tracker Plugin
|
||||
"""
|
||||
|
||||
from .plugin import LootTrackerPlugin
|
||||
|
||||
__all__ = ["LootTrackerPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Mining Helper Plugin
|
||||
"""
|
||||
|
||||
from .plugin import MiningHelperPlugin
|
||||
|
||||
__all__ = ["MiningHelperPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Mission Tracker Plugin
|
||||
"""
|
||||
|
||||
from .plugin import MissionTrackerPlugin
|
||||
|
||||
__all__ = ["MissionTrackerPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Nexus Search Plugin for EU-Utility
|
||||
"""
|
||||
|
||||
from .plugin import NexusSearchPlugin
|
||||
|
||||
__all__ = ["NexusSearchPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Plugin Store UI Plugin
|
||||
"""
|
||||
|
||||
from .plugin import PluginStoreUIPlugin
|
||||
|
||||
__all__ = ["PluginStoreUIPlugin"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||
|
|
@ -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
Loading…
Reference in New Issue