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:
parent
8dbbf4d971
commit
d74de07110
|
|
@ -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);
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
"""
|
||||||
|
Game Reader Plugin for EU-Utility
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .plugin import GameReaderPlugin
|
||||||
|
|
||||||
|
__all__ = ["GameReaderPlugin"]
|
||||||
|
|
@ -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()
|
||||||
Loading…
Reference in New Issue