feat: Add Smart Auto-Scan with Hotkey Fallback to Skill Scanner

NEW FEATURE - Smart Multi-Page Scanning:

MODES:
1. 🤖 Smart Auto + Hotkey Fallback (default)
   - Tries to auto-detect page changes
   - Monitors page number area (1/12, 2/12, etc.)
   - If detection fails, falls back to F12 hotkey
   - User gets notified: 'Auto-detect unreliable. Use F12!'

2. ⌨️ Manual Hotkey Only
   - User navigates pages in EU
   - Presses F12 to scan each page
   - Simple and 100% reliable

3. 🖱️ Manual Click Only
   - Original click-based scanning
   - Click button, wait for beep, next page

SMART AUTO FEATURES:
- Checks page number area every 500ms
- Detects when page number changes (1→2, 2→3, etc.)
- Automatically triggers scan on page change
- Tracks failures - after 10 failures, falls back to hotkey
- Plays beep sound on successful auto-scan

HOTKEY FEATURES:
- F12 key registered globally
- Works even when EU-Utility not focused
- Triggers scan immediately
- Can be used as primary mode or fallback

UI UPDATES:
- Mode selector dropdown
- Dynamic instructions based on mode
- Hotkey info displayed (F12 = Scan)
- Status shows when auto-detect vs hotkey is active

TECHNICAL:
- Uses keyboard library for global hotkeys
- QTimer for auto-detection polling
- Tesseract OCR for page number reading
- Graceful fallback when auto fails

This gives users the best of both worlds:
- Try auto for convenience
- Fallback to hotkey for reliability
This commit is contained in:
LemonNexus 2026-02-15 00:42:37 +00:00
parent 482ec9aea4
commit e132a80f2b
1 changed files with 166 additions and 12 deletions

View File

@ -12,9 +12,9 @@ from pathlib import Path
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTableWidget, QTableWidgetItem, QProgressBar, QPushButton, QTableWidget, QTableWidgetItem, QProgressBar,
QFrame, QGroupBox, QTextEdit, QSplitter QFrame, QGroupBox, QTextEdit, QSplitter, QComboBox
) )
from PyQt6.QtCore import Qt, QThread, pyqtSignal from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
from plugins.base_plugin import BasePlugin from plugins.base_plugin import BasePlugin
@ -270,16 +270,33 @@ class SkillScannerPlugin(BasePlugin):
multi_page_group = QGroupBox("Multi-Page Scanner") multi_page_group = QGroupBox("Multi-Page Scanner")
multi_page_layout = QVBoxLayout(multi_page_group) multi_page_layout = QVBoxLayout(multi_page_group)
# Mode selection
mode_layout = QHBoxLayout()
mode_layout.addWidget(QLabel("Mode:"))
self.scan_mode_combo = QComboBox()
self.scan_mode_combo.addItems(["Smart Auto + Hotkey Fallback", "Manual Hotkey Only", "Manual Click Only"])
self.scan_mode_combo.currentIndexChanged.connect(self._on_scan_mode_changed)
mode_layout.addWidget(self.scan_mode_combo)
mode_layout.addStretch()
multi_page_layout.addLayout(mode_layout)
# Instructions # Instructions
instructions = QLabel( self.instructions_label = QLabel(
"1. Position your Skills window to show skills\n" "🤖 SMART MODE:\n"
"2. Click 'Scan Page' below\n" "1. Position Skills window\n"
"3. When you hear the beep, click Next Page in game\n" "2. Click 'Start Smart Scan'\n"
"4. Repeat until all pages are scanned\n" "3. Navigate pages in EU - auto-detect will scan for you\n"
"5. Click 'Save All' to store combined results" "4. If auto fails, use hotkey F12 to scan manually\n"
"5. Click 'Save All' when done"
) )
instructions.setStyleSheet("color: #888; font-size: 11px;") self.instructions_label.setStyleSheet("color: #888; font-size: 11px;")
multi_page_layout.addWidget(instructions) multi_page_layout.addWidget(self.instructions_label)
# Hotkey info
self.hotkey_info = QLabel("Hotkey: F12 = Scan Current Page")
self.hotkey_info.setStyleSheet("color: #4ecdc4; font-weight: bold;")
multi_page_layout.addWidget(self.hotkey_info)
# Status row # Status row
status_layout = QHBoxLayout() status_layout = QHBoxLayout()
@ -302,7 +319,7 @@ class SkillScannerPlugin(BasePlugin):
# Buttons # Buttons
mp_buttons_layout = QHBoxLayout() mp_buttons_layout = QHBoxLayout()
self.scan_page_btn = QPushButton("📷 Scan Current Page") self.scan_page_btn = QPushButton("▶️ Start Smart Scan")
self.scan_page_btn.setStyleSheet(""" self.scan_page_btn.setStyleSheet("""
QPushButton { QPushButton {
background-color: #4ecdc4; background-color: #4ecdc4;
@ -317,7 +334,7 @@ class SkillScannerPlugin(BasePlugin):
background-color: #3dbdb4; background-color: #3dbdb4;
} }
""") """)
self.scan_page_btn.clicked.connect(self._scan_page_for_multi) self.scan_page_btn.clicked.connect(self._start_smart_scan)
mp_buttons_layout.addWidget(self.scan_page_btn) mp_buttons_layout.addWidget(self.scan_page_btn)
save_all_btn = QPushButton("💾 Save All Scanned") save_all_btn = QPushButton("💾 Save All Scanned")
@ -665,8 +682,145 @@ class SkillScannerPlugin(BasePlugin):
"""Clear the current multi-page scanning session.""" """Clear the current multi-page scanning session."""
self.current_scan_session = {} self.current_scan_session = {}
self.pages_scanned = 0 self.pages_scanned = 0
self.auto_scan_active = False
self.session_table.setRowCount(0) self.session_table.setRowCount(0)
self.multi_page_status.setText("⏳ Ready to scan page 1") self.multi_page_status.setText("⏳ Ready to scan page 1")
self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;") self.multi_page_status.setStyleSheet("color: #ff8c42; font-size: 14px;")
self.pages_scanned_label.setText("Pages: 0") self.pages_scanned_label.setText("Pages: 0")
self.total_skills_label.setText("Skills: 0") self.total_skills_label.setText("Skills: 0")
# Unregister hotkey if active
self._unregister_hotkey()
def _on_scan_mode_changed(self, index):
"""Handle scan mode change."""
modes = [
"🤖 SMART MODE:\n1. Position Skills window\n2. Click 'Start Smart Scan'\n3. Navigate pages - auto-detect will scan\n4. If auto fails, press F12\n5. Click 'Save All' when done",
"⌨️ HOTKEY MODE:\n1. Position Skills window\n2. Navigate to page 1 in EU\n3. Press F12 to scan each page\n4. Click Next Page in EU\n5. Repeat F12 for each page",
"🖱️ MANUAL MODE:\n1. Position Skills window\n2. Click 'Scan Current Page'\n3. Wait for beep\n4. Click Next Page in EU\n5. Repeat"
]
self.instructions_label.setText(modes[index])
def _start_smart_scan(self):
"""Start smart auto-scan with hotkey fallback."""
mode = self.scan_mode_combo.currentIndex()
if mode == 0: # Smart Auto + Hotkey
self._start_auto_scan_with_hotkey()
elif mode == 1: # Hotkey only
self._register_hotkey()
self._update_multi_page_status("Hotkey F12 ready! Navigate to first page and press F12", success=True)
else: # Manual click
self._scan_page_for_multi()
def _start_auto_scan_with_hotkey(self):
"""Start auto-detection with fallback to hotkey."""
self.auto_scan_active = True
self.auto_scan_failures = 0
self.last_page_number = None
# Register F12 hotkey as fallback
self._register_hotkey()
# Start monitoring
self._update_multi_page_status("🤖 Auto-detect started! Navigate to page 1...", success=True)
# Start auto-detection timer
self.auto_scan_timer = QTimer()
self.auto_scan_timer.timeout.connect(self._check_for_page_change)
self.auto_scan_timer.start(500) # Check every 500ms
def _register_hotkey(self):
"""Register F12 hotkey for manual scan."""
try:
import keyboard
keyboard.on_press_key('f12', lambda e: self._hotkey_scan())
self.hotkey_registered = True
except Exception as e:
print(f"[SkillScanner] Could not register hotkey: {e}")
self.hotkey_registered = False
def _unregister_hotkey(self):
"""Unregister hotkey."""
try:
if hasattr(self, 'hotkey_registered') and self.hotkey_registered:
import keyboard
keyboard.unhook_all()
self.hotkey_registered = False
except:
pass
# Stop auto-scan timer
if hasattr(self, 'auto_scan_timer') and self.auto_scan_timer:
self.auto_scan_timer.stop()
self.auto_scan_active = False
def _hotkey_scan(self):
"""Scan triggered by F12 hotkey."""
from PyQt6.QtCore import QMetaObject, Qt, Q_ARG
QMetaObject.invokeMethod(
self, "_scan_page_for_multi",
Qt.ConnectionType.QueuedConnection
)
def _check_for_page_change(self):
"""Auto-detect page changes by monitoring page number area."""
if not self.auto_scan_active:
return
try:
from PIL import ImageGrab
import pytesseract
# Capture page number area (bottom center of skills window)
# This is approximate - may need adjustment
screen = ImageGrab.grab()
width, height = screen.size
# Try to capture the page number area (bottom center, small region)
# EU skills window shows page like "1/12" at bottom
page_area = (width // 2 - 50, height - 100, width // 2 + 50, height - 50)
page_img = ImageGrab.grab(bbox=page_area)
# OCR just the page number
page_text = pytesseract.image_to_string(page_img, config='--psm 7 -c tessedit_char_whitelist=0123456789/')
# Extract current page number
import re
match = re.search(r'(\d+)/(\d+)', page_text)
if match:
current_page = int(match.group(1))
total_pages = int(match.group(2))
# If page changed, trigger scan
if self.last_page_number is not None and current_page != self.last_page_number:
self._update_multi_page_status(f"📄 Page change detected: {current_page}/{total_pages}", success=True)
self._scan_page_for_multi()
self.last_page_number = current_page
else:
# Failed to detect page number
self.auto_scan_failures += 1
if self.auto_scan_failures >= 10: # After 5 seconds of failures
self._fallback_to_hotkey()
except Exception as e:
self.auto_scan_failures += 1
if self.auto_scan_failures >= 10:
self._fallback_to_hotkey()
def _fallback_to_hotkey(self):
"""Fallback to hotkey mode when auto-detection fails."""
if hasattr(self, 'auto_scan_timer') and self.auto_scan_timer:
self.auto_scan_timer.stop()
self.auto_scan_active = False
# Keep hotkey registered
self._update_multi_page_status(
"⚠️ Auto-detect unreliable. Use F12 hotkey to scan each page manually!",
error=True
)
# Play alert sound
self._play_beep()