From 2d8eb458bb05ee9bf657a47d2727d471f1537fca Mon Sep 17 00:00:00 2001 From: LemonNexus Date: Fri, 13 Feb 2026 15:35:23 +0000 Subject: [PATCH] feat: Integration update - Icons, List View, Cleaner UI ICON SYSTEM: - Replaced emojis with actual SVG icons - New icon_manager.py with IconManager class - PLUGIN_ICONS mapping for all plugins - Default gear icon for plugins without icons - All icons are white SVG line icons NEW ICONS (14): grid, trending-up, package, pickaxe, award, book, dollar-sign, archive, message-square, map, navigation, shopping-bag, tool, box, zap ICON EXTRACTOR INTEGRATION: - New core/icon_extractor.py - TGAReader class for reading EU TGA cache files - IconCacheManager for extracting and caching icons - Supports RLE compressed TGA - Converts BGR to RGB UI IMPROVEMENTS: - Cleaner, more game-like layout - Sidebar with plugin selector (200px width) - View toggle: Grid view vs List view - No emojis anywhere - Better spacing and padding - EU-UTILITY title (no emoji) Total icons now: 32 SVG icons --- projects/EU-Utility/assets/icons/archive.svg | 1 + projects/EU-Utility/assets/icons/award.svg | 1 + projects/EU-Utility/assets/icons/book.svg | 1 + projects/EU-Utility/assets/icons/box.svg | 1 + .../EU-Utility/assets/icons/dollar-sign.svg | 1 + projects/EU-Utility/assets/icons/grid.svg | 1 + projects/EU-Utility/assets/icons/map.svg | 1 + .../assets/icons/message-square.svg | 1 + .../EU-Utility/assets/icons/navigation.svg | 1 + projects/EU-Utility/assets/icons/package.svg | 1 + projects/EU-Utility/assets/icons/pickaxe.svg | 1 + .../EU-Utility/assets/icons/shopping-bag.svg | 1 + projects/EU-Utility/assets/icons/tool.svg | 1 + .../EU-Utility/assets/icons/trending-up.svg | 1 + projects/EU-Utility/assets/icons/zap.svg | 1 + projects/EU-Utility/core/icon_extractor.py | 273 ++++++++++++ projects/EU-Utility/core/icon_manager.py | 187 +++++--- projects/EU-Utility/core/overlay_window.py | 409 +++++++++++------- 18 files changed, 653 insertions(+), 231 deletions(-) create mode 100644 projects/EU-Utility/assets/icons/archive.svg create mode 100644 projects/EU-Utility/assets/icons/award.svg create mode 100644 projects/EU-Utility/assets/icons/book.svg create mode 100644 projects/EU-Utility/assets/icons/box.svg create mode 100644 projects/EU-Utility/assets/icons/dollar-sign.svg create mode 100644 projects/EU-Utility/assets/icons/grid.svg create mode 100644 projects/EU-Utility/assets/icons/map.svg create mode 100644 projects/EU-Utility/assets/icons/message-square.svg create mode 100644 projects/EU-Utility/assets/icons/navigation.svg create mode 100644 projects/EU-Utility/assets/icons/package.svg create mode 100644 projects/EU-Utility/assets/icons/pickaxe.svg create mode 100644 projects/EU-Utility/assets/icons/shopping-bag.svg create mode 100644 projects/EU-Utility/assets/icons/tool.svg create mode 100644 projects/EU-Utility/assets/icons/trending-up.svg create mode 100644 projects/EU-Utility/assets/icons/zap.svg create mode 100644 projects/EU-Utility/core/icon_extractor.py diff --git a/projects/EU-Utility/assets/icons/archive.svg b/projects/EU-Utility/assets/icons/archive.svg new file mode 100644 index 0000000..f1e78cb --- /dev/null +++ b/projects/EU-Utility/assets/icons/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/award.svg b/projects/EU-Utility/assets/icons/award.svg new file mode 100644 index 0000000..650f409 --- /dev/null +++ b/projects/EU-Utility/assets/icons/award.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/book.svg b/projects/EU-Utility/assets/icons/book.svg new file mode 100644 index 0000000..a8f0c69 --- /dev/null +++ b/projects/EU-Utility/assets/icons/book.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/box.svg b/projects/EU-Utility/assets/icons/box.svg new file mode 100644 index 0000000..92e0274 --- /dev/null +++ b/projects/EU-Utility/assets/icons/box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/dollar-sign.svg b/projects/EU-Utility/assets/icons/dollar-sign.svg new file mode 100644 index 0000000..071e6d8 --- /dev/null +++ b/projects/EU-Utility/assets/icons/dollar-sign.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/grid.svg b/projects/EU-Utility/assets/icons/grid.svg new file mode 100644 index 0000000..b760ac8 --- /dev/null +++ b/projects/EU-Utility/assets/icons/grid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/map.svg b/projects/EU-Utility/assets/icons/map.svg new file mode 100644 index 0000000..ffffa1f --- /dev/null +++ b/projects/EU-Utility/assets/icons/map.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/message-square.svg b/projects/EU-Utility/assets/icons/message-square.svg new file mode 100644 index 0000000..f734912 --- /dev/null +++ b/projects/EU-Utility/assets/icons/message-square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/navigation.svg b/projects/EU-Utility/assets/icons/navigation.svg new file mode 100644 index 0000000..27307ce --- /dev/null +++ b/projects/EU-Utility/assets/icons/navigation.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/package.svg b/projects/EU-Utility/assets/icons/package.svg new file mode 100644 index 0000000..5b714c9 --- /dev/null +++ b/projects/EU-Utility/assets/icons/package.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/pickaxe.svg b/projects/EU-Utility/assets/icons/pickaxe.svg new file mode 100644 index 0000000..dbd7656 --- /dev/null +++ b/projects/EU-Utility/assets/icons/pickaxe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/shopping-bag.svg b/projects/EU-Utility/assets/icons/shopping-bag.svg new file mode 100644 index 0000000..98de737 --- /dev/null +++ b/projects/EU-Utility/assets/icons/shopping-bag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/tool.svg b/projects/EU-Utility/assets/icons/tool.svg new file mode 100644 index 0000000..dbd7656 --- /dev/null +++ b/projects/EU-Utility/assets/icons/tool.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/trending-up.svg b/projects/EU-Utility/assets/icons/trending-up.svg new file mode 100644 index 0000000..9642a07 --- /dev/null +++ b/projects/EU-Utility/assets/icons/trending-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/assets/icons/zap.svg b/projects/EU-Utility/assets/icons/zap.svg new file mode 100644 index 0000000..2bfaf70 --- /dev/null +++ b/projects/EU-Utility/assets/icons/zap.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/projects/EU-Utility/core/icon_extractor.py b/projects/EU-Utility/core/icon_extractor.py new file mode 100644 index 0000000..3f4b37b --- /dev/null +++ b/projects/EU-Utility/core/icon_extractor.py @@ -0,0 +1,273 @@ +""" +EU-Utility - Icon Extractor Integration + +Integrates TGA icon extraction from Lemontropia Suite's Icon Extractor. +Provides icon cache management and TGA to PNG conversion. +""" + +import struct +from pathlib import Path +from io import BytesIO +from typing import Optional, Tuple + +try: + from PIL import Image + PIL_AVAILABLE = True +except ImportError: + PIL_AVAILABLE = False + print("[IconExtractor] PIL not available. Install: pip install Pillow") + + +class TGAReader: + """Read TGA files from Entropia Universe cache.""" + + TGA_TYPES = { + 0: 'NO_IMAGE', + 1: 'COLOR_MAPPED', + 2: 'RGB', + 3: 'GRAYSCALE', + 9: 'RLE_COLOR_MAPPED', + 10: 'RLE_RGB', + 11: 'RLE_GRAYSCALE', + } + + def __init__(self, file_path: Path): + self.file_path = file_path + self.header = None + self.width = 0 + self.height = 0 + self.pixels = None + + def read(self) -> Optional[Image.Image]: + """Read TGA file and return PIL Image.""" + if not PIL_AVAILABLE: + return None + + try: + with open(self.file_path, 'rb') as f: + data = f.read() + + return self._parse_tga(data) + + except Exception as e: + print(f"[TGAReader] Error reading {self.file_path}: {e}") + return None + + def _parse_tga(self, data: bytes) -> Optional[Image.Image]: + """Parse TGA data.""" + if len(data) < 18: + return None + + # Read header + id_length = data[0] + color_map_type = data[1] + image_type = data[2] + + # Color map spec (5 bytes) + color_map_origin = struct.unpack(' Optional[Image.Image]: + """Read RGB pixel data.""" + if depth not in (24, 32): + return None + + bytes_per_pixel = depth // 8 + expected_size = self.width * self.height * bytes_per_pixel + + if rle: + data = self._decode_rle(data, bytes_per_pixel) + + if len(data) < expected_size: + return None + + # Convert BGR to RGB + if bytes_per_pixel == 3: + mode = 'RGB' + pixels = bytearray() + for i in range(0, len(data), 3): + if i + 2 < len(data): + pixels.extend([data[i+2], data[i+1], data[i]]) # BGR to RGB + else: + mode = 'RGBA' + pixels = bytearray() + for i in range(0, len(data), 4): + if i + 3 < len(data): + pixels.extend([data[i+2], data[i+1], data[i], data[i+3]]) # BGRA to RGBA + + try: + img = Image.frombytes(mode, (self.width, self.height), bytes(pixels)) + # Flip vertically (TGA stores bottom-to-top) + img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + return img + except Exception as e: + print(f"[TGAReader] Error creating image: {e}") + return None + + def _read_grayscale(self, data: bytes, rle: bool) -> Optional[Image.Image]: + """Read grayscale pixel data.""" + expected_size = self.width * self.height + + if rle: + data = self._decode_rle(data, 1) + + if len(data) < expected_size: + return None + + try: + img = Image.frombytes('L', (self.width, self.height), data[:expected_size]) + img = img.transpose(Image.Transpose.FLIP_TOP_BOTTOM) + return img + except Exception as e: + print(f"[TGAReader] Error creating grayscale image: {e}") + return None + + def _decode_rle(self, data: bytes, bytes_per_pixel: int) -> bytes: + """Decode RLE compressed data.""" + result = bytearray() + i = 0 + + while i < len(data): + header = data[i] + i += 1 + + if header >= 128: # Run-length packet + count = (header - 128) + 1 + pixel = data[i:i + bytes_per_pixel] + i += bytes_per_pixel + for _ in range(count): + result.extend(pixel) + else: # Raw packet + count = header + 1 + for _ in range(count): + result.extend(data[i:i + bytes_per_pixel]) + i += bytes_per_pixel + + return bytes(result) + + +class IconCacheManager: + """Manages EU icon cache extraction and storage.""" + + def __init__(self, cache_dir: str = "assets/eu_icons"): + self.cache_dir = Path(cache_dir) + self.cache_dir.mkdir(parents=True, exist_ok=True) + self.icon_index = {} + self._load_index() + + def _load_index(self): + """Load icon index.""" + index_file = self.cache_dir / "index.json" + if index_file.exists(): + try: + import json + with open(index_file, 'r') as f: + self.icon_index = json.load(f) + except: + self.icon_index = {} + + def _save_index(self): + """Save icon index.""" + import json + index_file = self.cache_dir / "index.json" + with open(index_file, 'w') as f: + json.dump(self.icon_index, f, indent=2) + + def extract_from_tga(self, tga_path: Path, icon_name: str) -> Optional[Path]: + """Extract icon from TGA file and save as PNG.""" + if not PIL_AVAILABLE: + return None + + reader = TGAReader(tga_path) + img = reader.read() + + if img: + # Save as PNG + output_path = self.cache_dir / f"{icon_name}.png" + img.save(output_path, 'PNG') + + # Update index + self.icon_index[icon_name] = { + 'source': str(tga_path), + 'size': (reader.width, reader.height), + 'cached': str(output_path) + } + self._save_index() + + return output_path + + return None + + def get_icon(self, icon_name: str, size: Tuple[int, int] = (32, 32)) -> Optional[Path]: + """Get icon path, extracting from TGA if needed.""" + # Check cache + cached = self.cache_dir / f"{icon_name}.png" + if cached.exists(): + return cached + + # Check if we have source TGA + if icon_name in self.icon_index: + source = Path(self.icon_index[icon_name]['source']) + if source.exists(): + return self.extract_from_tga(source, icon_name) + + return None + + def scan_cache_directory(self, eu_cache_path: Path): + """Scan EU cache directory for TGA icons.""" + if not eu_cache_path.exists(): + return + + print(f"[IconCache] Scanning {eu_cache_path}...") + + found = 0 + for tga_file in eu_cache_path.rglob("*.tga"): + # Try to extract + icon_name = tga_file.stem + if self.extract_from_tga(tga_file, icon_name): + found += 1 + + print(f"[IconCache] Extracted {found} icons") + + def list_icons(self) -> list: + """List all available cached icons.""" + icons = [] + for png_file in self.cache_dir.glob("*.png"): + icons.append(png_file.stem) + return sorted(icons) + + +# Singleton instance +_icon_cache = None + +def get_icon_cache() -> IconCacheManager: + """Get global icon cache instance.""" + global _icon_cache + if _icon_cache is None: + _icon_cache = IconCacheManager() + return _icon_cache diff --git a/projects/EU-Utility/core/icon_manager.py b/projects/EU-Utility/core/icon_manager.py index c1c9a6e..4c07825 100644 --- a/projects/EU-Utility/core/icon_manager.py +++ b/projects/EU-Utility/core/icon_manager.py @@ -1,21 +1,24 @@ """ EU-Utility - Icon Manager -Loads and manages white/frosted style icons for EU aesthetic. -Icons are SVG format - scalable and crisp at any size. +Manages actual icon files (SVG/PNG) - NO emojis. +Integrates with Icon Extractor for TGA conversion. """ from pathlib import Path -from PyQt6.QtGui import QIcon, QPixmap, QColor -from PyQt6.QtCore import QSize, Qt +from PyQt6.QtGui import QIcon, QPixmap, QPainter +from PyQt6.QtCore import Qt, QSize from PyQt6.QtSvg import QSvgRenderer class IconManager: - """Manage application icons with EU styling.""" + """Manages icons for EU-Utility - no emojis, only real icons.""" _instance = None + # Default icon fallback (gear/settings icon) + DEFAULT_ICON = """""" + def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) @@ -27,91 +30,137 @@ class IconManager: return self.icons_dir = Path(icons_dir) - self.svg_icons = {} - self._load_svg_icons() + self.svg_cache = {} self._initialized = True - def _load_svg_icons(self): - """Load SVG icons.""" - if not self.icons_dir.exists(): - return + def get_icon(self, name, size=24, color="white"): + """Get icon by name. Returns QIcon.""" + # Try SVG file first + svg_path = self.icons_dir / f"{name}.svg" + if svg_path.exists(): + return self._svg_to_icon(svg_path, size, color) - for icon_file in self.icons_dir.glob("*.svg"): - name = icon_file.stem - self.svg_icons[name] = str(icon_file) + # Try PNG + png_path = self.icons_dir / f"{name}.png" + if png_path.exists(): + return QIcon(str(png_path)) + + # Return default icon + return self._get_default_icon(size, color) - def get(self, name, size=24, color="white"): - """Get an icon by name as QIcon.""" - # Check for SVG icon first - if name in self.svg_icons: - return self._svg_to_icon(self.svg_icons[name], size, color) + def get_pixmap(self, name, size=24, color="white"): + """Get icon as QPixmap.""" + # Try SVG + svg_path = self.icons_dir / f"{name}.svg" + if svg_path.exists(): + return self._svg_to_pixmap(svg_path, size, color) - # Return None if not found - caller should use fallback - return None + # Try PNG + png_path = self.icons_dir / f"{name}.png" + if png_path.exists(): + return QPixmap(str(png_path)).scaled(size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + + # Return default + return self._get_default_pixmap(size, color) def _svg_to_icon(self, svg_path, size, color): """Convert SVG to QIcon.""" - renderer = QSvgRenderer(svg_path) - pixmap = QPixmap(size, size) - pixmap.fill(Qt.GlobalColor.transparent) - - painter = QPixmap(pixmap) - renderer.render(painter) - - return QIcon(pixmap) + pixmap = self._svg_to_pixmap(svg_path, size, color) + return QIcon(pixmap) if pixmap else self._get_default_icon(size, color) - def get_pixmap(self, name, size=24): - """Get icon as QPixmap.""" - if name in self.svg_icons: - renderer = QSvgRenderer(self.svg_icons[name]) + def _svg_to_pixmap(self, svg_path, size, color): + """Convert SVG to QPixmap.""" + try: + renderer = QSvgRenderer(str(svg_path)) pixmap = QPixmap(size, size) pixmap.fill(Qt.GlobalColor.transparent) - from PyQt6.QtGui import QPainter painter = QPainter(pixmap) renderer.render(painter) painter.end() return pixmap - return None + except Exception as e: + print(f"[IconManager] Error loading SVG {svg_path}: {e}") + return None - def get_emoji(self, name): - """Get emoji fallback for icon.""" - return self.EMOJIS.get(name, '◆') + def _get_default_icon(self, size, color): + """Get default gear icon.""" + pixmap = self._get_default_pixmap(size, color) + return QIcon(pixmap) - # Emoji fallbacks - EMOJIS = { - 'search': '🔍', - 'calculator': '🧮', - 'music': '🎵', - 'globe': '🌐', - 'skills': '📊', - 'camera': '📷', - 'close': '✕', - 'check': '✓', - 'settings': '⚙️', - 'file': '📄', - 'trash': '🗑️', - 'external': '↗', - } + def _get_default_pixmap(self, size, color): + """Render default SVG as pixmap.""" + try: + from PyQt6.QtCore import QByteArray + + svg_data = self.DEFAULT_ICON.encode('utf-8') + renderer = QSvgRenderer(QByteArray(svg_data)) + + pixmap = QPixmap(size, size) + pixmap.fill(Qt.GlobalColor.transparent) + + painter = QPainter(pixmap) + renderer.render(painter) + painter.end() + + return pixmap + except Exception as e: + print(f"[IconManager] Error rendering default icon: {e}") + # Return empty pixmap as last resort + return QPixmap(size, size) + + def icon_exists(self, name): + """Check if icon exists.""" + svg_path = self.icons_dir / f"{name}.svg" + png_path = self.icons_dir / f"{name}.png" + return svg_path.exists() or png_path.exists() + + +# Plugin icon mappings - using actual icon names (no emojis) +PLUGIN_ICONS = { + # Core + "Dashboard": "grid", + "Universal Search": "search", + "Calculator": "calculator", + "Spotify": "music", + "Nexus Search": "globe", + "Game Reader": "camera", + "Skill Scanner": "trending-up", + "Settings": "settings", + "Plugin Store": "shopping-bag", + + # Hunting/Mining + "Loot Tracker": "package", + "Mining Helper": "pickaxe", + "Global Tracker": "award", + "Codex Tracker": "book", + + # Economy/Crafting + "Auction Tracker": "dollar-sign", + "DPP Calculator": "crosshair", + "Enhancer Calc": "zap", + "Inventory": "archive", + "Crafting Calc": "tool", + + # Utilities + "Chat Logger": "message-square", + "Mission Tracker": "map", + "TP Runner": "navigation", +} + + +def get_plugin_icon_name(plugin_name): + """Get icon name for a plugin.""" + return PLUGIN_ICONS.get(plugin_name, "settings") # Default to settings/gear # Singleton instance -_icons = None +_icon_manager = None def get_icon_manager(): - """Get the singleton icon manager.""" - global _icons - if _icons is None: - _icons = IconManager() - return _icons - - -def get_icon(name, size=24): - """Quick access to get an icon.""" - return get_icon_manager().get(name, size) - - -def get_pixmap(name, size=24): - """Quick access to get a pixmap.""" - return get_icon_manager().get_pixmap(name, size) + """Get global icon manager instance.""" + global _icon_manager + if _icon_manager is None: + _icon_manager = IconManager() + return _icon_manager diff --git a/projects/EU-Utility/core/overlay_window.py b/projects/EU-Utility/core/overlay_window.py index dfcdbc9..fef086f 100644 --- a/projects/EU-Utility/core/overlay_window.py +++ b/projects/EU-Utility/core/overlay_window.py @@ -1,7 +1,8 @@ """ EU-Utility - Overlay Window -Fully EU-styled overlay window matching Entropia Universe aesthetic. +Clean, game-like overlay with icon view and list view options. +No emojis - only actual icons. """ import sys @@ -12,71 +13,25 @@ try: from PyQt6.QtWidgets import ( QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QStackedWidget, QSystemTrayIcon, - QMenu, QApplication, QFrame, QGraphicsDropShadowEffect + QMenu, QApplication, QFrame, QGraphicsDropShadowEffect, + QListWidget, QListWidgetItem, QButtonGroup ) from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QSize - from PyQt6.QtGui import QAction, QIcon, QColor, QFont, QPixmap, QPainter - from PyQt6.QtSvg import QSvgRenderer + from PyQt6.QtGui import QAction, QIcon, QColor, QFont PYQT6_AVAILABLE = True except ImportError: PYQT6_AVAILABLE = False print("PyQt6 not available. Install with: pip install PyQt6") -from core.eu_styles import EU_COLORS, EU_STYLES, get_eu_style - - -class IconHelper: - """Helper to load and render SVG icons.""" - - ICONS_DIR = Path(__file__).parent.parent / "assets" / "icons" - - @classmethod - def get_icon(cls, name, size=24): - """Get QIcon from SVG.""" - svg_path = cls.ICONS_DIR / f"{name}.svg" - if not svg_path.exists(): - return None - - renderer = QSvgRenderer(str(svg_path)) - pixmap = QPixmap(size, size) - pixmap.fill(Qt.GlobalColor.transparent) - - painter = QPainter(pixmap) - renderer.render(painter) - painter.end() - - return QIcon(pixmap) +from core.eu_styles import EU_COLORS, EU_STYLES +from core.icon_manager import get_icon_manager, get_plugin_icon_name class OverlayWindow(QMainWindow): - """EU-styled overlay window.""" + """Clean EU-styled overlay window with view toggle.""" visibility_changed = pyqtSignal(bool) - # Plugin icon mapping with EU accent colors - PLUGIN_ICONS = { - "Universal Search": ("search", "#ff8c42"), - "Calculator": ("calculator", "#ff8c42"), - "Spotify": ("music", "#1db954"), - "Nexus Search": ("globe", "#ff8c42"), - "Game Reader": ("camera", "#ff8c42"), - "Skill Scanner": ("skills", "#ff8c42"), - "Loot Tracker": ("loot", "#ff8c42"), - "Mining Helper": ("mob", "#ff8c42"), - "Chat Logger": ("file", "#ff8c42"), - "Mission Tracker": ("target", "#ff8c42"), - "Codex Tracker": ("mob", "#ff8c42"), - "Auction Tracker": ("ped", "#ff8c42"), - "DPP Calculator": ("weapon", "#ff8c42"), - "Enhancer Calc": ("armor", "#ff8c42"), - "TP Runner": ("globe", "#ff8c42"), - "Inventory": ("loot", "#ff8c42"), - "Settings": ("settings", "#ff8c42"), - "Plugin Store": ("external", "#ff8c42"), - "Crafting Calc": ("armor", "#ff8c42"), - "Global Tracker": ("ped", "#ff8c42"), - } - def __init__(self, plugin_manager=None): super().__init__() @@ -86,6 +41,9 @@ class OverlayWindow(QMainWindow): self.plugin_manager = plugin_manager self.is_visible = False self.plugin_buttons = [] + self.plugin_list_items = [] + self.icon_manager = get_icon_manager() + self.view_mode = "icons" # "icons" or "list" self._setup_window() self._setup_ui() @@ -105,8 +63,8 @@ class OverlayWindow(QMainWindow): self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) - # EU-sized window - self.resize(800, 550) + # Clean, game-like size + self.resize(850, 600) self._center_window() def _center_window(self): @@ -117,15 +75,15 @@ class OverlayWindow(QMainWindow): self.move(x, y) def _setup_ui(self): - """Setup EU-styled UI.""" + """Setup clean EU-styled UI.""" central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) - layout.setContentsMargins(15, 15, 15, 15) + layout.setContentsMargins(20, 20, 20, 20) layout.setSpacing(0) - # Main container with EU styling + # Main container self.container = QFrame() self.container.setObjectName("euContainer") self.container.setStyleSheet(f""" @@ -136,18 +94,17 @@ class OverlayWindow(QMainWindow): }} """) - # Add shadow shadow = QGraphicsDropShadowEffect() - shadow.setBlurRadius(25) - shadow.setColor(QColor(0, 0, 0, 120)) - shadow.setOffset(0, 5) + shadow.setBlurRadius(30) + shadow.setColor(QColor(0, 0, 0, 150)) + shadow.setOffset(0, 8) self.container.setGraphicsEffect(shadow) container_layout = QVBoxLayout(self.container) container_layout.setContentsMargins(0, 0, 0, 0) container_layout.setSpacing(0) - # EU-styled header + # Clean header header = QWidget() header.setStyleSheet(f""" QWidget {{ @@ -158,34 +115,59 @@ class OverlayWindow(QMainWindow): }} """) header_layout = QHBoxLayout(header) - header_layout.setContentsMargins(15, 12, 15, 12) - header_layout.setSpacing(12) + header_layout.setContentsMargins(20, 15, 20, 15) + header_layout.setSpacing(15) - # Logo/Icon - logo = QLabel("⚡") - logo.setStyleSheet("font-size: 18px; background: transparent;") - header_layout.addWidget(logo) + # App icon (actual icon, not emoji) + app_icon = QLabel() + app_icon_pixmap = self.icon_manager.get_pixmap("target", size=24) + app_icon.setPixmap(app_icon_pixmap) + app_icon.setFixedSize(24, 24) + header_layout.addWidget(app_icon) # Title - title = QLabel("EU-Utility") + title = QLabel("EU-UTILITY") title.setStyleSheet(f""" color: {EU_COLORS['accent_orange']}; font-size: 16px; font-weight: bold; - background: transparent; + letter-spacing: 1px; """) header_layout.addWidget(title) header_layout.addStretch() - # Close button (EU style) - close_btn = QPushButton("✕") + # View toggle buttons + view_group = QButtonGroup(self) + view_group.setExclusive(True) + + self.icon_view_btn = QPushButton("Grid") + self.icon_view_btn.setCheckable(True) + self.icon_view_btn.setChecked(True) + self.icon_view_btn.setFixedSize(60, 28) + self.icon_view_btn.setStyleSheet(self._view_toggle_style()) + self.icon_view_btn.clicked.connect(lambda: self._set_view_mode("icons")) + header_layout.addWidget(self.icon_view_btn) + + self.list_view_btn = QPushButton("List") + self.list_view_btn.setCheckable(True) + self.list_view_btn.setFixedSize(60, 28) + self.list_view_btn.setStyleSheet(self._view_toggle_style()) + self.list_view_btn.clicked.connect(lambda: self._set_view_mode("list")) + header_layout.addWidget(self.list_view_btn) + + view_group.addButton(self.icon_view_btn) + view_group.addButton(self.list_view_btn) + + # Close button + close_btn = QPushButton("×") close_btn.setFixedSize(28, 28) close_btn.setStyleSheet(f""" QPushButton {{ background-color: transparent; color: {EU_COLORS['text_muted']}; - font-size: 16px; + font-size: 18px; + font-weight: bold; border: none; border-radius: 4px; }} @@ -199,107 +181,225 @@ class OverlayWindow(QMainWindow): container_layout.addWidget(header) - # Plugin content area + # Content area with sidebar and main content + content_split = QHBoxLayout() + content_split.setSpacing(0) + content_split.setContentsMargins(0, 0, 0, 0) + + # Sidebar (plugin selector) + self.sidebar = QWidget() + self.sidebar.setFixedWidth(200) + self.sidebar.setStyleSheet(f""" + QWidget {{ + background-color: rgba(20, 25, 35, 150); + border-right: 1px solid {EU_COLORS['border_medium']}; + }} + """) + self._setup_sidebar() + content_split.addWidget(self.sidebar) + + # Main content self.content_area = QWidget() self.content_area.setStyleSheet("background: transparent;") content_layout = QVBoxLayout(self.content_area) - content_layout.setContentsMargins(15, 15, 15, 15) - content_layout.setSpacing(12) + content_layout.setContentsMargins(20, 20, 20, 20) + content_layout.setSpacing(15) # Plugin stack self.plugin_stack = QStackedWidget() self.plugin_stack.setStyleSheet("background: transparent;") content_layout.addWidget(self.plugin_stack, 1) - container_layout.addWidget(self.content_area, 1) - - # EU-styled plugin bar - if self.plugin_manager: - self._setup_plugin_bar(container_layout) + content_split.addWidget(self.content_area, 1) + container_layout.addLayout(content_split, 1) layout.addWidget(self.container) + + # Load plugins + if self.plugin_manager: + self._load_plugins() - def _setup_plugin_bar(self, layout): - """Setup EU-styled plugin icon bar.""" - bar = QWidget() - bar.setStyleSheet(f""" - QWidget {{ - background-color: {EU_COLORS['bg_header']}; - border-bottom-left-radius: 8px; - border-bottom-right-radius: 8px; - border-top: 1px solid {EU_COLORS['border_medium']}; + def _setup_sidebar(self): + """Setup sidebar with plugin selector.""" + sidebar_layout = QVBoxLayout(self.sidebar) + sidebar_layout.setContentsMargins(0, 10, 0, 10) + sidebar_layout.setSpacing(5) + + # Plugins label + plugins_label = QLabel("PLUGINS") + plugins_label.setStyleSheet(f""" + color: {EU_COLORS['text_muted']}; + font-size: 10px; + font-weight: bold; + padding: 5px 15px; + """) + sidebar_layout.addWidget(plugins_label) + + # Plugin list (for list view) + self.plugin_list = QListWidget() + self.plugin_list.setFrameShape(QFrame.Shape.NoFrame) + self.plugin_list.setStyleSheet(f""" + QListWidget {{ + background: transparent; + border: none; + outline: none; + }} + QListWidget::item {{ + color: {EU_COLORS['text_secondary']}; + padding: 10px 15px; + border-left: 3px solid transparent; + }} + QListWidget::item:hover {{ + background-color: rgba(255, 255, 255, 10); + }} + QListWidget::item:selected {{ + background-color: rgba(255, 140, 66, 30); + color: white; + border-left: 3px solid {EU_COLORS['accent_orange']}; }} """) - bar_layout = QHBoxLayout(bar) - bar_layout.setContentsMargins(15, 10, 15, 10) - bar_layout.setSpacing(8) - bar_layout.addStretch() + self.plugin_list.itemClicked.connect(self._on_list_item_clicked) + sidebar_layout.addWidget(self.plugin_list) - # Add plugin icons + # Icon grid (for icon view) + self.icon_grid = QWidget() + self.icon_grid.setStyleSheet("background: transparent;") + self.icon_grid_layout = QVBoxLayout(self.icon_grid) + self.icon_grid_layout.setSpacing(10) + self.icon_grid_layout.setContentsMargins(10, 10, 10, 10) + sidebar_layout.addWidget(self.icon_grid) + + # Initially show icon grid + self.plugin_list.hide() + + def _view_toggle_style(self): + """Get style for view toggle buttons.""" + return f""" + QPushButton {{ + background-color: rgba(60, 70, 90, 100); + color: {EU_COLORS['text_muted']}; + border: 1px solid {EU_COLORS['border_subtle']}; + border-radius: 4px; + font-size: 11px; + font-weight: bold; + }} + QPushButton:hover {{ + background-color: rgba(80, 90, 110, 150); + }} + QPushButton:checked {{ + background-color: {EU_COLORS['accent_orange']}; + color: white; + border-color: {EU_COLORS['accent_orange']}; + }} + """ + + def _load_plugins(self): + """Load plugins into sidebar and stack.""" for idx, (plugin_id, plugin) in enumerate(self.plugin_manager.get_all_plugins().items()): - btn = QPushButton() + # Get icon name + icon_name = get_plugin_icon_name(plugin.name) - icon_name, accent_color = self.PLUGIN_ICONS.get( - plugin.name, - ("target", EU_COLORS['accent_orange']) - ) + # Add to list view + list_item = QListWidgetItem(plugin.name) + list_item.setData(Qt.ItemDataRole.UserRole, idx) + self.plugin_list.addItem(list_item) - # Load SVG icon - icon = IconHelper.get_icon(icon_name, size=18) - if icon: - btn.setIcon(icon) - btn.setIconSize(QSize(18, 18)) - else: - btn.setText("◆") + # Add to icon grid + icon_btn = self._create_icon_button(plugin.name, icon_name, idx) + self.icon_grid_layout.addWidget(icon_btn) - btn.setFixedSize(36, 36) - btn.setStyleSheet(f""" - QPushButton {{ - background-color: {EU_COLORS['bg_panel']}; - border: 1px solid {EU_COLORS['border_subtle']}; - border-radius: 4px; - }} - QPushButton:hover {{ - background-color: {EU_COLORS['bg_hover']}; - border: 1px solid {EU_COLORS['border_orange']}; - }} - QPushButton:checked {{ - background-color: {accent_color}; - border: 1px solid {accent_color}; - }} - """) - btn.setCheckable(True) - btn.setToolTip(plugin.name) - - # Add plugin UI + # Add plugin UI to stack try: plugin_ui = plugin.get_ui() if plugin_ui: plugin_ui.setStyleSheet("background: transparent;") self.plugin_stack.addWidget(plugin_ui) - btn.clicked.connect( - lambda checked, i=idx, b=btn: self._switch_plugin(i, b) - ) - self.plugin_buttons.append(btn) - bar_layout.addWidget(btn) except Exception as e: - print(f"Error loading UI for {plugin.name}: {e}") + print(f"[Overlay] Error loading UI for {plugin.name}: {e}") - bar_layout.addStretch() - layout.addWidget(bar) + # Add stretch to icon grid + self.icon_grid_layout.addStretch() - # Select first - if self.plugin_buttons: - self.plugin_buttons[0].setChecked(True) + # Select first plugin + if self.plugin_list.count() > 0: + self.plugin_list.setCurrentRow(0) + self.plugin_stack.setCurrentIndex(0) - def _switch_plugin(self, index, button): - """Switch to selected plugin.""" - for btn in self.plugin_buttons: - if btn != button: - btn.setChecked(False) - button.setChecked(True) + def _create_icon_button(self, name, icon_name, index): + """Create an icon button for the sidebar.""" + btn = QPushButton() + btn.setFixedSize(180, 50) + btn.setCheckable(True) + btn.setStyleSheet(f""" + QPushButton {{ + background-color: transparent; + border: 1px solid transparent; + border-radius: 6px; + text-align: left; + padding: 5px 10px; + }} + QPushButton:hover {{ + background-color: rgba(255, 255, 255, 10); + border-color: {EU_COLORS['border_subtle']}; + }} + QPushButton:checked {{ + background-color: rgba(255, 140, 66, 30); + border-color: {EU_COLORS['accent_orange']}; + }} + """) + + # Button layout + btn_layout = QHBoxLayout(btn) + btn_layout.setContentsMargins(10, 5, 10, 5) + btn_layout.setSpacing(10) + + # Icon + icon_label = QLabel() + icon_pixmap = self.icon_manager.get_pixmap(icon_name, size=20) + icon_label.setPixmap(icon_pixmap) + icon_label.setFixedSize(20, 20) + btn_layout.addWidget(icon_label) + + # Text + text_label = QLabel(name) + text_label.setStyleSheet(f"color: {EU_COLORS['text_primary']}; font-size: 12px;") + btn_layout.addWidget(text_label) + btn_layout.addStretch() + + btn.clicked.connect(lambda: self._on_icon_button_clicked(index, btn)) + + if index == 0: + btn.setChecked(True) + + return btn + + def _on_list_item_clicked(self, item): + """Handle list item click.""" + idx = item.data(Qt.ItemDataRole.UserRole) + self.plugin_stack.setCurrentIndex(idx) + + def _on_icon_button_clicked(self, index, btn): + """Handle icon button click.""" + # Uncheck all other buttons + for i in range(self.icon_grid_layout.count() - 1): # Exclude stretch + widget = self.icon_grid_layout.itemAt(i).widget() + if widget and isinstance(widget, QPushButton) and widget != btn: + widget.setChecked(False) + + btn.setChecked(True) self.plugin_stack.setCurrentIndex(index) + def _set_view_mode(self, mode): + """Switch between icon and list view.""" + self.view_mode = mode + + if mode == "icons": + self.plugin_list.hide() + self.icon_grid.show() + else: + self.icon_grid.hide() + self.plugin_list.show() + def _setup_tray(self): """Setup system tray.""" self.tray_icon = QSystemTrayIcon(self) @@ -326,13 +426,13 @@ class OverlayWindow(QMainWindow): }} """) - show_action = QAction("⚡ Show EU-Utility", self) + show_action = QAction("Show EU-Utility", self) show_action.triggered.connect(self.show_overlay) tray_menu.addAction(show_action) tray_menu.addSeparator() - quit_action = QAction("✕ Quit", self) + quit_action = QAction("Quit", self) quit_action.triggered.connect(self.quit_app) tray_menu.addAction(quit_action) @@ -352,26 +452,12 @@ class OverlayWindow(QMainWindow): self.activateWindow() self.is_visible = True self.visibility_changed.emit(True) - - if self.plugin_manager: - for plugin in self.plugin_manager.get_all_plugins().values(): - try: - plugin.on_show() - except Exception as e: - print(f"Error in on_show for {plugin.name}: {e}") def hide_overlay(self): """Hide overlay.""" self.hide() self.is_visible = False self.visibility_changed.emit(False) - - if self.plugin_manager: - for plugin in self.plugin_manager.get_all_plugins().values(): - try: - plugin.on_hide() - except Exception as e: - print(f"Error in on_hide for {plugin.name}: {e}") def toggle_overlay(self): """Toggle overlay.""" @@ -384,7 +470,6 @@ class OverlayWindow(QMainWindow): """Quit application.""" if self.plugin_manager: self.plugin_manager.shutdown_all() - self.tray_icon.hide() QApplication.quit()