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:
parent
8b97dbc58c
commit
eba76cf59c
|
|
@ -1,45 +1,57 @@
|
|||
"""
|
||||
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,
|
||||
convert them to PNG, and optionally upscale them for better quality.
|
||||
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
|
||||
|
||||
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 logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Tuple
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import ctypes
|
||||
|
||||
try:
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QPushButton, QComboBox, QListWidget, QListWidgetItem,
|
||||
QFileDialog, QProgressBar, QGroupBox, QMessageBox, QCheckBox,
|
||||
QSpinBox, QSplitter
|
||||
QSplitter, QTextEdit
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings
|
||||
from PyQt6.QtGui import QIcon, QPixmap
|
||||
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("PIL not available. Install with: pip install Pillow")
|
||||
|
||||
import numpy as np
|
||||
print("Pillow not available. Install with: pip install Pillow")
|
||||
sys.exit(1)
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
|
|
@ -49,6 +61,14 @@ logging.basicConfig(
|
|||
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):
|
||||
|
|
@ -70,26 +90,28 @@ class TGAHeader:
|
|||
|
||||
|
||||
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):
|
||||
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._cache_path: Optional[Path] = None
|
||||
|
||||
def find_cache_folder(self) -> Optional[Path]:
|
||||
"""Find the Entropia Universe icon cache folder."""
|
||||
possible_paths = [
|
||||
Path("C:") / "ProgramData" / "Entropia Universe" / "public_users_data" / "cache" / "icon",
|
||||
Path.home() / "Documents" / "Entropia Universe" / "cache" / "icons",
|
||||
]
|
||||
# Hardcoded path - works on any system with EU installed
|
||||
cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon")
|
||||
|
||||
for path in possible_paths:
|
||||
if path.exists():
|
||||
# Check if this folder or any subfolder has .tga files
|
||||
if list(path.rglob("*.tga")):
|
||||
self._cache_path = path
|
||||
return path
|
||||
if cache_path.exists():
|
||||
self._cache_path = cache_path
|
||||
return cache_path
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -105,25 +127,25 @@ class TGAConverter:
|
|||
logger.error(f"Error reading TGA header: {e}")
|
||||
return None
|
||||
|
||||
def convert_tga_to_png(
|
||||
self,
|
||||
tga_path: Path,
|
||||
output_name: Optional[str] = None,
|
||||
canvas_size: Optional[Tuple[int, int]] = None,
|
||||
upscale: bool = False,
|
||||
upscale_method: str = 'nearest'
|
||||
) -> Optional[Path]:
|
||||
"""Convert a TGA file to PNG."""
|
||||
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:
|
||||
# Try PIL first
|
||||
# Load TGA
|
||||
image = Image.open(tga_path)
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
# Apply canvas sizing if requested
|
||||
if canvas_size:
|
||||
do_upscale = upscale and upscale_method != 'none'
|
||||
image = self._apply_canvas(image, canvas_size, do_upscale, upscale_method)
|
||||
# Apply 320x320 canvas (centered, no upscaling)
|
||||
image = self._apply_canvas(image)
|
||||
|
||||
# Save
|
||||
if output_name is None:
|
||||
|
|
@ -138,45 +160,18 @@ class TGAConverter:
|
|||
logger.error(f"Conversion failed: {e}")
|
||||
return None
|
||||
|
||||
def _apply_canvas(
|
||||
self,
|
||||
image: Image.Image,
|
||||
canvas_size: Tuple[int, int],
|
||||
upscale: bool = False,
|
||||
upscale_method: str = 'nearest'
|
||||
) -> Image.Image:
|
||||
"""Place image centered on a canvas."""
|
||||
canvas_w, canvas_h = canvas_size
|
||||
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', canvas_size, (0, 0, 0, 0))
|
||||
canvas = Image.new('RGBA', self.CANVAS_SIZE, (0, 0, 0, 0))
|
||||
|
||||
# Calculate 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
|
||||
# Center on canvas (no scaling)
|
||||
x = (canvas_w - img_w) // 2
|
||||
y = (canvas_h - img_h) // 2
|
||||
|
||||
|
|
@ -191,18 +186,10 @@ class ConversionWorker(QThread):
|
|||
finished = pyqtSignal(int, int) # success, total
|
||||
error = pyqtSignal(str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
files: List[Path],
|
||||
converter: TGAConverter,
|
||||
canvas_size: Optional[Tuple[int, int]],
|
||||
upscale_method: str
|
||||
):
|
||||
def __init__(self, files: List[Path], converter: TGAConverter):
|
||||
super().__init__()
|
||||
self.files = files
|
||||
self.converter = converter
|
||||
self.canvas_size = canvas_size
|
||||
self.upscale_method = upscale_method
|
||||
self._running = True
|
||||
|
||||
def run(self):
|
||||
|
|
@ -217,12 +204,7 @@ class ConversionWorker(QThread):
|
|||
|
||||
self.progress.emit(f"[{i+1}/{total}] {filepath.name}")
|
||||
|
||||
output = self.converter.convert_tga_to_png(
|
||||
filepath,
|
||||
canvas_size=self.canvas_size,
|
||||
upscale=True,
|
||||
upscale_method=self.upscale_method
|
||||
)
|
||||
output = self.converter.convert_tga_to_png(filepath)
|
||||
|
||||
if output:
|
||||
success += 1
|
||||
|
|
@ -242,18 +224,17 @@ class IconExtractorWindow(QMainWindow):
|
|||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("🎮 Entropia Universe Icon Extractor")
|
||||
self.setMinimumSize(900, 700)
|
||||
self.setWindowTitle(APP_NAME)
|
||||
self.setMinimumSize(950, 750)
|
||||
|
||||
self.converter = TGAConverter()
|
||||
self.worker: Optional[ConversionWorker] = None
|
||||
self.found_files: List[Path] = []
|
||||
self.current_subfolder: Optional[Path] = None
|
||||
|
||||
# Hardcoded base cache path
|
||||
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._load_settings()
|
||||
|
|
@ -268,18 +249,44 @@ class IconExtractorWindow(QMainWindow):
|
|||
layout.setSpacing(12)
|
||||
|
||||
# Header
|
||||
header = QLabel("🎮 Entropia Universe Icon Extractor")
|
||||
header.setStyleSheet("font-size: 20px; font-weight: bold; color: #4caf50;")
|
||||
header = QLabel(APP_NAME)
|
||||
header.setStyleSheet("font-size: 22px; font-weight: bold; color: #4caf50;")
|
||||
layout.addWidget(header)
|
||||
|
||||
# Description
|
||||
desc = QLabel(
|
||||
"Extract and convert item icons from Entropia Universe game cache.\n"
|
||||
"Icons are saved as PNG files with transparent backgrounds."
|
||||
"Extract item icons from Entropia Universe game cache and convert them to PNG format.\n"
|
||||
"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)
|
||||
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
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
|
||||
|
|
@ -289,15 +296,15 @@ class IconExtractorWindow(QMainWindow):
|
|||
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Cache folder
|
||||
cache_group = QGroupBox("📁 Cache Folder")
|
||||
cache_group = QGroupBox("Cache Source")
|
||||
cache_layout = QVBoxLayout(cache_group)
|
||||
|
||||
# Base path (hardcoded)
|
||||
base_label = QLabel("Base Path:")
|
||||
base_label = QLabel("Game Cache Location:")
|
||||
cache_layout.addWidget(base_label)
|
||||
|
||||
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)
|
||||
|
||||
# Subfolder selector
|
||||
|
|
@ -308,7 +315,7 @@ class IconExtractorWindow(QMainWindow):
|
|||
self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed)
|
||||
subfolder_layout.addWidget(self.subfolder_combo)
|
||||
|
||||
refresh_btn = QPushButton("🔄 Refresh")
|
||||
refresh_btn = QPushButton("Refresh")
|
||||
refresh_btn.clicked.connect(self._detect_subfolders)
|
||||
subfolder_layout.addWidget(refresh_btn)
|
||||
|
||||
|
|
@ -316,81 +323,68 @@ class IconExtractorWindow(QMainWindow):
|
|||
cache_layout.addLayout(subfolder_layout)
|
||||
|
||||
# All subfolders checkbox
|
||||
self.all_subfolders_check = QCheckBox("Include ALL subfolders (merge everything)")
|
||||
self.all_subfolders_check.setToolTip("If checked, will find TGA files from ALL version subfolders")
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# Output folder
|
||||
output_group = QGroupBox("💾 Output Folder")
|
||||
output_group = QGroupBox("Output Location")
|
||||
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.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_btn = QPushButton("Change Output Folder...")
|
||||
output_btn.clicked.connect(self._browse_output)
|
||||
output_layout.addWidget(output_btn)
|
||||
change_btn = QPushButton("Change Output Folder...")
|
||||
change_btn.clicked.connect(self._browse_output)
|
||||
output_layout.addWidget(change_btn)
|
||||
|
||||
left_layout.addWidget(output_group)
|
||||
|
||||
# Settings
|
||||
settings_group = QGroupBox("⚙️ Conversion Settings")
|
||||
# Settings (simplified - just 320x320)
|
||||
settings_group = QGroupBox("Export Settings")
|
||||
settings_layout = QVBoxLayout(settings_group)
|
||||
|
||||
# Canvas size
|
||||
size_layout = QHBoxLayout()
|
||||
size_layout.addWidget(QLabel("Canvas Size:"))
|
||||
self.size_combo = QComboBox()
|
||||
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)"
|
||||
settings_info = QLabel(
|
||||
"Export Format: PNG with transparent background\n"
|
||||
"Canvas Size: 320x320 pixels (centered)\n"
|
||||
"Upscaling: None (original icon size)"
|
||||
)
|
||||
method_layout.addWidget(self.method_combo)
|
||||
method_layout.addStretch()
|
||||
settings_layout.addLayout(method_layout)
|
||||
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_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
|
||||
self.convert_btn = QPushButton("🚀 Convert All Icons")
|
||||
self.convert_btn = QPushButton("Start Extracting Icons")
|
||||
self.convert_btn.setMinimumHeight(60)
|
||||
self.convert_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
|
|
@ -423,9 +417,13 @@ class IconExtractorWindow(QMainWindow):
|
|||
right_layout = QVBoxLayout(right_panel)
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
files_group = QGroupBox("📄 Found Icons")
|
||||
files_group = QGroupBox("Available Icons")
|
||||
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")
|
||||
files_layout.addWidget(self.files_count_label)
|
||||
|
||||
|
|
@ -446,7 +444,7 @@ class IconExtractorWindow(QMainWindow):
|
|||
|
||||
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)
|
||||
sel_layout.addWidget(open_folder_btn)
|
||||
|
||||
|
|
@ -454,44 +452,44 @@ class IconExtractorWindow(QMainWindow):
|
|||
right_layout.addWidget(files_group)
|
||||
|
||||
splitter.addWidget(right_panel)
|
||||
splitter.setSizes([350, 550])
|
||||
splitter.setSizes([380, 520])
|
||||
|
||||
layout.addWidget(splitter, 1)
|
||||
|
||||
# Footer
|
||||
footer = QLabel("Entropia Universe Icon Extractor | Standalone Tool")
|
||||
footer.setStyleSheet("color: #555; font-size: 11px; padding: 5px;")
|
||||
footer = QLabel(
|
||||
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.setWordWrap(True)
|
||||
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)
|
||||
|
||||
# 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):
|
||||
"""Save current settings."""
|
||||
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):
|
||||
"""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 - check if path is correct")
|
||||
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
|
||||
|
|
@ -504,7 +502,7 @@ class IconExtractorWindow(QMainWindow):
|
|||
subfolders.append((item.name, tga_count, item))
|
||||
|
||||
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")
|
||||
return
|
||||
|
||||
|
|
@ -517,10 +515,10 @@ class IconExtractorWindow(QMainWindow):
|
|||
|
||||
# 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.insertItem(0, f"All Folders ({total_icons} icons)", "all")
|
||||
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")
|
||||
|
||||
# Load files
|
||||
|
|
@ -535,23 +533,18 @@ class IconExtractorWindow(QMainWindow):
|
|||
self.subfolder_combo.setEnabled(not self.all_subfolders_check.isChecked())
|
||||
self._refresh_file_list()
|
||||
|
||||
def _auto_scan(self):
|
||||
"""Auto-detect cache folder - just refreshes subfolder list."""
|
||||
self.status_label.setText("Scanning for version folders...")
|
||||
self._detect_subfolders()
|
||||
|
||||
def _browse_cache(self):
|
||||
"""Browse for cache folder."""
|
||||
def _browse_output(self):
|
||||
"""Browse for output folder."""
|
||||
folder = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"Select Entropia Universe Cache Folder",
|
||||
str(self.base_cache_path.parent)
|
||||
"Select Output Folder",
|
||||
str(self.converter.output_dir)
|
||||
)
|
||||
|
||||
if folder:
|
||||
self.base_cache_path = Path(folder)
|
||||
self.cache_label.setText(str(self.base_cache_path))
|
||||
self._detect_subfolders()
|
||||
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."""
|
||||
|
|
@ -602,19 +595,6 @@ class IconExtractorWindow(QMainWindow):
|
|||
|
||||
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):
|
||||
"""Start batch conversion."""
|
||||
# Get selected files or all files
|
||||
|
|
@ -628,30 +608,21 @@ class IconExtractorWindow(QMainWindow):
|
|||
files_to_convert = self.found_files
|
||||
|
||||
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
|
||||
|
||||
# Get settings
|
||||
canvas_size = self.size_combo.currentData()
|
||||
upscale_method = self.method_combo.currentData()
|
||||
|
||||
# Save settings
|
||||
self._save_settings()
|
||||
|
||||
# Setup UI
|
||||
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.setValue(0)
|
||||
self.progress_bar.setVisible(True)
|
||||
|
||||
# Start worker
|
||||
self.worker = ConversionWorker(
|
||||
files_to_convert,
|
||||
self.converter,
|
||||
canvas_size,
|
||||
upscale_method
|
||||
)
|
||||
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)
|
||||
|
|
@ -663,28 +634,29 @@ class IconExtractorWindow(QMainWindow):
|
|||
self.progress_bar.setValue(self.progress_bar.value() + 1)
|
||||
|
||||
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):
|
||||
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.status_label.setText(f"✅ Converted {success}/{total} files")
|
||||
self.status_label.setText(f"Extracted {success}/{total} icons")
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Conversion Complete",
|
||||
f"Successfully converted {success} of {total} icons.\n\n"
|
||||
f"Output folder:\n{self.converter.output_dir}"
|
||||
"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("🚀 Convert All Icons")
|
||||
self.convert_btn.setText("Start Extracting Icons")
|
||||
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):
|
||||
"""Open output folder in file manager."""
|
||||
|
|
@ -709,19 +681,38 @@ class IconExtractorWindow(QMainWindow):
|
|||
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."""
|
||||
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.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 {
|
||||
|
|
@ -777,6 +768,10 @@ def main():
|
|||
QProgressBar::chunk {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
QTextEdit {
|
||||
background-color: #222;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
""")
|
||||
|
||||
window = IconExtractorWindow()
|
||||
|
|
|
|||
Loading…
Reference in New Issue