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
This commit is contained in:
LemonNexus 2026-02-13 15:35:23 +00:00
parent 5e524e31a5
commit 2d8eb458bb
18 changed files with 653 additions and 231 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>

After

Width:  |  Height:  |  Size: 265 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>

After

Width:  |  Height:  |  Size: 286 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 22 8.5 22 15.5 12 22 2 15.5 2 8.5 12 2"></polygon><line x1="12" y1="22" x2="12" y2="15.5"></line><polyline points="22 8.5 12 15.5 2 8.5"></polyline><polyline points="2 15.5 12 8.5 22 15.5"></polyline><line x1="12" y1="2" x2="12" y2="8.5"></line></svg>

After

Width:  |  Height:  |  Size: 424 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"></line><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path></svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="1 6 1 22 8 18 16 22 16 6 1 6"></polygon><path d="M17 3a2 2 0 0 1 2 2v16l-4-3"></path></svg>

After

Width:  |  Height:  |  Size: 259 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 11 22 2 13 21 11 13 3 11"></polygon></svg>

After

Width:  |  Height:  |  Size: 212 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"></path></svg>

After

Width:  |  Height:  |  Size: 230 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"></path><line x1="3" y1="6" x2="21" y2="6"></line><path d="M16 10a4 4 0 0 1-8 0"></path></svg>

After

Width:  |  Height:  |  Size: 305 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -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('<H', data[3:5])[0]
color_map_length = struct.unpack('<H', data[5:7])[0]
color_map_entry_size = data[7]
# Image spec (10 bytes)
x_origin = struct.unpack('<H', data[8:10])[0]
y_origin = struct.unpack('<H', data[10:12])[0]
self.width = struct.unpack('<H', data[12:14])[0]
self.height = struct.unpack('<H', data[14:16])[0]
pixel_depth = data[16]
descriptor = data[17]
# Skip ID and color map
offset = 18 + id_length
if color_map_type == 1:
offset += color_map_length * (color_map_entry_size // 8)
# Read pixel data
pixel_data = data[offset:]
# Create image based on type
if image_type in (2, 10): # RGB or RLE RGB
return self._read_rgb(pixel_data, pixel_depth, image_type == 10)
elif image_type in (3, 11): # Grayscale
return self._read_grayscale(pixel_data, image_type == 11)
return None
def _read_rgb(self, data: bytes, depth: int, rle: bool) -> 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

View File

@ -1,21 +1,24 @@
""" """
EU-Utility - Icon Manager EU-Utility - Icon Manager
Loads and manages white/frosted style icons for EU aesthetic. Manages actual icon files (SVG/PNG) - NO emojis.
Icons are SVG format - scalable and crisp at any size. Integrates with Icon Extractor for TGA conversion.
""" """
from pathlib import Path from pathlib import Path
from PyQt6.QtGui import QIcon, QPixmap, QColor from PyQt6.QtGui import QIcon, QPixmap, QPainter
from PyQt6.QtCore import QSize, Qt from PyQt6.QtCore import Qt, QSize
from PyQt6.QtSvg import QSvgRenderer from PyQt6.QtSvg import QSvgRenderer
class IconManager: class IconManager:
"""Manage application icons with EU styling.""" """Manages icons for EU-Utility - no emojis, only real icons."""
_instance = None _instance = None
# Default icon fallback (gear/settings icon)
DEFAULT_ICON = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>"""
def __new__(cls): def __new__(cls):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
@ -27,91 +30,137 @@ class IconManager:
return return
self.icons_dir = Path(icons_dir) self.icons_dir = Path(icons_dir)
self.svg_icons = {} self.svg_cache = {}
self._load_svg_icons()
self._initialized = True self._initialized = True
def _load_svg_icons(self): def get_icon(self, name, size=24, color="white"):
"""Load SVG icons.""" """Get icon by name. Returns QIcon."""
if not self.icons_dir.exists(): # Try SVG file first
return 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"): # Try PNG
name = icon_file.stem png_path = self.icons_dir / f"{name}.png"
self.svg_icons[name] = str(icon_file) 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"): def get_pixmap(self, name, size=24, color="white"):
"""Get an icon by name as QIcon.""" """Get icon as QPixmap."""
# Check for SVG icon first # Try SVG
if name in self.svg_icons: svg_path = self.icons_dir / f"{name}.svg"
return self._svg_to_icon(self.svg_icons[name], size, color) if svg_path.exists():
return self._svg_to_pixmap(svg_path, size, color)
# Return None if not found - caller should use fallback # Try PNG
return None 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): def _svg_to_icon(self, svg_path, size, color):
"""Convert SVG to QIcon.""" """Convert SVG to QIcon."""
renderer = QSvgRenderer(svg_path) pixmap = self._svg_to_pixmap(svg_path, size, color)
pixmap = QPixmap(size, size) return QIcon(pixmap) if pixmap else self._get_default_icon(size, color)
pixmap.fill(Qt.GlobalColor.transparent)
painter = QPixmap(pixmap)
renderer.render(painter)
return QIcon(pixmap)
def get_pixmap(self, name, size=24): def _svg_to_pixmap(self, svg_path, size, color):
"""Get icon as QPixmap.""" """Convert SVG to QPixmap."""
if name in self.svg_icons: try:
renderer = QSvgRenderer(self.svg_icons[name]) renderer = QSvgRenderer(str(svg_path))
pixmap = QPixmap(size, size) pixmap = QPixmap(size, size)
pixmap.fill(Qt.GlobalColor.transparent) pixmap.fill(Qt.GlobalColor.transparent)
from PyQt6.QtGui import QPainter
painter = QPainter(pixmap) painter = QPainter(pixmap)
renderer.render(painter) renderer.render(painter)
painter.end() painter.end()
return pixmap return pixmap
return None except Exception as e:
print(f"[IconManager] Error loading SVG {svg_path}: {e}")
return None
def get_emoji(self, name): def _get_default_icon(self, size, color):
"""Get emoji fallback for icon.""" """Get default gear icon."""
return self.EMOJIS.get(name, '') pixmap = self._get_default_pixmap(size, color)
return QIcon(pixmap)
# Emoji fallbacks def _get_default_pixmap(self, size, color):
EMOJIS = { """Render default SVG as pixmap."""
'search': '🔍', try:
'calculator': '🧮', from PyQt6.QtCore import QByteArray
'music': '🎵',
'globe': '🌐', svg_data = self.DEFAULT_ICON.encode('utf-8')
'skills': '📊', renderer = QSvgRenderer(QByteArray(svg_data))
'camera': '📷',
'close': '', pixmap = QPixmap(size, size)
'check': '', pixmap.fill(Qt.GlobalColor.transparent)
'settings': '⚙️',
'file': '📄', painter = QPainter(pixmap)
'trash': '🗑️', renderer.render(painter)
'external': '', 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 # Singleton instance
_icons = None _icon_manager = None
def get_icon_manager(): def get_icon_manager():
"""Get the singleton icon manager.""" """Get global icon manager instance."""
global _icons global _icon_manager
if _icons is None: if _icon_manager is None:
_icons = IconManager() _icon_manager = IconManager()
return _icons return _icon_manager
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)

View File

@ -1,7 +1,8 @@
""" """
EU-Utility - Overlay Window 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 import sys
@ -12,71 +13,25 @@ try:
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QStackedWidget, QSystemTrayIcon, 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.QtCore import Qt, QTimer, pyqtSignal, QSize
from PyQt6.QtGui import QAction, QIcon, QColor, QFont, QPixmap, QPainter from PyQt6.QtGui import QAction, QIcon, QColor, QFont
from PyQt6.QtSvg import QSvgRenderer
PYQT6_AVAILABLE = True PYQT6_AVAILABLE = True
except ImportError: except ImportError:
PYQT6_AVAILABLE = False PYQT6_AVAILABLE = False
print("PyQt6 not available. Install with: pip install PyQt6") print("PyQt6 not available. Install with: pip install PyQt6")
from core.eu_styles import EU_COLORS, EU_STYLES, get_eu_style from core.eu_styles import EU_COLORS, EU_STYLES
from core.icon_manager import get_icon_manager, get_plugin_icon_name
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)
class OverlayWindow(QMainWindow): class OverlayWindow(QMainWindow):
"""EU-styled overlay window.""" """Clean EU-styled overlay window with view toggle."""
visibility_changed = pyqtSignal(bool) 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): def __init__(self, plugin_manager=None):
super().__init__() super().__init__()
@ -86,6 +41,9 @@ class OverlayWindow(QMainWindow):
self.plugin_manager = plugin_manager self.plugin_manager = plugin_manager
self.is_visible = False self.is_visible = False
self.plugin_buttons = [] 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_window()
self._setup_ui() self._setup_ui()
@ -105,8 +63,8 @@ class OverlayWindow(QMainWindow):
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
# EU-sized window # Clean, game-like size
self.resize(800, 550) self.resize(850, 600)
self._center_window() self._center_window()
def _center_window(self): def _center_window(self):
@ -117,15 +75,15 @@ class OverlayWindow(QMainWindow):
self.move(x, y) self.move(x, y)
def _setup_ui(self): def _setup_ui(self):
"""Setup EU-styled UI.""" """Setup clean EU-styled UI."""
central = QWidget() central = QWidget()
self.setCentralWidget(central) self.setCentralWidget(central)
layout = QVBoxLayout(central) layout = QVBoxLayout(central)
layout.setContentsMargins(15, 15, 15, 15) layout.setContentsMargins(20, 20, 20, 20)
layout.setSpacing(0) layout.setSpacing(0)
# Main container with EU styling # Main container
self.container = QFrame() self.container = QFrame()
self.container.setObjectName("euContainer") self.container.setObjectName("euContainer")
self.container.setStyleSheet(f""" self.container.setStyleSheet(f"""
@ -136,18 +94,17 @@ class OverlayWindow(QMainWindow):
}} }}
""") """)
# Add shadow
shadow = QGraphicsDropShadowEffect() shadow = QGraphicsDropShadowEffect()
shadow.setBlurRadius(25) shadow.setBlurRadius(30)
shadow.setColor(QColor(0, 0, 0, 120)) shadow.setColor(QColor(0, 0, 0, 150))
shadow.setOffset(0, 5) shadow.setOffset(0, 8)
self.container.setGraphicsEffect(shadow) self.container.setGraphicsEffect(shadow)
container_layout = QVBoxLayout(self.container) container_layout = QVBoxLayout(self.container)
container_layout.setContentsMargins(0, 0, 0, 0) container_layout.setContentsMargins(0, 0, 0, 0)
container_layout.setSpacing(0) container_layout.setSpacing(0)
# EU-styled header # Clean header
header = QWidget() header = QWidget()
header.setStyleSheet(f""" header.setStyleSheet(f"""
QWidget {{ QWidget {{
@ -158,34 +115,59 @@ class OverlayWindow(QMainWindow):
}} }}
""") """)
header_layout = QHBoxLayout(header) header_layout = QHBoxLayout(header)
header_layout.setContentsMargins(15, 12, 15, 12) header_layout.setContentsMargins(20, 15, 20, 15)
header_layout.setSpacing(12) header_layout.setSpacing(15)
# Logo/Icon # App icon (actual icon, not emoji)
logo = QLabel("") app_icon = QLabel()
logo.setStyleSheet("font-size: 18px; background: transparent;") app_icon_pixmap = self.icon_manager.get_pixmap("target", size=24)
header_layout.addWidget(logo) app_icon.setPixmap(app_icon_pixmap)
app_icon.setFixedSize(24, 24)
header_layout.addWidget(app_icon)
# Title # Title
title = QLabel("EU-Utility") title = QLabel("EU-UTILITY")
title.setStyleSheet(f""" title.setStyleSheet(f"""
color: {EU_COLORS['accent_orange']}; color: {EU_COLORS['accent_orange']};
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
background: transparent; letter-spacing: 1px;
""") """)
header_layout.addWidget(title) header_layout.addWidget(title)
header_layout.addStretch() header_layout.addStretch()
# Close button (EU style) # View toggle buttons
close_btn = QPushButton("") 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.setFixedSize(28, 28)
close_btn.setStyleSheet(f""" close_btn.setStyleSheet(f"""
QPushButton {{ QPushButton {{
background-color: transparent; background-color: transparent;
color: {EU_COLORS['text_muted']}; color: {EU_COLORS['text_muted']};
font-size: 16px; font-size: 18px;
font-weight: bold;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
}} }}
@ -199,107 +181,225 @@ class OverlayWindow(QMainWindow):
container_layout.addWidget(header) 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 = QWidget()
self.content_area.setStyleSheet("background: transparent;") self.content_area.setStyleSheet("background: transparent;")
content_layout = QVBoxLayout(self.content_area) content_layout = QVBoxLayout(self.content_area)
content_layout.setContentsMargins(15, 15, 15, 15) content_layout.setContentsMargins(20, 20, 20, 20)
content_layout.setSpacing(12) content_layout.setSpacing(15)
# Plugin stack # Plugin stack
self.plugin_stack = QStackedWidget() self.plugin_stack = QStackedWidget()
self.plugin_stack.setStyleSheet("background: transparent;") self.plugin_stack.setStyleSheet("background: transparent;")
content_layout.addWidget(self.plugin_stack, 1) content_layout.addWidget(self.plugin_stack, 1)
container_layout.addWidget(self.content_area, 1) content_split.addWidget(self.content_area, 1)
container_layout.addLayout(content_split, 1)
# EU-styled plugin bar
if self.plugin_manager:
self._setup_plugin_bar(container_layout)
layout.addWidget(self.container) layout.addWidget(self.container)
# Load plugins
if self.plugin_manager:
self._load_plugins()
def _setup_plugin_bar(self, layout): def _setup_sidebar(self):
"""Setup EU-styled plugin icon bar.""" """Setup sidebar with plugin selector."""
bar = QWidget() sidebar_layout = QVBoxLayout(self.sidebar)
bar.setStyleSheet(f""" sidebar_layout.setContentsMargins(0, 10, 0, 10)
QWidget {{ sidebar_layout.setSpacing(5)
background-color: {EU_COLORS['bg_header']};
border-bottom-left-radius: 8px; # Plugins label
border-bottom-right-radius: 8px; plugins_label = QLabel("PLUGINS")
border-top: 1px solid {EU_COLORS['border_medium']}; 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) self.plugin_list.itemClicked.connect(self._on_list_item_clicked)
bar_layout.setContentsMargins(15, 10, 15, 10) sidebar_layout.addWidget(self.plugin_list)
bar_layout.setSpacing(8)
bar_layout.addStretch()
# 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()): 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( # Add to list view
plugin.name, list_item = QListWidgetItem(plugin.name)
("target", EU_COLORS['accent_orange']) list_item.setData(Qt.ItemDataRole.UserRole, idx)
) self.plugin_list.addItem(list_item)
# Load SVG icon # Add to icon grid
icon = IconHelper.get_icon(icon_name, size=18) icon_btn = self._create_icon_button(plugin.name, icon_name, idx)
if icon: self.icon_grid_layout.addWidget(icon_btn)
btn.setIcon(icon)
btn.setIconSize(QSize(18, 18))
else:
btn.setText("")
btn.setFixedSize(36, 36) # Add plugin UI to stack
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
try: try:
plugin_ui = plugin.get_ui() plugin_ui = plugin.get_ui()
if plugin_ui: if plugin_ui:
plugin_ui.setStyleSheet("background: transparent;") plugin_ui.setStyleSheet("background: transparent;")
self.plugin_stack.addWidget(plugin_ui) 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: 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() # Add stretch to icon grid
layout.addWidget(bar) self.icon_grid_layout.addStretch()
# Select first # Select first plugin
if self.plugin_buttons: if self.plugin_list.count() > 0:
self.plugin_buttons[0].setChecked(True) self.plugin_list.setCurrentRow(0)
self.plugin_stack.setCurrentIndex(0)
def _switch_plugin(self, index, button): def _create_icon_button(self, name, icon_name, index):
"""Switch to selected plugin.""" """Create an icon button for the sidebar."""
for btn in self.plugin_buttons: btn = QPushButton()
if btn != button: btn.setFixedSize(180, 50)
btn.setChecked(False) btn.setCheckable(True)
button.setChecked(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) 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): def _setup_tray(self):
"""Setup system tray.""" """Setup system tray."""
self.tray_icon = QSystemTrayIcon(self) 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) show_action.triggered.connect(self.show_overlay)
tray_menu.addAction(show_action) tray_menu.addAction(show_action)
tray_menu.addSeparator() tray_menu.addSeparator()
quit_action = QAction("Quit", self) quit_action = QAction("Quit", self)
quit_action.triggered.connect(self.quit_app) quit_action.triggered.connect(self.quit_app)
tray_menu.addAction(quit_action) tray_menu.addAction(quit_action)
@ -352,26 +452,12 @@ class OverlayWindow(QMainWindow):
self.activateWindow() self.activateWindow()
self.is_visible = True self.is_visible = True
self.visibility_changed.emit(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): def hide_overlay(self):
"""Hide overlay.""" """Hide overlay."""
self.hide() self.hide()
self.is_visible = False self.is_visible = False
self.visibility_changed.emit(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): def toggle_overlay(self):
"""Toggle overlay.""" """Toggle overlay."""
@ -384,7 +470,6 @@ class OverlayWindow(QMainWindow):
"""Quit application.""" """Quit application."""
if self.plugin_manager: if self.plugin_manager:
self.plugin_manager.shutdown_all() self.plugin_manager.shutdown_all()
self.tray_icon.hide() self.tray_icon.hide()
QApplication.quit() QApplication.quit()