The tray icon was blocking the main UI thread because:
1. QTimer was updating menu state every second
2. Complex stylesheet on menu may cause blocking
3. Emojis in menu items might cause encoding issues
Simplified:
- Removed update timer (no longer needed)
- Removed complex stylesheet (use default)
- Removed emojis from menu items
- Made TrayIcon inherit QWidget for proper parent
- Simplified signal connections
1. System Tray Icon (replaces floating button):
- Right-click menu with: Dashboard, Activity Bar, Settings, Quit
- Orange 'EU' icon
- Double-click to open dashboard
- Notifications support
2. Removed Floating Icon:
- No more floating button on desktop
- All interaction through tray icon
3. EU Window Focus Detection:
- Activity bar auto-shows when EU is focused
- Activity bar auto-hides when EU loses focus
- Checks every 500ms for focus changes
- Tray icon checkbox reflects activity bar state
New Files:
- core/tray_icon.py: System tray implementation
Modified:
- core/main.py: Use tray instead of floating icon, add focus detection
Usage:
- Start EU-Utility: Tray icon appears in system tray
- Open EU game: Activity bar appears automatically
- Alt-tab away from EU: Activity bar hides
- Right-click tray icon: Access settings, toggle bar, quit
The hide_timer QTimer was being created after _apply_config() was called,
but _apply_config() tries to set the timer interval. This caused:
AttributeError: 'WindowsTaskbar' object has no attribute 'hide_timer'
Fix: Move hide_timer creation to before _apply_config() call.
Complete redesign of the in-game Activity Bar:
- Transparent background (no visible background)
- Windows-style start button (⊞ icon)
- Search box with rounded corners (Windows 11 style)
- Pinned plugins expand the bar dynamically
- Clean minimal design
- System clock display
- Right-click context menu for settings
- Auto-hide functionality
- Draggable positioning
Features:
- Click ⊞ button to open app drawer
- Type in search box to find plugins
- Pin plugins to taskbar for quick access
- Clock shows current time (updates every minute)
- Right-click for settings
The bar now looks like a floating Windows taskbar
perfect for in-game overlay use.
1. Removed box-shadow CSS properties (not supported by Qt)
2. Fixed TypeError: 'QVBoxLayout' object is not callable
- Removed redundant placeholder.layout() call
- Layout was already set up correctly
App should launch without errors now.
Line 351 had malformed docstring with 8 quotes instead of 3:
- Before: """Handle incoming webhooks."""""" (8 quotes)
- After: """Handle incoming webhooks.""" (3 quotes)
This caused Python to report an unterminated string at EOF.
File now parses correctly.
- Created compatibility shim at core/plugin_api.py
- Imports from new core/api/plugin_api.py
- Existing plugins continue to work without changes
- Encourages migration to: from core.api import get_api
NEW: core/api/ directory with comprehensive three-tier API
PluginAPI (core/api/plugin_api.py):
- 12 core service integrations (Log, Window, OCR, Screenshot, Nexus, HTTP, Audio, Notifications, Clipboard, Event Bus, Data Store, Tasks)
- Full docstrings with examples for every method
- Thread-safe design with Qt signal marshaling
- Proper error handling with custom exceptions
- Service availability checking
WidgetAPI (core/api/widget_api.py):
- Widget creation and management
- WidgetConfig dataclass for configuration
- WidgetType enum (MINI, CONTROL, CHART, ALERT, CUSTOM)
- WidgetAnchor enum for positioning
- Event system (moved, resized, closing, closed, update)
- Layout helpers (grid, horizontal, vertical, cascade)
- Persistence (save/load widget states)
- Widget presets for reuse
ExternalAPI (core/api/external_api.py):
- REST API server with aiohttp
- API endpoint registration (decorator and programmatic)
- Incoming webhooks with HMAC verification
- Outgoing webhook POST support
- API key authentication
- IPC (inter-process communication)
- File watcher for config changes
- Server-Sent Events (SSE) support
- CORS configuration
- Webhook history tracking
core/api/__init__.py:
- Unified imports for all three APIs
- Version tracking (2.2.0)
- Clean namespace exports
docs/API_REFERENCE.md:
- Comprehensive 12,000+ word reference
- Quick start examples for each API
- Service-by-service documentation
- Error handling guide
- Integration examples (Discord, custom widget)
Integration:
- Updated core/main.py to import from new API structure
- All three APIs available via: from core.api import get_api, get_widget_api, get_external_api
Benefits:
- Clear separation of concerns (plugins vs widgets vs external)
- Well-documented APIs for developers
- Easy to extend with new services
- Type hints throughout
- Production-ready error handling
- Third-party integration support out of the box
- Added Ctrl+Shift+B hotkey to toggle activity bar visibility
- Added _on_activity_bar_hotkey() handler method
- Updated startup messages to show all hotkeys including activity bar
Hotkeys:
- Ctrl+Shift+U: Toggle main overlay
- Ctrl+Shift+H: Hide all overlays
- Ctrl+Shift+B: Toggle activity bar
NEW: core/activity_bar.py
- ActivityBar class for in-game overlay
- Configurable layouts: Horizontal, Vertical, Grid
- Pinned plugins in bar
- Plugin drawer (app drawer style)
- Draggable, resizable
- Opacity and size controls
- Auto-hide feature
- Settings dialog
Features:
- Horizontal/Vertical/Grid layouts
- Adjustable icon size (32-96px)
- Opacity slider (20-100%)
- Auto-hide when not in use
- Plugin drawer with all enabled plugins
- Click plugin to open mini widget or full UI
- Drag to reposition anywhere on screen
INTEGRATION:
- Added to core/main.py
- Auto-created on startup if enabled
- Toggle with Ctrl+Shift+B (configurable)
- Integrated with plugin manager
Usage:
- Install plugins
- They appear in Activity Bar (if pinned) or Drawer
- Click to open mini widget or full UI
- Right-click for settings
This provides a macOS-style dock experience for in-game use,
while keeping the desktop app for configuration.
ISSUE: Widgets tab was built once at startup. If a plugin was installed
after the app started, the widget wouldn't show up until restart.
FIX:
1. Added _refresh_widgets_tab() method that rebuilds the tab content
2. Called _refresh_widgets_tab() when switching to Widgets tab
3. Added debug logging to show registered widgets count
4. Lists all found widgets in console for debugging
Now when you:
1. Install Clock Widget plugin
2. Click on 🎨 Widgets tab
3. The tab refreshes and shows the Clock Widget
The widget appears immediately without needing to restart!
Widgets have Qt::WindowDoesNotAcceptFocus flag set so they don't
steal focus from the game. Calling activateWindow() on them
produces a warning.
Removed activateWindow() call since:
1. Widgets should not accept focus (they're overlays)
2. raise_() is sufficient to bring them to front
3. WindowDoesNotAcceptFocus is intentional for game overlay widgets
ISSUE: Clock and System Monitor widgets were created but not visible.
The widgets were being garbage collected because they weren't stored.
FIX:
1. Store widget in self._active_widgets list to prevent GC
2. Set parent=self to keep widget alive with overlay
3. Position widget at center of screen (visible location)
4. Call raise_() and activateWindow() to bring to front
5. Added debug output showing widget position
WIDGET CREATION NOW:
- Creates widget with parent
- Positions at center of screen
- Shows, raises, activates
- Stores reference in list
- Prints position for debugging
The issue was that installed plugins (calculator, clock_widget, etc.)
could not import 'plugins.base_plugin' because the plugins package
wasn't properly in Python's module path when loading via
spec_from_file_location.
FIX:
1. Added project root to sys.path so 'plugins' package is findable
2. Added _ensure_plugins_package() method to preload plugins module
3. Modified discover_plugins() to try normal import first
4. Added proper error logging with traceback for debugging
This ensures installed plugins can properly import BasePlugin
from plugins.base_plugin as expected.
Installed plugins (calculator, clock_widget, etc.) import from
plugins.base_plugin, but we moved BasePlugin to core.base_plugin.
This shim re-exports BasePlugin so installed plugins work without
needing to know about core module structure.
FIX:
- plugins/base_plugin.py now re-exports from core.base_plugin
- Installed plugins can continue using: from plugins.base_plugin import BasePlugin
This maintains backward compatibility for all installable plugins.
NEW UI LAYOUT:
- Added tab bar at top of content area with 3 tabs:
* 🔌 Plugins - Shows plugin list from sidebar + plugin content
* 🎨 Widgets - Widget gallery to add overlay widgets
* ⚙️ Settings - All settings in one place
WIDGETS TAB FEATURES:
- Built-in widgets section with Clock and System Monitor
- Each widget has Add button to create overlay
- Plugin widgets section (placeholder for plugin-added widgets)
- Descriptions for each widget type
SETTINGS TAB FEATURES:
- Moved settings from dialog into dedicated tab
- Sub-tabs: Plugins, Plugin Store, Hotkeys, Appearance, About
- Removed Settings button from header (now in tabs)
CHANGES:
- _create_content_area() replaced with _create_content_area_with_tabs()
- Added _switch_tab() method
- Added _create_plugins_tab(), _create_widgets_tab(), _create_settings_tab()
- Added _create_widget_button() helper
- Added _add_clock_widget() and _add_system_monitor_widget()
- Removed Settings button from header bar
- Removed old _open_settings() dialog approach
The UI is now organized with clear navigation between
plugins, widgets, and settings via top tabs.
CHANGES:
1. Fixed plugin download to use raw git files instead of git clone
- Avoids Windows permission issues with .git/objects
- Downloads __init__.py and plugin.py directly
2. Added clickable dependencies button showing full dependency dialog
- Shows Core Services Required
- Shows Plugins Required
- Lists what will be auto-installed
3. Integrated Plugin Store into Settings dialog
- Added 🔌 Store tab to Settings
- Plugin Store is now built-in, not a plugin
4. Removed plugins/settings/ and plugins/plugin_store_ui/
- Settings is now built into overlay_window.py
- Users access via Settings button
5. Added _create_plugin_store_tab() method to OverlayWindow
NOTE: Pull latest EU-Utility-Plugins-Repo to get Clock Widget
The base_plugin.py was removed from plugins/ folder during cleanup
but is still needed by core/plugin_manager.py and built-in plugins.
CHANGES:
- Restored base_plugin.py from plugin repo to core/base_plugin.py
- Updated imports in core/plugin_manager.py
- Updated imports in core/plugin_manager_optimized.py
- Updated imports in plugins/settings/plugin.py
- Updated imports in plugins/plugin_store_ui/plugin.py
This fixes ModuleNotFoundError when starting EU-Utility.
All user-facing plugins have been moved to:
https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo
REMOVED FROM CORE (30+ plugins):
- analytics, auction_tracker, auto_screenshot, auto_updater
- calculator, chat_logger, codex_tracker, crafting_calc
- dashboard, discord_presence, dpp_calculator, enhancer_calc
- event_bus_example, game_reader, game_reader_test, global_tracker
- import_export, inventory_manager, log_parser_test, loot_tracker
- mining_helper, mission_tracker, nexus_search, price_alerts
- profession_scanner, session_exporter, skill_scanner
- spotify_controller, tp_runner, universal_search
ALSO REMOVED:
- plugins/base_plugin.py (was duplicate, should be in package root)
- plugins/__pycache__ (shouldn't be in git)
REMAINING IN CORE:
- plugins/__init__.py
- plugins/settings/ (essential for configuration)
- plugins/plugin_store_ui/ (essential for plugin installation)
EU-Utility is now a pure framework. Users install plugins via
Settings → Plugin Store or manually to the plugins/ folder.
This separation enables:
- Independent plugin development
- Modular installation (only what you need)
- Community contributions via plugin repo
- Cleaner core codebase focused on framework
BREAKING CHANGE: EU-Utility is now a framework-only application.
All user-facing features have been moved to separate plugin repository.
NEW FEATURES:
1. Plugin Store Core Module (core/plugin_store.py)
- PluginStoreWorker: Background operations (fetch, download, updates)
- PluginStoreUI: Grid-based plugin browser with cards
- PluginInfo dataclass for plugin metadata
- Fetches from remote git repository
2. Plugin Store UI Features:
- Grid layout with plugin cards (300x200px each)
- Search/filter by name, description, tags
- Category filter dropdown
- Visual indicators:
* 📦 Plugin icon (emoji-based)
* Version badge
* Status badges (✅ Enabled, 📦 Installed)
* Tag display
* 🔗 Dependency count with tooltip
- Install/Enable/Disable/Uninstall buttons
- Progress bar for operations
- Refresh and Check Updates buttons
3. Settings Integration:
- New 'Plugin Store' tab in Settings
- Moved plugin management to 'My Plugins' tab
- Plugin Store uses core module directly
4. Plugin Store UI Plugin (plugins/plugin_store_ui/):
- Standalone plugin for overlay integration
- Hotkey: Ctrl+Shift+P (configurable)
ARCHITECTURE CHANGES:
- EU-Utility Core: Framework only (PluginAPI, services, overlay)
- Plugin Repository: https://git.lemonlink.eu/impulsivefps/EU-Utility-Plugins-Repo
- Plugins installed via Store → user plugins/ directory
- Local plugins/ folder still supported for development
MANIFEST FORMAT:
USER WORKFLOW:
1. Open Settings → Plugin Store
2. Browse/search available plugins
3. Click Install (with dependency confirmation)
4. Restart EU-Utility
5. Enable plugin in 'My Plugins' tab
DEVELOPER WORKFLOW:
1. Develop plugin locally in plugins/
2. Test with core framework
3. Submit to plugin repository
4. Users install via Store
This enables limitless modularity - users only install
what they need, developers can publish independently.
REFACTOR: Hotkeys are no longer hardcoded in Settings UI
NEW SYSTEM:
1. Plugins advertise their hotkeys via class attributes:
Legacy (single hotkey):
hotkey = 'ctrl+shift+s'
New format (multiple hotkeys with descriptions):
hotkeys = [
{
'action': 'toggle',
'description': 'Toggle Skill Scanner',
'default': 'ctrl+shift+s',
'config_key': 'skillscanner_toggle' # optional
},
{
'action': 'quick_scan',
'description': 'Quick Scan',
'default': 'f12',
}
]
2. Settings UI dynamically discovers hotkeys:
- Scans all plugins for hotkey/hotkeys attributes
- Groups hotkeys by plugin name
- Shows description + input field + reset button
- Core system hotkeys in separate 'Core System' group
3. Visual improvements:
- Scrollable hotkey list
- Reset button (↺) for each hotkey
- Tooltips showing default value
- Plugin grouping for organization
4. Backward compatible:
- Still supports legacy 'hotkey' attribute
- Converts to new format automatically
- Existing settings preserved
BASE PLUGIN:
- Added hotkeys attribute documentation
- Added docstring with usage examples
- Shows both formats in class docstring
SETTINGS PLUGIN:
- _create_hotkeys_tab() now dynamic
- _collect_plugin_hotkeys() scans all plugins
- Groups by plugin with QGroupBox
- Reset buttons restore defaults
- ScrollArea for long lists
This allows plugins to define multiple hotkeys with
descriptions, and they'll automatically appear in
Settings without hardcoding them in the core.
NEW FEATURES:
1. Dependency Indicators:
- 🔗 (cyan) - Plugin has dependencies (hover for list)
- ⚠️ (yellow) - Plugin is required by other enabled plugins
- 🔄 (orange) - Auto-enabled due to dependency
2. Legend Bar:
Shows what each indicator means at the top of the plugins tab
3. Dependency Tooltips:
- 🔗 Shows: 'This plugin requires: X, Y, Z. These will be auto-enabled.'
- ⚠️ Shows: 'Required by enabled plugins: A, B. Disable those first.'
- 🔄 Shows: 'Auto-enabled by: PluginName'
4. Enable with Dependencies:
When enabling a plugin with unmet dependencies:
- Shows confirmation dialog listing all plugins to be enabled
- User can cancel before enabling
- Dependencies are auto-enabled on confirmation
5. Disable Protection:
When trying to disable a plugin that others depend on:
- Shows warning dialog listing dependent plugins
- Prevents accidental breaking of dependencies
- User must disable dependents first
6. Dependency Report Dialog:
- New '📋 Dependency Report' button
- Shows HTML report with:
* Summary stats (total/enabled plugins)
* Plugins with dependencies list
* Plugins required by others list
* Full dependency chains
7. Enable/Disable All with Ordering:
- Dependencies are enabled first (topological sort)
- Dependents are disabled first (reverse order)
- Prevents enable/disable failures due to ordering
8. Auto-refresh UI:
After enabling/disabling, plugin list refreshes to show:
- Updated auto-enabled status
- Updated dependency indicators
- Updated checkbox states
VISUAL IMPROVEMENTS:
- Better spacing and layout
- Color-coded indicators
- Clear visual hierarchy
- Informative tooltips throughout
This makes plugin management much more intuitive and prevents
common mistakes like accidentally breaking dependencies.
PROBLEM: UI text like 'Position Skills window Wounding' and
'Scan Current Page Serendipity' was being parsed as skills.
FIXES:
1. Enhanced is_valid_skill_text() filtering:
- Added more UI patterns: 'Position Skills', 'Scan Current Page',
'Select Area', 'Drag over', 'Navigate pages'
- Added combined patterns: 'Combat Wounding', 'Scan Serendipity',
'Position Wounding', etc.
- Added action word detection: Click, Scan, Position, Select,
Navigate, Start, Save, Clear - any line with these is UI text
- Reduced max words from 10 to 7 for skill names
2. Added validation in _parse_skills_from_text():
- After extracting skill name, validates with is_valid_skill_text()
- Logs filtered names for debugging
- Only adds to results if validation passes
USER ACTION NEEDED:
- Pull latest code: git pull origin main
- Select Area button should appear in UI
- Drag to select your Skills window area
- Scan will only read from that area
This should eliminate UI text from scan results.
NEW FEATURE - Snipping Tool Area Selection:
WORKFLOW:
1. Click 'Select Area' button in Skill Scanner
2. Screen dims with semi-transparent overlay
3. Drag to draw rectangle over your Skills window
4. Release to confirm (right-click or Escape to cancel)
5. Selected area is saved for all future scans
6. Click 'Start Smart Scan' to begin
SNIPPING WIDGET FEATURES:
- Fullscreen overlay with darkened background
- Drag to draw selection rectangle
- White border around selection
- Dimensions displayed (e.g., '800 x 600')
- Right-click to cancel
- Escape key to cancel
- Minimum 50x50 pixels required
UI UPDATES:
- Added 'Select Area' button with blue color (#4a9eff)
- Area label shows current selection: '800x600 at (100, 200)'
- All mode instructions updated to mention Select Area step
TECHNICAL:
- SnippingWidget inherits from QWidget
- Uses Qt.TranslucentBackground for transparency
- QPainter.CompositionMode_Clear for cutout effect
- Selected area stored as self.scan_area (x, y, w, h)
- SkillOCRThread accepts scan_area parameter
- Both single scan and multi-page scan use selected area
BENEFITS:
- No more window detection errors
- User has full control over scan region
- Works regardless of window title or process name
- Precise selection of Skills window area
If no area selected, falls back to full game window capture.
BUG: SkillScannerPlugin cannot be converted to PyQt6.QtCore.QObject
CAUSE: BasePlugin inherits from ABC, not QObject. Qt signals (pyqtSignal)
must be defined in a QObject subclass.
FIX:
1. Created SignalHelper(QObject) class to hold all signals:
- hotkey_triggered
- update_status_signal
- update_session_table_signal
- update_counters_signal
- enable_scan_button_signal
2. In SkillScannerPlugin.initialize():
- Create self._signals = SignalHelper()
- Connect signals from self._signals (not self)
3. In get_ui():
- Connect enable_scan_button_signal after scan_page_btn is created
4. Updated all signal emits to use self._signals.emit()
This allows the plugin to use Qt signals for thread-safe UI updates
without requiring BasePlugin to inherit from QObject (which would
break other plugins).
BUG: OCR was reading text from Discord, EU-Utility UI, and other windows.
FIX:
1. Added find_entropia_window() - Uses win32gui + psutil on Windows to find
the game window by process name 'Entropia.exe' and window title containing
'Entropia Universe'. Returns (left, top, width, height).
2. Added capture_entropia_region() - Captures only the game window region,
falls back to full screen if window not found.
3. Added is_valid_skill_text() - Filters out non-game text patterns:
- Discord, Event Bus, Game Reader, Test, Page Scanner, HOTKEY MODE
- UI elements like 'Skill Tracker', 'Calculator', 'Nexus Search'
- Debug text like '[SkillScanner]', 'Parsed:', 'Cleared'
- Process names like 'Entropia.exe', 'Client (64 bit)', 'Arkadia'
- Lines with >10 words (skills aren't that long)
4. Added recognize_image() method to OCRService for convenience.
5. Modified SkillOCRThread.run() to:
- Capture only Entropia window
- Filter text before parsing
- Use _parse_skills_filtered() which validates each line
6. Added _parse_skills_filtered() method that:
- Splits text by lines
- Only keeps lines containing a valid rank
- Validates each line with is_valid_skill_text()
- Logs filtered lines for debugging
RESULT:
- Scanner now ONLY reads from the game window
- Invalid text (Discord, UI, debug) is filtered out
- Much cleaner skill parsing results
Note: Window title varies by location (e.g., '[Arkadia]', '[Calypso]')
but process name is always 'Entropia.exe'.
BUG: QMetaObject.invokeMethod with Q_ARG doesn't work properly in PyQt6
and was causing TypeError exceptions.
FIX:
- Added proper Qt signals at class level:
* update_status_signal(str, bool, bool)
* update_session_table_signal(object)
* update_counters_signal()
* enable_scan_button_signal(bool)
- Connected all signals to slot methods in initialize()
- Replaced all invokeMethod calls with signal.emit()
- Thread callbacks now emit signals instead of calling invokeMethod
- UI updates happen in main Qt thread via signal/slot mechanism
This is the correct PyQt6 way to do cross-thread communication.
All UI updates are now thread-safe and won't cause TypeErrors.
BUG: TypeError when using F12 hotkey - invokeMethod syntax was wrong
for PyQt6.
FIX:
1. Added hotkey_triggered = pyqtSignal() at class level
2. Connected signal to _scan_page_for_multi in initialize()
3. _hotkey_scan() now just emits the signal (thread-safe)
4. Signal ensures scan runs on main Qt thread
This is the proper Qt way to handle cross-thread communication.
The hotkey callback runs in keyboard library's thread, but the
scan must run in Qt's main thread to update UI safely.
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
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!