feat: floating icon + game reader OCR plugin

- Add draggable floating icon () in top-left corner
- Double-click floating icon to toggle overlay
- Game Reader plugin for OCR scanning
- Capture screen and extract text from game menus
- Copy captured text to clipboard
- Hotkey: Ctrl+Shift+R for quick capture

Installation for OCR:
pip install easyocr
or
pip install pytesseract

The floating icon makes EU-Utility easily accessible during gameplay!
This commit is contained in:
LemonNexus 2026-02-13 13:15:36 +00:00
parent 8dbbf4d971
commit d74de07110
4 changed files with 385 additions and 1 deletions

View File

@ -0,0 +1,103 @@
"""
EU-Utility - Floating Icon
A draggable floating button for easy access in-game.
"""
from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QApplication
from PyQt6.QtCore import Qt, QPoint, pyqtSignal
from PyQt6.QtGui import QMouseEvent, QEnterEvent, QPainter, QBrush, QColor
class FloatingIcon(QWidget):
"""Draggable floating icon for in-game use."""
clicked = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
# Frameless, always on top
self.setWindowFlags(
Qt.WindowType.FramelessWindowHint |
Qt.WindowType.WindowStaysOnTopHint |
Qt.WindowType.Tool |
Qt.WindowType.WindowTransparentForInput
)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
self.setFixedSize(48, 48)
# Position - top left with offset
screen = QApplication.primaryScreen().geometry()
self.move(10, 10)
self.dragging = False
self.drag_position = QPoint()
self._setup_ui()
def _setup_ui(self):
"""Setup the floating icon."""
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
self.icon_label = QLabel("")
self.icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.icon_label.setStyleSheet("""
QLabel {
font-size: 24px;
background-color: rgba(30, 30, 30, 200);
border-radius: 24px;
border: 2px solid rgba(74, 158, 255, 150);
}
""")
layout.addWidget(self.icon_label)
def mousePressEvent(self, event: QMouseEvent):
"""Handle mouse press."""
if event.button() == Qt.MouseButton.LeftButton:
self.dragging = True
self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft()
event.accept()
def mouseMoveEvent(self, event: QMouseEvent):
"""Handle mouse move for dragging."""
if self.dragging:
new_pos = event.globalPosition().toPoint() - self.drag_position
self.move(new_pos)
event.accept()
def mouseReleaseEvent(self, event: QMouseEvent):
"""Handle mouse release."""
if event.button() == Qt.MouseButton.LeftButton:
self.dragging = False
# If didn't move much, treat as click
event.accept()
def mouseDoubleClickEvent(self, event: QMouseEvent):
"""Handle double click."""
self.clicked.emit()
event.accept()
def enterEvent(self, event: QEnterEvent):
"""Mouse entered - highlight."""
self.icon_label.setStyleSheet("""
QLabel {
font-size: 24px;
background-color: rgba(74, 158, 255, 200);
border-radius: 24px;
border: 2px solid rgba(255, 255, 255, 200);
}
""")
def leaveEvent(self, event):
"""Mouse left - normal."""
self.icon_label.setStyleSheet("""
QLabel {
font-size: 24px;
background-color: rgba(30, 30, 30, 200);
border-radius: 24px;
border: 2px solid rgba(74, 158, 255, 150);
}
""")

View File

@ -1,7 +1,7 @@
""" """
EU-Utility - Main Entry Point EU-Utility - Main Entry Point
Launch the overlay and plugin system. Launch the overlay, floating icon, and plugin system.
""" """
import sys import sys
@ -33,6 +33,7 @@ except ImportError:
from core.plugin_manager import PluginManager from core.plugin_manager import PluginManager
from core.overlay_window import OverlayWindow from core.overlay_window import OverlayWindow
from core.floating_icon import FloatingIcon
class HotkeyHandler(QObject): class HotkeyHandler(QObject):
@ -46,6 +47,7 @@ class EUUtilityApp:
def __init__(self): def __init__(self):
self.app = None self.app = None
self.overlay = None self.overlay = None
self.floating_icon = None
self.plugin_manager = None self.plugin_manager = None
self.hotkey_handler = None self.hotkey_handler = None
@ -71,6 +73,12 @@ class EUUtilityApp:
self.overlay = OverlayWindow(self.plugin_manager) self.overlay = OverlayWindow(self.plugin_manager)
self.plugin_manager.overlay = self.overlay self.plugin_manager.overlay = self.overlay
# Create floating icon
print("Creating floating icon...")
self.floating_icon = FloatingIcon()
self.floating_icon.clicked.connect(self._toggle_overlay)
self.floating_icon.show()
# Connect hotkey signal # Connect hotkey signal
self.hotkey_handler.toggle_signal.connect(self._on_toggle_signal) self.hotkey_handler.toggle_signal.connect(self._on_toggle_signal)
@ -79,6 +87,7 @@ class EUUtilityApp:
print("EU-Utility started!") print("EU-Utility started!")
print("Press Ctrl+Shift+U to toggle overlay") print("Press Ctrl+Shift+U to toggle overlay")
print("Or double-click the ⚡ floating icon")
# Run # Run
return self.app.exec() return self.app.exec()
@ -99,6 +108,10 @@ class EUUtilityApp:
def _on_toggle_signal(self): def _on_toggle_signal(self):
"""Handle toggle signal in main thread.""" """Handle toggle signal in main thread."""
self._toggle_overlay()
def _toggle_overlay(self):
"""Toggle overlay visibility."""
if self.overlay: if self.overlay:
self.overlay.toggle_overlay() self.overlay.toggle_overlay()

View File

@ -0,0 +1,7 @@
"""
Game Reader Plugin for EU-Utility
"""
from .plugin import GameReaderPlugin
__all__ = ["GameReaderPlugin"]

View File

@ -0,0 +1,261 @@
"""
EU-Utility - OCR Scanner Plugin
Reads text from in-game menus using OCR.
"""
import subprocess
import platform
import tempfile
from pathlib import Path
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QTextEdit, QComboBox,
QFrame, QScrollArea, QGroupBox
)
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer
from PyQt6.QtGui import QPixmap, QImage
from plugins.base_plugin import BasePlugin
class OCRScannerThread(QThread):
"""Background thread for OCR scanning."""
result_ready = pyqtSignal(str)
error_occurred = pyqtSignal(str)
def __init__(self, region=None):
super().__init__()
self.region = region # (x, y, width, height)
def run(self):
"""Capture screen and perform OCR."""
try:
system = platform.system()
# Create temp file for screenshot
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
screenshot_path = tmp.name
# Capture screenshot
if system == "Windows":
# Use PowerShell to capture screen
ps_cmd = f'''
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
$bitmap.Save("{screenshot_path}")
$graphics.Dispose()
$bitmap.Dispose()
'''
subprocess.run(['powershell', '-Command', ps_cmd], capture_output=True, timeout=10)
elif system == "Linux":
# Use gnome-screenshot or import
try:
subprocess.run(['gnome-screenshot', '-f', screenshot_path],
capture_output=True, timeout=10)
except:
subprocess.run(['import', '-window', 'root', screenshot_path],
capture_output=True, timeout=10)
# Perform OCR
text = self._perform_ocr(screenshot_path)
# Clean up
Path(screenshot_path).unlink(missing_ok=True)
self.result_ready.emit(text)
except Exception as e:
self.error_occurred.emit(str(e))
def _perform_ocr(self, image_path):
"""Perform OCR on image."""
try:
# Try easyocr first
import easyocr
reader = easyocr.Reader(['en'])
results = reader.readtext(image_path)
text = '\n'.join([result[1] for result in results])
return text if text else "No text detected"
except:
pass
try:
# Try pytesseract
import pytesseract
from PIL import Image
image = Image.open(image_path)
text = pytesseract.image_to_string(image)
return text if text.strip() else "No text detected"
except:
pass
return "OCR not available. Install: pip install easyocr or pytesseract"
class GameReaderPlugin(BasePlugin):
"""Read in-game menus and text using OCR."""
name = "Game Reader"
version = "1.0.0"
author = "ImpulsiveFPS"
description = "OCR scanner for in-game menus and text"
hotkey = "ctrl+shift+r" # R for Read
def initialize(self):
"""Setup game reader."""
self.scan_thread = None
self.last_result = ""
def get_ui(self):
"""Create game reader UI."""
widget = QWidget()
widget.setStyleSheet("background: transparent;")
layout = QVBoxLayout(widget)
layout.setSpacing(15)
layout.setContentsMargins(0, 0, 0, 0)
# Title
title = QLabel("📷 Game Reader (OCR)")
title.setStyleSheet("color: white; font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Info
info = QLabel("Capture in-game menus and read the text")
info.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
layout.addWidget(info)
# Scan button
scan_btn = QPushButton("📸 Capture Screen")
scan_btn.setStyleSheet("""
QPushButton {
background-color: #4a9eff;
color: white;
padding: 15px;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: bold;
}
QPushButton:hover {
background-color: #5aafff;
}
QPushButton:pressed {
background-color: #3a8eef;
}
""")
scan_btn.clicked.connect(self._capture_screen)
layout.addWidget(scan_btn)
# Status
self.status_label = QLabel("Ready to capture")
self.status_label.setStyleSheet("color: #666; font-size: 11px;")
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.status_label)
# Results area
results_frame = QFrame()
results_frame.setStyleSheet("""
QFrame {
background-color: rgba(0, 0, 0, 50);
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 20);
}
""")
results_layout = QVBoxLayout(results_frame)
results_layout.setContentsMargins(10, 10, 10, 10)
results_label = QLabel("📄 Captured Text:")
results_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 12px;")
results_layout.addWidget(results_label)
self.result_text = QTextEdit()
self.result_text.setPlaceholderText("Captured text will appear here...")
self.result_text.setStyleSheet("""
QTextEdit {
background-color: rgba(30, 30, 30, 100);
color: white;
border: none;
border-radius: 6px;
padding: 8px;
font-size: 13px;
}
""")
self.result_text.setMaximumHeight(150)
results_layout.addWidget(self.result_text)
# Copy button
copy_btn = QPushButton("📋 Copy Text")
copy_btn.setStyleSheet("""
QPushButton {
background-color: rgba(255, 255, 255, 20);
color: white;
padding: 8px;
border: none;
border-radius: 6px;
}
QPushButton:hover {
background-color: rgba(255, 255, 255, 30);
}
""")
copy_btn.clicked.connect(self._copy_text)
results_layout.addWidget(copy_btn)
layout.addWidget(results_frame)
# Common uses
uses_label = QLabel("💡 Common Uses:")
uses_label.setStyleSheet("color: rgba(255, 255, 255, 150); font-size: 11px; margin-top: 10px;")
layout.addWidget(uses_label)
uses_text = QLabel(
"• Read NPC dialogue\n"
"• Capture mission text\n"
"• Extract item stats\n"
"• Read shop prices"
)
uses_text.setStyleSheet("color: rgba(255, 255, 255, 100); font-size: 11px;")
layout.addWidget(uses_text)
layout.addStretch()
return widget
def _capture_screen(self):
"""Capture screen and perform OCR."""
self.status_label.setText("📸 Capturing...")
self.status_label.setStyleSheet("color: #4a9eff;")
# Start scan thread
self.scan_thread = OCRScannerThread()
self.scan_thread.result_ready.connect(self._on_result)
self.scan_thread.error_occurred.connect(self._on_error)
self.scan_thread.start()
def _on_result(self, text):
"""Handle OCR result."""
self.result_text.setText(text)
self.last_result = text
self.status_label.setText(f"✅ Captured {len(text)} characters")
self.status_label.setStyleSheet("color: #4caf50;")
def _on_error(self, error):
"""Handle OCR error."""
self.status_label.setText(f"❌ Error: {error}")
self.status_label.setStyleSheet("color: #f44336;")
def _copy_text(self):
"""Copy text to clipboard."""
from PyQt6.QtWidgets import QApplication
clipboard = QApplication.clipboard()
clipboard.setText(self.result_text.toPlainText())
self.status_label.setText("📋 Copied to clipboard!")
def on_hotkey(self):
"""Capture on hotkey."""
self._capture_screen()