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:
parent
482ec9aea4
commit
e132a80f2b
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue