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
|
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,16 +452,26 @@ 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
|
||||||
|
|
@ -471,27 +479,17 @@ class IconExtractorWindow(QMainWindow):
|
||||||
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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue