806 lines
28 KiB
Python
806 lines
28 KiB
Python
"""
|
|
Entropia Universe Icon Extractor
|
|
A standalone tool for extracting game icons from cache.
|
|
|
|
Description: Extracts item icons from Entropia Universe game cache and converts
|
|
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:
|
|
python standalone_icon_extractor.py
|
|
|
|
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:
|
|
Entropia Nexus is a fan-made resource and is not affiliated with MindArk PE AB.
|
|
Entropia Universe is a trademark of MindArk PE AB.
|
|
"""
|
|
|
|
import sys
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Optional, List, Tuple
|
|
import ctypes
|
|
|
|
try:
|
|
from PyQt6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QLabel, QPushButton, QComboBox, QListWidget, QListWidgetItem,
|
|
QFileDialog, QProgressBar, QGroupBox, QMessageBox, QCheckBox,
|
|
QSplitter, QTextEdit
|
|
)
|
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings
|
|
from PyQt6.QtGui import QIcon, QPixmap, QFont
|
|
PYQT_AVAILABLE = True
|
|
except ImportError:
|
|
PYQT_AVAILABLE = False
|
|
print("PyQt6 not available. Install with: pip install PyQt6")
|
|
sys.exit(1)
|
|
|
|
try:
|
|
from PIL import Image, ImageFilter
|
|
PIL_AVAILABLE = True
|
|
except ImportError:
|
|
PIL_AVAILABLE = False
|
|
print("Pillow not available. Install with: pip install Pillow")
|
|
sys.exit(1)
|
|
|
|
# Setup logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
|
)
|
|
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:
|
|
"""TGA file header structure."""
|
|
def __init__(self, data: bytes):
|
|
self.id_length = data[0]
|
|
self.color_map_type = data[1]
|
|
self.image_type = data[2]
|
|
self.color_map_origin = int.from_bytes(data[3:5], 'little')
|
|
self.color_map_length = int.from_bytes(data[5:7], 'little')
|
|
self.color_map_depth = data[7]
|
|
self.x_origin = int.from_bytes(data[8:10], 'little')
|
|
self.y_origin = int.from_bytes(data[10:12], 'little')
|
|
self.width = int.from_bytes(data[12:14], 'little')
|
|
self.height = int.from_bytes(data[14:16], 'little')
|
|
self.pixel_depth = data[16]
|
|
self.image_descriptor = data[17]
|
|
|
|
def __str__(self):
|
|
return f"{self.width}x{self.height}, {self.pixel_depth}bpp"
|
|
|
|
|
|
class TGAConverter:
|
|
"""Converter for TGA files to PNG with 320x320 canvas."""
|
|
|
|
CANVAS_SIZE = (320, 320) # Hardcoded to 320x320
|
|
|
|
def __init__(self, output_dir: Optional[Path] = None):
|
|
# 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._cache_path: Optional[Path] = None
|
|
|
|
def find_cache_folder(self) -> Optional[Path]:
|
|
"""Find the Entropia Universe icon cache folder."""
|
|
# Hardcoded path - works on any system with EU installed
|
|
cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon")
|
|
|
|
if cache_path.exists():
|
|
self._cache_path = cache_path
|
|
return cache_path
|
|
|
|
return None
|
|
|
|
def read_tga_header(self, filepath: Path) -> Optional[TGAHeader]:
|
|
"""Read TGA header from file."""
|
|
try:
|
|
with open(filepath, 'rb') as f:
|
|
header_data = f.read(18)
|
|
if len(header_data) < 18:
|
|
return None
|
|
return TGAHeader(header_data)
|
|
except Exception as e:
|
|
logger.error(f"Error reading TGA header: {e}")
|
|
return None
|
|
|
|
def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None) -> Optional[Path]:
|
|
"""
|
|
Convert a TGA file to PNG with 320x320 canvas.
|
|
|
|
Args:
|
|
tga_path: Path to TGA file
|
|
output_name: Optional custom output name
|
|
|
|
Returns:
|
|
Path to output PNG file or None if failed
|
|
"""
|
|
try:
|
|
# Load TGA
|
|
image = Image.open(tga_path)
|
|
if image.mode != 'RGBA':
|
|
image = image.convert('RGBA')
|
|
|
|
# Apply 320x320 canvas (centered, no upscaling)
|
|
image = self._apply_canvas(image)
|
|
|
|
# Save
|
|
if output_name is None:
|
|
output_name = tga_path.stem
|
|
|
|
output_path = self.output_dir / f"{output_name}.png"
|
|
image.save(output_path, 'PNG')
|
|
|
|
return output_path
|
|
|
|
except Exception as e:
|
|
logger.error(f"Conversion failed: {e}")
|
|
return None
|
|
|
|
def _apply_canvas(self, image: Image.Image) -> Image.Image:
|
|
"""
|
|
Place image centered on a 320x320 canvas.
|
|
No upscaling - original size centered on canvas.
|
|
"""
|
|
canvas_w, canvas_h = self.CANVAS_SIZE
|
|
img_w, img_h = image.size
|
|
|
|
# Create transparent canvas
|
|
canvas = Image.new('RGBA', self.CANVAS_SIZE, (0, 0, 0, 0))
|
|
|
|
# Center on canvas (no scaling)
|
|
x = (canvas_w - img_w) // 2
|
|
y = (canvas_h - img_h) // 2
|
|
|
|
canvas.paste(image, (x, y), image if image.mode == 'RGBA' else None)
|
|
return canvas
|
|
|
|
|
|
class ConversionWorker(QThread):
|
|
"""Background worker for batch conversion."""
|
|
progress = pyqtSignal(str)
|
|
file_done = pyqtSignal(str, str) # filename, output_path
|
|
finished = pyqtSignal(int, int) # success, total
|
|
error = pyqtSignal(str)
|
|
|
|
def __init__(self, files: List[Path], converter: TGAConverter):
|
|
super().__init__()
|
|
self.files = files
|
|
self.converter = converter
|
|
self._running = True
|
|
|
|
def run(self):
|
|
"""Run conversion."""
|
|
try:
|
|
success = 0
|
|
total = len(self.files)
|
|
|
|
for i, filepath in enumerate(self.files):
|
|
if not self._running:
|
|
break
|
|
|
|
self.progress.emit(f"[{i+1}/{total}] {filepath.name}")
|
|
|
|
output = self.converter.convert_tga_to_png(filepath)
|
|
|
|
if output:
|
|
success += 1
|
|
self.file_done.emit(filepath.name, str(output))
|
|
|
|
self.finished.emit(success, total)
|
|
|
|
except Exception as e:
|
|
self.error.emit(str(e))
|
|
|
|
def stop(self):
|
|
self._running = False
|
|
|
|
|
|
class IconExtractorWindow(QMainWindow):
|
|
"""Main window for the standalone icon extractor."""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setWindowTitle(APP_NAME)
|
|
self.setMinimumSize(1000, 800)
|
|
self.resize(1100, 850)
|
|
|
|
self.converter = TGAConverter()
|
|
self.worker: Optional[ConversionWorker] = None
|
|
self.found_files: List[Path] = []
|
|
|
|
# Hardcoded base cache path
|
|
self.base_cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon")
|
|
|
|
self.settings = QSettings("ImpulsiveFPS", "EUIconExtractor")
|
|
|
|
self._setup_ui()
|
|
self._load_settings()
|
|
self._detect_subfolders()
|
|
|
|
def _setup_ui(self):
|
|
"""Setup the UI."""
|
|
central = QWidget()
|
|
self.setCentralWidget(central)
|
|
layout = QVBoxLayout(central)
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
layout.setSpacing(12)
|
|
|
|
# Header
|
|
header = QLabel(APP_NAME)
|
|
header.setStyleSheet("font-size: 22px; font-weight: bold; color: #4caf50;")
|
|
layout.addWidget(header)
|
|
|
|
# Description
|
|
desc = QLabel(
|
|
"Extract item icons from Entropia Universe cache and convert them to PNG. "
|
|
"Submit these to EntropiaNexus.com to help complete the item database."
|
|
)
|
|
desc.setStyleSheet("color: #aaaaaa; padding: 5px;")
|
|
desc.setWordWrap(True)
|
|
layout.addWidget(desc)
|
|
|
|
# Important notice
|
|
notice_group = QGroupBox("Important Information")
|
|
notice_layout = QVBoxLayout(notice_group)
|
|
notice_layout.setContentsMargins(10, 15, 10, 10)
|
|
|
|
notice_text = QTextEdit()
|
|
notice_text.setReadOnly(True)
|
|
notice_text.setStyleSheet("""
|
|
QTextEdit {
|
|
background-color: #2a2520;
|
|
color: #ffcc80;
|
|
border: 1px solid #5d4037;
|
|
border-radius: 3px;
|
|
font-size: 12px;
|
|
padding: 5px;
|
|
}
|
|
""")
|
|
notice_text.setText(
|
|
"REQUIREMENT: Items must be seen/rendered in-game before they appear in the cache!\n"
|
|
"If an item icon is missing, view it in your inventory or see it dropped as loot first.\n\n"
|
|
f"Output: Documents/Entropia Universe/Icons/ (same folder as chat.log)"
|
|
)
|
|
notice_layout.addWidget(notice_text)
|
|
layout.addWidget(notice_group)
|
|
|
|
# Main splitter
|
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
|
|
# Left panel - Settings
|
|
left_panel = QWidget()
|
|
left_layout = QVBoxLayout(left_panel)
|
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
# Cache folder
|
|
cache_group = QGroupBox("Cache Source")
|
|
cache_layout = QVBoxLayout(cache_group)
|
|
cache_layout.setContentsMargins(10, 15, 10, 10)
|
|
cache_layout.setSpacing(8)
|
|
|
|
# Base path (hardcoded) - use a shorter display
|
|
path_display = str(self.base_cache_path).replace("/", "\\")
|
|
self.cache_label = QLabel(path_display)
|
|
self.cache_label.setStyleSheet(
|
|
"font-family: Consolas; font-size: 10px; color: #888; "
|
|
"padding: 8px; background: #1a1a1a; border-radius: 3px;"
|
|
)
|
|
self.cache_label.setWordWrap(True)
|
|
self.cache_label.setMinimumHeight(40)
|
|
cache_layout.addWidget(self.cache_label)
|
|
|
|
# Subfolder selector
|
|
subfolder_layout = QHBoxLayout()
|
|
subfolder_layout.setSpacing(8)
|
|
subfolder_layout.addWidget(QLabel("Version:"))
|
|
self.subfolder_combo = QComboBox()
|
|
self.subfolder_combo.setMinimumWidth(180)
|
|
self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed)
|
|
subfolder_layout.addWidget(self.subfolder_combo, 1)
|
|
|
|
refresh_btn = QPushButton("Refresh")
|
|
refresh_btn.setMaximumWidth(70)
|
|
refresh_btn.clicked.connect(self._detect_subfolders)
|
|
subfolder_layout.addWidget(refresh_btn)
|
|
|
|
cache_layout.addLayout(subfolder_layout)
|
|
|
|
# All subfolders checkbox
|
|
self.all_subfolders_check = QCheckBox("Include ALL version folders")
|
|
self.all_subfolders_check.setToolTip("Merge icons from all game versions")
|
|
self.all_subfolders_check.stateChanged.connect(self._on_all_subfolders_changed)
|
|
cache_layout.addWidget(self.all_subfolders_check)
|
|
|
|
left_layout.addWidget(cache_group)
|
|
|
|
# Output folder
|
|
output_group = QGroupBox("Output Location")
|
|
output_layout = QVBoxLayout(output_group)
|
|
output_layout.setContentsMargins(10, 15, 10, 10)
|
|
output_layout.setSpacing(8)
|
|
|
|
output_info = QLabel("Icons saved to your Documents folder (same as chat.log)")
|
|
output_info.setStyleSheet("color: #888; font-size: 11px;")
|
|
output_info.setWordWrap(True)
|
|
output_layout.addWidget(output_info)
|
|
|
|
# Show relative path instead of full path
|
|
rel_path = "Documents/Entropia Universe/Icons/"
|
|
self.output_label = QLabel(rel_path)
|
|
self.output_label.setStyleSheet(
|
|
"font-family: Consolas; font-size: 10px; color: #888; "
|
|
"padding: 8px; background: #1a1a1a; border-radius: 3px;"
|
|
)
|
|
output_layout.addWidget(self.output_label)
|
|
|
|
change_btn = QPushButton("Change Output Folder...")
|
|
change_btn.clicked.connect(self._browse_output)
|
|
output_layout.addWidget(change_btn)
|
|
|
|
left_layout.addWidget(output_group)
|
|
|
|
# Settings (simplified - just 320x320)
|
|
settings_group = QGroupBox("Export Settings")
|
|
settings_layout = QVBoxLayout(settings_group)
|
|
settings_layout.setContentsMargins(10, 15, 10, 10)
|
|
|
|
settings_info = QLabel(
|
|
"Format: PNG with transparency\n"
|
|
"Canvas: 320x320 pixels (centered)\n"
|
|
"Size: Original icon size (no upscaling)"
|
|
)
|
|
settings_info.setStyleSheet("color: #888; font-size: 11px;")
|
|
settings_layout.addWidget(settings_info)
|
|
|
|
left_layout.addWidget(settings_group)
|
|
|
|
# Nexus link
|
|
nexus_group = QGroupBox("EntropiaNexus.com")
|
|
nexus_layout = QVBoxLayout(nexus_group)
|
|
nexus_layout.setContentsMargins(10, 15, 10, 10)
|
|
|
|
nexus_info = QLabel("Submit icons to help complete the item database!")
|
|
nexus_info.setStyleSheet("color: #4caf50; font-size: 11px;")
|
|
nexus_info.setWordWrap(True)
|
|
nexus_layout.addWidget(nexus_info)
|
|
|
|
nexus_btn = QPushButton("Open EntropiaNexus.com")
|
|
nexus_btn.setMaximumHeight(30)
|
|
nexus_btn.clicked.connect(lambda: self._open_url(WEBSITE))
|
|
nexus_layout.addWidget(nexus_btn)
|
|
|
|
left_layout.addWidget(nexus_group)
|
|
|
|
# Convert button
|
|
self.convert_btn = QPushButton("Start Extracting Icons")
|
|
self.convert_btn.setMinimumHeight(50)
|
|
self.convert_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #0d47a1;
|
|
font-weight: bold;
|
|
font-size: 14px;
|
|
border-radius: 5px;
|
|
padding: 10px;
|
|
}
|
|
QPushButton:hover { background-color: #1565c0; }
|
|
QPushButton:disabled { background-color: #333; color: #666; }
|
|
""")
|
|
self.convert_btn.clicked.connect(self._start_conversion)
|
|
left_layout.addWidget(self.convert_btn)
|
|
|
|
# Progress
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setTextVisible(True)
|
|
self.progress_bar.setVisible(False)
|
|
left_layout.addWidget(self.progress_bar)
|
|
|
|
self.status_label = QLabel("Ready")
|
|
self.status_label.setStyleSheet("color: #888; padding: 5px;")
|
|
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
left_layout.addWidget(self.status_label)
|
|
|
|
left_layout.addStretch()
|
|
|
|
splitter.addWidget(left_panel)
|
|
|
|
# Right panel - File list
|
|
right_panel = QWidget()
|
|
right_layout = QVBoxLayout(right_panel)
|
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
|
|
|
files_group = QGroupBox("Available Icons")
|
|
files_layout = QVBoxLayout(files_group)
|
|
files_layout.setContentsMargins(10, 15, 10, 10)
|
|
|
|
files_info = QLabel("Select icons to extract (or leave blank for 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.setStyleSheet("font-weight: bold; padding: 5px 0;")
|
|
files_layout.addWidget(self.files_count_label)
|
|
|
|
self.files_list = QListWidget()
|
|
self.files_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
|
|
files_layout.addWidget(self.files_list, 1)
|
|
|
|
# Selection buttons
|
|
sel_layout = QHBoxLayout()
|
|
sel_layout.setSpacing(8)
|
|
|
|
select_all_btn = QPushButton("Select All")
|
|
select_all_btn.setMaximumWidth(100)
|
|
select_all_btn.clicked.connect(self.files_list.selectAll)
|
|
sel_layout.addWidget(select_all_btn)
|
|
|
|
select_none_btn = QPushButton("Select None")
|
|
select_none_btn.setMaximumWidth(100)
|
|
select_none_btn.clicked.connect(self.files_list.clearSelection)
|
|
sel_layout.addWidget(select_none_btn)
|
|
|
|
sel_layout.addStretch()
|
|
|
|
open_folder_btn = QPushButton("Open Output Folder")
|
|
open_folder_btn.setMaximumWidth(130)
|
|
open_folder_btn.clicked.connect(self._open_output_folder)
|
|
sel_layout.addWidget(open_folder_btn)
|
|
|
|
files_layout.addLayout(sel_layout)
|
|
right_layout.addWidget(files_group)
|
|
|
|
splitter.addWidget(right_panel)
|
|
splitter.setSizes([350, 550])
|
|
|
|
layout.addWidget(splitter, 1)
|
|
|
|
# Footer
|
|
footer = QLabel(
|
|
f"Developed by {DEVELOPER} | Discord: {DISCORD}\n"
|
|
f"{WEBSITE}\n"
|
|
"Entropia Nexus is a fan-made resource and is not affiliated with MindArk PE AB. "
|
|
"Entropia Universe is a trademark of MindArk PE AB."
|
|
)
|
|
footer.setStyleSheet("color: #555; font-size: 9px; padding: 8px;")
|
|
footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
footer.setWordWrap(True)
|
|
footer.setMinimumHeight(60)
|
|
layout.addWidget(footer)
|
|
|
|
def _open_url(self, url: str):
|
|
"""Open URL in default browser."""
|
|
import webbrowser
|
|
webbrowser.open(url)
|
|
|
|
def _load_settings(self):
|
|
"""Load saved settings."""
|
|
# Output folder
|
|
saved_output = self.settings.value("output_dir", str(self.converter.output_dir))
|
|
self.converter.output_dir = Path(saved_output)
|
|
self.output_label.setText(saved_output)
|
|
|
|
def _save_settings(self):
|
|
"""Save current settings."""
|
|
self.settings.setValue("output_dir", str(self.converter.output_dir))
|
|
|
|
def _detect_subfolders(self):
|
|
"""Detect version subfolders in the cache directory."""
|
|
self.subfolder_combo.clear()
|
|
|
|
if not self.base_cache_path.exists():
|
|
self.cache_label.setText(f"Not found: {self.base_cache_path}")
|
|
self.status_label.setText("Cache folder not found - is Entropia Universe installed?")
|
|
return
|
|
|
|
# Find all subfolders that contain TGA files
|
|
subfolders = []
|
|
for item in self.base_cache_path.iterdir():
|
|
if item.is_dir():
|
|
# Check if this folder has TGA files
|
|
tga_count = len(list(item.glob("*.tga")))
|
|
if tga_count > 0:
|
|
subfolders.append((item.name, tga_count, item))
|
|
|
|
if not subfolders:
|
|
self.cache_label.setText(f"No subfolders with icons in {self.base_cache_path}")
|
|
self.status_label.setText("No version folders found")
|
|
return
|
|
|
|
# Sort by name (version)
|
|
subfolders.sort(key=lambda x: x[0])
|
|
|
|
# Add to combo
|
|
for name, count, path in subfolders:
|
|
self.subfolder_combo.addItem(f"{name} ({count} icons)", str(path))
|
|
|
|
# Add "All folders" option at top
|
|
total_icons = sum(s[1] for s in subfolders)
|
|
self.subfolder_combo.insertItem(0, f"All Folders ({total_icons} icons)", "all")
|
|
self.subfolder_combo.setCurrentIndex(0)
|
|
|
|
self.cache_label.setText(f"{self.base_cache_path}")
|
|
self.status_label.setText(f"Found {len(subfolders)} version folders")
|
|
|
|
# Load files
|
|
self._refresh_file_list()
|
|
|
|
def _on_subfolder_changed(self):
|
|
"""Handle subfolder selection change."""
|
|
self._refresh_file_list()
|
|
|
|
def _on_all_subfolders_changed(self):
|
|
"""Handle 'all subfolders' checkbox change."""
|
|
self.subfolder_combo.setEnabled(not self.all_subfolders_check.isChecked())
|
|
self._refresh_file_list()
|
|
|
|
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 _refresh_file_list(self):
|
|
"""Refresh the list of found files based on current selection."""
|
|
self.files_list.clear()
|
|
self.found_files = []
|
|
|
|
if not self.base_cache_path.exists():
|
|
return
|
|
|
|
# Determine which folders to scan
|
|
if self.all_subfolders_check.isChecked():
|
|
# Scan all subfolders
|
|
folders_to_scan = [d for d in self.base_cache_path.iterdir() if d.is_dir()]
|
|
else:
|
|
# Scan selected subfolder
|
|
path_data = self.subfolder_combo.currentData()
|
|
if path_data == "all" or path_data is None:
|
|
folders_to_scan = [d for d in self.base_cache_path.iterdir() if d.is_dir()]
|
|
else:
|
|
folders_to_scan = [Path(path_data)]
|
|
|
|
# Collect TGA files
|
|
tga_files = []
|
|
for folder in folders_to_scan:
|
|
if folder.exists():
|
|
tga_files.extend(folder.glob("*.tga"))
|
|
|
|
self.files_count_label.setText(f"Found {len(tga_files)} icon files")
|
|
self.status_label.setText(f"Found {len(tga_files)} files")
|
|
|
|
for tga_file in sorted(tga_files):
|
|
# Show folder prefix for clarity
|
|
try:
|
|
rel_path = f"{tga_file.parent.name}/{tga_file.name}"
|
|
except:
|
|
rel_path = tga_file.name
|
|
|
|
item = QListWidgetItem(rel_path)
|
|
item.setData(Qt.ItemDataRole.UserRole, str(tga_file))
|
|
|
|
# Get info
|
|
header = self.converter.read_tga_header(tga_file)
|
|
if header:
|
|
item.setToolTip(f"{header.width}x{header.height}, {header.pixel_depth}bpp")
|
|
|
|
self.files_list.addItem(item)
|
|
self.found_files.append(tga_file)
|
|
|
|
self.convert_btn.setEnabled(len(tga_files) > 0)
|
|
|
|
def _start_conversion(self):
|
|
"""Start batch conversion."""
|
|
# Get selected files or all files
|
|
selected_items = self.files_list.selectedItems()
|
|
if selected_items:
|
|
files_to_convert = [
|
|
Path(item.data(Qt.ItemDataRole.UserRole))
|
|
for item in selected_items
|
|
]
|
|
else:
|
|
files_to_convert = self.found_files
|
|
|
|
if not files_to_convert:
|
|
QMessageBox.warning(self, "No Files", "No files selected for extraction.")
|
|
return
|
|
|
|
# Save settings
|
|
self._save_settings()
|
|
|
|
# Setup UI
|
|
self.convert_btn.setEnabled(False)
|
|
self.convert_btn.setText("Extracting...")
|
|
self.progress_bar.setRange(0, len(files_to_convert))
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setVisible(True)
|
|
|
|
# Start worker
|
|
self.worker = ConversionWorker(files_to_convert, self.converter)
|
|
self.worker.progress.connect(self._on_progress)
|
|
self.worker.file_done.connect(self._on_file_done)
|
|
self.worker.finished.connect(self._on_finished)
|
|
self.worker.error.connect(self._on_error)
|
|
self.worker.start()
|
|
|
|
def _on_progress(self, msg: str):
|
|
self.status_label.setText(msg)
|
|
self.progress_bar.setValue(self.progress_bar.value() + 1)
|
|
|
|
def _on_file_done(self, filename: str, output_path: str):
|
|
logger.info(f"Extracted: {filename} -> {output_path}")
|
|
|
|
def _on_finished(self, success: int, total: int):
|
|
self.convert_btn.setEnabled(True)
|
|
self.convert_btn.setText("Start Extracting Icons")
|
|
self.progress_bar.setVisible(False)
|
|
self.status_label.setText(f"Extracted {success}/{total} icons")
|
|
|
|
QMessageBox.information(
|
|
self,
|
|
"Extraction Complete",
|
|
f"Successfully extracted {success} of {total} icons.\n\n"
|
|
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):
|
|
self.convert_btn.setEnabled(True)
|
|
self.convert_btn.setText("Start Extracting Icons")
|
|
self.progress_bar.setVisible(False)
|
|
self.status_label.setText(f"Error: {error_msg}")
|
|
|
|
QMessageBox.critical(self, "Error", f"Extraction failed:\n{error_msg}")
|
|
|
|
def _open_output_folder(self):
|
|
"""Open output folder in file manager."""
|
|
import os
|
|
import subprocess
|
|
|
|
path = str(self.converter.output_dir)
|
|
|
|
if os.name == 'nt':
|
|
os.startfile(path)
|
|
elif sys.platform == 'darwin':
|
|
subprocess.run(['open', path])
|
|
else:
|
|
subprocess.run(['xdg-open', path])
|
|
|
|
def closeEvent(self, event):
|
|
"""Save settings on close."""
|
|
self._save_settings()
|
|
if self.worker and self.worker.isRunning():
|
|
self.worker.stop()
|
|
self.worker.wait(1000)
|
|
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():
|
|
"""Main entry point."""
|
|
app = QApplication(sys.argv)
|
|
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
|
|
app.setStyleSheet("""
|
|
QMainWindow, QDialog {
|
|
background-color: #1a1a1a;
|
|
}
|
|
QWidget {
|
|
background-color: #1a1a1a;
|
|
color: #e0e0e0;
|
|
}
|
|
QGroupBox {
|
|
font-weight: bold;
|
|
border: 1px solid #333;
|
|
border-radius: 5px;
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
}
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
}
|
|
QPushButton {
|
|
background-color: #333;
|
|
border: 1px solid #555;
|
|
padding: 8px 15px;
|
|
border-radius: 3px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #444;
|
|
}
|
|
QComboBox {
|
|
background-color: #2a2a2a;
|
|
border: 1px solid #555;
|
|
padding: 5px;
|
|
min-width: 150px;
|
|
}
|
|
QListWidget {
|
|
background-color: #222;
|
|
border: 1px solid #444;
|
|
border-radius: 3px;
|
|
}
|
|
QListWidget::item {
|
|
padding: 5px;
|
|
}
|
|
QListWidget::item:selected {
|
|
background-color: #0d47a1;
|
|
}
|
|
QProgressBar {
|
|
border: 1px solid #444;
|
|
border-radius: 3px;
|
|
text-align: center;
|
|
}
|
|
QProgressBar::chunk {
|
|
background-color: #4caf50;
|
|
}
|
|
QTextEdit {
|
|
background-color: #222;
|
|
border: 1px solid #444;
|
|
}
|
|
""")
|
|
|
|
window = IconExtractorWindow()
|
|
window.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |