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:
LemonNexus 2026-02-13 14:23:45 +00:00
parent bf7a2bb682
commit 5f5a3db481
11 changed files with 2138 additions and 9 deletions

View File

@ -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

View File

@ -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()
@ -114,6 +136,34 @@ class EUUtilityApp:
"""Toggle overlay visibility.""" """Toggle overlay visibility."""
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():

View File

@ -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)

View File

@ -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"
},
]
}

View File

@ -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

View File

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

View File

@ -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)
]

View File

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

View File

@ -0,0 +1,224 @@
"""
EU-Utility - Loot Tracker Plugin
Track and analyze hunting loot with statistics and ROI.
"""
import re
from datetime import datetime
from collections import defaultdict
from pathlib import Path
import json
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem,
QComboBox, QLineEdit, QTabWidget, QFrame
)
from PyQt6.QtCore import Qt
from plugins.base_plugin import BasePlugin
class LootTrackerPlugin(BasePlugin):
"""Track hunting loot and calculate ROI."""
name = "Loot Tracker"
version = "1.0.0"
author = "ImpulsiveFPS"
description = "Track hunting loot with stats and ROI analysis"
hotkey = "ctrl+shift+l" # L for Loot
def initialize(self):
"""Setup loot tracker."""
self.data_file = Path("data/loot_tracker.json")
self.data_file.parent.mkdir(parents=True, exist_ok=True)
self.sessions = []
self.current_session = {
'start_time': None,
'kills': 0,
'loot_items': [],
'total_tt': 0.0,
'ammo_cost': 0.0,
'weapon_decay': 0.0,
}
self._load_data()
def _load_data(self):
"""Load historical data."""
if self.data_file.exists():
try:
with open(self.data_file, 'r') as f:
self.sessions = json.load(f)
except:
self.sessions = []
def _save_data(self):
"""Save data to file."""
with open(self.data_file, 'w') as f:
json.dump(self.sessions, f, indent=2)
def get_ui(self):
"""Create plugin UI."""
widget = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Title
title = QLabel("🎁 Loot Tracker")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Stats summary
stats_frame = QFrame()
stats_frame.setStyleSheet("""
QFrame {
background-color: rgba(0, 0, 0, 50);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 20);
}
""")
stats_layout = QHBoxLayout(stats_frame)
self.kills_label = QLabel("Kills: 0")
self.kills_label.setStyleSheet("color: #4caf50; font-size: 14px; font-weight: bold;")
stats_layout.addWidget(self.kills_label)
self.tt_label = QLabel("TT: 0.00 PED")
self.tt_label.setStyleSheet("color: #ffc107; font-size: 14px; font-weight: bold;")
stats_layout.addWidget(self.tt_label)
self.roi_label = QLabel("ROI: 0%")
self.roi_label.setStyleSheet("color: #4a9eff; font-size: 14px; font-weight: bold;")
stats_layout.addWidget(self.roi_label)
layout.addWidget(stats_frame)
# Session controls
controls = QHBoxLayout()
self.start_btn = QPushButton("▶ Start Session")
self.start_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-weight: bold;
}
QPushButton:hover {
background-color: #5cbf60;
}
""")
self.start_btn.clicked.connect(self._start_session)
controls.addWidget(self.start_btn)
self.stop_btn = QPushButton("⏹ Stop Session")
self.stop_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
padding: 10px 20px;
border: none;
border-radius: 8px;
font-weight: bold;
}
QPushButton:hover {
background-color: #f55a4e;
}
""")
self.stop_btn.clicked.connect(self._stop_session)
self.stop_btn.setEnabled(False)
controls.addWidget(self.stop_btn)
layout.addLayout(controls)
# Loot table
self.loot_table = QTableWidget()
self.loot_table.setColumnCount(4)
self.loot_table.setHorizontalHeaderLabels(["Item", "Qty", "TT Value", "Time"])
self.loot_table.setStyleSheet("""
QTableWidget {
background-color: rgba(30, 30, 30, 100);
color: white;
border: none;
border-radius: 6px;
}
QHeaderView::section {
background-color: rgba(74, 158, 255, 100);
color: white;
padding: 6px;
}
""")
self.loot_table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(self.loot_table)
layout.addStretch()
return widget
def _start_session(self):
"""Start new hunting session."""
self.current_session = {
'start_time': datetime.now().isoformat(),
'kills': 0,
'loot_items': [],
'total_tt': 0.0,
'ammo_cost': 0.0,
'weapon_decay': 0.0,
}
self.start_btn.setEnabled(False)
self.stop_btn.setEnabled(True)
def _stop_session(self):
"""Stop current session and save."""
self.current_session['end_time'] = datetime.now().isoformat()
self.sessions.append(self.current_session)
self._save_data()
self.start_btn.setEnabled(True)
self.stop_btn.setEnabled(False)
self._update_stats()
def _update_stats(self):
"""Update statistics display."""
kills = self.current_session.get('kills', 0)
tt = self.current_session.get('total_tt', 0.0)
self.kills_label.setText(f"Kills: {kills}")
self.tt_label.setText(f"TT: {tt:.2f} PED")
def parse_chat_message(self, message):
"""Parse loot from chat."""
# Look for loot patterns
# Example: "You received Animal Hide (0.03 PED)"
loot_pattern = r'You received (.+?) \(([\d.]+) PED\)'
match = re.search(loot_pattern, message)
if match:
item_name = match.group(1)
tt_value = float(match.group(2))
self.current_session['loot_items'].append({
'item': item_name,
'tt': tt_value,
'time': datetime.now().isoformat()
})
self.current_session['total_tt'] += tt_value
self._update_stats()
# Check for new kill
# Multiple items in quick succession = one kill
# Longer gap = new kill
self.current_session['kills'] += 1
def on_hotkey(self):
"""Toggle session on hotkey."""
if self.start_btn.isEnabled():
self._start_session()
else:
self._stop_session()

View File

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

View File

@ -0,0 +1,273 @@
"""
EU-Utility - Mining Helper Plugin
Track mining finds, claims, and locations.
"""
import json
from datetime import datetime
from pathlib import Path
from collections import defaultdict
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem,
QComboBox, QTextEdit
)
from PyQt6.QtCore import Qt
from plugins.base_plugin import BasePlugin
class MiningHelperPlugin(BasePlugin):
"""Track mining activities and claims."""
name = "Mining Helper"
version = "1.0.0"
author = "ImpulsiveFPS"
description = "Track mining finds, claims, and hotspot locations"
hotkey = "ctrl+shift+n" # N for miNiNg
# Resource types
RESOURCES = [
"Alicenies Liquid",
"Ares Powder",
"Blausariam",
"Caldorite",
"Cobalt",
"Copper",
"Dianthus",
"Erdorium",
"Frigulite",
"Ganganite",
"Himi",
"Ignisium",
"Iron",
"Kaz Ingot",
"Lysterium",
"Maganite",
"Niksarium",
"Oil",
"Platinum",
"Redulite",
"Rubio",
"Sopur",
"Titan",
"Typonolic Steam",
"Uranium",
"Veldspar",
"Xantium",
"Zinc",
]
def initialize(self):
"""Setup mining helper."""
self.data_file = Path("data/mining_helper.json")
self.data_file.parent.mkdir(parents=True, exist_ok=True)
self.claims = []
self.current_run = {
'start_time': None,
'drops': 0,
'finds': [],
'total_tt': 0.0,
}
self._load_data()
def _load_data(self):
"""Load historical data."""
if self.data_file.exists():
try:
with open(self.data_file, 'r') as f:
data = json.load(f)
self.claims = data.get('claims', [])
except:
self.claims = []
def _save_data(self):
"""Save data."""
with open(self.data_file, 'w') as f:
json.dump({'claims': self.claims}, f, indent=2)
def get_ui(self):
"""Create plugin UI."""
widget = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Title
title = QLabel("⛏️ Mining Helper")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Stats
stats = QHBoxLayout()
self.drops_label = QLabel("Drops: 0")
self.drops_label.setStyleSheet("color: #9c27b0; font-size: 14px; font-weight: bold;")
stats.addWidget(self.drops_label)
self.finds_label = QLabel("Finds: 0")
self.finds_label.setStyleSheet("color: #4caf50; font-size: 14px; font-weight: bold;")
stats.addWidget(self.finds_label)
self.hit_rate_label = QLabel("Hit Rate: 0%")
self.hit_rate_label.setStyleSheet("color: #ffc107; font-size: 14px; font-weight: bold;")
stats.addWidget(self.hit_rate_label)
layout.addLayout(stats)
# Quick add claim
add_frame = QWidget()
add_frame.setStyleSheet("""
QWidget {
background-color: rgba(0, 0, 0, 50);
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 20);
}
""")
add_layout = QHBoxLayout(add_frame)
add_layout.setContentsMargins(10, 10, 10, 10)
self.resource_combo = QComboBox()
self.resource_combo.addItems(self.RESOURCES)
self.resource_combo.setStyleSheet("""
QComboBox {
background-color: rgba(255, 255, 255, 15);
color: white;
border: none;
padding: 5px;
border-radius: 4px;
}
""")
add_layout.addWidget(self.resource_combo)
self.size_combo = QComboBox()
self.size_combo.addItems(["Tiny", "Small", "Medium", "Large", "Huge", "Massive"])
self.size_combo.setStyleSheet(self.resource_combo.styleSheet())
add_layout.addWidget(self.size_combo)
add_btn = QPushButton("+ Add Claim")
add_btn.setStyleSheet("""
QPushButton {
background-color: #9c27b0;
color: white;
padding: 8px 15px;
border: none;
border-radius: 6px;
font-weight: bold;
}
QPushButton:hover {
background-color: #ab47bc;
}
""")
add_btn.clicked.connect(self._add_claim)
add_layout.addWidget(add_btn)
layout.addWidget(add_frame)
# Claims table
self.claims_table = QTableWidget()
self.claims_table.setColumnCount(4)
self.claims_table.setHorizontalHeaderLabels(["Resource", "Size", "TT", "Time"])
self.claims_table.setStyleSheet("""
QTableWidget {
background-color: rgba(30, 30, 30, 100);
color: white;
border: none;
border-radius: 6px;
}
QHeaderView::section {
background-color: rgba(156, 39, 176, 100);
color: white;
padding: 6px;
}
""")
self.claims_table.horizontalHeader().setStretchLastSection(True)
layout.addWidget(self.claims_table)
layout.addStretch()
return widget
def _add_claim(self):
"""Add a claim manually."""
resource = self.resource_combo.currentText()
size = self.size_combo.currentText()
claim = {
'resource': resource,
'size': size,
'tt_value': self._estimate_tt(size),
'time': datetime.now().isoformat(),
'location': None, # Could get from game
}
self.claims.append(claim)
self._save_data()
self._update_table()
self._update_stats()
def _estimate_tt(self, size):
"""Estimate TT value based on claim size."""
estimates = {
'Tiny': 0.05,
'Small': 0.25,
'Medium': 1.00,
'Large': 5.00,
'Huge': 25.00,
'Massive': 100.00,
}
return estimates.get(size, 0.05)
def _update_table(self):
"""Update claims table."""
recent = self.claims[-20:] # Show last 20
self.claims_table.setRowCount(len(recent))
for i, claim in enumerate(recent):
self.claims_table.setItem(i, 0, QTableWidgetItem(claim['resource']))
self.claims_table.setItem(i, 1, QTableWidgetItem(claim['size']))
self.claims_table.setItem(i, 2, QTableWidgetItem(f"{claim['tt_value']:.2f}"))
time_str = claim['time'][11:16] if claim['time'] else '-'
self.claims_table.setItem(i, 3, QTableWidgetItem(time_str))
def _update_stats(self):
"""Update statistics."""
drops = len(self.claims) + 10 # Estimate
finds = len(self.claims)
hit_rate = (finds / drops * 100) if drops > 0 else 0
self.drops_label.setText(f"Drops: ~{drops}")
self.finds_label.setText(f"Finds: {finds}")
self.hit_rate_label.setText(f"Hit Rate: {hit_rate:.1f}%")
def parse_chat_message(self, message):
"""Parse mining claims from chat."""
# Look for claim patterns
# Example: "You found a Tiny Lysterium claim"
for resource in self.RESOURCES:
if resource in message and "claim" in message.lower():
# Extract size
sizes = ["Tiny", "Small", "Medium", "Large", "Huge", "Massive"]
size = "Unknown"
for s in sizes:
if s in message:
size = s
break
claim = {
'resource': resource,
'size': size,
'tt_value': self._estimate_tt(size),
'time': datetime.now().isoformat(),
'location': None,
}
self.claims.append(claim)
self._save_data()
self._update_table()
self._update_stats()
break