feat: Add Multi-Page Scanner to Skill Scanner plugin
NEW FEATURE - Multi-Page Scanner: WORKFLOW: 1. User positions Skills window to show skills 2. User clicks 'Scan Current Page' 3. App scans, shows checkmark ✅, plays BEEP sound 4. Status shows: 'Page X scanned! Click Next Page in game →' 5. User manually clicks Next Page in EU 6. User clicks 'Scan Current Page' again 7. Repeat until all pages scanned 8. User clicks 'Save All' to store combined results FEATURES: - ✅ Checkmark icon and green text on successful scan - 🔊 Beep sound (Windows MessageBeep) to notify user - 📊 Live counters: Pages scanned, Total skills - 🗑 Clear Session button to start over - 💾 Save All button merges session into main data - 📝 Session table shows all skills collected so far UI ELEMENTS: - Instructions panel explaining the workflow - Status label with color-coded feedback - Pages: X counter - Skills: X counter - Three buttons: Scan Page, Save All, Clear Session - Session table showing accumulated skills TECHNICAL: - current_scan_session dict accumulates skills across pages - pages_scanned counter tracks progress - Thread-safe UI updates via QMetaObject.invokeMethod - Windows beep via winsound module (with fallback) This gives users full control while guiding them through multi-page scanning without any auto-clicking!
This commit is contained in:
parent
46a76a91e8
commit
482ec9aea4
|
|
@ -160,6 +160,10 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
self.skill_gains = []
|
self.skill_gains = []
|
||||||
self._load_data()
|
self._load_data()
|
||||||
|
|
||||||
|
# Multi-page scanning state
|
||||||
|
self.current_scan_session = {} # Skills collected in current multi-page scan
|
||||||
|
self.pages_scanned = 0
|
||||||
|
|
||||||
# Subscribe to skill gain events from core Log service
|
# Subscribe to skill gain events from core Log service
|
||||||
try:
|
try:
|
||||||
from core.plugin_api import get_api
|
from core.plugin_api import get_api
|
||||||
|
|
@ -262,6 +266,110 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
|
|
||||||
splitter.addWidget(scan_group)
|
splitter.addWidget(scan_group)
|
||||||
|
|
||||||
|
# Multi-Page Scanning section
|
||||||
|
multi_page_group = QGroupBox("Multi-Page Scanner")
|
||||||
|
multi_page_layout = QVBoxLayout(multi_page_group)
|
||||||
|
|
||||||
|
# Instructions
|
||||||
|
instructions = QLabel(
|
||||||
|
"1. Position your Skills window to show skills\n"
|
||||||
|
"2. Click 'Scan Page' below\n"
|
||||||
|
"3. When you hear the beep, click Next Page in game\n"
|
||||||
|
"4. Repeat until all pages are scanned\n"
|
||||||
|
"5. Click 'Save All' to store combined results"
|
||||||
|
)
|
||||||
|
instructions.setStyleSheet("color: #888; font-size: 11px;")
|
||||||
|
multi_page_layout.addWidget(instructions)
|
||||||
|
|
||||||
|
# Status row
|
||||||
|
status_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
self.multi_page_status = QLabel("⏳ Ready to scan page 1")
|
||||||
|
self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;")
|
||||||
|
status_layout.addWidget(self.multi_page_status)
|
||||||
|
|
||||||
|
self.pages_scanned_label = QLabel("Pages: 0")
|
||||||
|
self.pages_scanned_label.setStyleSheet("color: #4ecdc4;")
|
||||||
|
status_layout.addWidget(self.pages_scanned_label)
|
||||||
|
|
||||||
|
self.total_skills_label = QLabel("Skills: 0")
|
||||||
|
self.total_skills_label.setStyleSheet("color: #4ecdc4;")
|
||||||
|
status_layout.addWidget(self.total_skills_label)
|
||||||
|
|
||||||
|
status_layout.addStretch()
|
||||||
|
multi_page_layout.addLayout(status_layout)
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
mp_buttons_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
self.scan_page_btn = QPushButton("📷 Scan Current Page")
|
||||||
|
self.scan_page_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #4ecdc4;
|
||||||
|
color: #141f23;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #3dbdb4;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
self.scan_page_btn.clicked.connect(self._scan_page_for_multi)
|
||||||
|
mp_buttons_layout.addWidget(self.scan_page_btn)
|
||||||
|
|
||||||
|
save_all_btn = QPushButton("💾 Save All Scanned")
|
||||||
|
save_all_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #ff8c42;
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
save_all_btn.clicked.connect(self._save_multi_page_scan)
|
||||||
|
mp_buttons_layout.addWidget(save_all_btn)
|
||||||
|
|
||||||
|
clear_session_btn = QPushButton("🗑 Clear Session")
|
||||||
|
clear_session_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #666;
|
||||||
|
color: white;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
clear_session_btn.clicked.connect(self._clear_multi_page_session)
|
||||||
|
mp_buttons_layout.addWidget(clear_session_btn)
|
||||||
|
|
||||||
|
multi_page_layout.addLayout(mp_buttons_layout)
|
||||||
|
|
||||||
|
# Current session table
|
||||||
|
self.session_table = QTableWidget()
|
||||||
|
self.session_table.setColumnCount(3)
|
||||||
|
self.session_table.setHorizontalHeaderLabels(["Skill", "Rank", "Points"])
|
||||||
|
self.session_table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
self.session_table.setMaximumHeight(200)
|
||||||
|
self.session_table.setStyleSheet("""
|
||||||
|
QTableWidget {
|
||||||
|
background-color: #0d1117;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
QTableWidget::item {
|
||||||
|
padding: 4px;
|
||||||
|
color: #c9d1d9;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
multi_page_layout.addWidget(self.session_table)
|
||||||
|
|
||||||
|
splitter.addWidget(multi_page_group)
|
||||||
|
|
||||||
# Log section
|
# Log section
|
||||||
log_group = QGroupBox("Log Tracking (Core Service)")
|
log_group = QGroupBox("Log Tracking (Core Service)")
|
||||||
log_layout = QVBoxLayout(log_group)
|
log_layout = QVBoxLayout(log_group)
|
||||||
|
|
@ -363,3 +471,202 @@ class SkillScannerPlugin(BasePlugin):
|
||||||
self.skills_table.setItem(i, 0, QTableWidgetItem(name))
|
self.skills_table.setItem(i, 0, QTableWidgetItem(name))
|
||||||
self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
|
self.skills_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
|
||||||
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
|
self.skills_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
|
||||||
|
|
||||||
|
def _scan_page_for_multi(self):
|
||||||
|
"""Scan current page and add to multi-page session."""
|
||||||
|
from PyQt6.QtCore import QTimer
|
||||||
|
|
||||||
|
self.multi_page_status.setText("📷 Scanning...")
|
||||||
|
self.multi_page_status.setStyleSheet("color: #ffd93d;")
|
||||||
|
self.scan_page_btn.setEnabled(False)
|
||||||
|
|
||||||
|
# Run scan in thread
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
def do_scan():
|
||||||
|
try:
|
||||||
|
from core.ocr_service import get_ocr_service
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
ocr_service = get_ocr_service()
|
||||||
|
if not ocr_service.is_available():
|
||||||
|
self._update_multi_page_status("Error: OCR not available", error=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Capture and OCR
|
||||||
|
result = ocr_service.recognize()
|
||||||
|
text = result.get('text', '')
|
||||||
|
|
||||||
|
# Parse skills
|
||||||
|
skills = self._parse_skills_from_text(text)
|
||||||
|
|
||||||
|
# Add to session
|
||||||
|
for skill_name, data in skills.items():
|
||||||
|
self.current_scan_session[skill_name] = data
|
||||||
|
|
||||||
|
self.pages_scanned += 1
|
||||||
|
|
||||||
|
# Update UI
|
||||||
|
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
|
||||||
|
QMetaObject.invokeMethod(
|
||||||
|
self, "_update_session_table",
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
Q_ARG(object, self.current_scan_session)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show success with checkmark and beep
|
||||||
|
self._update_multi_page_status(
|
||||||
|
f"✅ Page {self.pages_scanned} scanned! {len(skills)} skills found. Click Next Page in game →",
|
||||||
|
success=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Play beep sound
|
||||||
|
self._play_beep()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self._update_multi_page_status(f"Error: {str(e)}", error=True)
|
||||||
|
finally:
|
||||||
|
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
|
||||||
|
QMetaObject.invokeMethod(
|
||||||
|
self.scan_page_btn,
|
||||||
|
"setEnabled",
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
Q_ARG(bool, True)
|
||||||
|
)
|
||||||
|
|
||||||
|
thread = Thread(target=do_scan)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def _parse_skills_from_text(self, text):
|
||||||
|
"""Parse skills from OCR text."""
|
||||||
|
skills = {}
|
||||||
|
|
||||||
|
# Ranks in Entropia Universe - multi-word first for proper matching
|
||||||
|
SINGLE_RANKS = [
|
||||||
|
'Newbie', 'Inept', 'Beginner', 'Amateur', 'Average',
|
||||||
|
'Skilled', 'Expert', 'Professional', 'Master',
|
||||||
|
'Champion', 'Legendary', 'Guru', 'Astonishing', 'Remarkable',
|
||||||
|
'Outstanding', 'Marvelous', 'Prodigious', 'Amazing', 'Incredible', 'Awesome'
|
||||||
|
]
|
||||||
|
MULTI_RANKS = ['Arch Master', 'Grand Master']
|
||||||
|
ALL_RANKS = MULTI_RANKS + SINGLE_RANKS
|
||||||
|
rank_pattern = '|'.join(ALL_RANKS)
|
||||||
|
|
||||||
|
# Clean text
|
||||||
|
text = text.replace('SKILLS', '').replace('ALL CATEGORIES', '')
|
||||||
|
text = text.replace('SKILL NAME', '').replace('RANK', '').replace('POINTS', '')
|
||||||
|
|
||||||
|
# Remove category names
|
||||||
|
for category in ['Attributes', 'COMBAT', 'Combat', 'Design', 'Construction',
|
||||||
|
'Defense', 'General', 'Handgun', 'Heavy Melee Weapons',
|
||||||
|
'Heavy Weapons', 'Information', 'Inflict Melee Damage',
|
||||||
|
'Inflict Ranged Damage', 'Light Melee Weapons', 'Longblades',
|
||||||
|
'Medical', 'Mining', 'Science', 'Social', 'Beauty', 'Mindforce']:
|
||||||
|
text = text.replace(category, ' ')
|
||||||
|
|
||||||
|
text = ' '.join(text.split())
|
||||||
|
|
||||||
|
# Find all skills
|
||||||
|
import re
|
||||||
|
for match in re.finditer(
|
||||||
|
rf'([A-Za-z][A-Za-z\s]{{2,50}}?)\s+({rank_pattern})\s+(\d{{1,6}})(?:\s|$)',
|
||||||
|
text, re.IGNORECASE
|
||||||
|
):
|
||||||
|
skill_name = match.group(1).strip()
|
||||||
|
rank = match.group(2)
|
||||||
|
points = int(match.group(3))
|
||||||
|
|
||||||
|
skill_name = re.sub(r'^(Skill|SKILL)\s*', '', skill_name, flags=re.IGNORECASE)
|
||||||
|
skill_name = skill_name.strip()
|
||||||
|
|
||||||
|
if points > 0 and skill_name and len(skill_name) > 2:
|
||||||
|
skills[skill_name] = {'rank': rank, 'points': points}
|
||||||
|
|
||||||
|
return skills
|
||||||
|
|
||||||
|
def _update_multi_page_status(self, message, success=False, error=False):
|
||||||
|
"""Update multi-page status label."""
|
||||||
|
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
|
||||||
|
|
||||||
|
color = "#4ecdc4" if success else "#ff4757" if error else "#ff8c42"
|
||||||
|
|
||||||
|
QMetaObject.invokeMethod(
|
||||||
|
self.multi_page_status,
|
||||||
|
"setText",
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
Q_ARG(str, message)
|
||||||
|
)
|
||||||
|
QMetaObject.invokeMethod(
|
||||||
|
self.multi_page_status,
|
||||||
|
"setStyleSheet",
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
Q_ARG(str, f"color: {color}; font-size: 14px;")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update counters
|
||||||
|
QMetaObject.invokeMethod(
|
||||||
|
self.pages_scanned_label,
|
||||||
|
"setText",
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
Q_ARG(str, f"Pages: {self.pages_scanned}")
|
||||||
|
)
|
||||||
|
QMetaObject.invokeMethod(
|
||||||
|
self.total_skills_label,
|
||||||
|
"setText",
|
||||||
|
Qt.ConnectionType.QueuedConnection,
|
||||||
|
Q_ARG(str, f"Skills: {len(self.current_scan_session)}")
|
||||||
|
)
|
||||||
|
|
||||||
|
def _play_beep(self):
|
||||||
|
"""Play a beep sound to notify user."""
|
||||||
|
try:
|
||||||
|
import winsound
|
||||||
|
winsound.MessageBeep(winsound.MB_OK)
|
||||||
|
except:
|
||||||
|
# Fallback - try to use system beep
|
||||||
|
try:
|
||||||
|
print('\a') # ASCII bell character
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _update_session_table(self, skills):
|
||||||
|
"""Update the session table with current scan data."""
|
||||||
|
self.session_table.setRowCount(len(skills))
|
||||||
|
for i, (name, data) in enumerate(sorted(skills.items())):
|
||||||
|
self.session_table.setItem(i, 0, QTableWidgetItem(name))
|
||||||
|
self.session_table.setItem(i, 1, QTableWidgetItem(data.get('rank', '-')))
|
||||||
|
self.session_table.setItem(i, 2, QTableWidgetItem(str(data.get('points', 0))))
|
||||||
|
|
||||||
|
def _save_multi_page_scan(self):
|
||||||
|
"""Save all scanned skills from multi-page session."""
|
||||||
|
if not self.current_scan_session:
|
||||||
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
QMessageBox.information(None, "No Data", "No skills scanned yet. Scan some pages first!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Merge with existing data
|
||||||
|
self.skills_data.update(self.current_scan_session)
|
||||||
|
self._save_data()
|
||||||
|
self._refresh_skills_table()
|
||||||
|
|
||||||
|
from PyQt6.QtWidgets import QMessageBox
|
||||||
|
QMessageBox.information(
|
||||||
|
None,
|
||||||
|
"Scan Complete",
|
||||||
|
f"Saved {len(self.current_scan_session)} skills from {self.pages_scanned} pages!"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clear session after saving
|
||||||
|
self._clear_multi_page_session()
|
||||||
|
|
||||||
|
def _clear_multi_page_session(self):
|
||||||
|
"""Clear the current multi-page scanning session."""
|
||||||
|
self.current_scan_session = {}
|
||||||
|
self.pages_scanned = 0
|
||||||
|
self.session_table.setRowCount(0)
|
||||||
|
self.multi_page_status.setText("⏳ Ready to scan page 1")
|
||||||
|
self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;")
|
||||||
|
self.pages_scanned_label.setText("Pages: 0")
|
||||||
|
self.total_skills_label.setText("Skills: 0")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue