feat: standalone extractor - professional polish and hardcoded settings

Major updates to standalone_icon_extractor.py:

Settings:
- Hardcoded to 320x320 canvas size (removed upscale options)
- Output folder uses Path.home() for any Windows username
- Added explanation about output location (Documents/Entropia Universe/Icons)

UI Changes:
- Removed game controller emoji from title
- Button changed to "Start Extracting Icons" (no rocket emoji)
- Removed all emojis from UI for professional look

Attribution & Legal:
- Footer: "Developed by ImpulsiveFPS"
- Discord contact: impulsivefps
- Disclaimer: Not affiliated with Entropia Universe or Mindark PE AB
- Trademark notice for Entropia Universe

Features:
- Added EntropiaNexus.com link and button
- Added explanation that items must be seen in-game to be cached
- Added "Important Information" box explaining cache requirements
- Icon support (looks for assets/icon.ico or icon.ico)

Technical:
- Uses QSettings under ImpulsiveFPS organization
- Added set_app_icon() function for custom icon support
This commit is contained in:
LemonNexus 2026-02-11 18:13:05 +00:00
parent 8b97dbc58c
commit eba76cf59c
1 changed files with 223 additions and 228 deletions

View File

@ -1,45 +1,57 @@
""" """
Entropia Universe Icon Extractor Entropia Universe Icon Extractor
A standalone tool for extracting and converting game icons from cache. A standalone tool for extracting game icons from cache.
Description: Standalone GUI tool to extract TGA icons from Entropia Universe cache, Description: Extracts item icons from Entropia Universe game cache and converts
convert them to PNG, and optionally upscale them for better quality. them to PNG format for use with EntropiaNexus.com wiki submissions.
Important: Items must be seen/rendered in-game before they appear in the cache.
Usage: Usage:
python standalone_icon_extractor.py python standalone_icon_extractor.py
Author: LemonNexus Output Location:
Icons are saved to your Documents/Entropia Universe/Icons/ folder
(same location where chat.log is normally stored)
Developer: ImpulsiveFPS
Discord: impulsivefps
Website: https://EntropiaNexus.com
Disclaimer:
This tool is not affiliated with, endorsed by, or connected to
Entropia Universe or Mindark PE AB in any way.
Entropia Universe is a registered trademark of Mindark PE AB.
""" """
import sys import sys
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Optional, List, Tuple from typing import Optional, List, Tuple
from dataclasses import dataclass import ctypes
from enum import Enum
try: try:
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QComboBox, QListWidget, QListWidgetItem, QLabel, QPushButton, QComboBox, QListWidget, QListWidgetItem,
QFileDialog, QProgressBar, QGroupBox, QMessageBox, QCheckBox, QFileDialog, QProgressBar, QGroupBox, QMessageBox, QCheckBox,
QSpinBox, QSplitter QSplitter, QTextEdit
) )
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings
from PyQt6.QtGui import QIcon, QPixmap from PyQt6.QtGui import QIcon, QPixmap, QFont
PYQT_AVAILABLE = True PYQT_AVAILABLE = True
except ImportError: except ImportError:
PYQT_AVAILABLE = False PYQT_AVAILABLE = False
print("PyQt6 not available. Install with: pip install PyQt6") print("PyQt6 not available. Install with: pip install PyQt6")
sys.exit(1)
try: try:
from PIL import Image, ImageFilter from PIL import Image, ImageFilter
PIL_AVAILABLE = True PIL_AVAILABLE = True
except ImportError: except ImportError:
PIL_AVAILABLE = False PIL_AVAILABLE = False
print("PIL not available. Install with: pip install Pillow") print("Pillow not available. Install with: pip install Pillow")
sys.exit(1)
import numpy as np
# Setup logging # Setup logging
logging.basicConfig( logging.basicConfig(
@ -49,6 +61,14 @@ logging.basicConfig(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Application metadata
APP_NAME = "Entropia Universe Icon Extractor"
APP_VERSION = "1.0.0"
DEVELOPER = "ImpulsiveFPS"
DISCORD = "impulsivefps"
WEBSITE = "https://EntropiaNexus.com"
class TGAHeader: class TGAHeader:
"""TGA file header structure.""" """TGA file header structure."""
def __init__(self, data: bytes): def __init__(self, data: bytes):
@ -70,26 +90,28 @@ class TGAHeader:
class TGAConverter: class TGAConverter:
"""Converter for TGA files to PNG with canvas and upscaling options.""" """Converter for TGA files to PNG with 320x320 canvas."""
CANVAS_SIZE = (320, 320) # Hardcoded to 320x320
def __init__(self, output_dir: Optional[Path] = None): def __init__(self, output_dir: Optional[Path] = None):
self.output_dir = output_dir or Path.home() / "Documents" / "Entropia Universe" / "Icons" # Default to user's Documents/Entropia Universe/Icons/
# This works on any Windows username
if output_dir is None:
self.output_dir = Path.home() / "Documents" / "Entropia Universe" / "Icons"
else:
self.output_dir = output_dir
self.output_dir.mkdir(parents=True, exist_ok=True) self.output_dir.mkdir(parents=True, exist_ok=True)
self._cache_path: Optional[Path] = None self._cache_path: Optional[Path] = None
def find_cache_folder(self) -> Optional[Path]: def find_cache_folder(self) -> Optional[Path]:
"""Find the Entropia Universe icon cache folder.""" """Find the Entropia Universe icon cache folder."""
possible_paths = [ # Hardcoded path - works on any system with EU installed
Path("C:") / "ProgramData" / "Entropia Universe" / "public_users_data" / "cache" / "icon", cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon")
Path.home() / "Documents" / "Entropia Universe" / "cache" / "icons",
]
for path in possible_paths: if cache_path.exists():
if path.exists(): self._cache_path = cache_path
# Check if this folder or any subfolder has .tga files return cache_path
if list(path.rglob("*.tga")):
self._cache_path = path
return path
return None return None
@ -105,25 +127,25 @@ class TGAConverter:
logger.error(f"Error reading TGA header: {e}") logger.error(f"Error reading TGA header: {e}")
return None return None
def convert_tga_to_png( def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None) -> Optional[Path]:
self, """
tga_path: Path, Convert a TGA file to PNG with 320x320 canvas.
output_name: Optional[str] = None,
canvas_size: Optional[Tuple[int, int]] = None, Args:
upscale: bool = False, tga_path: Path to TGA file
upscale_method: str = 'nearest' output_name: Optional custom output name
) -> Optional[Path]:
"""Convert a TGA file to PNG.""" Returns:
Path to output PNG file or None if failed
"""
try: try:
# Try PIL first # Load TGA
image = Image.open(tga_path) image = Image.open(tga_path)
if image.mode != 'RGBA': if image.mode != 'RGBA':
image = image.convert('RGBA') image = image.convert('RGBA')
# Apply canvas sizing if requested # Apply 320x320 canvas (centered, no upscaling)
if canvas_size: image = self._apply_canvas(image)
do_upscale = upscale and upscale_method != 'none'
image = self._apply_canvas(image, canvas_size, do_upscale, upscale_method)
# Save # Save
if output_name is None: if output_name is None:
@ -138,45 +160,18 @@ class TGAConverter:
logger.error(f"Conversion failed: {e}") logger.error(f"Conversion failed: {e}")
return None return None
def _apply_canvas( def _apply_canvas(self, image: Image.Image) -> Image.Image:
self, """
image: Image.Image, Place image centered on a 320x320 canvas.
canvas_size: Tuple[int, int], No upscaling - original size centered on canvas.
upscale: bool = False, """
upscale_method: str = 'nearest' canvas_w, canvas_h = self.CANVAS_SIZE
) -> Image.Image:
"""Place image centered on a canvas."""
canvas_w, canvas_h = canvas_size
img_w, img_h = image.size img_w, img_h = image.size
# Create transparent canvas # Create transparent canvas
canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) canvas = Image.new('RGBA', self.CANVAS_SIZE, (0, 0, 0, 0))
# Calculate scaling # Center on canvas (no scaling)
if upscale:
max_size = int(min(canvas_w, canvas_h) * 0.85)
scale = min(max_size / img_w, max_size / img_h)
if scale > 1:
new_w = int(img_w * scale)
new_h = int(img_h * scale)
if upscale_method == 'nearest':
image = image.resize((new_w, new_h), Image.Resampling.NEAREST)
elif upscale_method == 'hq4x':
int_scale = max(2, int(scale))
temp_w = img_w * int_scale
temp_h = img_h * int_scale
image = image.resize((temp_w, temp_h), Image.Resampling.NEAREST)
image = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
image = image.filter(ImageFilter.EDGE_ENHANCE_MORE)
else: # lanczos
image = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
image = image.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3))
img_w, img_h = new_w, new_h
# Center on canvas
x = (canvas_w - img_w) // 2 x = (canvas_w - img_w) // 2
y = (canvas_h - img_h) // 2 y = (canvas_h - img_h) // 2
@ -191,18 +186,10 @@ class ConversionWorker(QThread):
finished = pyqtSignal(int, int) # success, total finished = pyqtSignal(int, int) # success, total
error = pyqtSignal(str) error = pyqtSignal(str)
def __init__( def __init__(self, files: List[Path], converter: TGAConverter):
self,
files: List[Path],
converter: TGAConverter,
canvas_size: Optional[Tuple[int, int]],
upscale_method: str
):
super().__init__() super().__init__()
self.files = files self.files = files
self.converter = converter self.converter = converter
self.canvas_size = canvas_size
self.upscale_method = upscale_method
self._running = True self._running = True
def run(self): def run(self):
@ -217,12 +204,7 @@ class ConversionWorker(QThread):
self.progress.emit(f"[{i+1}/{total}] {filepath.name}") self.progress.emit(f"[{i+1}/{total}] {filepath.name}")
output = self.converter.convert_tga_to_png( output = self.converter.convert_tga_to_png(filepath)
filepath,
canvas_size=self.canvas_size,
upscale=True,
upscale_method=self.upscale_method
)
if output: if output:
success += 1 success += 1
@ -242,18 +224,17 @@ class IconExtractorWindow(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setWindowTitle("🎮 Entropia Universe Icon Extractor") self.setWindowTitle(APP_NAME)
self.setMinimumSize(900, 700) self.setMinimumSize(950, 750)
self.converter = TGAConverter() self.converter = TGAConverter()
self.worker: Optional[ConversionWorker] = None self.worker: Optional[ConversionWorker] = None
self.found_files: List[Path] = [] self.found_files: List[Path] = []
self.current_subfolder: Optional[Path] = None
# Hardcoded base cache path # Hardcoded base cache path
self.base_cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon") self.base_cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon")
self.settings = QSettings("Lemontropia", "IconExtractor") self.settings = QSettings("ImpulsiveFPS", "EUIconExtractor")
self._setup_ui() self._setup_ui()
self._load_settings() self._load_settings()
@ -268,18 +249,44 @@ class IconExtractorWindow(QMainWindow):
layout.setSpacing(12) layout.setSpacing(12)
# Header # Header
header = QLabel("🎮 Entropia Universe Icon Extractor") header = QLabel(APP_NAME)
header.setStyleSheet("font-size: 20px; font-weight: bold; color: #4caf50;") header.setStyleSheet("font-size: 22px; font-weight: bold; color: #4caf50;")
layout.addWidget(header) layout.addWidget(header)
# Description
desc = QLabel( desc = QLabel(
"Extract and convert item icons from Entropia Universe game cache.\n" "Extract item icons from Entropia Universe game cache and convert them to PNG format.\n"
"Icons are saved as PNG files with transparent backgrounds." "These icons can be submitted to EntropiaNexus.com to help complete the item database."
) )
desc.setStyleSheet("color: #888; padding: 5px;") desc.setStyleSheet("color: #aaaaaa; padding: 5px;")
desc.setWordWrap(True) desc.setWordWrap(True)
layout.addWidget(desc) layout.addWidget(desc)
# Important notice
notice_group = QGroupBox("Important Information")
notice_layout = QVBoxLayout(notice_group)
notice_text = QTextEdit()
notice_text.setReadOnly(True)
notice_text.setMaximumHeight(120)
notice_text.setStyleSheet("""
QTextEdit {
background-color: #2a2520;
color: #ffcc80;
border: 1px solid #5d4037;
border-radius: 3px;
font-size: 12px;
}
""")
notice_text.setText(
"REQUIREMENT: Items must be seen/rendered in-game before they appear in the cache!\n"
"If an item icon is missing, you need to view the item in your inventory or see it dropped as loot first.\n\n"
f"Output Location: Icons are saved to your Documents folder ({self.converter.output_dir})\n"
"in the same location where Entropia Universe normally stores your chat.log file."
)
notice_layout.addWidget(notice_text)
layout.addWidget(notice_group)
# Main splitter # Main splitter
splitter = QSplitter(Qt.Orientation.Horizontal) splitter = QSplitter(Qt.Orientation.Horizontal)
@ -289,15 +296,15 @@ class IconExtractorWindow(QMainWindow):
left_layout.setContentsMargins(0, 0, 0, 0) left_layout.setContentsMargins(0, 0, 0, 0)
# Cache folder # Cache folder
cache_group = QGroupBox("📁 Cache Folder") cache_group = QGroupBox("Cache Source")
cache_layout = QVBoxLayout(cache_group) cache_layout = QVBoxLayout(cache_group)
# Base path (hardcoded) # Base path (hardcoded)
base_label = QLabel("Base Path:") base_label = QLabel("Game Cache Location:")
cache_layout.addWidget(base_label) cache_layout.addWidget(base_label)
self.cache_label = QLabel(str(self.base_cache_path)) self.cache_label = QLabel(str(self.base_cache_path))
self.cache_label.setStyleSheet("font-family: Consolas; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;") self.cache_label.setStyleSheet("font-family: Consolas; font-size: 11px; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;")
cache_layout.addWidget(self.cache_label) cache_layout.addWidget(self.cache_label)
# Subfolder selector # Subfolder selector
@ -308,7 +315,7 @@ class IconExtractorWindow(QMainWindow):
self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed) self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed)
subfolder_layout.addWidget(self.subfolder_combo) subfolder_layout.addWidget(self.subfolder_combo)
refresh_btn = QPushButton("🔄 Refresh") refresh_btn = QPushButton("Refresh")
refresh_btn.clicked.connect(self._detect_subfolders) refresh_btn.clicked.connect(self._detect_subfolders)
subfolder_layout.addWidget(refresh_btn) subfolder_layout.addWidget(refresh_btn)
@ -316,81 +323,68 @@ class IconExtractorWindow(QMainWindow):
cache_layout.addLayout(subfolder_layout) cache_layout.addLayout(subfolder_layout)
# All subfolders checkbox # All subfolders checkbox
self.all_subfolders_check = QCheckBox("Include ALL subfolders (merge everything)") self.all_subfolders_check = QCheckBox("Include ALL version folders")
self.all_subfolders_check.setToolTip("If checked, will find TGA files from ALL version subfolders") self.all_subfolders_check.setToolTip("Merge icons from all game versions")
self.all_subfolders_check.stateChanged.connect(self._on_all_subfolders_changed) self.all_subfolders_check.stateChanged.connect(self._on_all_subfolders_changed)
cache_layout.addWidget(self.all_subfolders_check) cache_layout.addWidget(self.all_subfolders_check)
cache_btn_layout = QHBoxLayout()
scan_btn = QPushButton("🔍 Auto-Detect")
scan_btn.clicked.connect(self._auto_scan)
cache_btn_layout.addWidget(scan_btn)
browse_btn = QPushButton("Browse...")
browse_btn.clicked.connect(self._browse_cache)
cache_btn_layout.addWidget(browse_btn)
cache_btn_layout.addStretch()
cache_layout.addLayout(cache_btn_layout)
left_layout.addWidget(cache_group) left_layout.addWidget(cache_group)
# Output folder # Output folder
output_group = QGroupBox("💾 Output Folder") output_group = QGroupBox("Output Location")
output_layout = QVBoxLayout(output_group) output_layout = QVBoxLayout(output_group)
output_info = QLabel(
"Icons will be saved to your Documents folder:\n"
"(Same location as chat.log)"
)
output_info.setStyleSheet("color: #888; font-size: 11px;")
output_layout.addWidget(output_info)
self.output_label = QLabel(str(self.converter.output_dir)) self.output_label = QLabel(str(self.converter.output_dir))
self.output_label.setStyleSheet("font-family: Consolas; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;") self.output_label.setStyleSheet("font-family: Consolas; font-size: 11px; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;")
self.output_label.setWordWrap(True)
output_layout.addWidget(self.output_label) output_layout.addWidget(self.output_label)
output_btn = QPushButton("Change Output Folder...") change_btn = QPushButton("Change Output Folder...")
output_btn.clicked.connect(self._browse_output) change_btn.clicked.connect(self._browse_output)
output_layout.addWidget(output_btn) output_layout.addWidget(change_btn)
left_layout.addWidget(output_group) left_layout.addWidget(output_group)
# Settings # Settings (simplified - just 320x320)
settings_group = QGroupBox("⚙️ Conversion Settings") settings_group = QGroupBox("Export Settings")
settings_layout = QVBoxLayout(settings_group) settings_layout = QVBoxLayout(settings_group)
# Canvas size settings_info = QLabel(
size_layout = QHBoxLayout() "Export Format: PNG with transparent background\n"
size_layout.addWidget(QLabel("Canvas Size:")) "Canvas Size: 320x320 pixels (centered)\n"
self.size_combo = QComboBox() "Upscaling: None (original icon size)"
self.size_combo.addItem("Original (no canvas)", None)
self.size_combo.addItem("64x64", (64, 64))
self.size_combo.addItem("128x128", (128, 128))
self.size_combo.addItem("256x256", (256, 256))
self.size_combo.addItem("280x280 (Forum)", (280, 280))
self.size_combo.addItem("320x320", (320, 320))
self.size_combo.addItem("512x512", (512, 512))
self.size_combo.setCurrentIndex(5) # Default 320x320
size_layout.addWidget(self.size_combo)
size_layout.addStretch()
settings_layout.addLayout(size_layout)
# Upscale method
method_layout = QHBoxLayout()
method_layout.addWidget(QLabel("Upscale:"))
self.method_combo = QComboBox()
self.method_combo.addItem("❌ No Upscaling", "none")
self.method_combo.addItem("Sharp Pixels (NEAREST)", "nearest")
self.method_combo.addItem("Smooth (HQ4x-style)", "hq4x")
self.method_combo.addItem("Photorealistic (LANCZOS)", "lanczos")
self.method_combo.setToolTip(
"None: Keep original size\n"
"NEAREST: Sharp edges (pixel art)\n"
"HQ4x: Smooth (game icons)\n"
"LANCZOS: Very smooth (photos)"
) )
method_layout.addWidget(self.method_combo) settings_info.setStyleSheet("color: #888; font-size: 11px;")
method_layout.addStretch() settings_layout.addWidget(settings_info)
settings_layout.addLayout(method_layout)
left_layout.addWidget(settings_group) left_layout.addWidget(settings_group)
# Nexus link
nexus_group = QGroupBox("EntropiaNexus.com")
nexus_layout = QVBoxLayout(nexus_group)
nexus_info = QLabel(
"Submit extracted icons to help complete the\n"
"Entropia Universe item database and wiki!"
)
nexus_info.setStyleSheet("color: #4caf50;")
nexus_layout.addWidget(nexus_info)
nexus_btn = QPushButton("Open EntropiaNexus.com")
nexus_btn.clicked.connect(lambda: self._open_url(WEBSITE))
nexus_layout.addWidget(nexus_btn)
left_layout.addWidget(nexus_group)
# Convert button # Convert button
self.convert_btn = QPushButton("🚀 Convert All Icons") self.convert_btn = QPushButton("Start Extracting Icons")
self.convert_btn.setMinimumHeight(60) self.convert_btn.setMinimumHeight(60)
self.convert_btn.setStyleSheet(""" self.convert_btn.setStyleSheet("""
QPushButton { QPushButton {
@ -423,9 +417,13 @@ class IconExtractorWindow(QMainWindow):
right_layout = QVBoxLayout(right_panel) right_layout = QVBoxLayout(right_panel)
right_layout.setContentsMargins(0, 0, 0, 0) right_layout.setContentsMargins(0, 0, 0, 0)
files_group = QGroupBox("📄 Found Icons") files_group = QGroupBox("Available Icons")
files_layout = QVBoxLayout(files_group) files_layout = QVBoxLayout(files_group)
files_info = QLabel("Select icons to extract (or leave unselected to extract all)")
files_info.setStyleSheet("color: #888; font-size: 11px;")
files_layout.addWidget(files_info)
self.files_count_label = QLabel("No files found") self.files_count_label = QLabel("No files found")
files_layout.addWidget(self.files_count_label) files_layout.addWidget(self.files_count_label)
@ -446,7 +444,7 @@ class IconExtractorWindow(QMainWindow):
sel_layout.addStretch() sel_layout.addStretch()
open_folder_btn = QPushButton("📂 Open Output Folder") open_folder_btn = QPushButton("Open Output Folder")
open_folder_btn.clicked.connect(self._open_output_folder) open_folder_btn.clicked.connect(self._open_output_folder)
sel_layout.addWidget(open_folder_btn) sel_layout.addWidget(open_folder_btn)
@ -454,44 +452,44 @@ class IconExtractorWindow(QMainWindow):
right_layout.addWidget(files_group) right_layout.addWidget(files_group)
splitter.addWidget(right_panel) splitter.addWidget(right_panel)
splitter.setSizes([350, 550]) splitter.setSizes([380, 520])
layout.addWidget(splitter, 1) layout.addWidget(splitter, 1)
# Footer # Footer
footer = QLabel("Entropia Universe Icon Extractor | Standalone Tool") footer = QLabel(
footer.setStyleSheet("color: #555; font-size: 11px; padding: 5px;") f"Developed by {DEVELOPER} | Discord: {DISCORD} | {WEBSITE}\n"
"Not affiliated with or endorsed by Entropia Universe or Mindark PE AB."
" Entropia Universe is a trademark of Mindark PE AB."
)
footer.setStyleSheet("color: #555; font-size: 10px; padding: 5px;")
footer.setAlignment(Qt.AlignmentFlag.AlignCenter) footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
footer.setWordWrap(True)
layout.addWidget(footer) layout.addWidget(footer)
def _open_url(self, url: str):
"""Open URL in default browser."""
import webbrowser
webbrowser.open(url)
def _load_settings(self): def _load_settings(self):
"""Load saved settings.""" """Load saved settings."""
# Output folder # Output folder
saved_output = self.settings.value("output_dir", str(self.converter.output_dir)) saved_output = self.settings.value("output_dir", str(self.converter.output_dir))
self.converter.output_dir = Path(saved_output) self.converter.output_dir = Path(saved_output)
self.output_label.setText(saved_output) self.output_label.setText(saved_output)
# Canvas size
size_index = int(self.settings.value("canvas_size_index", 5))
self.size_combo.setCurrentIndex(size_index)
# Upscale method
method_index = int(self.settings.value("upscale_method_index", 0))
self.method_combo.setCurrentIndex(method_index)
def _save_settings(self): def _save_settings(self):
"""Save current settings.""" """Save current settings."""
self.settings.setValue("output_dir", str(self.converter.output_dir)) self.settings.setValue("output_dir", str(self.converter.output_dir))
self.settings.setValue("canvas_size_index", self.size_combo.currentIndex())
self.settings.setValue("upscale_method_index", self.method_combo.currentIndex())
def _detect_subfolders(self): def _detect_subfolders(self):
"""Detect version subfolders in the cache directory.""" """Detect version subfolders in the cache directory."""
self.subfolder_combo.clear() self.subfolder_combo.clear()
if not self.base_cache_path.exists(): if not self.base_cache_path.exists():
self.cache_label.setText(f"Not found: {self.base_cache_path}") self.cache_label.setText(f"Not found: {self.base_cache_path}")
self.status_label.setText("Cache folder not found - check if path is correct") self.status_label.setText("Cache folder not found - is Entropia Universe installed?")
return return
# Find all subfolders that contain TGA files # Find all subfolders that contain TGA files
@ -504,7 +502,7 @@ class IconExtractorWindow(QMainWindow):
subfolders.append((item.name, tga_count, item)) subfolders.append((item.name, tga_count, item))
if not subfolders: if not subfolders:
self.cache_label.setText(f"⚠️ No subfolders with TGA files in {self.base_cache_path}") self.cache_label.setText(f"No subfolders with icons in {self.base_cache_path}")
self.status_label.setText("No version folders found") self.status_label.setText("No version folders found")
return return
@ -517,10 +515,10 @@ class IconExtractorWindow(QMainWindow):
# Add "All folders" option at top # Add "All folders" option at top
total_icons = sum(s[1] for s in subfolders) total_icons = sum(s[1] for s in subfolders)
self.subfolder_combo.insertItem(0, f"📁 All Folders ({total_icons} icons)", "all") self.subfolder_combo.insertItem(0, f"All Folders ({total_icons} icons)", "all")
self.subfolder_combo.setCurrentIndex(0) self.subfolder_combo.setCurrentIndex(0)
self.cache_label.setText(f"{self.base_cache_path}") self.cache_label.setText(f"{self.base_cache_path}")
self.status_label.setText(f"Found {len(subfolders)} version folders") self.status_label.setText(f"Found {len(subfolders)} version folders")
# Load files # Load files
@ -535,23 +533,18 @@ class IconExtractorWindow(QMainWindow):
self.subfolder_combo.setEnabled(not self.all_subfolders_check.isChecked()) self.subfolder_combo.setEnabled(not self.all_subfolders_check.isChecked())
self._refresh_file_list() self._refresh_file_list()
def _auto_scan(self): def _browse_output(self):
"""Auto-detect cache folder - just refreshes subfolder list.""" """Browse for output folder."""
self.status_label.setText("Scanning for version folders...")
self._detect_subfolders()
def _browse_cache(self):
"""Browse for cache folder."""
folder = QFileDialog.getExistingDirectory( folder = QFileDialog.getExistingDirectory(
self, self,
"Select Entropia Universe Cache Folder", "Select Output Folder",
str(self.base_cache_path.parent) str(self.converter.output_dir)
) )
if folder: if folder:
self.base_cache_path = Path(folder) self.converter.output_dir = Path(folder)
self.cache_label.setText(str(self.base_cache_path)) self.output_label.setText(folder)
self._detect_subfolders() self._save_settings()
def _refresh_file_list(self): def _refresh_file_list(self):
"""Refresh the list of found files based on current selection.""" """Refresh the list of found files based on current selection."""
@ -602,19 +595,6 @@ class IconExtractorWindow(QMainWindow):
self.convert_btn.setEnabled(len(tga_files) > 0) self.convert_btn.setEnabled(len(tga_files) > 0)
def _browse_output(self):
"""Browse for output folder."""
folder = QFileDialog.getExistingDirectory(
self,
"Select Output Folder",
str(self.converter.output_dir)
)
if folder:
self.converter.output_dir = Path(folder)
self.output_label.setText(folder)
self._save_settings()
def _start_conversion(self): def _start_conversion(self):
"""Start batch conversion.""" """Start batch conversion."""
# Get selected files or all files # Get selected files or all files
@ -628,30 +608,21 @@ class IconExtractorWindow(QMainWindow):
files_to_convert = self.found_files files_to_convert = self.found_files
if not files_to_convert: if not files_to_convert:
QMessageBox.warning(self, "No Files", "No files selected for conversion.") QMessageBox.warning(self, "No Files", "No files selected for extraction.")
return return
# Get settings
canvas_size = self.size_combo.currentData()
upscale_method = self.method_combo.currentData()
# Save settings # Save settings
self._save_settings() self._save_settings()
# Setup UI # Setup UI
self.convert_btn.setEnabled(False) self.convert_btn.setEnabled(False)
self.convert_btn.setText("⏳ Converting...") self.convert_btn.setText("Extracting...")
self.progress_bar.setRange(0, len(files_to_convert)) self.progress_bar.setRange(0, len(files_to_convert))
self.progress_bar.setValue(0) self.progress_bar.setValue(0)
self.progress_bar.setVisible(True) self.progress_bar.setVisible(True)
# Start worker # Start worker
self.worker = ConversionWorker( self.worker = ConversionWorker(files_to_convert, self.converter)
files_to_convert,
self.converter,
canvas_size,
upscale_method
)
self.worker.progress.connect(self._on_progress) self.worker.progress.connect(self._on_progress)
self.worker.file_done.connect(self._on_file_done) self.worker.file_done.connect(self._on_file_done)
self.worker.finished.connect(self._on_finished) self.worker.finished.connect(self._on_finished)
@ -663,28 +634,29 @@ class IconExtractorWindow(QMainWindow):
self.progress_bar.setValue(self.progress_bar.value() + 1) self.progress_bar.setValue(self.progress_bar.value() + 1)
def _on_file_done(self, filename: str, output_path: str): def _on_file_done(self, filename: str, output_path: str):
logger.info(f"Converted: {filename} -> {output_path}") logger.info(f"Extracted: {filename} -> {output_path}")
def _on_finished(self, success: int, total: int): def _on_finished(self, success: int, total: int):
self.convert_btn.setEnabled(True) self.convert_btn.setEnabled(True)
self.convert_btn.setText("🚀 Convert All Icons") self.convert_btn.setText("Start Extracting Icons")
self.progress_bar.setVisible(False) self.progress_bar.setVisible(False)
self.status_label.setText(f"✅ Converted {success}/{total} files") self.status_label.setText(f"Extracted {success}/{total} icons")
QMessageBox.information( QMessageBox.information(
self, self,
"Conversion Complete", "Extraction Complete",
f"Successfully converted {success} of {total} icons.\n\n" f"Successfully extracted {success} of {total} icons.\n\n"
f"Output folder:\n{self.converter.output_dir}" f"Output location:\n{self.converter.output_dir}\n\n"
f"Submit these icons to EntropiaNexus.com to help the community!"
) )
def _on_error(self, error_msg: str): def _on_error(self, error_msg: str):
self.convert_btn.setEnabled(True) self.convert_btn.setEnabled(True)
self.convert_btn.setText("🚀 Convert All Icons") self.convert_btn.setText("Start Extracting Icons")
self.progress_bar.setVisible(False) self.progress_bar.setVisible(False)
self.status_label.setText(f"Error: {error_msg}") self.status_label.setText(f"Error: {error_msg}")
QMessageBox.critical(self, "Error", f"Conversion failed:\n{error_msg}") QMessageBox.critical(self, "Error", f"Extraction failed:\n{error_msg}")
def _open_output_folder(self): def _open_output_folder(self):
"""Open output folder in file manager.""" """Open output folder in file manager."""
@ -709,19 +681,38 @@ class IconExtractorWindow(QMainWindow):
event.accept() event.accept()
def set_app_icon(app: QApplication):
"""Set application icon."""
# Try to load icon from various locations
icon_paths = [
Path(__file__).parent / "assets" / "icon.ico",
Path(__file__).parent / "assets" / "icon.png",
Path(__file__).parent / "icon.ico",
Path(__file__).parent / "icon.png",
]
for icon_path in icon_paths:
if icon_path.exists():
app.setWindowIcon(QIcon(str(icon_path)))
return
# If no icon file, we can't set one programmatically
# User would need to provide an icon file
def main(): def main():
"""Main entry point.""" """Main entry point."""
if not PYQT_AVAILABLE:
print("ERROR: PyQt6 is required. Install with: pip install PyQt6")
sys.exit(1)
if not PIL_AVAILABLE:
print("ERROR: Pillow is required. Install with: pip install Pillow")
sys.exit(1)
app = QApplication(sys.argv) app = QApplication(sys.argv)
app.setStyle('Fusion') app.setStyle('Fusion')
# Set application info for proper Windows taskbar icon
app.setApplicationName(APP_NAME)
app.setApplicationVersion(APP_VERSION)
app.setOrganizationName("ImpulsiveFPS")
# Try to set icon
set_app_icon(app)
# Dark theme # Dark theme
app.setStyleSheet(""" app.setStyleSheet("""
QMainWindow, QDialog { QMainWindow, QDialog {
@ -777,6 +768,10 @@ def main():
QProgressBar::chunk { QProgressBar::chunk {
background-color: #4caf50; background-color: #4caf50;
} }
QTextEdit {
background-color: #222;
border: 1px solid #444;
}
""") """)
window = IconExtractorWindow() window = IconExtractorWindow()