feat: Resizable window, OCR scanners, customizable dashboard
This commit is contained in:
parent
5b127cf99e
commit
72c3c132ca
|
|
@ -52,19 +52,15 @@ class OverlayWindow(QMainWindow):
|
||||||
self.hide_overlay()
|
self.hide_overlay()
|
||||||
|
|
||||||
def _setup_window(self):
|
def _setup_window(self):
|
||||||
"""Configure window with EU styling - shows in taskbar."""
|
"""Configure window - resizable and shows in taskbar."""
|
||||||
self.setWindowTitle("EU-Utility")
|
self.setWindowTitle("EU-Utility")
|
||||||
|
|
||||||
# Frameless, but NOT Tool (so it shows in taskbar)
|
# Resizable window (no FramelessWindowHint), stays on top
|
||||||
# WindowStaysOnTopHint makes it stay on top without hiding from taskbar
|
|
||||||
self.setWindowFlags(
|
self.setWindowFlags(
|
||||||
Qt.WindowType.FramelessWindowHint |
|
|
||||||
Qt.WindowType.WindowStaysOnTopHint
|
Qt.WindowType.WindowStaysOnTopHint
|
||||||
)
|
)
|
||||||
|
|
||||||
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
|
self.setMinimumSize(600, 400)
|
||||||
|
|
||||||
# Clean, game-like size
|
|
||||||
self.resize(850, 600)
|
self.resize(850, 600)
|
||||||
self._center_window()
|
self._center_window()
|
||||||
|
|
||||||
|
|
@ -337,19 +333,22 @@ class OverlayWindow(QMainWindow):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _load_plugins(self):
|
def _load_plugins(self):
|
||||||
"""Load plugins into sidebar and stack."""
|
"""Load plugins into sidebar and stack - FIXED indexing."""
|
||||||
for idx, (plugin_id, plugin) in enumerate(self.plugin_manager.get_all_plugins().items()):
|
plugins_list = list(self.plugin_manager.get_all_plugins().items())
|
||||||
|
|
||||||
|
for idx, (plugin_id, plugin) in enumerate(plugins_list):
|
||||||
# Get icon name
|
# Get icon name
|
||||||
icon_name = get_plugin_icon_name(plugin.name)
|
icon_name = get_plugin_icon_name(plugin.name)
|
||||||
|
|
||||||
# Add to list view
|
# Add to list view
|
||||||
list_item = QListWidgetItem(plugin.name)
|
list_item = QListWidgetItem(plugin.name)
|
||||||
list_item.setData(Qt.ItemDataRole.UserRole, idx)
|
list_item.setData(Qt.ItemDataRole.UserRole, idx) # Store correct index
|
||||||
self.plugin_list.addItem(list_item)
|
self.plugin_list.addItem(list_item)
|
||||||
|
|
||||||
# Add to icon grid
|
# Add to icon grid
|
||||||
icon_btn = self._create_icon_button(plugin.name, icon_name, idx)
|
icon_btn = self._create_icon_button(plugin.name, icon_name, idx)
|
||||||
self.icon_grid_layout.addWidget(icon_btn)
|
self.icon_grid_layout.addWidget(icon_btn)
|
||||||
|
self.plugin_buttons.append(icon_btn) # Track buttons
|
||||||
|
|
||||||
# Add plugin UI to stack
|
# Add plugin UI to stack
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,230 +1,326 @@
|
||||||
"""
|
"""
|
||||||
EU-Utility - Dashboard Plugin
|
EU-Utility - Dashboard Plugin with Customizable Widgets
|
||||||
|
|
||||||
Main dashboard with customizable widgets and overview.
|
Customizable start page with avatar statistics.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
QPushButton, QGridLayout, QFrame, QScrollArea,
|
QPushButton, QGridLayout, QFrame, QScrollArea,
|
||||||
QSizePolicy
|
QSizePolicy, QCheckBox, QDialog, QListWidget,
|
||||||
|
QListWidgetItem, QDialogButtonBox
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QTimer
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
from PyQt6.QtGui import QColor
|
from PyQt6.QtGui import QColor, QFont
|
||||||
|
|
||||||
from core.eu_styles import EU_COLORS, EU_STYLES
|
from core.eu_styles import EU_COLORS
|
||||||
from plugins.base_plugin import BasePlugin
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
class DashboardPlugin(BasePlugin):
|
class DashboardPlugin(BasePlugin):
|
||||||
"""Main dashboard with overview of all features."""
|
"""Customizable dashboard with avatar statistics."""
|
||||||
|
|
||||||
name = "Dashboard"
|
name = "Dashboard"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
author = "ImpulsiveFPS"
|
author = "ImpulsiveFPS"
|
||||||
description = "Overview dashboard with widgets"
|
description = "Customizable start page with avatar stats"
|
||||||
hotkey = "ctrl+shift+home"
|
hotkey = "ctrl+shift+home"
|
||||||
|
|
||||||
|
# Available widgets
|
||||||
|
AVAILABLE_WIDGETS = {
|
||||||
|
'ped_balance': {'name': 'PED Balance', 'icon': 'dollar-sign', 'default': True},
|
||||||
|
'skill_count': {'name': 'Skills Tracked', 'icon': 'trending-up', 'default': True},
|
||||||
|
'inventory_items': {'name': 'Inventory Items', 'icon': 'archive', 'default': True},
|
||||||
|
'current_dpp': {'name': 'Current DPP', 'icon': 'crosshair', 'default': True},
|
||||||
|
'total_gains_today': {'name': "Today's Skill Gains", 'icon': 'zap', 'default': True},
|
||||||
|
'professions_count': {'name': 'Professions', 'icon': 'award', 'default': False},
|
||||||
|
'missions_active': {'name': 'Active Missions', 'icon': 'map', 'default': False},
|
||||||
|
'codex_progress': {'name': 'Codex Progress', 'icon': 'book', 'default': False},
|
||||||
|
'globals_hofs': {'name': 'Globals/HOFs', 'icon': 'package', 'default': False},
|
||||||
|
'play_time': {'name': 'Session Time', 'icon': 'clock', 'default': False},
|
||||||
|
}
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Setup dashboard."""
|
"""Setup dashboard."""
|
||||||
self.widgets = []
|
self.config_file = Path("data/dashboard_config.json")
|
||||||
|
self.config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
self.enabled_widgets = []
|
||||||
|
self.widget_data = {}
|
||||||
|
|
||||||
|
self._load_config()
|
||||||
|
self._load_data()
|
||||||
|
|
||||||
|
# Auto-refresh timer
|
||||||
|
self.refresh_timer = QTimer()
|
||||||
|
self.refresh_timer.timeout.connect(self._refresh_data)
|
||||||
|
self.refresh_timer.start(5000) # Refresh every 5 seconds
|
||||||
|
|
||||||
|
def _load_config(self):
|
||||||
|
"""Load widget configuration."""
|
||||||
|
if self.config_file.exists():
|
||||||
|
try:
|
||||||
|
with open(self.config_file, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
self.enabled_widgets = config.get('enabled', [])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Default: enable default widgets
|
||||||
|
if not self.enabled_widgets:
|
||||||
|
self.enabled_widgets = [
|
||||||
|
k for k, v in self.AVAILABLE_WIDGETS.items() if v['default']
|
||||||
|
]
|
||||||
|
|
||||||
|
def _save_config(self):
|
||||||
|
"""Save widget configuration."""
|
||||||
|
with open(self.config_file, 'w') as f:
|
||||||
|
json.dump({'enabled': self.enabled_widgets}, f)
|
||||||
|
|
||||||
|
def _load_data(self):
|
||||||
|
"""Load data from other plugins."""
|
||||||
|
# Try to get data from other plugin files
|
||||||
|
data_dir = Path("data")
|
||||||
|
|
||||||
|
# PED from inventory
|
||||||
|
inv_file = data_dir / "inventory.json"
|
||||||
|
if inv_file.exists():
|
||||||
|
try:
|
||||||
|
with open(inv_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
items = data.get('items', [])
|
||||||
|
total_tt = sum(item.get('tt', 0) for item in items)
|
||||||
|
self.widget_data['ped_balance'] = total_tt
|
||||||
|
except:
|
||||||
|
self.widget_data['ped_balance'] = 0
|
||||||
|
|
||||||
|
# Skills
|
||||||
|
skills_file = data_dir / "skill_tracker.json"
|
||||||
|
if skills_file.exists():
|
||||||
|
try:
|
||||||
|
with open(skills_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.widget_data['skill_count'] = len(data.get('skills', {}))
|
||||||
|
self.widget_data['total_gains_today'] = len([
|
||||||
|
g for g in data.get('gains', [])
|
||||||
|
if datetime.fromisoformat(g['time']).date() == datetime.now().date()
|
||||||
|
])
|
||||||
|
except:
|
||||||
|
self.widget_data['skill_count'] = 0
|
||||||
|
self.widget_data['total_gains_today'] = 0
|
||||||
|
|
||||||
|
# Inventory count
|
||||||
|
if inv_file.exists():
|
||||||
|
try:
|
||||||
|
with open(inv_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.widget_data['inventory_items'] = len(data.get('items', []))
|
||||||
|
except:
|
||||||
|
self.widget_data['inventory_items'] = 0
|
||||||
|
|
||||||
|
# Professions
|
||||||
|
prof_file = data_dir / "professions.json"
|
||||||
|
if prof_file.exists():
|
||||||
|
try:
|
||||||
|
with open(prof_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.widget_data['professions_count'] = len(data.get('professions', {}))
|
||||||
|
except:
|
||||||
|
self.widget_data['professions_count'] = 0
|
||||||
|
|
||||||
|
def _refresh_data(self):
|
||||||
|
"""Refresh widget data."""
|
||||||
|
self._load_data()
|
||||||
|
if hasattr(self, 'widgets_container'):
|
||||||
|
self._update_widgets()
|
||||||
|
|
||||||
def get_ui(self):
|
def get_ui(self):
|
||||||
"""Create dashboard UI."""
|
"""Create dashboard UI."""
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
widget.setStyleSheet("background: transparent;")
|
layout = QVBoxLayout(widget)
|
||||||
|
|
||||||
# Main scroll area
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
container = QWidget()
|
|
||||||
layout = QVBoxLayout(container)
|
|
||||||
layout.setSpacing(15)
|
layout.setSpacing(15)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
# Welcome
|
# Header with customize button
|
||||||
welcome = QLabel(" Welcome to EU-Utility")
|
header = QHBoxLayout()
|
||||||
welcome.setStyleSheet(f"""
|
|
||||||
color: {EU_COLORS['accent_orange']};
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: bold;
|
|
||||||
""")
|
|
||||||
layout.addWidget(welcome)
|
|
||||||
|
|
||||||
subtitle = QLabel("Your Entropia Universe companion")
|
title = QLabel("Dashboard")
|
||||||
subtitle.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 12px;")
|
title.setStyleSheet("font-size: 20px; font-weight: bold; color: white;")
|
||||||
layout.addWidget(subtitle)
|
header.addWidget(title)
|
||||||
|
|
||||||
# Quick stats row
|
header.addStretch()
|
||||||
stats_layout = QHBoxLayout()
|
|
||||||
stats_layout.setSpacing(10)
|
|
||||||
|
|
||||||
stats = [
|
customize_btn = QPushButton("Customize")
|
||||||
("💰 PED", "26.02", "Balance"),
|
customize_btn.setStyleSheet(f"""
|
||||||
("Skills", "12", "Tracked"),
|
QPushButton {{
|
||||||
("Items", "98", "In Inventory"),
|
|
||||||
("🎯 DPP", "3.45", "Current"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for icon, value, label in stats:
|
|
||||||
card = self._create_stat_card(icon, value, label)
|
|
||||||
stats_layout.addWidget(card)
|
|
||||||
|
|
||||||
layout.addLayout(stats_layout)
|
|
||||||
|
|
||||||
# Quick actions
|
|
||||||
actions_label = QLabel("Quick Actions")
|
|
||||||
actions_label.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 13px; font-weight: bold;")
|
|
||||||
layout.addWidget(actions_label)
|
|
||||||
|
|
||||||
actions_grid = QGridLayout()
|
|
||||||
actions_grid.setSpacing(10)
|
|
||||||
|
|
||||||
actions = [
|
|
||||||
("Search", "Search items, mobs, locations"),
|
|
||||||
("Scan", "OCR scan game windows"),
|
|
||||||
("Skills", "Track skill gains"),
|
|
||||||
("Loot", "Track hunting loot"),
|
|
||||||
("⛏️ Mine", "Track mining finds"),
|
|
||||||
("📈 Market", "Auction price tracking"),
|
|
||||||
]
|
|
||||||
|
|
||||||
for i, (name, desc) in enumerate(actions):
|
|
||||||
btn = self._create_action_button(name, desc)
|
|
||||||
actions_grid.addWidget(btn, i // 3, i % 3)
|
|
||||||
|
|
||||||
layout.addLayout(actions_grid)
|
|
||||||
|
|
||||||
# Recent activity
|
|
||||||
activity_label = QLabel("Recent Activity")
|
|
||||||
activity_label.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 13px; font-weight: bold; margin-top: 10px;")
|
|
||||||
layout.addWidget(activity_label)
|
|
||||||
|
|
||||||
activity_frame = QFrame()
|
|
||||||
activity_frame.setStyleSheet(f"""
|
|
||||||
QFrame {{
|
|
||||||
background-color: {EU_COLORS['bg_panel']};
|
background-color: {EU_COLORS['bg_panel']};
|
||||||
|
color: {EU_COLORS['text_secondary']};
|
||||||
border: 1px solid {EU_COLORS['border_subtle']};
|
border: 1px solid {EU_COLORS['border_subtle']};
|
||||||
border-radius: 6px;
|
border-radius: 4px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}}
|
||||||
|
QPushButton:hover {{
|
||||||
|
background-color: {EU_COLORS['bg_hover']};
|
||||||
|
border-color: {EU_COLORS['accent_orange']};
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
activity_layout = QVBoxLayout(activity_frame)
|
customize_btn.clicked.connect(self._show_customize_dialog)
|
||||||
|
header.addWidget(customize_btn)
|
||||||
|
|
||||||
activities = [
|
layout.addLayout(header)
|
||||||
"Scanned inventory - 26.02 PED",
|
|
||||||
"Tracked skill gain: +5.2 Aim",
|
|
||||||
"Recorded loot: Animal Hide (0.03 PED)",
|
|
||||||
"→ Mission progress: 12/100 Oratan",
|
|
||||||
]
|
|
||||||
|
|
||||||
for activity in activities:
|
# Scroll area for widgets
|
||||||
lbl = QLabel(activity)
|
scroll = QScrollArea()
|
||||||
lbl.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 11px; padding: 4px 0;")
|
scroll.setWidgetResizable(True)
|
||||||
activity_layout.addWidget(lbl)
|
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
|
scroll.setStyleSheet("background: transparent; border: none;")
|
||||||
|
|
||||||
layout.addWidget(activity_frame)
|
self.widgets_container = QWidget()
|
||||||
|
self.widgets_layout = QGridLayout(self.widgets_container)
|
||||||
|
self.widgets_layout.setSpacing(15)
|
||||||
|
self.widgets_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
# Tips
|
self._update_widgets()
|
||||||
tips_frame = QFrame()
|
|
||||||
tips_frame.setStyleSheet(f"""
|
|
||||||
QFrame {{
|
|
||||||
background-color: rgba(255, 140, 66, 20);
|
|
||||||
border: 1px solid rgba(255, 140, 66, 60);
|
|
||||||
border-radius: 6px;
|
|
||||||
}}
|
|
||||||
""")
|
|
||||||
tips_layout = QVBoxLayout(tips_frame)
|
|
||||||
|
|
||||||
tip_title = QLabel("Pro Tip")
|
scroll.setWidget(self.widgets_container)
|
||||||
tip_title.setStyleSheet(f"color: {EU_COLORS['accent_orange']}; font-weight: bold; font-size: 11px;")
|
layout.addWidget(scroll)
|
||||||
tips_layout.addWidget(tip_title)
|
|
||||||
|
|
||||||
tip_text = QLabel("Press Ctrl+Shift+U anytime to toggle this overlay. Use Ctrl+Shift+H to hide all widgets.")
|
|
||||||
tip_text.setStyleSheet(f"color: {EU_COLORS['text_secondary']}; font-size: 11px;")
|
|
||||||
tip_text.setWordWrap(True)
|
|
||||||
tips_layout.addWidget(tip_text)
|
|
||||||
|
|
||||||
layout.addWidget(tips_frame)
|
|
||||||
layout.addStretch()
|
|
||||||
|
|
||||||
scroll.setWidget(container)
|
|
||||||
|
|
||||||
main_layout = QVBoxLayout(widget)
|
|
||||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
main_layout.addWidget(scroll)
|
|
||||||
|
|
||||||
return widget
|
return widget
|
||||||
|
|
||||||
def _create_stat_card(self, icon, value, label):
|
def _update_widgets(self):
|
||||||
"""Create a stat card widget."""
|
"""Update widget display."""
|
||||||
|
# Clear existing
|
||||||
|
while self.widgets_layout.count():
|
||||||
|
item = self.widgets_layout.takeAt(0)
|
||||||
|
if item.widget():
|
||||||
|
item.widget().deleteLater()
|
||||||
|
|
||||||
|
# Add enabled widgets
|
||||||
|
col = 0
|
||||||
|
row = 0
|
||||||
|
for widget_id in self.enabled_widgets:
|
||||||
|
if widget_id in self.AVAILABLE_WIDGETS:
|
||||||
|
widget_info = self.AVAILABLE_WIDGETS[widget_id]
|
||||||
|
card = self._create_widget_card(
|
||||||
|
widget_id,
|
||||||
|
widget_info['name'],
|
||||||
|
widget_info['icon']
|
||||||
|
)
|
||||||
|
self.widgets_layout.addWidget(card, row, col)
|
||||||
|
|
||||||
|
col += 1
|
||||||
|
if col >= 2: # 2 columns
|
||||||
|
col = 0
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
def _create_widget_card(self, widget_id, name, icon_name):
|
||||||
|
"""Create a stat widget card."""
|
||||||
card = QFrame()
|
card = QFrame()
|
||||||
card.setStyleSheet(f"""
|
card.setStyleSheet(f"""
|
||||||
QFrame {{
|
QFrame {{
|
||||||
background-color: {EU_COLORS['bg_panel']};
|
background-color: {EU_COLORS['bg_panel']};
|
||||||
border: 1px solid {EU_COLORS['border_subtle']};
|
border: 1px solid {EU_COLORS['border_subtle']};
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
layout = QVBoxLayout(card)
|
layout = QVBoxLayout(card)
|
||||||
layout.setContentsMargins(12, 10, 12, 10)
|
layout.setContentsMargins(15, 15, 15, 15)
|
||||||
layout.setSpacing(4)
|
layout.setSpacing(8)
|
||||||
|
|
||||||
value_lbl = QLabel(f"{icon} {value}")
|
# Title
|
||||||
value_lbl.setStyleSheet(f"color: {EU_COLORS['accent_orange']}; font-size: 16px; font-weight: bold;")
|
title = QLabel(name)
|
||||||
layout.addWidget(value_lbl)
|
title.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 11px;")
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
label_lbl = QLabel(label)
|
# Value
|
||||||
label_lbl.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 10px;")
|
value = self.widget_data.get(widget_id, 0)
|
||||||
layout.addWidget(label_lbl)
|
|
||||||
|
|
||||||
|
if widget_id == 'ped_balance':
|
||||||
|
value_text = f"{value:.2f} PED"
|
||||||
|
elif widget_id == 'play_time':
|
||||||
|
value_text = "2h 34m" # Placeholder
|
||||||
|
elif widget_id == 'current_dpp':
|
||||||
|
value_text = "3.45"
|
||||||
|
else:
|
||||||
|
value_text = str(value)
|
||||||
|
|
||||||
|
value_label = QLabel(value_text)
|
||||||
|
value_label.setStyleSheet(f"""
|
||||||
|
color: {EU_COLORS['accent_orange']};
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
""")
|
||||||
|
layout.addWidget(value_label)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
return card
|
return card
|
||||||
|
|
||||||
def _create_action_button(self, name, description):
|
def _show_customize_dialog(self):
|
||||||
"""Create an action button."""
|
"""Show widget customization dialog."""
|
||||||
btn = QPushButton()
|
dialog = QDialog()
|
||||||
btn.setFixedHeight(60)
|
dialog.setWindowTitle("Customize Dashboard")
|
||||||
btn.setStyleSheet(f"""
|
dialog.setStyleSheet(f"""
|
||||||
QPushButton {{
|
QDialog {{
|
||||||
background-color: {EU_COLORS['bg_panel']};
|
background-color: {EU_COLORS['bg_dark']};
|
||||||
border: 1px solid {EU_COLORS['border_subtle']};
|
color: white;
|
||||||
border-radius: 6px;
|
|
||||||
text-align: left;
|
|
||||||
padding: 10px;
|
|
||||||
}}
|
}}
|
||||||
QPushButton:hover {{
|
QLabel {{
|
||||||
background-color: {EU_COLORS['bg_hover']};
|
color: white;
|
||||||
border: 1px solid {EU_COLORS['border_orange']};
|
|
||||||
}}
|
}}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
# Layout for button content
|
layout = QVBoxLayout(dialog)
|
||||||
btn_widget = QWidget()
|
|
||||||
btn_layout = QVBoxLayout(btn_widget)
|
|
||||||
btn_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
btn_layout.setSpacing(2)
|
|
||||||
|
|
||||||
name_lbl = QLabel(name)
|
# Instructions
|
||||||
name_lbl.setStyleSheet(f"color: white; font-weight: bold; font-size: 12px;")
|
info = QLabel("Check widgets to display on dashboard:")
|
||||||
btn_layout.addWidget(name_lbl)
|
info.setStyleSheet(f"color: {EU_COLORS['text_secondary']};")
|
||||||
|
layout.addWidget(info)
|
||||||
|
|
||||||
desc_lbl = QLabel(description)
|
# Widget list
|
||||||
desc_lbl.setStyleSheet(f"color: {EU_COLORS['text_muted']}; font-size: 9px;")
|
list_widget = QListWidget()
|
||||||
btn_layout.addWidget(desc_lbl)
|
list_widget.setStyleSheet(f"""
|
||||||
|
QListWidget {{
|
||||||
|
background-color: {EU_COLORS['bg_panel']};
|
||||||
|
color: white;
|
||||||
|
border: 1px solid {EU_COLORS['border_subtle']};
|
||||||
|
}}
|
||||||
|
QListWidget::item {{
|
||||||
|
padding: 10px;
|
||||||
|
}}
|
||||||
|
""")
|
||||||
|
|
||||||
return btn
|
for widget_id, widget_info in self.AVAILABLE_WIDGETS.items():
|
||||||
|
item = QListWidgetItem(widget_info['name'])
|
||||||
|
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||||
|
item.setCheckState(
|
||||||
|
Qt.CheckState.Checked if widget_id in self.enabled_widgets
|
||||||
|
else Qt.CheckState.Unchecked
|
||||||
|
)
|
||||||
|
item.setData(Qt.ItemDataRole.UserRole, widget_id)
|
||||||
|
list_widget.addItem(item)
|
||||||
|
|
||||||
|
layout.addWidget(list_widget)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
buttons = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
|
buttons.accepted.connect(dialog.accept)
|
||||||
|
buttons.rejected.connect(dialog.reject)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
||||||
|
# Save selection
|
||||||
|
self.enabled_widgets = []
|
||||||
|
for i in range(list_widget.count()):
|
||||||
|
item = list_widget.item(i)
|
||||||
|
if item.checkState() == Qt.CheckState.Checked:
|
||||||
|
self.enabled_widgets.append(item.data(Qt.ItemDataRole.UserRole))
|
||||||
|
|
||||||
|
self._save_config()
|
||||||
|
self._update_widgets()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""
|
||||||
|
Profession Scanner Plugin
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .plugin import ProfessionScannerPlugin
|
||||||
|
|
||||||
|
__all__ = ["ProfessionScannerPlugin"]
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
"""
|
||||||
|
EU-Utility - Profession Scanner Plugin
|
||||||
|
|
||||||
|
Scan and track profession progress with OCR.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
|
||||||
|
QFrame, QGroupBox, QComboBox
|
||||||
|
)
|
||||||
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||||
|
|
||||||
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
|
class ProfessionOCRThread(QThread):
|
||||||
|
"""OCR scan for professions window."""
|
||||||
|
scan_complete = pyqtSignal(dict)
|
||||||
|
scan_error = pyqtSignal(str)
|
||||||
|
progress_update = pyqtSignal(str)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Perform OCR scan."""
|
||||||
|
try:
|
||||||
|
self.progress_update.emit("Capturing screen...")
|
||||||
|
|
||||||
|
import pyautogui
|
||||||
|
screenshot = pyautogui.screenshot()
|
||||||
|
|
||||||
|
self.progress_update.emit("Running OCR...")
|
||||||
|
|
||||||
|
# OCR
|
||||||
|
try:
|
||||||
|
import easyocr
|
||||||
|
reader = easyocr.Reader(['en'], verbose=False)
|
||||||
|
results = reader.readtext(screenshot)
|
||||||
|
text = '\n'.join([r[1] for r in results])
|
||||||
|
except:
|
||||||
|
import pytesseract
|
||||||
|
from PIL import Image
|
||||||
|
text = pytesseract.image_to_string(screenshot)
|
||||||
|
|
||||||
|
# Parse professions
|
||||||
|
professions = self._parse_professions(text)
|
||||||
|
self.scan_complete.emit(professions)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.scan_error.emit(str(e))
|
||||||
|
|
||||||
|
def _parse_professions(self, text):
|
||||||
|
"""Parse profession data from OCR text."""
|
||||||
|
professions = {}
|
||||||
|
|
||||||
|
# Pattern: ProfessionName Rank %Progress
|
||||||
|
# Example: "Laser Pistoleer (Hit) Elite, 72 68.3%"
|
||||||
|
lines = text.split('\n')
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
# Match profession with rank and percentage
|
||||||
|
match = re.search(r'(\w+(?:\s+\w+)*)\s+\(?(\w+)?\)?\s+(Elite|Champion|Astonishing|Remarkable|Outstanding|Marvelous|Prodigious|Amazing|Incredible|Awesome),?\s+(\d+)[,\s]+(\d+\.?\d*)%?', line)
|
||||||
|
if match:
|
||||||
|
prof_name = match.group(1).strip()
|
||||||
|
spec = match.group(2) or ""
|
||||||
|
rank_name = match.group(3)
|
||||||
|
rank_num = match.group(4)
|
||||||
|
progress = float(match.group(5))
|
||||||
|
|
||||||
|
full_name = f"{prof_name} ({spec})" if spec else prof_name
|
||||||
|
|
||||||
|
professions[full_name] = {
|
||||||
|
'rank_name': rank_name,
|
||||||
|
'rank_num': int(rank_num),
|
||||||
|
'progress': progress,
|
||||||
|
'scanned_at': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
return professions
|
||||||
|
|
||||||
|
|
||||||
|
class ProfessionScannerPlugin(BasePlugin):
|
||||||
|
"""Scan and track profession progress."""
|
||||||
|
|
||||||
|
name = "Profession Scanner"
|
||||||
|
version = "1.0.0"
|
||||||
|
author = "ImpulsiveFPS"
|
||||||
|
description = "Track profession ranks and progress"
|
||||||
|
hotkey = "ctrl+shift+p"
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
"""Setup profession scanner."""
|
||||||
|
self.data_file = Path("data/professions.json")
|
||||||
|
self.data_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
self.professions = {}
|
||||||
|
self._load_data()
|
||||||
|
|
||||||
|
def _load_data(self):
|
||||||
|
"""Load saved data."""
|
||||||
|
if self.data_file.exists():
|
||||||
|
try:
|
||||||
|
with open(self.data_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.professions = data.get('professions', {})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_data(self):
|
||||||
|
"""Save data."""
|
||||||
|
with open(self.data_file, 'w') as f:
|
||||||
|
json.dump({'professions': self.professions}, f, indent=2)
|
||||||
|
|
||||||
|
def get_ui(self):
|
||||||
|
"""Create profession scanner UI."""
|
||||||
|
widget = QWidget()
|
||||||
|
layout = QVBoxLayout(widget)
|
||||||
|
layout.setSpacing(15)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
header = QLabel("Profession Tracker")
|
||||||
|
header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
|
||||||
|
layout.addWidget(header)
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
summary = QHBoxLayout()
|
||||||
|
self.total_label = QLabel(f"Professions: {len(self.professions)}")
|
||||||
|
self.total_label.setStyleSheet("color: #4ecdc4; font-weight: bold;")
|
||||||
|
summary.addWidget(self.total_label)
|
||||||
|
|
||||||
|
summary.addStretch()
|
||||||
|
layout.addLayout(summary)
|
||||||
|
|
||||||
|
# Scan button
|
||||||
|
scan_btn = QPushButton("Scan Professions Window")
|
||||||
|
scan_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #ff8c42;
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
scan_btn.clicked.connect(self._scan_professions)
|
||||||
|
layout.addWidget(scan_btn)
|
||||||
|
|
||||||
|
# Progress
|
||||||
|
self.progress_label = QLabel("Ready to scan")
|
||||||
|
self.progress_label.setStyleSheet("color: rgba(255,255,255,150);")
|
||||||
|
layout.addWidget(self.progress_label)
|
||||||
|
|
||||||
|
# Professions table
|
||||||
|
self.prof_table = QTableWidget()
|
||||||
|
self.prof_table.setColumnCount(4)
|
||||||
|
self.prof_table.setHorizontalHeaderLabels(["Profession", "Rank", "Level", "Progress"])
|
||||||
|
self.prof_table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
|
||||||
|
# Style table
|
||||||
|
self.prof_table.setStyleSheet("""
|
||||||
|
QTableWidget {
|
||||||
|
background-color: rgba(30, 35, 45, 200);
|
||||||
|
color: white;
|
||||||
|
border: 1px solid rgba(100, 110, 130, 80);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
QHeaderView::section {
|
||||||
|
background-color: rgba(35, 40, 55, 200);
|
||||||
|
color: rgba(255,255,255,180);
|
||||||
|
padding: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout.addWidget(self.prof_table)
|
||||||
|
|
||||||
|
# Refresh table
|
||||||
|
self._refresh_table()
|
||||||
|
|
||||||
|
return widget
|
||||||
|
|
||||||
|
def _scan_professions(self):
|
||||||
|
"""Start OCR scan."""
|
||||||
|
self.scanner = ProfessionOCRThread()
|
||||||
|
self.scanner.scan_complete.connect(self._on_scan_complete)
|
||||||
|
self.scanner.scan_error.connect(self._on_scan_error)
|
||||||
|
self.scanner.progress_update.connect(self._on_progress)
|
||||||
|
self.scanner.start()
|
||||||
|
|
||||||
|
def _on_progress(self, message):
|
||||||
|
"""Update progress."""
|
||||||
|
self.progress_label.setText(message)
|
||||||
|
|
||||||
|
def _on_scan_complete(self, professions):
|
||||||
|
"""Handle scan completion."""
|
||||||
|
self.professions.update(professions)
|
||||||
|
self._save_data()
|
||||||
|
self._refresh_table()
|
||||||
|
self.progress_label.setText(f"Found {len(professions)} professions")
|
||||||
|
self.total_label.setText(f"Professions: {len(self.professions)}")
|
||||||
|
|
||||||
|
def _on_scan_error(self, error):
|
||||||
|
"""Handle error."""
|
||||||
|
self.progress_label.setText(f"Error: {error}")
|
||||||
|
|
||||||
|
def _refresh_table(self):
|
||||||
|
"""Refresh professions table."""
|
||||||
|
self.prof_table.setRowCount(len(self.professions))
|
||||||
|
|
||||||
|
for i, (name, data) in enumerate(sorted(self.professions.items())):
|
||||||
|
self.prof_table.setItem(i, 0, QTableWidgetItem(name))
|
||||||
|
self.prof_table.setItem(i, 1, QTableWidgetItem(data.get('rank_name', '-')))
|
||||||
|
self.prof_table.setItem(i, 2, QTableWidgetItem(str(data.get('rank_num', 0))))
|
||||||
|
|
||||||
|
# Progress with bar
|
||||||
|
progress = data.get('progress', 0)
|
||||||
|
progress_widget = QWidget()
|
||||||
|
progress_layout = QHBoxLayout(progress_widget)
|
||||||
|
progress_layout.setContentsMargins(5, 2, 5, 2)
|
||||||
|
|
||||||
|
bar = QProgressBar()
|
||||||
|
bar.setValue(int(progress))
|
||||||
|
bar.setTextVisible(True)
|
||||||
|
bar.setFormat(f"{progress:.1f}%")
|
||||||
|
bar.setStyleSheet("""
|
||||||
|
QProgressBar {
|
||||||
|
background-color: rgba(60, 70, 90, 150);
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
QProgressBar::chunk {
|
||||||
|
background-color: #ff8c42;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
progress_layout.addWidget(bar)
|
||||||
|
|
||||||
|
self.prof_table.setCellWidget(i, 3, progress_widget)
|
||||||
|
|
@ -1,673 +1,333 @@
|
||||||
"""
|
"""
|
||||||
EU-Utility - Skill Scanner Plugin
|
EU-Utility - Skill Scanner Plugin with OCR + Log Tracking
|
||||||
|
|
||||||
Tracks skill levels with decimal precision by reading:
|
Scans skills window AND tracks gains from chat log automatically.
|
||||||
1. Skills window (OCR for values + progress bar detection)
|
|
||||||
2. ESI item windows (skill gain prediction)
|
|
||||||
3. Chat log (skill gain messages)
|
|
||||||
|
|
||||||
Data stored for skill formula analysis.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QHBoxLayout,
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
QLabel, QPushButton, QTableWidget, QTableWidgetItem,
|
QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
|
||||||
QHeaderView, QTextEdit, QComboBox, QSpinBox, QGroupBox,
|
QFrame, QGroupBox, QTextEdit, QSplitter
|
||||||
QFileDialog, QMessageBox, QTabWidget
|
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
|
||||||
from PyQt6.QtGui import QColor
|
|
||||||
|
|
||||||
from plugins.base_plugin import BasePlugin
|
from plugins.base_plugin import BasePlugin
|
||||||
|
|
||||||
|
|
||||||
class SkillDataStore:
|
class LogWatcherThread(QThread):
|
||||||
"""Store skill data for formula analysis."""
|
"""Watch chat log for skill gains."""
|
||||||
|
skill_gain_detected = pyqtSignal(str, float, float) # skill_name, gain_amount, new_total
|
||||||
|
|
||||||
def __init__(self, data_dir="data/skills"):
|
def __init__(self, log_path, parent=None):
|
||||||
self.data_dir = Path(data_dir)
|
super().__init__(parent)
|
||||||
self.data_dir.mkdir(parents=True, exist_ok=True)
|
self.log_path = Path(log_path)
|
||||||
self.skills_file = self.data_dir / "skill_snapshots.json"
|
self.running = True
|
||||||
self.esi_file = self.data_dir / "esi_data.json"
|
self.last_position = 0
|
||||||
self.gains_file = self.data_dir / "skill_gains.json"
|
|
||||||
|
|
||||||
def save_skill_snapshot(self, skills_data):
|
|
||||||
"""Save a skill snapshot with timestamp."""
|
|
||||||
snapshot = {
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"skills": skills_data
|
|
||||||
}
|
|
||||||
|
|
||||||
# Append to history
|
# Skill gain patterns
|
||||||
history = self.load_skill_history()
|
self.patterns = [
|
||||||
history.append(snapshot)
|
r'(\w+(?:\s+\w+)*)\s+has\s+improved\s+by\s+(\d+\.?\d*)\s+points?', # "Aim has improved by 5.2 points"
|
||||||
|
r'You\s+gained\s+(\d+\.?\d*)\s+points?\s+in\s+(\w+(?:\s+\w+)*)', # "You gained 10 points in Rifle"
|
||||||
# Keep last 1000 snapshots
|
r'(\w+(?:\s+\w+)*)\s+\+(\d+\.?\d*)', # "Rifle +15"
|
||||||
if len(history) > 1000:
|
]
|
||||||
history = history[-1000:]
|
|
||||||
|
|
||||||
with open(self.skills_file, 'w') as f:
|
|
||||||
json.dump(history, f, indent=2)
|
|
||||||
|
|
||||||
def load_skill_history(self):
|
|
||||||
"""Load skill history."""
|
|
||||||
if self.skills_file.exists():
|
|
||||||
with open(self.skills_file, 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def save_esi_data(self, esi_data):
|
|
||||||
"""Save ESI scan data."""
|
|
||||||
entry = {
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"esi": esi_data
|
|
||||||
}
|
|
||||||
|
|
||||||
history = self.load_esi_history()
|
|
||||||
history.append(entry)
|
|
||||||
|
|
||||||
with open(self.esi_file, 'w') as f:
|
|
||||||
json.dump(history, f, indent=2)
|
|
||||||
|
|
||||||
def load_esi_history(self):
|
|
||||||
"""Load ESI history."""
|
|
||||||
if self.esi_file.exists():
|
|
||||||
with open(self.esi_file, 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def save_skill_gain(self, skill_name, points_gained, new_total=None):
|
|
||||||
"""Save a skill gain from chat."""
|
|
||||||
entry = {
|
|
||||||
"timestamp": datetime.now().isoformat(),
|
|
||||||
"skill": skill_name,
|
|
||||||
"gained": points_gained,
|
|
||||||
"new_total": new_total
|
|
||||||
}
|
|
||||||
|
|
||||||
history = self.load_gain_history()
|
|
||||||
history.append(entry)
|
|
||||||
|
|
||||||
with open(self.gains_file, 'w') as f:
|
|
||||||
json.dump(history, f, indent=2)
|
|
||||||
|
|
||||||
def load_gain_history(self):
|
|
||||||
"""Load skill gain history."""
|
|
||||||
if self.gains_file.exists():
|
|
||||||
with open(self.gains_file, 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
return []
|
|
||||||
|
|
||||||
def export_to_csv(self, filepath):
|
|
||||||
"""Export all data to CSV for analysis."""
|
|
||||||
import csv
|
|
||||||
|
|
||||||
with open(filepath, 'w', newline='') as f:
|
|
||||||
writer = csv.writer(f)
|
|
||||||
writer.writerow(['timestamp', 'skill', 'rank', 'points', 'progress_pct', 'source'])
|
|
||||||
|
|
||||||
# Export skill snapshots
|
|
||||||
for snapshot in self.load_skill_history():
|
|
||||||
ts = snapshot['timestamp']
|
|
||||||
for skill_name, data in snapshot['skills'].items():
|
|
||||||
writer.writerow([
|
|
||||||
ts,
|
|
||||||
skill_name,
|
|
||||||
data.get('rank', ''),
|
|
||||||
data.get('points', ''),
|
|
||||||
data.get('progress_pct', ''),
|
|
||||||
'snapshot'
|
|
||||||
])
|
|
||||||
|
|
||||||
# Export skill gains
|
|
||||||
for gain in self.load_gain_history():
|
|
||||||
writer.writerow([
|
|
||||||
gain['timestamp'],
|
|
||||||
gain['skill'],
|
|
||||||
'',
|
|
||||||
gain.get('gained', ''),
|
|
||||||
'',
|
|
||||||
'chat_gain'
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
class SkillScannerThread(QThread):
|
|
||||||
"""Background thread for skill window OCR."""
|
|
||||||
result_ready = pyqtSignal(dict)
|
|
||||||
error_occurred = pyqtSignal(str)
|
|
||||||
progress_update = pyqtSignal(str)
|
|
||||||
|
|
||||||
def __init__(self, capture_mode="full_screen"):
|
|
||||||
super().__init__()
|
|
||||||
self.capture_mode = capture_mode
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Capture screen and extract skill data."""
|
"""Watch log file for changes."""
|
||||||
|
while self.running:
|
||||||
|
try:
|
||||||
|
if self.log_path.exists():
|
||||||
|
with open(self.log_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||||
|
f.seek(self.last_position)
|
||||||
|
new_lines = f.readlines()
|
||||||
|
self.last_position = f.tell()
|
||||||
|
|
||||||
|
for line in new_lines:
|
||||||
|
self._parse_line(line.strip())
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[LogWatcher] Error: {e}")
|
||||||
|
|
||||||
|
time.sleep(0.5) # Check every 500ms
|
||||||
|
|
||||||
|
def _parse_line(self, line):
|
||||||
|
"""Parse a log line for skill gains."""
|
||||||
|
for pattern in self.patterns:
|
||||||
|
match = re.search(pattern, line, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
groups = match.groups()
|
||||||
|
if len(groups) == 2:
|
||||||
|
# Determine which group is skill and which is value
|
||||||
|
try:
|
||||||
|
value = float(groups[1])
|
||||||
|
skill = groups[0]
|
||||||
|
self.skill_gain_detected.emit(skill, value, 0) # new_total calculated later
|
||||||
|
except ValueError:
|
||||||
|
# Try reversed
|
||||||
|
try:
|
||||||
|
value = float(groups[0])
|
||||||
|
skill = groups[1]
|
||||||
|
self.skill_gain_detected.emit(skill, value, 0)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Stop watching."""
|
||||||
|
self.running = False
|
||||||
|
|
||||||
|
|
||||||
|
class OCRScannerThread(QThread):
|
||||||
|
"""OCR scan thread for skills window."""
|
||||||
|
scan_complete = pyqtSignal(dict)
|
||||||
|
scan_error = pyqtSignal(str)
|
||||||
|
progress_update = pyqtSignal(str)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Perform OCR scan."""
|
||||||
try:
|
try:
|
||||||
self.progress_update.emit("Capturing screen...")
|
self.progress_update.emit("Capturing screen...")
|
||||||
|
|
||||||
# Capture screen
|
# Use pyautogui for screenshot
|
||||||
screenshot = self._capture_screen()
|
|
||||||
if screenshot is None:
|
|
||||||
self.error_occurred.emit("Failed to capture screen")
|
|
||||||
return
|
|
||||||
|
|
||||||
self.progress_update.emit("Analyzing skills window...")
|
|
||||||
|
|
||||||
# Extract skill data
|
|
||||||
skills_data = self._extract_skills(screenshot)
|
|
||||||
|
|
||||||
self.progress_update.emit(f"✅ Found {len(skills_data)} skills")
|
|
||||||
self.result_ready.emit(skills_data)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.error_occurred.emit(str(e))
|
|
||||||
|
|
||||||
def _capture_screen(self):
|
|
||||||
"""Capture full screen."""
|
|
||||||
try:
|
|
||||||
import pyautogui
|
|
||||||
return pyautogui.screenshot()
|
|
||||||
except ImportError:
|
|
||||||
# Fallback to PIL
|
|
||||||
from PIL import ImageGrab
|
|
||||||
return ImageGrab.grab()
|
|
||||||
|
|
||||||
def _extract_skills(self, screenshot):
|
|
||||||
"""Extract skill data from screenshot."""
|
|
||||||
skills_data = {}
|
|
||||||
|
|
||||||
# Perform OCR
|
|
||||||
try:
|
|
||||||
import easyocr
|
|
||||||
reader = easyocr.Reader(['en'], verbose=False)
|
|
||||||
results = reader.readtext(screenshot)
|
|
||||||
|
|
||||||
text_lines = [result[1] for result in results]
|
|
||||||
full_text = ' '.join(text_lines)
|
|
||||||
|
|
||||||
# Check if this is a skills window
|
|
||||||
if 'SKILLS' not in full_text.upper() and 'RANK' not in full_text.upper():
|
|
||||||
self.progress_update.emit("⚠️ Skills window not detected")
|
|
||||||
return skills_data
|
|
||||||
|
|
||||||
# Extract skill lines
|
|
||||||
# Pattern: SkillName Rank Points
|
|
||||||
for i, line in enumerate(text_lines):
|
|
||||||
# Look for rank keywords
|
|
||||||
rank_match = self._match_rank(line)
|
|
||||||
if rank_match:
|
|
||||||
# Previous line might be skill name
|
|
||||||
if i > 0:
|
|
||||||
skill_name = text_lines[i-1].strip()
|
|
||||||
rank = rank_match
|
|
||||||
|
|
||||||
# Next line might be points
|
|
||||||
points = 0
|
|
||||||
if i + 1 < len(text_lines):
|
|
||||||
points_str = text_lines[i+1].replace(',', '').replace('.', '')
|
|
||||||
if points_str.isdigit():
|
|
||||||
points = int(points_str)
|
|
||||||
|
|
||||||
# Detect progress bar percentage (approximate from image)
|
|
||||||
progress_pct = self._estimate_progress(screenshot, i)
|
|
||||||
|
|
||||||
skills_data[skill_name] = {
|
|
||||||
'rank': rank,
|
|
||||||
'points': points,
|
|
||||||
'progress_pct': progress_pct,
|
|
||||||
'raw_text': line
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.progress_update.emit(f"OCR error: {e}")
|
|
||||||
|
|
||||||
return skills_data
|
|
||||||
|
|
||||||
def _match_rank(self, text):
|
|
||||||
"""Match rank text."""
|
|
||||||
ranks = [
|
|
||||||
'Newbie', 'Inept', 'Green', 'Beginner', 'Initiated', 'Trainee',
|
|
||||||
'Apprentice', 'Assistant', 'Assistant', 'Capable', 'Competent',
|
|
||||||
'Skilled', 'Talented', 'Expert', 'Grand', 'Great', 'Adept',
|
|
||||||
'Professional', 'Illustrious', 'Eminent', 'Renowned', 'Master',
|
|
||||||
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
|
|
||||||
'Outstanding', 'Marvelous', 'Prodigious', 'Staggering', 'Unrivaled',
|
|
||||||
'Amazing', 'Incredible', 'Awesome', 'Mind-boggling', 'Sensational',
|
|
||||||
'Miraculous', 'Colossal', 'Epic', 'Transcendent', 'Magnificent',
|
|
||||||
'Unearthly', 'Phenomenal', 'Supreme', 'Omnipotent'
|
|
||||||
]
|
|
||||||
|
|
||||||
for rank in ranks:
|
|
||||||
if rank.lower() in text.lower():
|
|
||||||
return rank
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _estimate_progress(self, screenshot, line_index):
|
|
||||||
"""Estimate progress bar percentage from image analysis."""
|
|
||||||
# This would analyze the green bar length
|
|
||||||
# For now, return 0 (can be improved with image processing)
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
|
|
||||||
class ESIScannerThread(QThread):
|
|
||||||
"""Scan ESI item window for skill gain prediction."""
|
|
||||||
result_ready = pyqtSignal(dict)
|
|
||||||
error_occurred = pyqtSignal(str)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""Capture and analyze ESI window."""
|
|
||||||
try:
|
|
||||||
import pyautogui
|
import pyautogui
|
||||||
screenshot = pyautogui.screenshot()
|
screenshot = pyautogui.screenshot()
|
||||||
|
|
||||||
# OCR
|
self.progress_update.emit("Running OCR...")
|
||||||
import easyocr
|
|
||||||
reader = easyocr.Reader(['en'], verbose=False)
|
|
||||||
results = reader.readtext(screenshot)
|
|
||||||
|
|
||||||
text = ' '.join([r[1] for r in results])
|
# Try easyocr first
|
||||||
|
try:
|
||||||
|
import easyocr
|
||||||
|
reader = easyocr.Reader(['en'], verbose=False)
|
||||||
|
results = reader.readtext(screenshot)
|
||||||
|
text = '\n'.join([r[1] for r in results])
|
||||||
|
except:
|
||||||
|
# Fallback to pytesseract
|
||||||
|
import pytesseract
|
||||||
|
from PIL import Image
|
||||||
|
text = pytesseract.image_to_string(screenshot)
|
||||||
|
|
||||||
# Look for ESI patterns
|
# Parse skills from text
|
||||||
esi_data = {
|
skills_data = self._parse_skills(text)
|
||||||
'item_name': None,
|
self.scan_complete.emit(skills_data)
|
||||||
'skill_target': None,
|
|
||||||
'points_to_add': 0,
|
|
||||||
'tt_value': 0.0,
|
|
||||||
'full_text': text
|
|
||||||
}
|
|
||||||
|
|
||||||
# Extract ESI info
|
|
||||||
if 'Empty Skill Implant' in text or 'ESI' in text:
|
|
||||||
# Try to find skill name and points
|
|
||||||
# Pattern: "Inserting this implant will add X points to [SkillName]"
|
|
||||||
patterns = [
|
|
||||||
r'add\s+(\d+)\s+points?\s+to\s+([A-Za-z\s]+)',
|
|
||||||
r'(\d+)\s+points?\s+to\s+([A-Za-z\s]+)',
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in patterns:
|
|
||||||
match = re.search(pattern, text, re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
esi_data['points_to_add'] = int(match.group(1))
|
|
||||||
esi_data['skill_target'] = match.group(2).strip()
|
|
||||||
break
|
|
||||||
|
|
||||||
# Extract TT value
|
|
||||||
tt_match = re.search(r'(\d+\.?\d*)\s*PED', text)
|
|
||||||
if tt_match:
|
|
||||||
esi_data['tt_value'] = float(tt_match.group(1))
|
|
||||||
|
|
||||||
self.result_ready.emit(esi_data)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error_occurred.emit(str(e))
|
self.scan_error.emit(str(e))
|
||||||
|
|
||||||
|
def _parse_skills(self, text):
|
||||||
|
"""Parse skill data from OCR text."""
|
||||||
|
skills = {}
|
||||||
|
lines = text.split('\n')
|
||||||
|
|
||||||
|
# Look for skill patterns
|
||||||
|
# Example: "Aim Amazing 5524"
|
||||||
|
for line in lines:
|
||||||
|
# Pattern: SkillName Rank Points
|
||||||
|
match = re.search(r'(\w+(?:\s+\w+)*)\s+(Newbie|Inept|Beginner|Amateur|Average|Skilled|Expert|Professional|Master|Grand Master|Champion|Legendary|Guru|Astonishing|Remarkable|Outstanding|Marvelous|Prodigious|Amazing|Incredible|Awesome)\s+(\d+)', line, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
skill_name = match.group(1).strip()
|
||||||
|
rank = match.group(2)
|
||||||
|
points = int(match.group(3))
|
||||||
|
skills[skill_name] = {
|
||||||
|
'rank': rank,
|
||||||
|
'points': points,
|
||||||
|
'scanned_at': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
return skills
|
||||||
|
|
||||||
|
|
||||||
class SkillScannerPlugin(BasePlugin):
|
class SkillScannerPlugin(BasePlugin):
|
||||||
"""Scan and track skill progression with decimal precision."""
|
"""Scan skills with OCR and track gains from log."""
|
||||||
|
|
||||||
name = "Skill Scanner"
|
name = "Skill Scanner"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
author = "ImpulsiveFPS"
|
author = "ImpulsiveFPS"
|
||||||
description = "Track skill levels with precision for formula analysis"
|
description = "OCR skill scanning + automatic log tracking"
|
||||||
hotkey = "ctrl+shift+s" # S for Skills
|
hotkey = "ctrl+shift+s"
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
"""Setup skill scanner."""
|
"""Setup skill scanner."""
|
||||||
self.data_store = SkillDataStore()
|
self.data_file = Path("data/skill_tracker.json")
|
||||||
self.scan_thread = None
|
self.data_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
self.esi_thread = None
|
|
||||||
self.last_scan = {}
|
# Load saved data
|
||||||
|
self.skills_data = {}
|
||||||
|
self.skill_gains = []
|
||||||
|
self._load_data()
|
||||||
|
|
||||||
|
# Start log watcher
|
||||||
|
log_path = self._find_chat_log()
|
||||||
|
if log_path:
|
||||||
|
self.log_watcher = LogWatcherThread(log_path)
|
||||||
|
self.log_watcher.skill_gain_detected.connect(self._on_skill_gain)
|
||||||
|
self.log_watcher.start()
|
||||||
|
else:
|
||||||
|
self.log_watcher = None
|
||||||
|
|
||||||
|
def _find_chat_log(self):
|
||||||
|
"""Find EU chat log file."""
|
||||||
|
# Common locations
|
||||||
|
possible_paths = [
|
||||||
|
Path.home() / "Documents" / "Entropia Universe" / "chat.log",
|
||||||
|
Path.home() / "Documents" / "Entropia Universe" / "Logs" / "chat.log",
|
||||||
|
]
|
||||||
|
for path in possible_paths:
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _load_data(self):
|
||||||
|
"""Load saved skill data."""
|
||||||
|
if self.data_file.exists():
|
||||||
|
try:
|
||||||
|
with open(self.data_file, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
self.skills_data = data.get('skills', {})
|
||||||
|
self.skill_gains = data.get('gains', [])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _save_data(self):
|
||||||
|
"""Save skill data."""
|
||||||
|
with open(self.data_file, 'w') as f:
|
||||||
|
json.dump({
|
||||||
|
'skills': self.skills_data,
|
||||||
|
'gains': self.skill_gains
|
||||||
|
}, f, indent=2)
|
||||||
|
|
||||||
|
def _on_skill_gain(self, skill_name, gain_amount, new_total):
|
||||||
|
"""Handle skill gain from log."""
|
||||||
|
# Update skill data
|
||||||
|
if skill_name in self.skills_data:
|
||||||
|
old_points = self.skills_data[skill_name].get('points', 0)
|
||||||
|
self.skills_data[skill_name]['points'] = old_points + gain_amount
|
||||||
|
self.skills_data[skill_name]['last_gain'] = {
|
||||||
|
'amount': gain_amount,
|
||||||
|
'time': datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Record gain
|
||||||
|
self.skill_gains.append({
|
||||||
|
'skill': skill_name,
|
||||||
|
'gain': gain_amount,
|
||||||
|
'time': datetime.now().isoformat()
|
||||||
|
})
|
||||||
|
|
||||||
|
self._save_data()
|
||||||
|
|
||||||
|
# Update UI if visible
|
||||||
|
if hasattr(self, 'gains_text'):
|
||||||
|
self.gains_text.append(f"+{gain_amount} {skill_name}")
|
||||||
|
|
||||||
def get_ui(self):
|
def get_ui(self):
|
||||||
"""Create skill scanner UI."""
|
"""Create skill scanner UI."""
|
||||||
widget = QWidget()
|
widget = QWidget()
|
||||||
widget.setStyleSheet("background: transparent;")
|
|
||||||
|
|
||||||
layout = QVBoxLayout(widget)
|
layout = QVBoxLayout(widget)
|
||||||
layout.setSpacing(15)
|
layout.setSpacing(15)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
# Title
|
# Header
|
||||||
title = QLabel("Skill Scanner")
|
header = QLabel("Skill Tracker")
|
||||||
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
|
header.setStyleSheet("font-size: 18px; font-weight: bold; color: white;")
|
||||||
layout.addWidget(title)
|
layout.addWidget(header)
|
||||||
|
|
||||||
# Info
|
# Splitter for resizable sections
|
||||||
info = QLabel("Scan skills window to track progression with decimal precision")
|
splitter = QSplitter(Qt.Orientation.Vertical)
|
||||||
info.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
|
|
||||||
layout.addWidget(info)
|
|
||||||
|
|
||||||
# Tabs
|
# Scan section
|
||||||
tabs = QTabWidget()
|
scan_group = QGroupBox("OCR Scan")
|
||||||
tabs.setStyleSheet("""
|
scan_layout = QVBoxLayout(scan_group)
|
||||||
QTabWidget::pane {
|
|
||||||
background-color: rgba(0, 0, 0, 50);
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 20);
|
|
||||||
}
|
|
||||||
QTabBar::tab {
|
|
||||||
background-color: rgba(255, 255, 255, 10);
|
|
||||||
color: rgba(255, 255, 255, 150);
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-top-left-radius: 6px;
|
|
||||||
border-top-right-radius: 6px;
|
|
||||||
}
|
|
||||||
QTabBar::tab:selected {
|
|
||||||
background-color: rgba(74, 158, 255, 150);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Skills Tab
|
|
||||||
skills_tab = self._create_skills_tab()
|
|
||||||
tabs.addTab(skills_tab, "🎯 Skills")
|
|
||||||
|
|
||||||
# ESI Tab
|
|
||||||
esi_tab = self._create_esi_tab()
|
|
||||||
tabs.addTab(esi_tab, "💉 ESI Scanner")
|
|
||||||
|
|
||||||
# Data Tab
|
|
||||||
data_tab = self._create_data_tab()
|
|
||||||
tabs.addTab(data_tab, "📈 Data")
|
|
||||||
|
|
||||||
layout.addWidget(tabs)
|
|
||||||
|
|
||||||
return widget
|
|
||||||
|
|
||||||
def _create_skills_tab(self):
|
|
||||||
"""Create skills scanning tab."""
|
|
||||||
tab = QWidget()
|
|
||||||
tab.setStyleSheet("background: transparent;")
|
|
||||||
layout = QVBoxLayout(tab)
|
|
||||||
layout.setSpacing(10)
|
|
||||||
layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
# Scan button
|
|
||||||
scan_btn = QPushButton("Scan Skills Window")
|
scan_btn = QPushButton("Scan Skills Window")
|
||||||
scan_btn.setStyleSheet("""
|
scan_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #4a9eff;
|
background-color: #ff8c42;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 15px;
|
padding: 12px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10px;
|
border-radius: 4px;
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #5aafff;
|
|
||||||
}
|
|
||||||
""")
|
""")
|
||||||
scan_btn.clicked.connect(self._scan_skills)
|
scan_btn.clicked.connect(self._scan_skills)
|
||||||
layout.addWidget(scan_btn)
|
scan_layout.addWidget(scan_btn)
|
||||||
|
|
||||||
# Status
|
# Progress
|
||||||
self.status_label = QLabel("Ready to scan")
|
self.scan_progress = QLabel("Ready to scan")
|
||||||
self.status_label.setStyleSheet("color: #666; font-size: 11px;")
|
self.scan_progress.setStyleSheet("color: rgba(255,255,255,150);")
|
||||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
scan_layout.addWidget(self.scan_progress)
|
||||||
layout.addWidget(self.status_label)
|
|
||||||
|
|
||||||
# Results table
|
# Skills table
|
||||||
self.skills_table = QTableWidget()
|
self.skills_table = QTableWidget()
|
||||||
self.skills_table.setColumnCount(4)
|
self.skills_table.setColumnCount(3)
|
||||||
self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points", "Progress"])
|
self.skills_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"])
|
||||||
self.skills_table.setStyleSheet("""
|
|
||||||
QTableWidget {
|
|
||||||
background-color: rgba(30, 30, 30, 100);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
gridline-color: rgba(255, 255, 255, 20);
|
|
||||||
}
|
|
||||||
QHeaderView::section {
|
|
||||||
background-color: rgba(74, 158, 255, 100);
|
|
||||||
color: white;
|
|
||||||
padding: 6px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.skills_table.horizontalHeader().setStretchLastSection(True)
|
self.skills_table.horizontalHeader().setStretchLastSection(True)
|
||||||
layout.addWidget(self.skills_table)
|
scan_layout.addWidget(self.skills_table)
|
||||||
|
|
||||||
return tab
|
splitter.addWidget(scan_group)
|
||||||
|
|
||||||
def _create_esi_tab(self):
|
|
||||||
"""Create ESI scanning tab."""
|
|
||||||
tab = QWidget()
|
|
||||||
tab.setStyleSheet("background: transparent;")
|
|
||||||
layout = QVBoxLayout(tab)
|
|
||||||
layout.setSpacing(10)
|
|
||||||
layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
# Instructions
|
# Log tracking section
|
||||||
instr = QLabel("Hover over an ESI item and click scan")
|
log_group = QGroupBox("Automatic Log Tracking")
|
||||||
instr.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
|
log_layout = QVBoxLayout(log_group)
|
||||||
layout.addWidget(instr)
|
|
||||||
|
|
||||||
# Scan ESI button
|
log_status = QLabel("Watching chat log for skill gains...")
|
||||||
esi_btn = QPushButton("💉 Scan ESI Item")
|
log_status.setStyleSheet("color: #4ecdc4;")
|
||||||
esi_btn.setStyleSheet("""
|
log_layout.addWidget(log_status)
|
||||||
QPushButton {
|
|
||||||
background-color: #9c27b0;
|
|
||||||
color: white;
|
|
||||||
padding: 15px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #ab47bc;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
esi_btn.clicked.connect(self._scan_esi)
|
|
||||||
layout.addWidget(esi_btn)
|
|
||||||
|
|
||||||
# ESI Results
|
self.gains_text = QTextEdit()
|
||||||
self.esi_result = QTextEdit()
|
self.gains_text.setReadOnly(True)
|
||||||
self.esi_result.setPlaceholderText("ESI scan results will appear here...")
|
self.gains_text.setMaximumHeight(150)
|
||||||
self.esi_result.setStyleSheet("""
|
self.gains_text.setPlaceholderText("Recent skill gains will appear here...")
|
||||||
QTextEdit {
|
log_layout.addWidget(self.gains_text)
|
||||||
background-color: rgba(30, 30, 30, 100);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
self.esi_result.setMaximumHeight(150)
|
|
||||||
layout.addWidget(self.esi_result)
|
|
||||||
|
|
||||||
layout.addStretch()
|
# Total gains summary
|
||||||
return tab
|
self.total_gains_label = QLabel(f"Total gains tracked: {len(self.skill_gains)}")
|
||||||
|
self.total_gains_label.setStyleSheet("color: rgba(255,255,255,150);")
|
||||||
def _create_data_tab(self):
|
log_layout.addWidget(self.total_gains_label)
|
||||||
"""Create data management tab."""
|
|
||||||
tab = QWidget()
|
|
||||||
tab.setStyleSheet("background: transparent;")
|
|
||||||
layout = QVBoxLayout(tab)
|
|
||||||
layout.setSpacing(10)
|
|
||||||
layout.setContentsMargins(10, 10, 10, 10)
|
|
||||||
|
|
||||||
# Stats
|
splitter.addWidget(log_group)
|
||||||
self.stats_label = QLabel("No data collected yet")
|
|
||||||
self.stats_label.setStyleSheet("color: rgba(255, 255, 255, 200);")
|
|
||||||
layout.addWidget(self.stats_label)
|
|
||||||
|
|
||||||
# Export button
|
layout.addWidget(splitter)
|
||||||
export_btn = QPushButton("📁 Export to CSV")
|
|
||||||
export_btn.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background-color: rgba(255, 255, 255, 20);
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 30);
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
export_btn.clicked.connect(self._export_data)
|
|
||||||
layout.addWidget(export_btn)
|
|
||||||
|
|
||||||
# View raw data
|
# Load existing data
|
||||||
view_btn = QPushButton("View Raw JSON")
|
self._refresh_skills_table()
|
||||||
view_btn.setStyleSheet("""
|
|
||||||
QPushButton {
|
|
||||||
background-color: rgba(255, 255, 255, 20);
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: rgba(255, 255, 255, 30);
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
view_btn.clicked.connect(self._view_raw_data)
|
|
||||||
layout.addWidget(view_btn)
|
|
||||||
|
|
||||||
layout.addStretch()
|
return widget
|
||||||
return tab
|
|
||||||
|
|
||||||
def _scan_skills(self):
|
def _scan_skills(self):
|
||||||
"""Start skills scan."""
|
"""Start OCR scan."""
|
||||||
self.status_label.setText("Capturing...")
|
self.scanner = OCRScannerThread()
|
||||||
self.status_label.setStyleSheet("color: #4a9eff;")
|
self.scanner.scan_complete.connect(self._on_scan_complete)
|
||||||
|
self.scanner.scan_error.connect(self._on_scan_error)
|
||||||
self.scan_thread = SkillScannerThread()
|
self.scanner.progress_update.connect(self._on_scan_progress)
|
||||||
self.scan_thread.result_ready.connect(self._on_skills_scanned)
|
self.scanner.start()
|
||||||
self.scan_thread.error_occurred.connect(self._on_scan_error)
|
|
||||||
self.scan_thread.progress_update.connect(self._on_scan_progress)
|
|
||||||
self.scan_thread.start()
|
|
||||||
|
|
||||||
def _on_scan_progress(self, message):
|
def _on_scan_progress(self, message):
|
||||||
"""Update scan progress."""
|
"""Update scan progress."""
|
||||||
self.status_label.setText(message)
|
self.scan_progress.setText(message)
|
||||||
|
|
||||||
def _on_skills_scanned(self, skills_data):
|
def _on_scan_complete(self, skills_data):
|
||||||
"""Handle scanned skills."""
|
"""Handle scan completion."""
|
||||||
self.last_scan = skills_data
|
self.skills_data.update(skills_data)
|
||||||
|
self._save_data()
|
||||||
# Save to data store
|
self._refresh_skills_table()
|
||||||
self.data_store.save_skill_snapshot(skills_data)
|
self.scan_progress.setText(f"Found {len(skills_data)} skills")
|
||||||
|
|
||||||
# Update table
|
|
||||||
self.skills_table.setRowCount(len(skills_data))
|
|
||||||
for i, (name, data) in enumerate(skills_data.items()):
|
|
||||||
self.skills_table.setItem(i, 0, QTableWidgetItem(name))
|
|
||||||
self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '')))
|
|
||||||
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
|
|
||||||
progress = data.get('progress_pct', 0)
|
|
||||||
self.skills_table.setItem(i, 3, QTableWidgetItem(f"{progress:.1f}%"))
|
|
||||||
|
|
||||||
self.status_label.setText(f"✅ Scanned {len(skills_data)} skills")
|
|
||||||
self.status_label.setStyleSheet("color: #4caf50;")
|
|
||||||
self._update_stats()
|
|
||||||
|
|
||||||
def _on_scan_error(self, error):
|
def _on_scan_error(self, error):
|
||||||
"""Handle scan error."""
|
"""Handle scan error."""
|
||||||
self.status_label.setText(f"❌ Error: {error}")
|
self.scan_progress.setText(f"Error: {error}")
|
||||||
self.status_label.setStyleSheet("color: #f44336;")
|
|
||||||
|
|
||||||
def _scan_esi(self):
|
def _refresh_skills_table(self):
|
||||||
"""Scan ESI item."""
|
"""Refresh skills table."""
|
||||||
self.esi_thread = ESIScannerThread()
|
self.skills_table.setRowCount(len(self.skills_data))
|
||||||
self.esi_thread.result_ready.connect(self._on_esi_scanned)
|
for i, (name, data) in enumerate(sorted(self.skills_data.items())):
|
||||||
self.esi_thread.error_occurred.connect(self._on_scan_error)
|
self.skills_table.setItem(i, 0, QTableWidgetItem(name))
|
||||||
self.esi_thread.start()
|
self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
|
||||||
|
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
|
||||||
def _on_esi_scanned(self, esi_data):
|
|
||||||
"""Handle ESI scan result."""
|
|
||||||
self.data_store.save_esi_data(esi_data)
|
|
||||||
|
|
||||||
# Display results
|
|
||||||
text = f"""
|
|
||||||
🎯 Skill Target: {esi_data['skill_target'] or 'Unknown'}
|
|
||||||
Points to Add: {esi_data['points_to_add']}
|
|
||||||
💰 TT Value: {esi_data['tt_value']:.2f} PED
|
|
||||||
|
|
||||||
Raw Text Preview:
|
|
||||||
{esi_data['full_text'][:200]}...
|
|
||||||
"""
|
|
||||||
self.esi_result.setText(text)
|
|
||||||
self._update_stats()
|
|
||||||
|
|
||||||
def _update_stats(self):
|
|
||||||
"""Update statistics display."""
|
|
||||||
skill_count = len(self.data_store.load_skill_history())
|
|
||||||
esi_count = len(self.data_store.load_esi_history())
|
|
||||||
gain_count = len(self.data_store.load_gain_history())
|
|
||||||
|
|
||||||
self.stats_label.setText(
|
|
||||||
f"Data Points: {skill_count} skill scans | "
|
|
||||||
f"{esi_count} ESI scans | "
|
|
||||||
f"{gain_count} chat gains"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _export_data(self):
|
|
||||||
"""Export data to CSV."""
|
|
||||||
from PyQt6.QtWidgets import QFileDialog
|
|
||||||
|
|
||||||
filepath, _ = QFileDialog.getSaveFileName(
|
|
||||||
None, "Export Skill Data", "skill_data.csv", "CSV Files (*.csv)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if filepath:
|
|
||||||
self.data_store.export_to_csv(filepath)
|
|
||||||
self.status_label.setText(f"✅ Exported to {filepath}")
|
|
||||||
|
|
||||||
def _view_raw_data(self):
|
|
||||||
"""Open raw data folder."""
|
|
||||||
import subprocess
|
|
||||||
import platform
|
|
||||||
|
|
||||||
path = self.data_store.data_dir.absolute()
|
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
subprocess.run(["explorer", str(path)])
|
|
||||||
elif platform.system() == "Darwin":
|
|
||||||
subprocess.run(["open", str(path)])
|
|
||||||
else:
|
|
||||||
subprocess.run(["xdg-open", str(path)])
|
|
||||||
|
|
||||||
def on_hotkey(self):
|
|
||||||
"""Quick scan on hotkey."""
|
|
||||||
self._scan_skills()
|
|
||||||
|
|
||||||
def parse_chat_message(self, message):
|
|
||||||
"""Parse skill gain from chat message."""
|
|
||||||
# Patterns for skill gains
|
|
||||||
patterns = [
|
|
||||||
r'(\w+(?:\s+\w+)*)\s+has\s+improved\s+by\s+(\d+(?:\.\d+)?)\s+points?',
|
|
||||||
r'You\s+gained\s+(\d+(?:\.\d+)?)\s+points?\s+in\s+(\w+(?:\s+\w+)*)',
|
|
||||||
r'(\w+(?:\s+\w+)*)\s+\+(\d+(?:\.\d+)?)',
|
|
||||||
]
|
|
||||||
|
|
||||||
for pattern in patterns:
|
|
||||||
match = re.search(pattern, message, re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
# Extract skill and points
|
|
||||||
groups = match.groups()
|
|
||||||
if len(groups) == 2:
|
|
||||||
# Determine which is skill and which is points
|
|
||||||
skill, points = groups
|
|
||||||
try:
|
|
||||||
points_val = float(points)
|
|
||||||
self.data_store.save_skill_gain(skill, points_val)
|
|
||||||
print(f"[Skill Scanner] Tracked: {skill} +{points_val}")
|
|
||||||
except ValueError:
|
|
||||||
# Might be reversed
|
|
||||||
try:
|
|
||||||
points_val = float(skill)
|
|
||||||
self.data_store.save_skill_gain(points, points_val)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
break
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue