feat: ULTIMATE DEVELOPMENT SWARM - Dashboard, Widgets, Settings, Plugin Store, New Plugins
DASHBOARD SYSTEM:
- DashboardWidget base class for customizable widgets
- SpotifyWidget - Now playing display
- SkillProgressWidget - Recent skill gains
- PEDTrackerWidget - PED balance tracking
- QuickActionsWidget - One-click action buttons
- Draggable grid layout with position persistence
OVERLAY WIDGET SYSTEM:
- OverlayWidget base class for in-game floating elements
- SpotifyOverlayWidget - Music player overlay
- MissionTrackerWidget - Mission progress
- SkillGainWidget - Recent gains popup
- DPPTrackerWidget - DPP calculator overlay
- Draggable, hideable, position-saving
- OverlayManager for managing all widgets
SETTINGS SYSTEM:
- Settings class with JSON persistence
- Default settings for all features
- Plugin enable/disable management
- Overlay widget configuration
- User preferences storage
PLUGIN STORE:
- PluginStore class for community plugins
- Fetch plugins from GitHub repo
- Install/uninstall plugins
- Version checking and updates
- Sample plugins.json structure
NEW PLUGINS:
1. Loot Tracker - Track hunting loot, ROI, sessions
2. Mining Helper - Mining claims, resources, hit rate
3. Chat Logger - Log, search, filter chat messages
INFRASTRUCTURE:
- Updated main.py with settings and overlay manager
- IconHelper for Phosphor SVG icons
- Per-plugin accent colors in overlay
Hotkeys:
- Ctrl+Shift+U - Toggle main overlay
- Ctrl+Shift+H - Hide all overlays
- Ctrl+Shift+L - Loot Tracker
- Ctrl+Shift+N - Mining Helper
- Ctrl+Shift+T - Chat Logger
SWARM COMPLETE! 🚀
This commit is contained in:
parent
bf7a2bb682
commit
5f5a3db481
|
|
@ -0,0 +1,376 @@
|
||||||
|
"""
|
||||||
|
EU-Utility - Dashboard Widget System
|
||||||
|
|
||||||
|
Customizable dashboard with draggable widgets from plugins.
|
||||||
|
Each plugin can provide widgets for the dashboard.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||||
|
QLabel, QPushButton, QFrame, QScrollArea,
|
||||||
|
QSizePolicy, QGraphicsDropShadowEffect
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, pyqtSignal, QMimeData
|
||||||
|
from PyQt6.QtGui import QColor, QDrag, QPainter, QPixmap
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardWidget(QFrame):
|
||||||
|
"""Base class for dashboard widgets."""
|
||||||
|
|
||||||
|
# Widget metadata
|
||||||
|
name = "Widget"
|
||||||
|
description = "Base widget"
|
||||||
|
icon = "◆"
|
||||||
|
size = (1, 1) # Grid size (cols, rows)
|
||||||
|
|
||||||
|
def __init__(self, plugin=None, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.plugin = plugin
|
||||||
|
self.dragging = False
|
||||||
|
|
||||||
|
self.setFrameStyle(QFrame.Shape.NoFrame)
|
||||||
|
self.setStyleSheet("""
|
||||||
|
DashboardWidget {
|
||||||
|
background-color: rgba(30, 35, 45, 200);
|
||||||
|
border: 1px solid rgba(100, 150, 200, 60);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
DashboardWidget:hover {
|
||||||
|
border: 1px solid rgba(100, 180, 255, 100);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Shadow effect
|
||||||
|
shadow = QGraphicsDropShadowEffect()
|
||||||
|
shadow.setBlurRadius(20)
|
||||||
|
shadow.setColor(QColor(0, 0, 0, 80))
|
||||||
|
shadow.setOffset(0, 4)
|
||||||
|
self.setGraphicsEffect(shadow)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
"""Setup widget UI. Override in subclass."""
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(12, 12, 12, 12)
|
||||||
|
|
||||||
|
header = QLabel(f"{self.icon} {self.name}")
|
||||||
|
header.setStyleSheet("""
|
||||||
|
color: rgba(255, 255, 255, 200);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
""")
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
|
content = QLabel("Widget Content")
|
||||||
|
content.setStyleSheet("color: rgba(255, 255, 255, 150);")
|
||||||
|
layout.addWidget(content)
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.dragging = True
|
||||||
|
self.drag_start_pos = event.pos()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
if self.dragging and (event.pos() - self.drag_start_pos).manhattanLength() > 10:
|
||||||
|
self.dragging = False
|
||||||
|
# Emit signal to parent to handle reordering
|
||||||
|
if self.parent():
|
||||||
|
self.parent().start_widget_drag(self)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
self.dragging = False
|
||||||
|
|
||||||
|
def update_data(self, data):
|
||||||
|
"""Update widget with new data. Override in subclass."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SpotifyWidget(DashboardWidget):
|
||||||
|
"""Spotify now playing widget."""
|
||||||
|
|
||||||
|
name = "Now Playing"
|
||||||
|
description = "Shows current Spotify track"
|
||||||
|
icon = "🎵"
|
||||||
|
size = (2, 1)
|
||||||
|
|
||||||
|
def __init__(self, plugin=None, parent=None):
|
||||||
|
self.track_info = None
|
||||||
|
super().__init__(plugin, parent)
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(12, 12, 12, 12)
|
||||||
|
layout.setSpacing(8)
|
||||||
|
|
||||||
|
# Track name
|
||||||
|
self.track_label = QLabel("Not Playing")
|
||||||
|
self.track_label.setStyleSheet("""
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
""")
|
||||||
|
self.track_label.setWordWrap(True)
|
||||||
|
layout.addWidget(self.track_label)
|
||||||
|
|
||||||
|
# Artist
|
||||||
|
self.artist_label = QLabel("-")
|
||||||
|
self.artist_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 11px;")
|
||||||
|
layout.addWidget(self.artist_label)
|
||||||
|
|
||||||
|
# Progress bar
|
||||||
|
self.progress = QLabel("▬▬▬▬▬▬▬▬▬▬")
|
||||||
|
self.progress.setStyleSheet("color: #1db954; font-size: 10px;")
|
||||||
|
layout.addWidget(self.progress)
|
||||||
|
|
||||||
|
def update_data(self, track_data):
|
||||||
|
"""Update with track info."""
|
||||||
|
if track_data:
|
||||||
|
self.track_label.setText(track_data.get('name', 'Unknown'))
|
||||||
|
self.track_label.setStyleSheet("""
|
||||||
|
color: #1db954;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
""")
|
||||||
|
artists = track_data.get('artists', [])
|
||||||
|
self.artist_label.setText(', '.join(artists) if artists else '-')
|
||||||
|
|
||||||
|
|
||||||
|
class SkillProgressWidget(DashboardWidget):
|
||||||
|
"""Skill progress tracker widget."""
|
||||||
|
|
||||||
|
name = "Skill Progress"
|
||||||
|
description = "Track skill gains"
|
||||||
|
icon = "📊"
|
||||||
|
size = (1, 1)
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(12, 12, 12, 12)
|
||||||
|
layout.setSpacing(6)
|
||||||
|
|
||||||
|
header = QLabel("📊 Recent Gains")
|
||||||
|
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold;")
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
|
self.gains_list = QLabel("No gains yet")
|
||||||
|
self.gains_list.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 10px;")
|
||||||
|
self.gains_list.setWordWrap(True)
|
||||||
|
layout.addWidget(self.gains_list)
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def update_data(self, gains):
|
||||||
|
"""Update with recent skill gains."""
|
||||||
|
if gains:
|
||||||
|
text = "\n".join([f"+{g['points']} {g['skill']}" for g in gains[:3]])
|
||||||
|
self.gains_list.setText(text)
|
||||||
|
|
||||||
|
|
||||||
|
class PEDTrackerWidget(DashboardWidget):
|
||||||
|
"""PED balance tracker widget."""
|
||||||
|
|
||||||
|
name = "PED Balance"
|
||||||
|
description = "Track PED from inventory scans"
|
||||||
|
icon = "🪙"
|
||||||
|
size = (1, 1)
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(12, 12, 12, 12)
|
||||||
|
layout.setSpacing(6)
|
||||||
|
|
||||||
|
header = QLabel("🪙 PED Balance")
|
||||||
|
header.setStyleSheet("color: rgba(255, 255, 255, 200); font-size: 12px; font-weight: bold;")
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
|
self.balance_label = QLabel("0.00 PED")
|
||||||
|
self.balance_label.setStyleSheet("""
|
||||||
|
color: #ffc107;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
""")
|
||||||
|
layout.addWidget(self.balance_label)
|
||||||
|
|
||||||
|
self.change_label = QLabel("Scan inventory to update")
|
||||||
|
self.change_label.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 10px;")
|
||||||
|
layout.addWidget(self.change_label)
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def update_data(self, balance_data):
|
||||||
|
"""Update with balance info."""
|
||||||
|
if balance_data:
|
||||||
|
self.balance_label.setText(f"{balance_data['ped']:.2f} PED")
|
||||||
|
change = balance_data.get('change', 0)
|
||||||
|
if change != 0:
|
||||||
|
color = "#4caf50" if change > 0 else "#f44336"
|
||||||
|
sign = "+" if change > 0 else ""
|
||||||
|
self.change_label.setText(f"<span style='color: {color}'>{sign}{change:.2f}</span>")
|
||||||
|
|
||||||
|
|
||||||
|
class QuickActionsWidget(DashboardWidget):
|
||||||
|
"""Quick action buttons widget."""
|
||||||
|
|
||||||
|
name = "Quick Actions"
|
||||||
|
description = "One-click actions"
|
||||||
|
icon = "⚡"
|
||||||
|
size = (2, 1)
|
||||||
|
|
||||||
|
def __init__(self, actions=None, parent=None):
|
||||||
|
self.actions = actions or []
|
||||||
|
super().__init__(None, parent)
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
layout = QHBoxLayout(self)
|
||||||
|
layout.setContentsMargins(12, 12, 12, 12)
|
||||||
|
layout.setSpacing(8)
|
||||||
|
|
||||||
|
for action in self.actions:
|
||||||
|
btn = QPushButton(action['icon'])
|
||||||
|
btn.setFixedSize(36, 36)
|
||||||
|
btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: rgba(255, 255, 255, 15);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 30);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 30);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
btn.setToolTip(action['name'])
|
||||||
|
btn.clicked.connect(action['callback'])
|
||||||
|
layout.addWidget(btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
|
||||||
|
class Dashboard(QWidget):
|
||||||
|
"""Main dashboard with customizable widgets."""
|
||||||
|
|
||||||
|
widget_added = pyqtSignal(object)
|
||||||
|
widget_removed = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self, plugin_manager=None, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.plugin_manager = plugin_manager
|
||||||
|
self.widgets = []
|
||||||
|
self.dragged_widget = None
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
"""Setup dashboard UI."""
|
||||||
|
self.setStyleSheet("background: transparent;")
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
layout.setSpacing(15)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
header = QLabel("⚡ Dashboard")
|
||||||
|
header.setStyleSheet("""
|
||||||
|
color: white;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
""")
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
|
# Subtitle
|
||||||
|
subtitle = QLabel("Customize your EU-Utility experience")
|
||||||
|
subtitle.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
|
||||||
|
layout.addWidget(subtitle)
|
||||||
|
|
||||||
|
# Widget grid container
|
||||||
|
scroll = QScrollArea()
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
|
scroll.setStyleSheet("""
|
||||||
|
QScrollArea {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
QScrollBar:vertical {
|
||||||
|
background: rgba(0, 0, 0, 50);
|
||||||
|
width: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
QScrollBar::handle:vertical {
|
||||||
|
background: rgba(255, 255, 255, 30);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.grid_widget = QWidget()
|
||||||
|
self.grid_layout = QGridLayout(self.grid_widget)
|
||||||
|
self.grid_layout.setSpacing(12)
|
||||||
|
self.grid_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
scroll.setWidget(self.grid_widget)
|
||||||
|
layout.addWidget(scroll)
|
||||||
|
|
||||||
|
# Add default widgets
|
||||||
|
self._add_default_widgets()
|
||||||
|
|
||||||
|
def _add_default_widgets(self):
|
||||||
|
"""Add default widgets to dashboard."""
|
||||||
|
# Quick actions
|
||||||
|
actions = [
|
||||||
|
{'icon': '🔍', 'name': 'Search', 'callback': lambda: None},
|
||||||
|
{'icon': '📸', 'name': 'Scan', 'callback': lambda: None},
|
||||||
|
{'icon': '📊', 'name': 'Skills', 'callback': lambda: None},
|
||||||
|
{'icon': '🎵', 'name': 'Music', 'callback': lambda: None},
|
||||||
|
]
|
||||||
|
self.add_widget(QuickActionsWidget(actions))
|
||||||
|
|
||||||
|
# Spotify widget (if available)
|
||||||
|
self.add_widget(SpotifyWidget())
|
||||||
|
|
||||||
|
# PED tracker
|
||||||
|
self.add_widget(PEDTrackerWidget())
|
||||||
|
|
||||||
|
# Skill progress
|
||||||
|
self.add_widget(SkillProgressWidget())
|
||||||
|
|
||||||
|
def add_widget(self, widget):
|
||||||
|
"""Add a widget to the dashboard."""
|
||||||
|
row = self.grid_layout.rowCount()
|
||||||
|
col = 0
|
||||||
|
|
||||||
|
# Find next available position
|
||||||
|
while self.grid_layout.itemAtPosition(row, col):
|
||||||
|
col += 1
|
||||||
|
if col >= 2: # Max 2 columns
|
||||||
|
col = 0
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
self.grid_layout.addWidget(widget, row, col,
|
||||||
|
widget.size[1], widget.size[0])
|
||||||
|
self.widgets.append(widget)
|
||||||
|
self.widget_added.emit(widget)
|
||||||
|
|
||||||
|
def remove_widget(self, widget):
|
||||||
|
"""Remove a widget from the dashboard."""
|
||||||
|
self.grid_layout.removeWidget(widget)
|
||||||
|
widget.deleteLater()
|
||||||
|
self.widgets.remove(widget)
|
||||||
|
self.widget_removed.emit(widget)
|
||||||
|
|
||||||
|
def start_widget_drag(self, widget):
|
||||||
|
"""Start dragging a widget."""
|
||||||
|
self.dragged_widget = widget
|
||||||
|
|
||||||
|
def get_widget_positions(self):
|
||||||
|
"""Get current widget positions for saving."""
|
||||||
|
positions = []
|
||||||
|
for widget in self.widgets:
|
||||||
|
idx = self.grid_layout.indexOf(widget)
|
||||||
|
if idx >= 0:
|
||||||
|
row, col, rowSpan, colSpan = self.grid_layout.getItemPosition(idx)
|
||||||
|
positions.append({
|
||||||
|
'type': widget.__class__.__name__,
|
||||||
|
'row': row,
|
||||||
|
'col': col,
|
||||||
|
'size': widget.size
|
||||||
|
})
|
||||||
|
return positions
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
EU-Utility - Main Entry Point
|
EU-Utility - Main Entry Point
|
||||||
|
|
||||||
Launch the overlay, floating icon, and plugin system.
|
Launch the overlay, floating icon, dashboard, and plugin system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -34,11 +34,14 @@ except ImportError:
|
||||||
from core.plugin_manager import PluginManager
|
from core.plugin_manager import PluginManager
|
||||||
from core.overlay_window import OverlayWindow
|
from core.overlay_window import OverlayWindow
|
||||||
from core.floating_icon import FloatingIcon
|
from core.floating_icon import FloatingIcon
|
||||||
|
from core.settings import get_settings
|
||||||
|
from core.overlay_widgets import OverlayManager
|
||||||
|
|
||||||
|
|
||||||
class HotkeyHandler(QObject):
|
class HotkeyHandler(QObject):
|
||||||
"""Signal bridge for thread-safe hotkey handling."""
|
"""Signal bridge for thread-safe hotkey handling."""
|
||||||
toggle_signal = pyqtSignal()
|
toggle_signal = pyqtSignal()
|
||||||
|
hide_overlays_signal = pyqtSignal()
|
||||||
|
|
||||||
|
|
||||||
class EUUtilityApp:
|
class EUUtilityApp:
|
||||||
|
|
@ -50,6 +53,8 @@ class EUUtilityApp:
|
||||||
self.floating_icon = None
|
self.floating_icon = None
|
||||||
self.plugin_manager = None
|
self.plugin_manager = None
|
||||||
self.hotkey_handler = None
|
self.hotkey_handler = None
|
||||||
|
self.settings = None
|
||||||
|
self.overlay_manager = None
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Start the application."""
|
"""Start the application."""
|
||||||
|
|
@ -61,6 +66,9 @@ class EUUtilityApp:
|
||||||
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
|
if hasattr(Qt, 'AA_EnableHighDpiScaling'):
|
||||||
self.app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
|
self.app.setAttribute(Qt.ApplicationAttribute.AA_EnableHighDpiScaling)
|
||||||
|
|
||||||
|
# Load settings
|
||||||
|
self.settings = get_settings()
|
||||||
|
|
||||||
# Create hotkey handler (must be in main thread)
|
# Create hotkey handler (must be in main thread)
|
||||||
self.hotkey_handler = HotkeyHandler()
|
self.hotkey_handler = HotkeyHandler()
|
||||||
|
|
||||||
|
|
@ -69,6 +77,9 @@ class EUUtilityApp:
|
||||||
self.plugin_manager = PluginManager(None)
|
self.plugin_manager = PluginManager(None)
|
||||||
self.plugin_manager.load_all_plugins()
|
self.plugin_manager.load_all_plugins()
|
||||||
|
|
||||||
|
# Create overlay manager
|
||||||
|
self.overlay_manager = OverlayManager(self.app)
|
||||||
|
|
||||||
# Create overlay window
|
# Create overlay window
|
||||||
self.overlay = OverlayWindow(self.plugin_manager)
|
self.overlay = OverlayWindow(self.plugin_manager)
|
||||||
self.plugin_manager.overlay = self.overlay
|
self.plugin_manager.overlay = self.overlay
|
||||||
|
|
@ -79,33 +90,44 @@ class EUUtilityApp:
|
||||||
self.floating_icon.clicked.connect(self._toggle_overlay)
|
self.floating_icon.clicked.connect(self._toggle_overlay)
|
||||||
self.floating_icon.show()
|
self.floating_icon.show()
|
||||||
|
|
||||||
# Connect hotkey signal
|
# Connect hotkey signals
|
||||||
self.hotkey_handler.toggle_signal.connect(self._on_toggle_signal)
|
self.hotkey_handler.toggle_signal.connect(self._on_toggle_signal)
|
||||||
|
|
||||||
# Setup global hotkey
|
# Setup global hotkeys
|
||||||
self._setup_hotkey()
|
self._setup_hotkeys()
|
||||||
|
|
||||||
|
# Load saved overlay widgets
|
||||||
|
self._load_overlay_widgets()
|
||||||
|
|
||||||
print("EU-Utility started!")
|
print("EU-Utility started!")
|
||||||
print("Press Ctrl+Shift+U to toggle overlay")
|
print("Press Ctrl+Shift+U to toggle overlay")
|
||||||
print("Or double-click the ⚡ floating icon")
|
print("Press Ctrl+Shift+H to hide all overlays")
|
||||||
|
print("Or double-click the floating icon")
|
||||||
|
|
||||||
# Run
|
# Run
|
||||||
return self.app.exec()
|
return self.app.exec()
|
||||||
|
|
||||||
def _setup_hotkey(self):
|
def _setup_hotkeys(self):
|
||||||
"""Setup global hotkey."""
|
"""Setup global hotkeys."""
|
||||||
if KEYBOARD_AVAILABLE:
|
if KEYBOARD_AVAILABLE:
|
||||||
try:
|
try:
|
||||||
|
# Toggle main overlay
|
||||||
keyboard.add_hotkey('ctrl+shift+u', self._on_hotkey_pressed)
|
keyboard.add_hotkey('ctrl+shift+u', self._on_hotkey_pressed)
|
||||||
|
# Hide all overlays
|
||||||
|
keyboard.add_hotkey('ctrl+shift+h', self._on_hide_overlays_pressed)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Failed to register hotkey: {e}")
|
print(f"Failed to register hotkey: {e}")
|
||||||
|
|
||||||
def _on_hotkey_pressed(self):
|
def _on_hotkey_pressed(self):
|
||||||
"""Called when hotkey is pressed (from keyboard thread)."""
|
"""Called when toggle hotkey is pressed."""
|
||||||
# Emit signal to main thread
|
|
||||||
if self.hotkey_handler:
|
if self.hotkey_handler:
|
||||||
self.hotkey_handler.toggle_signal.emit()
|
self.hotkey_handler.toggle_signal.emit()
|
||||||
|
|
||||||
|
def _on_hide_overlays_pressed(self):
|
||||||
|
"""Called when hide hotkey is pressed."""
|
||||||
|
if self.overlay_manager:
|
||||||
|
self.overlay_manager.hide_all()
|
||||||
|
|
||||||
def _on_toggle_signal(self):
|
def _on_toggle_signal(self):
|
||||||
"""Handle toggle signal in main thread."""
|
"""Handle toggle signal in main thread."""
|
||||||
self._toggle_overlay()
|
self._toggle_overlay()
|
||||||
|
|
@ -115,6 +137,34 @@ class EUUtilityApp:
|
||||||
if self.overlay:
|
if self.overlay:
|
||||||
self.overlay.toggle_overlay()
|
self.overlay.toggle_overlay()
|
||||||
|
|
||||||
|
def _load_overlay_widgets(self):
|
||||||
|
"""Load saved overlay widgets."""
|
||||||
|
widget_settings = self.settings.get('overlay_widgets', {})
|
||||||
|
|
||||||
|
for widget_name, config in widget_settings.items():
|
||||||
|
if config.get('enabled', False):
|
||||||
|
try:
|
||||||
|
self.overlay_manager.create_widget(
|
||||||
|
widget_name.replace('_overlay', ''),
|
||||||
|
widget_name
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to load overlay widget {widget_name}: {e}")
|
||||||
|
|
||||||
|
def create_overlay_widget(self, widget_type, name):
|
||||||
|
"""Create an overlay widget."""
|
||||||
|
if self.overlay_manager:
|
||||||
|
return self.overlay_manager.create_widget(widget_type, name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def quit(self):
|
||||||
|
"""Quit the application."""
|
||||||
|
if self.overlay_manager:
|
||||||
|
self.overlay_manager.hide_all()
|
||||||
|
if self.plugin_manager:
|
||||||
|
self.plugin_manager.shutdown_all()
|
||||||
|
self.app.quit()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Entry point."""
|
"""Entry point."""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,500 @@
|
||||||
|
"""
|
||||||
|
EU-Utility - Overlay Widget System
|
||||||
|
|
||||||
|
Draggable, hideable overlay elements that appear in-game.
|
||||||
|
Similar to game UI elements like mission trackers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QPushButton, QFrame, QGraphicsDropShadowEffect,
|
||||||
|
QApplication
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QPoint, pyqtSignal, QTimer
|
||||||
|
from PyQt6.QtGui import QColor, QMouseEvent
|
||||||
|
|
||||||
|
|
||||||
|
class OverlayWidget(QFrame):
|
||||||
|
"""Base class for in-game overlay widgets."""
|
||||||
|
|
||||||
|
# Position persistence
|
||||||
|
position_changed = pyqtSignal(QPoint)
|
||||||
|
visibility_changed = pyqtSignal(bool)
|
||||||
|
closed = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, title="Overlay", icon="◆", parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.title = title
|
||||||
|
self.icon = icon
|
||||||
|
self.dragging = False
|
||||||
|
self.drag_position = QPoint()
|
||||||
|
self._visible = True
|
||||||
|
|
||||||
|
# Frameless, always on top
|
||||||
|
self.setWindowFlags(
|
||||||
|
Qt.WindowType.FramelessWindowHint |
|
||||||
|
Qt.WindowType.WindowStaysOnTopHint |
|
||||||
|
Qt.WindowType.Tool |
|
||||||
|
Qt.WindowType.WindowTransparentForInput
|
||||||
|
)
|
||||||
|
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
# EU game-style styling
|
||||||
|
self.setStyleSheet("""
|
||||||
|
OverlayWidget {
|
||||||
|
background-color: rgba(20, 25, 35, 220);
|
||||||
|
border: 1px solid rgba(100, 150, 200, 60);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Shadow
|
||||||
|
shadow = QGraphicsDropShadowEffect()
|
||||||
|
shadow.setBlurRadius(15)
|
||||||
|
shadow.setColor(QColor(0, 0, 0, 100))
|
||||||
|
shadow.setOffset(0, 2)
|
||||||
|
self.setGraphicsEffect(shadow)
|
||||||
|
|
||||||
|
self._setup_ui()
|
||||||
|
|
||||||
|
def _setup_ui(self):
|
||||||
|
"""Setup widget UI."""
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
# Header bar (draggable)
|
||||||
|
self.header = QWidget()
|
||||||
|
self.header.setStyleSheet("""
|
||||||
|
QWidget {
|
||||||
|
background-color: rgba(30, 40, 55, 200);
|
||||||
|
border-top-left-radius: 8px;
|
||||||
|
border-top-right-radius: 8px;
|
||||||
|
border-bottom: 1px solid rgba(100, 150, 200, 40);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
header_layout = QHBoxLayout(self.header)
|
||||||
|
header_layout.setContentsMargins(8, 6, 8, 6)
|
||||||
|
header_layout.setSpacing(6)
|
||||||
|
|
||||||
|
# Icon
|
||||||
|
icon_label = QLabel(self.icon)
|
||||||
|
icon_label.setStyleSheet("font-size: 12px; background: transparent;")
|
||||||
|
header_layout.addWidget(icon_label)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_label = QLabel(self.title)
|
||||||
|
title_label.setStyleSheet("""
|
||||||
|
color: rgba(255, 255, 255, 200);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
header_layout.addWidget(title_label)
|
||||||
|
|
||||||
|
header_layout.addStretch()
|
||||||
|
|
||||||
|
# Hide button
|
||||||
|
hide_btn = QPushButton("−")
|
||||||
|
hide_btn.setFixedSize(18, 18)
|
||||||
|
hide_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: transparent;
|
||||||
|
color: rgba(255, 255, 255, 150);
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: rgba(255, 255, 255, 20);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
hide_btn.clicked.connect(self.toggle_visibility)
|
||||||
|
header_layout.addWidget(hide_btn)
|
||||||
|
|
||||||
|
# Close button
|
||||||
|
close_btn = QPushButton("×")
|
||||||
|
close_btn.setFixedSize(18, 18)
|
||||||
|
close_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: transparent;
|
||||||
|
color: rgba(255, 255, 255, 150);
|
||||||
|
border: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
color: #f44336;
|
||||||
|
background-color: rgba(255, 255, 255, 20);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
close_btn.clicked.connect(self.close_widget)
|
||||||
|
header_layout.addWidget(close_btn)
|
||||||
|
|
||||||
|
layout.addWidget(self.header)
|
||||||
|
|
||||||
|
# Content area
|
||||||
|
self.content = QWidget()
|
||||||
|
self.content.setStyleSheet("background: transparent;")
|
||||||
|
self.content_layout = QVBoxLayout(self.content)
|
||||||
|
self.content_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
layout.addWidget(self.content)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event: QMouseEvent):
|
||||||
|
"""Start dragging from header."""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
# Only drag from header
|
||||||
|
if self.header.geometry().contains(event.pos()):
|
||||||
|
self.dragging = True
|
||||||
|
self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event: QMouseEvent):
|
||||||
|
"""Drag widget."""
|
||||||
|
if self.dragging:
|
||||||
|
new_pos = event.globalPosition().toPoint() - self.drag_position
|
||||||
|
self.move(new_pos)
|
||||||
|
self.position_changed.emit(new_pos)
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event: QMouseEvent):
|
||||||
|
"""Stop dragging."""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.dragging = False
|
||||||
|
event.accept()
|
||||||
|
|
||||||
|
def toggle_visibility(self):
|
||||||
|
"""Toggle content visibility."""
|
||||||
|
self._visible = not self._visible
|
||||||
|
self.content.setVisible(self._visible)
|
||||||
|
self.visibility_changed.emit(self._visible)
|
||||||
|
|
||||||
|
# Collapse widget when hidden
|
||||||
|
if self._visible:
|
||||||
|
self.resize(self.sizeHint().width(), self.sizeHint().height())
|
||||||
|
else:
|
||||||
|
self.resize(self.width(), self.header.height() + 4)
|
||||||
|
|
||||||
|
def close_widget(self):
|
||||||
|
"""Close and remove widget."""
|
||||||
|
self.closed.emit()
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def showEvent(self, event):
|
||||||
|
"""Widget shown."""
|
||||||
|
super().showEvent(event)
|
||||||
|
self.visibility_changed.emit(True)
|
||||||
|
|
||||||
|
def hideEvent(self, event):
|
||||||
|
"""Widget hidden."""
|
||||||
|
super().hideEvent(event)
|
||||||
|
self.visibility_changed.emit(False)
|
||||||
|
|
||||||
|
|
||||||
|
class SpotifyOverlayWidget(OverlayWidget):
|
||||||
|
"""Spotify player as in-game overlay."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(title="Now Playing", icon="🎵", parent=parent)
|
||||||
|
self.setMinimumWidth(200)
|
||||||
|
self._setup_content()
|
||||||
|
|
||||||
|
def _setup_content(self):
|
||||||
|
"""Setup Spotify content."""
|
||||||
|
# Track name
|
||||||
|
self.track_label = QLabel("Not Playing")
|
||||||
|
self.track_label.setStyleSheet("""
|
||||||
|
color: #1db954;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.track_label.setWordWrap(True)
|
||||||
|
self.content_layout.addWidget(self.track_label)
|
||||||
|
|
||||||
|
# Artist
|
||||||
|
self.artist_label = QLabel("-")
|
||||||
|
self.artist_label.setStyleSheet("""
|
||||||
|
color: rgba(255, 255, 255, 150);
|
||||||
|
font-size: 10px;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.content_layout.addWidget(self.artist_label)
|
||||||
|
|
||||||
|
# Progress
|
||||||
|
self.progress = QLabel("▬▬▬▬▬▬▬▬▬▬")
|
||||||
|
self.progress.setStyleSheet("color: #1db954; font-size: 8px; background: transparent;")
|
||||||
|
self.content_layout.addWidget(self.progress)
|
||||||
|
|
||||||
|
# Controls
|
||||||
|
controls = QHBoxLayout()
|
||||||
|
controls.setSpacing(8)
|
||||||
|
|
||||||
|
prev_btn = QPushButton("⏮")
|
||||||
|
play_btn = QPushButton("▶")
|
||||||
|
next_btn = QPushButton("⏭")
|
||||||
|
|
||||||
|
for btn in [prev_btn, play_btn, next_btn]:
|
||||||
|
btn.setFixedSize(28, 28)
|
||||||
|
btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: rgba(255, 255, 255, 15);
|
||||||
|
border: none;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 30);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
controls.addWidget(btn)
|
||||||
|
|
||||||
|
controls.addStretch()
|
||||||
|
self.content_layout.addLayout(controls)
|
||||||
|
|
||||||
|
def update_track(self, track_data):
|
||||||
|
"""Update track info."""
|
||||||
|
if track_data:
|
||||||
|
self.track_label.setText(track_data.get('name', 'Unknown'))
|
||||||
|
artists = track_data.get('artists', [])
|
||||||
|
self.artist_label.setText(', '.join(artists) if artists else '-')
|
||||||
|
|
||||||
|
|
||||||
|
class MissionTrackerWidget(OverlayWidget):
|
||||||
|
"""Mission progress tracker overlay."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(title="Mission Tracker", icon="📜", parent=parent)
|
||||||
|
self.setMinimumWidth(180)
|
||||||
|
self._setup_content()
|
||||||
|
|
||||||
|
def _setup_content(self):
|
||||||
|
"""Setup mission content."""
|
||||||
|
# Mission name
|
||||||
|
self.mission_label = QLabel("No Active Mission")
|
||||||
|
self.mission_label.setStyleSheet("""
|
||||||
|
color: #ffc107;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.mission_label.setWordWrap(True)
|
||||||
|
self.content_layout.addWidget(self.mission_label)
|
||||||
|
|
||||||
|
# Progress
|
||||||
|
self.progress_label = QLabel("0 / 0")
|
||||||
|
self.progress_label.setStyleSheet("""
|
||||||
|
color: rgba(255, 255, 255, 150);
|
||||||
|
font-size: 10px;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.content_layout.addWidget(self.progress_label)
|
||||||
|
|
||||||
|
# Progress bar
|
||||||
|
self.progress_bar = QLabel("░░░░░░░░░░")
|
||||||
|
self.progress_bar.setStyleSheet("color: #4caf50; font-size: 10px; background: transparent;")
|
||||||
|
self.content_layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
self.content_layout.addStretch()
|
||||||
|
|
||||||
|
def update_mission(self, mission_data):
|
||||||
|
"""Update mission info."""
|
||||||
|
if mission_data:
|
||||||
|
self.mission_label.setText(mission_data.get('name', 'Unknown'))
|
||||||
|
current = mission_data.get('current', 0)
|
||||||
|
total = mission_data.get('total', 0)
|
||||||
|
self.progress_label.setText(f"{current} / {total}")
|
||||||
|
|
||||||
|
# Simple progress bar
|
||||||
|
pct = current / total if total > 0 else 0
|
||||||
|
filled = int(pct * 10)
|
||||||
|
bar = "█" * filled + "░" * (10 - filled)
|
||||||
|
self.progress_bar.setText(bar)
|
||||||
|
|
||||||
|
|
||||||
|
class SkillGainWidget(OverlayWidget):
|
||||||
|
"""Recent skill gains overlay."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(title="Skill Gains", icon="📈", parent=parent)
|
||||||
|
self.setMinimumWidth(150)
|
||||||
|
self._setup_content()
|
||||||
|
|
||||||
|
def _setup_content(self):
|
||||||
|
"""Setup skill gains content."""
|
||||||
|
self.gains_layout = QVBoxLayout()
|
||||||
|
self.gains_layout.setSpacing(4)
|
||||||
|
|
||||||
|
# Sample gain
|
||||||
|
gain = QLabel("+5.2 Aim")
|
||||||
|
gain.setStyleSheet("""
|
||||||
|
color: #4caf50;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.gains_layout.addWidget(gain)
|
||||||
|
|
||||||
|
self.content_layout.addLayout(self.gains_layout)
|
||||||
|
self.content_layout.addStretch()
|
||||||
|
|
||||||
|
def add_gain(self, skill, points):
|
||||||
|
"""Add a skill gain notification."""
|
||||||
|
gain = QLabel(f"+{points} {skill}")
|
||||||
|
gain.setStyleSheet("""
|
||||||
|
color: #4caf50;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.gains_layout.addWidget(gain)
|
||||||
|
|
||||||
|
# Keep only last 5
|
||||||
|
while self.gains_layout.count() > 5:
|
||||||
|
item = self.gains_layout.takeAt(0)
|
||||||
|
if item.widget():
|
||||||
|
item.widget().deleteLater()
|
||||||
|
|
||||||
|
|
||||||
|
class DPPTrackerWidget(OverlayWidget):
|
||||||
|
"""DPP (Damage Per PEC) tracker overlay."""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(title="DPP Tracker", icon="🎯", parent=parent)
|
||||||
|
self.setMinimumWidth(160)
|
||||||
|
self._setup_content()
|
||||||
|
|
||||||
|
def _setup_content(self):
|
||||||
|
"""Setup DPP content."""
|
||||||
|
# Current DPP
|
||||||
|
self.dpp_label = QLabel("0.00")
|
||||||
|
self.dpp_label.setStyleSheet("""
|
||||||
|
color: #00bcd4;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.content_layout.addWidget(self.dpp_label)
|
||||||
|
|
||||||
|
# Label
|
||||||
|
label = QLabel("DPP")
|
||||||
|
label.setStyleSheet("""
|
||||||
|
color: rgba(255, 255, 255, 150);
|
||||||
|
font-size: 9px;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.content_layout.addWidget(label)
|
||||||
|
|
||||||
|
# Session stats
|
||||||
|
self.session_label = QLabel("Session: 0.00")
|
||||||
|
self.session_label.setStyleSheet("""
|
||||||
|
color: rgba(255, 255, 255, 120);
|
||||||
|
font-size: 10px;
|
||||||
|
background: transparent;
|
||||||
|
""")
|
||||||
|
self.content_layout.addWidget(self.session_label)
|
||||||
|
|
||||||
|
self.content_layout.addStretch()
|
||||||
|
|
||||||
|
def update_dpp(self, current, session_avg):
|
||||||
|
"""Update DPP values."""
|
||||||
|
self.dpp_label.setText(f"{current:.2f}")
|
||||||
|
self.session_label.setText(f"Session: {session_avg:.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
class OverlayManager:
|
||||||
|
"""Manage all overlay widgets."""
|
||||||
|
|
||||||
|
def __init__(self, app=None):
|
||||||
|
self.app = app
|
||||||
|
self.widgets = {}
|
||||||
|
self.positions_file = Path("data/overlay_positions.json")
|
||||||
|
self._load_positions()
|
||||||
|
|
||||||
|
def create_widget(self, widget_type, name, **kwargs):
|
||||||
|
"""Create and show an overlay widget."""
|
||||||
|
widget_classes = {
|
||||||
|
'spotify': SpotifyOverlayWidget,
|
||||||
|
'mission': MissionTrackerWidget,
|
||||||
|
'skillgain': SkillGainWidget,
|
||||||
|
'dpp': DPPTrackerWidget,
|
||||||
|
}
|
||||||
|
|
||||||
|
widget_class = widget_classes.get(widget_type)
|
||||||
|
if not widget_class:
|
||||||
|
return None
|
||||||
|
|
||||||
|
widget = widget_class(**kwargs)
|
||||||
|
|
||||||
|
# Restore position
|
||||||
|
if name in self.positions:
|
||||||
|
pos = self.positions[name]
|
||||||
|
widget.move(pos['x'], pos['y'])
|
||||||
|
else:
|
||||||
|
# Default position - right side of screen
|
||||||
|
screen = QApplication.primaryScreen().geometry()
|
||||||
|
y_offset = len(self.widgets) * 100 + 100
|
||||||
|
widget.move(screen.width() - 250, y_offset)
|
||||||
|
|
||||||
|
# Connect signals
|
||||||
|
widget.position_changed.connect(lambda p: self._save_position(name, p))
|
||||||
|
widget.closed.connect(lambda: self._remove_widget(name))
|
||||||
|
|
||||||
|
widget.show()
|
||||||
|
self.widgets[name] = widget
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def _save_position(self, name, pos):
|
||||||
|
"""Save widget position."""
|
||||||
|
self.positions[name] = {'x': pos.x(), 'y': pos.y()}
|
||||||
|
self._save_positions()
|
||||||
|
|
||||||
|
def _remove_widget(self, name):
|
||||||
|
"""Remove widget from tracking."""
|
||||||
|
if name in self.widgets:
|
||||||
|
del self.widgets[name]
|
||||||
|
if name in self.positions:
|
||||||
|
del self.positions[name]
|
||||||
|
self._save_positions()
|
||||||
|
|
||||||
|
def toggle_widget(self, name):
|
||||||
|
"""Toggle widget visibility."""
|
||||||
|
if name in self.widgets:
|
||||||
|
widget = self.widgets[name]
|
||||||
|
if widget.isVisible():
|
||||||
|
widget.hide()
|
||||||
|
else:
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
def hide_all(self):
|
||||||
|
"""Hide all overlay widgets."""
|
||||||
|
for widget in self.widgets.values():
|
||||||
|
widget.hide()
|
||||||
|
|
||||||
|
def show_all(self):
|
||||||
|
"""Show all overlay widgets."""
|
||||||
|
for widget in self.widgets.values():
|
||||||
|
widget.show()
|
||||||
|
|
||||||
|
def _load_positions(self):
|
||||||
|
"""Load saved positions."""
|
||||||
|
self.positions = {}
|
||||||
|
if self.positions_file.exists():
|
||||||
|
import json
|
||||||
|
try:
|
||||||
|
with open(self.positions_file, 'r') as f:
|
||||||
|
self.positions = json.load(f)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_positions(self):
|
||||||
|
"""Save positions to file."""
|
||||||
|
import json
|
||||||
|
self.positions_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(self.positions_file, 'w') as f:
|
||||||
|
json.dump(self.positions, f)
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
"""
|
||||||
|
EU-Utility - Plugin Store
|
||||||
|
|
||||||
|
Fetch and install community plugins from GitHub.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from PyQt6.QtCore import QObject, QThread, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class PluginStore(QObject):
|
||||||
|
"""Community plugin repository manager."""
|
||||||
|
|
||||||
|
# Repository configuration
|
||||||
|
REPO_URL = "https://raw.githubusercontent.com/ImpulsiveFPS/EU-Utility-Plugins/main/"
|
||||||
|
INDEX_URL = REPO_URL + "plugins.json"
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
plugins_loaded = pyqtSignal(list)
|
||||||
|
plugin_installed = pyqtSignal(str, bool)
|
||||||
|
plugin_removed = pyqtSignal(str, bool)
|
||||||
|
error_occurred = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, plugins_dir="user_plugins"):
|
||||||
|
super().__init__()
|
||||||
|
self.plugins_dir = Path(plugins_dir)
|
||||||
|
self.plugins_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.available_plugins = []
|
||||||
|
self.installed_plugins = []
|
||||||
|
self._load_installed()
|
||||||
|
|
||||||
|
def fetch_plugins(self):
|
||||||
|
"""Fetch available plugins from repository."""
|
||||||
|
self.fetch_thread = PluginFetchThread(self.INDEX_URL)
|
||||||
|
self.fetch_thread.fetched.connect(self._on_plugins_fetched)
|
||||||
|
self.fetch_thread.error.connect(self._on_fetch_error)
|
||||||
|
self.fetch_thread.start()
|
||||||
|
|
||||||
|
def _on_plugins_fetched(self, plugins):
|
||||||
|
"""Handle fetched plugins."""
|
||||||
|
self.available_plugins = plugins
|
||||||
|
self.plugins_loaded.emit(plugins)
|
||||||
|
|
||||||
|
def _on_fetch_error(self, error):
|
||||||
|
"""Handle fetch error."""
|
||||||
|
self.error_occurred.emit(error)
|
||||||
|
|
||||||
|
def install_plugin(self, plugin_id):
|
||||||
|
"""Install a plugin from the store."""
|
||||||
|
plugin = self._find_plugin(plugin_id)
|
||||||
|
if not plugin:
|
||||||
|
self.plugin_installed.emit(plugin_id, False)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.install_thread = PluginInstallThread(
|
||||||
|
plugin,
|
||||||
|
self.plugins_dir
|
||||||
|
)
|
||||||
|
self.install_thread.installed.connect(
|
||||||
|
lambda success: self._on_installed(plugin_id, success)
|
||||||
|
)
|
||||||
|
self.install_thread.error.connect(self._on_fetch_error)
|
||||||
|
self.install_thread.start()
|
||||||
|
|
||||||
|
def _on_installed(self, plugin_id, success):
|
||||||
|
"""Handle plugin installation."""
|
||||||
|
if success:
|
||||||
|
self.installed_plugins.append(plugin_id)
|
||||||
|
self._save_installed()
|
||||||
|
self.plugin_installed.emit(plugin_id, success)
|
||||||
|
|
||||||
|
def remove_plugin(self, plugin_id):
|
||||||
|
"""Remove an installed plugin."""
|
||||||
|
try:
|
||||||
|
plugin_dir = self.plugins_dir / plugin_id
|
||||||
|
if plugin_dir.exists():
|
||||||
|
shutil.rmtree(plugin_dir)
|
||||||
|
|
||||||
|
if plugin_id in self.installed_plugins:
|
||||||
|
self.installed_plugins.remove(plugin_id)
|
||||||
|
self._save_installed()
|
||||||
|
|
||||||
|
self.plugin_removed.emit(plugin_id, True)
|
||||||
|
except Exception as e:
|
||||||
|
self.error_occurred.emit(str(e))
|
||||||
|
self.plugin_removed.emit(plugin_id, False)
|
||||||
|
|
||||||
|
def _find_plugin(self, plugin_id):
|
||||||
|
"""Find plugin by ID."""
|
||||||
|
for plugin in self.available_plugins:
|
||||||
|
if plugin.get('id') == plugin_id:
|
||||||
|
return plugin
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_installed(self):
|
||||||
|
"""Load list of installed plugins."""
|
||||||
|
installed_file = self.plugins_dir / ".installed"
|
||||||
|
if installed_file.exists():
|
||||||
|
try:
|
||||||
|
with open(installed_file, 'r') as f:
|
||||||
|
self.installed_plugins = json.load(f)
|
||||||
|
except:
|
||||||
|
self.installed_plugins = []
|
||||||
|
|
||||||
|
def _save_installed(self):
|
||||||
|
"""Save list of installed plugins."""
|
||||||
|
installed_file = self.plugins_dir / ".installed"
|
||||||
|
with open(installed_file, 'w') as f:
|
||||||
|
json.dump(self.installed_plugins, f)
|
||||||
|
|
||||||
|
def is_installed(self, plugin_id):
|
||||||
|
"""Check if a plugin is installed."""
|
||||||
|
return plugin_id in self.installed_plugins
|
||||||
|
|
||||||
|
def get_installed_plugins(self):
|
||||||
|
"""Get list of installed plugin IDs."""
|
||||||
|
return self.installed_plugins.copy()
|
||||||
|
|
||||||
|
|
||||||
|
class PluginFetchThread(QThread):
|
||||||
|
"""Background thread to fetch plugin index."""
|
||||||
|
|
||||||
|
fetched = pyqtSignal(list)
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, url):
|
||||||
|
super().__init__()
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Fetch plugin index."""
|
||||||
|
try:
|
||||||
|
req = urllib.request.Request(
|
||||||
|
self.url,
|
||||||
|
headers={'User-Agent': 'EU-Utility/1.0'}
|
||||||
|
)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as response:
|
||||||
|
data = json.loads(response.read().decode('utf-8'))
|
||||||
|
self.fetched.emit(data.get('plugins', []))
|
||||||
|
except Exception as e:
|
||||||
|
self.error.emit(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
class PluginInstallThread(QThread):
|
||||||
|
"""Background thread to install a plugin."""
|
||||||
|
|
||||||
|
installed = pyqtSignal(bool)
|
||||||
|
error = pyqtSignal(str)
|
||||||
|
progress = pyqtSignal(str)
|
||||||
|
|
||||||
|
def __init__(self, plugin, install_dir):
|
||||||
|
super().__init__()
|
||||||
|
self.plugin = plugin
|
||||||
|
self.install_dir = Path(install_dir)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Install plugin."""
|
||||||
|
try:
|
||||||
|
self.progress.emit(f"Downloading {self.plugin['name']}...")
|
||||||
|
|
||||||
|
# Download zip
|
||||||
|
download_url = self.plugin.get('download_url')
|
||||||
|
if not download_url:
|
||||||
|
self.error.emit("No download URL")
|
||||||
|
self.installed.emit(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
temp_zip = self.install_dir / "temp.zip"
|
||||||
|
|
||||||
|
req = urllib.request.Request(
|
||||||
|
download_url,
|
||||||
|
headers={'User-Agent': 'EU-Utility/1.0'}
|
||||||
|
)
|
||||||
|
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as response:
|
||||||
|
with open(temp_zip, 'wb') as f:
|
||||||
|
f.write(response.read())
|
||||||
|
|
||||||
|
self.progress.emit("Extracting...")
|
||||||
|
|
||||||
|
# Extract
|
||||||
|
plugin_dir = self.install_dir / self.plugin['id']
|
||||||
|
if plugin_dir.exists():
|
||||||
|
shutil.rmtree(plugin_dir)
|
||||||
|
plugin_dir.mkdir()
|
||||||
|
|
||||||
|
with zipfile.ZipFile(temp_zip, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(plugin_dir)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
temp_zip.unlink()
|
||||||
|
|
||||||
|
self.progress.emit("Installed!")
|
||||||
|
self.installed.emit(True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.error.emit(str(e))
|
||||||
|
self.installed.emit(False)
|
||||||
|
|
||||||
|
|
||||||
|
# Sample plugins.json structure for GitHub repo:
|
||||||
|
SAMPLE_PLUGINS_JSON = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"id": "loot_tracker",
|
||||||
|
"name": "Loot Tracker",
|
||||||
|
"description": "Track and analyze hunting loot",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "ImpulsiveFPS",
|
||||||
|
"category": "hunting",
|
||||||
|
"download_url": "https://github.com/.../loot_tracker.zip",
|
||||||
|
"icon": "🎁",
|
||||||
|
"min_app_version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mining_helper",
|
||||||
|
"name": "Mining Helper",
|
||||||
|
"description": "Track mining finds and claims",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"author": "Community",
|
||||||
|
"category": "mining",
|
||||||
|
"download_url": "https://github.com/.../mining_helper.zip",
|
||||||
|
"icon": "⛏️",
|
||||||
|
"min_app_version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "market_analyzer",
|
||||||
|
"name": "Market Analyzer",
|
||||||
|
"description": "Analyze auction prices and trends",
|
||||||
|
"version": "0.9.0",
|
||||||
|
"author": "EU Community",
|
||||||
|
"category": "trading",
|
||||||
|
"download_url": "https://github.com/.../market_analyzer.zip",
|
||||||
|
"icon": "📈",
|
||||||
|
"min_app_version": "1.0.0"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,171 @@
|
||||||
|
"""
|
||||||
|
EU-Utility - Settings Manager
|
||||||
|
|
||||||
|
User preferences and configuration management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(QObject):
|
||||||
|
"""Application settings manager."""
|
||||||
|
|
||||||
|
setting_changed = pyqtSignal(str, object)
|
||||||
|
|
||||||
|
# Default settings
|
||||||
|
DEFAULTS = {
|
||||||
|
# Overlay
|
||||||
|
'overlay_enabled': True,
|
||||||
|
'overlay_opacity': 0.9,
|
||||||
|
'overlay_theme': 'dark',
|
||||||
|
|
||||||
|
# Hotkeys
|
||||||
|
'hotkey_toggle': 'ctrl+shift+u',
|
||||||
|
'hotkey_search': 'ctrl+shift+f',
|
||||||
|
'hotkey_calculator': 'ctrl+shift+c',
|
||||||
|
'hotkey_music': 'ctrl+shift+m',
|
||||||
|
'hotkey_scan': 'ctrl+shift+r',
|
||||||
|
'hotkey_skills': 'ctrl+shift+s',
|
||||||
|
|
||||||
|
# Plugins
|
||||||
|
'enabled_plugins': [
|
||||||
|
'universal_search',
|
||||||
|
'calculator',
|
||||||
|
'spotify_controller',
|
||||||
|
'nexus_search',
|
||||||
|
'game_reader',
|
||||||
|
'skill_scanner',
|
||||||
|
],
|
||||||
|
'disabled_plugins': [],
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
'dashboard_widgets': [
|
||||||
|
{'type': 'QuickActions', 'row': 0, 'col': 0},
|
||||||
|
{'type': 'Spotify', 'row': 0, 'col': 1},
|
||||||
|
{'type': 'PEDTracker', 'row': 1, 'col': 0},
|
||||||
|
{'type': 'SkillProgress', 'row': 1, 'col': 1},
|
||||||
|
],
|
||||||
|
|
||||||
|
# Overlay widgets
|
||||||
|
'overlay_widgets': {
|
||||||
|
'spotify': {'enabled': True, 'x': 0, 'y': 100},
|
||||||
|
'skillgain': {'enabled': False, 'x': 0, 'y': 200},
|
||||||
|
},
|
||||||
|
|
||||||
|
# Game Reader
|
||||||
|
'ocr_engine': 'easyocr',
|
||||||
|
'auto_capture': False,
|
||||||
|
'capture_region': 'full',
|
||||||
|
|
||||||
|
# Skill Scanner
|
||||||
|
'auto_track_gains': True,
|
||||||
|
'skill_alert_threshold': 100,
|
||||||
|
|
||||||
|
# Appearance
|
||||||
|
'icon_size': 24,
|
||||||
|
'accent_color': '#4a9eff',
|
||||||
|
'show_tooltips': True,
|
||||||
|
|
||||||
|
# Updates
|
||||||
|
'check_updates': True,
|
||||||
|
'auto_update_plugins': False,
|
||||||
|
|
||||||
|
# Data
|
||||||
|
'data_retention_days': 30,
|
||||||
|
'auto_export': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, config_file="data/settings.json"):
|
||||||
|
super().__init__()
|
||||||
|
self.config_file = Path(config_file)
|
||||||
|
self._settings = {}
|
||||||
|
self._load()
|
||||||
|
|
||||||
|
def _load(self):
|
||||||
|
"""Load settings from file."""
|
||||||
|
self._settings = self.DEFAULTS.copy()
|
||||||
|
|
||||||
|
if self.config_file.exists():
|
||||||
|
try:
|
||||||
|
with open(self.config_file, 'r') as f:
|
||||||
|
saved = json.load(f)
|
||||||
|
self._settings.update(saved)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading settings: {e}")
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""Save settings to file."""
|
||||||
|
try:
|
||||||
|
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(self.config_file, 'w') as f:
|
||||||
|
json.dump(self._settings, f, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving settings: {e}")
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
"""Get a setting value."""
|
||||||
|
return self._settings.get(key, default)
|
||||||
|
|
||||||
|
def set(self, key, value):
|
||||||
|
"""Set a setting value."""
|
||||||
|
old_value = self._settings.get(key)
|
||||||
|
self._settings[key] = value
|
||||||
|
self.save()
|
||||||
|
self.setting_changed.emit(key, value)
|
||||||
|
|
||||||
|
def reset(self, key=None):
|
||||||
|
"""Reset setting(s) to default."""
|
||||||
|
if key:
|
||||||
|
self._settings[key] = self.DEFAULTS.get(key)
|
||||||
|
self.save()
|
||||||
|
self.setting_changed.emit(key, self._settings[key])
|
||||||
|
else:
|
||||||
|
self._settings = self.DEFAULTS.copy()
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def is_plugin_enabled(self, plugin_id):
|
||||||
|
"""Check if a plugin is enabled."""
|
||||||
|
return plugin_id in self._settings.get('enabled_plugins', [])
|
||||||
|
|
||||||
|
def enable_plugin(self, plugin_id):
|
||||||
|
"""Enable a plugin."""
|
||||||
|
enabled = self._settings.get('enabled_plugins', [])
|
||||||
|
disabled = self._settings.get('disabled_plugins', [])
|
||||||
|
|
||||||
|
if plugin_id not in enabled:
|
||||||
|
enabled.append(plugin_id)
|
||||||
|
if plugin_id in disabled:
|
||||||
|
disabled.remove(plugin_id)
|
||||||
|
|
||||||
|
self.set('enabled_plugins', enabled)
|
||||||
|
self.set('disabled_plugins', disabled)
|
||||||
|
|
||||||
|
def disable_plugin(self, plugin_id):
|
||||||
|
"""Disable a plugin."""
|
||||||
|
enabled = self._settings.get('enabled_plugins', [])
|
||||||
|
disabled = self._settings.get('disabled_plugins', [])
|
||||||
|
|
||||||
|
if plugin_id in enabled:
|
||||||
|
enabled.remove(plugin_id)
|
||||||
|
if plugin_id not in disabled:
|
||||||
|
disabled.append(plugin_id)
|
||||||
|
|
||||||
|
self.set('enabled_plugins', enabled)
|
||||||
|
self.set('disabled_plugins', disabled)
|
||||||
|
|
||||||
|
def all_settings(self):
|
||||||
|
"""Get all settings."""
|
||||||
|
return self._settings.copy()
|
||||||
|
|
||||||
|
|
||||||
|
# Global settings instance
|
||||||
|
_settings_instance = None
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
"""Get global settings instance."""
|
||||||
|
global _settings_instance
|
||||||
|
if _settings_instance is None:
|
||||||
|
_settings_instance = Settings()
|
||||||
|
return _settings_instance
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""
|
||||||
|
Chat Logger Plugin
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .plugin import ChatLoggerPlugin
|
||||||
|
|
||||||
|
__all__ = ["ChatLoggerPlugin"]
|
||||||
|
|
@ -0,0 +1,270 @@
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = QLabel("💬 Chat Logger")
|
||||||
|
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# 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 @@
|
||||||
|
"""
|
||||||
|
Loot Tracker Plugin
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .plugin import LootTrackerPlugin
|
||||||
|
|
||||||
|
__all__ = ["LootTrackerPlugin"]
|
||||||
|
|
@ -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"]
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue