feat: standalone extractor - improved UI and TGA preview
Major improvements: 1. Disclaimer updated: - "Entropia Universe Icon Extractor is a fan-made resource..." 2. Important Information moved to bottom: - Orange header, amber text on dark background - Larger, easier to read font (13px) - Better line spacing 3. Improved text readability throughout: - Larger font sizes (12-13px) - Better contrast (lighter text on dark bg) - Bold headers in group boxes - Consistent spacing and padding 4. Fixed box layouts: - All content fits properly in Cache Source and Output Location - Better margins (12, 18, 12, 12) - Path labels wrap properly with minimum heights 5. Added TGA Preview feature: - Double-click any icon in the list to preview - Shows original size and scaled preview - Modal dialog with close button - Tooltip on items indicates preview is available 6. Updated styling: - Better hover effects on list items - Improved color scheme - Larger window default size (1150x900)
This commit is contained in:
parent
6bf90e5230
commit
f498dffa69
|
|
@ -19,8 +19,9 @@ Discord: impulsivefps
|
||||||
Website: https://EntropiaNexus.com
|
Website: https://EntropiaNexus.com
|
||||||
|
|
||||||
Disclaimer:
|
Disclaimer:
|
||||||
Entropia Nexus is a fan-made resource and is not affiliated with MindArk PE AB.
|
Entropia Universe Icon Extractor is a fan-made resource and is not
|
||||||
Entropia Universe is a trademark of MindArk PE AB.
|
affiliated with MindArk PE AB. Entropia Universe is a trademark of
|
||||||
|
MindArk PE AB.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -34,10 +35,10 @@ try:
|
||||||
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,
|
||||||
QSplitter, QTextEdit
|
QSplitter, QTextEdit, QDialog, QScrollArea, QFrame
|
||||||
)
|
)
|
||||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings, QSize
|
||||||
from PyQt6.QtGui import QIcon, QPixmap, QFont
|
from PyQt6.QtGui import QIcon, QPixmap, QFont, QImage
|
||||||
PYQT_AVAILABLE = True
|
PYQT_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PYQT_AVAILABLE = False
|
PYQT_AVAILABLE = False
|
||||||
|
|
@ -52,6 +53,8 @@ except ImportError:
|
||||||
print("Pillow not available. Install with: pip install Pillow")
|
print("Pillow not available. Install with: pip install Pillow")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
|
@ -126,6 +129,17 @@ class TGAConverter:
|
||||||
logger.error(f"Error reading TGA header: {e}")
|
logger.error(f"Error reading TGA header: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def load_tga_image(self, filepath: Path) -> Optional[Image.Image]:
|
||||||
|
"""Load a TGA file as PIL Image."""
|
||||||
|
try:
|
||||||
|
image = Image.open(filepath)
|
||||||
|
if image.mode != 'RGBA':
|
||||||
|
image = image.convert('RGBA')
|
||||||
|
return image
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error loading TGA: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None) -> Optional[Path]:
|
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.
|
Convert a TGA file to PNG with 320x320 canvas.
|
||||||
|
|
@ -139,9 +153,9 @@ class TGAConverter:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Load TGA
|
# Load TGA
|
||||||
image = Image.open(tga_path)
|
image = self.load_tga_image(tga_path)
|
||||||
if image.mode != 'RGBA':
|
if not image:
|
||||||
image = image.convert('RGBA')
|
return None
|
||||||
|
|
||||||
# Apply 320x320 canvas (centered, no upscaling)
|
# Apply 320x320 canvas (centered, no upscaling)
|
||||||
image = self._apply_canvas(image)
|
image = self._apply_canvas(image)
|
||||||
|
|
@ -218,14 +232,64 @@ class ConversionWorker(QThread):
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
|
|
||||||
|
class PreviewDialog(QDialog):
|
||||||
|
"""Dialog to preview a TGA file."""
|
||||||
|
|
||||||
|
def __init__(self, tga_path: Path, converter: TGAConverter, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle(f"Preview: {tga_path.name}")
|
||||||
|
self.setMinimumSize(400, 450)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(15, 15, 15, 15)
|
||||||
|
|
||||||
|
# Info
|
||||||
|
info = converter.read_tga_header(tga_path)
|
||||||
|
if info:
|
||||||
|
info_label = QLabel(f"Original: {info.width}x{info.height}, {info.pixel_depth}bpp")
|
||||||
|
info_label.setStyleSheet("color: #888; font-size: 12px;")
|
||||||
|
layout.addWidget(info_label)
|
||||||
|
|
||||||
|
# Load and display TGA
|
||||||
|
image = converter.load_tga_image(tga_path)
|
||||||
|
if image:
|
||||||
|
# Convert to QPixmap
|
||||||
|
img_data = image.tobytes("raw", "RGBA")
|
||||||
|
qimage = QImage(img_data, image.width, image.height, QImage.Format.Format_RGBA8888)
|
||||||
|
pixmap = QPixmap.fromImage(qimage)
|
||||||
|
|
||||||
|
# Scale for display (max 320x320)
|
||||||
|
scaled = pixmap.scaled(320, 320, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
|
||||||
|
|
||||||
|
img_label = QLabel()
|
||||||
|
img_label.setPixmap(scaled)
|
||||||
|
img_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
img_label.setStyleSheet("background-color: #2a2a2a; border: 1px solid #444; padding: 10px;")
|
||||||
|
layout.addWidget(img_label)
|
||||||
|
|
||||||
|
size_label = QLabel(f"Displayed at: {scaled.width()}x{scaled.height()}")
|
||||||
|
size_label.setStyleSheet("color: #888; font-size: 11px;")
|
||||||
|
size_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
layout.addWidget(size_label)
|
||||||
|
else:
|
||||||
|
error_label = QLabel("Failed to load image")
|
||||||
|
error_label.setStyleSheet("color: #f44336;")
|
||||||
|
layout.addWidget(error_label)
|
||||||
|
|
||||||
|
# Close button
|
||||||
|
close_btn = QPushButton("Close")
|
||||||
|
close_btn.clicked.connect(self.accept)
|
||||||
|
layout.addWidget(close_btn)
|
||||||
|
|
||||||
|
|
||||||
class IconExtractorWindow(QMainWindow):
|
class IconExtractorWindow(QMainWindow):
|
||||||
"""Main window for the standalone icon extractor."""
|
"""Main window for the standalone icon extractor."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle(APP_NAME)
|
self.setWindowTitle(APP_NAME)
|
||||||
self.setMinimumSize(1000, 800)
|
self.setMinimumSize(1050, 850)
|
||||||
self.resize(1100, 850)
|
self.resize(1150, 900)
|
||||||
|
|
||||||
self.converter = TGAConverter()
|
self.converter = TGAConverter()
|
||||||
self.worker: Optional[ConversionWorker] = None
|
self.worker: Optional[ConversionWorker] = None
|
||||||
|
|
@ -246,11 +310,11 @@ class IconExtractorWindow(QMainWindow):
|
||||||
self.setCentralWidget(central)
|
self.setCentralWidget(central)
|
||||||
layout = QVBoxLayout(central)
|
layout = QVBoxLayout(central)
|
||||||
layout.setContentsMargins(15, 15, 15, 15)
|
layout.setContentsMargins(15, 15, 15, 15)
|
||||||
layout.setSpacing(12)
|
layout.setSpacing(10)
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
header = QLabel(APP_NAME)
|
header = QLabel(APP_NAME)
|
||||||
header.setStyleSheet("font-size: 22px; font-weight: bold; color: #4caf50;")
|
header.setStyleSheet("font-size: 24px; font-weight: bold; color: #4caf50; padding-bottom: 5px;")
|
||||||
layout.addWidget(header)
|
layout.addWidget(header)
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
|
@ -258,35 +322,10 @@ class IconExtractorWindow(QMainWindow):
|
||||||
"Extract item icons from Entropia Universe cache and convert them to PNG. "
|
"Extract item icons from Entropia Universe cache and convert them to PNG. "
|
||||||
"Submit these to EntropiaNexus.com to help complete the item database."
|
"Submit these to EntropiaNexus.com to help complete the item database."
|
||||||
)
|
)
|
||||||
desc.setStyleSheet("color: #aaaaaa; padding: 5px;")
|
desc.setStyleSheet("color: #cccccc; font-size: 13px; 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_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
|
# Main splitter
|
||||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||||
|
|
||||||
|
|
@ -294,35 +333,42 @@ class IconExtractorWindow(QMainWindow):
|
||||||
left_panel = QWidget()
|
left_panel = QWidget()
|
||||||
left_layout = QVBoxLayout(left_panel)
|
left_layout = QVBoxLayout(left_panel)
|
||||||
left_layout.setContentsMargins(0, 0, 0, 0)
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
left_layout.setSpacing(10)
|
||||||
|
|
||||||
# Cache folder
|
# Cache folder
|
||||||
cache_group = QGroupBox("Cache Source")
|
cache_group = QGroupBox("Cache Source")
|
||||||
|
cache_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }")
|
||||||
cache_layout = QVBoxLayout(cache_group)
|
cache_layout = QVBoxLayout(cache_group)
|
||||||
cache_layout.setContentsMargins(10, 15, 10, 10)
|
cache_layout.setContentsMargins(12, 18, 12, 12)
|
||||||
cache_layout.setSpacing(8)
|
cache_layout.setSpacing(10)
|
||||||
|
|
||||||
# Base path (hardcoded) - use a shorter display
|
# Base path (hardcoded)
|
||||||
path_display = str(self.base_cache_path).replace("/", "\\")
|
path_display = str(self.base_cache_path).replace("/", "\\")
|
||||||
self.cache_label = QLabel(path_display)
|
self.cache_label = QLabel(path_display)
|
||||||
self.cache_label.setStyleSheet(
|
self.cache_label.setStyleSheet(
|
||||||
"font-family: Consolas; font-size: 10px; color: #888; "
|
"font-family: Consolas; font-size: 11px; color: #aaa; "
|
||||||
"padding: 8px; background: #1a1a1a; border-radius: 3px;"
|
"padding: 10px; background: #252525; border-radius: 4px;"
|
||||||
)
|
)
|
||||||
self.cache_label.setWordWrap(True)
|
self.cache_label.setWordWrap(True)
|
||||||
self.cache_label.setMinimumHeight(40)
|
self.cache_label.setMinimumHeight(35)
|
||||||
cache_layout.addWidget(self.cache_label)
|
cache_layout.addWidget(self.cache_label)
|
||||||
|
|
||||||
# Subfolder selector
|
# Subfolder selector
|
||||||
subfolder_layout = QHBoxLayout()
|
subfolder_layout = QHBoxLayout()
|
||||||
subfolder_layout.setSpacing(8)
|
subfolder_layout.setSpacing(10)
|
||||||
subfolder_layout.addWidget(QLabel("Version:"))
|
subfolder_label = QLabel("Version:")
|
||||||
|
subfolder_label.setStyleSheet("font-size: 12px;")
|
||||||
|
subfolder_layout.addWidget(subfolder_label)
|
||||||
|
|
||||||
self.subfolder_combo = QComboBox()
|
self.subfolder_combo = QComboBox()
|
||||||
self.subfolder_combo.setMinimumWidth(180)
|
self.subfolder_combo.setMinimumWidth(200)
|
||||||
|
self.subfolder_combo.setStyleSheet("font-size: 12px; padding: 4px;")
|
||||||
self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed)
|
self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed)
|
||||||
subfolder_layout.addWidget(self.subfolder_combo, 1)
|
subfolder_layout.addWidget(self.subfolder_combo, 1)
|
||||||
|
|
||||||
refresh_btn = QPushButton("Refresh")
|
refresh_btn = QPushButton("Refresh")
|
||||||
refresh_btn.setMaximumWidth(70)
|
refresh_btn.setMaximumWidth(80)
|
||||||
|
refresh_btn.setStyleSheet("font-size: 11px; padding: 5px;")
|
||||||
refresh_btn.clicked.connect(self._detect_subfolders)
|
refresh_btn.clicked.connect(self._detect_subfolders)
|
||||||
subfolder_layout.addWidget(refresh_btn)
|
subfolder_layout.addWidget(refresh_btn)
|
||||||
|
|
||||||
|
|
@ -330,6 +376,7 @@ class IconExtractorWindow(QMainWindow):
|
||||||
|
|
||||||
# All subfolders checkbox
|
# All subfolders checkbox
|
||||||
self.all_subfolders_check = QCheckBox("Include ALL version folders")
|
self.all_subfolders_check = QCheckBox("Include ALL version folders")
|
||||||
|
self.all_subfolders_check.setStyleSheet("font-size: 12px;")
|
||||||
self.all_subfolders_check.setToolTip("Merge icons from all game versions")
|
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)
|
||||||
|
|
@ -338,25 +385,26 @@ class IconExtractorWindow(QMainWindow):
|
||||||
|
|
||||||
# Output folder
|
# Output folder
|
||||||
output_group = QGroupBox("Output Location")
|
output_group = QGroupBox("Output Location")
|
||||||
|
output_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }")
|
||||||
output_layout = QVBoxLayout(output_group)
|
output_layout = QVBoxLayout(output_group)
|
||||||
output_layout.setContentsMargins(10, 15, 10, 10)
|
output_layout.setContentsMargins(12, 18, 12, 12)
|
||||||
output_layout.setSpacing(8)
|
output_layout.setSpacing(10)
|
||||||
|
|
||||||
output_info = QLabel("Icons saved to your Documents folder (same as chat.log)")
|
output_info = QLabel("Icons saved to your Documents folder (same as chat.log)")
|
||||||
output_info.setStyleSheet("color: #888; font-size: 11px;")
|
output_info.setStyleSheet("color: #aaaaaa; font-size: 12px;")
|
||||||
output_info.setWordWrap(True)
|
output_info.setWordWrap(True)
|
||||||
output_layout.addWidget(output_info)
|
output_layout.addWidget(output_info)
|
||||||
|
|
||||||
# Show relative path instead of full path
|
|
||||||
rel_path = "Documents/Entropia Universe/Icons/"
|
rel_path = "Documents/Entropia Universe/Icons/"
|
||||||
self.output_label = QLabel(rel_path)
|
self.output_label = QLabel(rel_path)
|
||||||
self.output_label.setStyleSheet(
|
self.output_label.setStyleSheet(
|
||||||
"font-family: Consolas; font-size: 10px; color: #888; "
|
"font-family: Consolas; font-size: 11px; color: #aaa; "
|
||||||
"padding: 8px; background: #1a1a1a; border-radius: 3px;"
|
"padding: 10px; background: #252525; border-radius: 4px;"
|
||||||
)
|
)
|
||||||
output_layout.addWidget(self.output_label)
|
output_layout.addWidget(self.output_label)
|
||||||
|
|
||||||
change_btn = QPushButton("Change Output Folder...")
|
change_btn = QPushButton("Change Output Folder...")
|
||||||
|
change_btn.setStyleSheet("font-size: 11px; padding: 6px;")
|
||||||
change_btn.clicked.connect(self._browse_output)
|
change_btn.clicked.connect(self._browse_output)
|
||||||
output_layout.addWidget(change_btn)
|
output_layout.addWidget(change_btn)
|
||||||
|
|
||||||
|
|
@ -364,31 +412,34 @@ class IconExtractorWindow(QMainWindow):
|
||||||
|
|
||||||
# Settings (simplified - just 320x320)
|
# Settings (simplified - just 320x320)
|
||||||
settings_group = QGroupBox("Export Settings")
|
settings_group = QGroupBox("Export Settings")
|
||||||
|
settings_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }")
|
||||||
settings_layout = QVBoxLayout(settings_group)
|
settings_layout = QVBoxLayout(settings_group)
|
||||||
settings_layout.setContentsMargins(10, 15, 10, 10)
|
settings_layout.setContentsMargins(12, 18, 12, 12)
|
||||||
|
|
||||||
settings_info = QLabel(
|
settings_info = QLabel(
|
||||||
"Format: PNG with transparency\n"
|
"Format: PNG with transparency\n"
|
||||||
"Canvas: 320x320 pixels (centered)\n"
|
"Canvas: 320x320 pixels (centered)\n"
|
||||||
"Size: Original icon size (no upscaling)"
|
"Size: Original icon size (no upscaling)"
|
||||||
)
|
)
|
||||||
settings_info.setStyleSheet("color: #888; font-size: 11px;")
|
settings_info.setStyleSheet("color: #aaaaaa; font-size: 12px; line-height: 1.5;")
|
||||||
settings_layout.addWidget(settings_info)
|
settings_layout.addWidget(settings_info)
|
||||||
|
|
||||||
left_layout.addWidget(settings_group)
|
left_layout.addWidget(settings_group)
|
||||||
|
|
||||||
# Nexus link
|
# Nexus link
|
||||||
nexus_group = QGroupBox("EntropiaNexus.com")
|
nexus_group = QGroupBox("EntropiaNexus.com")
|
||||||
|
nexus_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; color: #4caf50; }")
|
||||||
nexus_layout = QVBoxLayout(nexus_group)
|
nexus_layout = QVBoxLayout(nexus_group)
|
||||||
nexus_layout.setContentsMargins(10, 15, 10, 10)
|
nexus_layout.setContentsMargins(12, 18, 12, 12)
|
||||||
|
|
||||||
nexus_info = QLabel("Submit icons to help complete the item database!")
|
nexus_info = QLabel("Submit icons to help complete the item database!")
|
||||||
nexus_info.setStyleSheet("color: #4caf50; font-size: 11px;")
|
nexus_info.setStyleSheet("color: #cccccc; font-size: 12px;")
|
||||||
nexus_info.setWordWrap(True)
|
nexus_info.setWordWrap(True)
|
||||||
nexus_layout.addWidget(nexus_info)
|
nexus_layout.addWidget(nexus_info)
|
||||||
|
|
||||||
nexus_btn = QPushButton("Open EntropiaNexus.com")
|
nexus_btn = QPushButton("Open EntropiaNexus.com")
|
||||||
nexus_btn.setMaximumHeight(30)
|
nexus_btn.setMaximumHeight(32)
|
||||||
|
nexus_btn.setStyleSheet("font-size: 11px; padding: 6px;")
|
||||||
nexus_btn.clicked.connect(lambda: self._open_url(WEBSITE))
|
nexus_btn.clicked.connect(lambda: self._open_url(WEBSITE))
|
||||||
nexus_layout.addWidget(nexus_btn)
|
nexus_layout.addWidget(nexus_btn)
|
||||||
|
|
||||||
|
|
@ -396,17 +447,18 @@ class IconExtractorWindow(QMainWindow):
|
||||||
|
|
||||||
# Convert button
|
# Convert button
|
||||||
self.convert_btn = QPushButton("Start Extracting Icons")
|
self.convert_btn = QPushButton("Start Extracting Icons")
|
||||||
self.convert_btn.setMinimumHeight(50)
|
self.convert_btn.setMinimumHeight(55)
|
||||||
self.convert_btn.setStyleSheet("""
|
self.convert_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #0d47a1;
|
background-color: #1565c0;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
padding: 10px;
|
padding: 12px;
|
||||||
|
color: white;
|
||||||
}
|
}
|
||||||
QPushButton:hover { background-color: #1565c0; }
|
QPushButton:hover { background-color: #1976d2; }
|
||||||
QPushButton:disabled { background-color: #333; color: #666; }
|
QPushButton:disabled { background-color: #424242; color: #888; }
|
||||||
""")
|
""")
|
||||||
self.convert_btn.clicked.connect(self._start_conversion)
|
self.convert_btn.clicked.connect(self._start_conversion)
|
||||||
left_layout.addWidget(self.convert_btn)
|
left_layout.addWidget(self.convert_btn)
|
||||||
|
|
@ -414,11 +466,12 @@ class IconExtractorWindow(QMainWindow):
|
||||||
# Progress
|
# Progress
|
||||||
self.progress_bar = QProgressBar()
|
self.progress_bar = QProgressBar()
|
||||||
self.progress_bar.setTextVisible(True)
|
self.progress_bar.setTextVisible(True)
|
||||||
|
self.progress_bar.setStyleSheet("font-size: 11px;")
|
||||||
self.progress_bar.setVisible(False)
|
self.progress_bar.setVisible(False)
|
||||||
left_layout.addWidget(self.progress_bar)
|
left_layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
self.status_label = QLabel("Ready")
|
self.status_label = QLabel("Ready")
|
||||||
self.status_label.setStyleSheet("color: #888; padding: 5px;")
|
self.status_label.setStyleSheet("color: #888; font-size: 12px; padding: 5px;")
|
||||||
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
left_layout.addWidget(self.status_label)
|
left_layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
|
@ -432,32 +485,38 @@ class IconExtractorWindow(QMainWindow):
|
||||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
files_group = QGroupBox("Available Icons")
|
files_group = QGroupBox("Available Icons")
|
||||||
|
files_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }")
|
||||||
files_layout = QVBoxLayout(files_group)
|
files_layout = QVBoxLayout(files_group)
|
||||||
files_layout.setContentsMargins(10, 15, 10, 10)
|
files_layout.setContentsMargins(12, 18, 12, 12)
|
||||||
|
files_layout.setSpacing(10)
|
||||||
|
|
||||||
files_info = QLabel("Select icons to extract (or leave blank for all)")
|
files_info = QLabel("Double-click an icon to preview. Select icons to extract (or leave blank for all).")
|
||||||
files_info.setStyleSheet("color: #888; font-size: 11px;")
|
files_info.setStyleSheet("color: #aaaaaa; font-size: 12px;")
|
||||||
files_layout.addWidget(files_info)
|
files_layout.addWidget(files_info)
|
||||||
|
|
||||||
self.files_count_label = QLabel("No files found")
|
self.files_count_label = QLabel("No files found")
|
||||||
self.files_count_label.setStyleSheet("font-weight: bold; padding: 5px 0;")
|
self.files_count_label.setStyleSheet("font-weight: bold; font-size: 12px; padding: 5px 0;")
|
||||||
files_layout.addWidget(self.files_count_label)
|
files_layout.addWidget(self.files_count_label)
|
||||||
|
|
||||||
self.files_list = QListWidget()
|
self.files_list = QListWidget()
|
||||||
self.files_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
|
self.files_list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
|
||||||
|
self.files_list.setStyleSheet("font-size: 12px; padding: 3px;")
|
||||||
|
self.files_list.doubleClicked.connect(self._on_file_double_clicked)
|
||||||
files_layout.addWidget(self.files_list, 1)
|
files_layout.addWidget(self.files_list, 1)
|
||||||
|
|
||||||
# Selection buttons
|
# Selection buttons
|
||||||
sel_layout = QHBoxLayout()
|
sel_layout = QHBoxLayout()
|
||||||
sel_layout.setSpacing(8)
|
sel_layout.setSpacing(10)
|
||||||
|
|
||||||
select_all_btn = QPushButton("Select All")
|
select_all_btn = QPushButton("Select All")
|
||||||
select_all_btn.setMaximumWidth(100)
|
select_all_btn.setMaximumWidth(100)
|
||||||
|
select_all_btn.setStyleSheet("font-size: 11px; padding: 5px;")
|
||||||
select_all_btn.clicked.connect(self.files_list.selectAll)
|
select_all_btn.clicked.connect(self.files_list.selectAll)
|
||||||
sel_layout.addWidget(select_all_btn)
|
sel_layout.addWidget(select_all_btn)
|
||||||
|
|
||||||
select_none_btn = QPushButton("Select None")
|
select_none_btn = QPushButton("Select None")
|
||||||
select_none_btn.setMaximumWidth(100)
|
select_none_btn.setMaximumWidth(100)
|
||||||
|
select_none_btn.setStyleSheet("font-size: 11px; padding: 5px;")
|
||||||
select_none_btn.clicked.connect(self.files_list.clearSelection)
|
select_none_btn.clicked.connect(self.files_list.clearSelection)
|
||||||
sel_layout.addWidget(select_none_btn)
|
sel_layout.addWidget(select_none_btn)
|
||||||
|
|
||||||
|
|
@ -465,6 +524,7 @@ class IconExtractorWindow(QMainWindow):
|
||||||
|
|
||||||
open_folder_btn = QPushButton("Open Output Folder")
|
open_folder_btn = QPushButton("Open Output Folder")
|
||||||
open_folder_btn.setMaximumWidth(130)
|
open_folder_btn.setMaximumWidth(130)
|
||||||
|
open_folder_btn.setStyleSheet("font-size: 11px; padding: 5px;")
|
||||||
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)
|
||||||
|
|
||||||
|
|
@ -472,23 +532,68 @@ 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([360, 560])
|
||||||
|
|
||||||
layout.addWidget(splitter, 1)
|
layout.addWidget(splitter, 1)
|
||||||
|
|
||||||
|
# Important Information (moved to bottom)
|
||||||
|
notice_group = QGroupBox("Important Information")
|
||||||
|
notice_group.setStyleSheet("""
|
||||||
|
QGroupBox {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #ff9800;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
notice_layout = QVBoxLayout(notice_group)
|
||||||
|
notice_layout.setContentsMargins(12, 18, 12, 12)
|
||||||
|
|
||||||
|
notice_text = QTextEdit()
|
||||||
|
notice_text.setReadOnly(True)
|
||||||
|
notice_text.setStyleSheet("""
|
||||||
|
QTextEdit {
|
||||||
|
background-color: #2d2818;
|
||||||
|
color: #ffc107;
|
||||||
|
border: 1px solid #5d4e37;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
notice_text.setText(
|
||||||
|
"REQUIREMENT: Items must be seen/rendered in-game before they appear in the cache!\n\n"
|
||||||
|
"If an item icon is missing, view it in your inventory or see it dropped as loot first. "
|
||||||
|
"The game only caches icons for items you have actually seen.\n\n"
|
||||||
|
"Output: Documents/Entropia Universe/Icons/ (same folder as chat.log)"
|
||||||
|
)
|
||||||
|
notice_layout.addWidget(notice_text)
|
||||||
|
layout.addWidget(notice_group)
|
||||||
|
|
||||||
# Footer
|
# Footer
|
||||||
footer = QLabel(
|
footer = QLabel(
|
||||||
f"Developed by {DEVELOPER} | Discord: {DISCORD}\n"
|
f"Developed by {DEVELOPER} | Discord: {DISCORD} | {WEBSITE}\n"
|
||||||
f"{WEBSITE}\n"
|
"Entropia Universe Icon Extractor is a fan-made resource and is not affiliated with MindArk PE AB. "
|
||||||
"Entropia Nexus is a fan-made resource and is not affiliated with MindArk PE AB. "
|
|
||||||
"Entropia Universe is a trademark of MindArk PE AB."
|
"Entropia Universe is a trademark of MindArk PE AB."
|
||||||
)
|
)
|
||||||
footer.setStyleSheet("color: #555; font-size: 9px; padding: 8px;")
|
footer.setStyleSheet("color: #666; font-size: 10px; padding: 10px;")
|
||||||
footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
footer.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
footer.setWordWrap(True)
|
footer.setWordWrap(True)
|
||||||
footer.setMinimumHeight(60)
|
footer.setMinimumHeight(55)
|
||||||
layout.addWidget(footer)
|
layout.addWidget(footer)
|
||||||
|
|
||||||
|
def _on_file_double_clicked(self, index):
|
||||||
|
"""Handle double-click on file to preview."""
|
||||||
|
item = self.files_list.item(index.row())
|
||||||
|
if item:
|
||||||
|
filepath = Path(item.data(Qt.ItemDataRole.UserRole))
|
||||||
|
self._preview_file(filepath)
|
||||||
|
|
||||||
|
def _preview_file(self, filepath: Path):
|
||||||
|
"""Open preview dialog for a TGA file."""
|
||||||
|
dialog = PreviewDialog(filepath, self.converter, self)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
def _open_url(self, url: str):
|
def _open_url(self, url: str):
|
||||||
"""Open URL in default browser."""
|
"""Open URL in default browser."""
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
@ -499,7 +604,7 @@ class IconExtractorWindow(QMainWindow):
|
||||||
# Output folder
|
# Output folder
|
||||||
saved_output = self.settings.value("output_dir", str(self.converter.output_dir))
|
saved_output = self.settings.value("output_dir", str(self.converter.output_dir))
|
||||||
self.converter.output_dir = Path(saved_output)
|
self.converter.output_dir = Path(saved_output)
|
||||||
self.output_label.setText(saved_output)
|
self.output_label.setText("Documents/Entropia Universe/Icons/")
|
||||||
|
|
||||||
def _save_settings(self):
|
def _save_settings(self):
|
||||||
"""Save current settings."""
|
"""Save current settings."""
|
||||||
|
|
@ -565,7 +670,8 @@ class IconExtractorWindow(QMainWindow):
|
||||||
|
|
||||||
if folder:
|
if folder:
|
||||||
self.converter.output_dir = Path(folder)
|
self.converter.output_dir = Path(folder)
|
||||||
self.output_label.setText(folder)
|
rel_path = "Documents/Entropia Universe/Icons/"
|
||||||
|
self.output_label.setText(rel_path)
|
||||||
self._save_settings()
|
self._save_settings()
|
||||||
|
|
||||||
def _refresh_file_list(self):
|
def _refresh_file_list(self):
|
||||||
|
|
@ -610,7 +716,9 @@ class IconExtractorWindow(QMainWindow):
|
||||||
# Get info
|
# Get info
|
||||||
header = self.converter.read_tga_header(tga_file)
|
header = self.converter.read_tga_header(tga_file)
|
||||||
if header:
|
if header:
|
||||||
item.setToolTip(f"{header.width}x{header.height}, {header.pixel_depth}bpp")
|
item.setToolTip(f"Double-click to preview\n{header.width}x{header.height}, {header.pixel_depth}bpp")
|
||||||
|
else:
|
||||||
|
item.setToolTip("Double-click to preview")
|
||||||
|
|
||||||
self.files_list.addItem(item)
|
self.files_list.addItem(item)
|
||||||
self.found_files.append(tga_file)
|
self.found_files.append(tga_file)
|
||||||
|
|
@ -718,9 +826,6 @@ def set_app_icon(app: QApplication):
|
||||||
app.setWindowIcon(QIcon(str(icon_path)))
|
app.setWindowIcon(QIcon(str(icon_path)))
|
||||||
return
|
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."""
|
||||||
|
|
@ -735,64 +840,74 @@ def main():
|
||||||
# Try to set icon
|
# Try to set icon
|
||||||
set_app_icon(app)
|
set_app_icon(app)
|
||||||
|
|
||||||
# Dark theme
|
# Dark theme with better readability
|
||||||
app.setStyleSheet("""
|
app.setStyleSheet("""
|
||||||
QMainWindow, QDialog {
|
QMainWindow, QDialog {
|
||||||
background-color: #1a1a1a;
|
background-color: #1e1e1e;
|
||||||
}
|
}
|
||||||
QWidget {
|
QWidget {
|
||||||
background-color: #1a1a1a;
|
background-color: #1e1e1e;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
QGroupBox {
|
QGroupBox {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border: 1px solid #333;
|
border: 1px solid #404040;
|
||||||
border-radius: 5px;
|
border-radius: 6px;
|
||||||
margin-top: 10px;
|
margin-top: 12px;
|
||||||
padding-top: 10px;
|
padding-top: 12px;
|
||||||
}
|
}
|
||||||
QGroupBox::title {
|
QGroupBox::title {
|
||||||
subcontrol-origin: margin;
|
subcontrol-origin: margin;
|
||||||
left: 10px;
|
left: 12px;
|
||||||
padding: 0 5px;
|
padding: 0 8px;
|
||||||
}
|
}
|
||||||
QPushButton {
|
QPushButton {
|
||||||
background-color: #333;
|
background-color: #3d3d3d;
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
padding: 8px 15px;
|
padding: 6px 12px;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
QPushButton:hover {
|
QPushButton:hover {
|
||||||
background-color: #444;
|
background-color: #4d4d4d;
|
||||||
}
|
}
|
||||||
QComboBox {
|
QComboBox {
|
||||||
background-color: #2a2a2a;
|
background-color: #2d2d2d;
|
||||||
border: 1px solid #555;
|
border: 1px solid #555;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
min-width: 150px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
QListWidget {
|
QListWidget {
|
||||||
background-color: #222;
|
background-color: #252525;
|
||||||
border: 1px solid #444;
|
border: 1px solid #404040;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
QListWidget::item {
|
QListWidget::item {
|
||||||
padding: 5px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
QListWidget::item:selected {
|
QListWidget::item:selected {
|
||||||
background-color: #0d47a1;
|
background-color: #1565c0;
|
||||||
|
}
|
||||||
|
QListWidget::item:hover {
|
||||||
|
background-color: #2a4d6e;
|
||||||
}
|
}
|
||||||
QProgressBar {
|
QProgressBar {
|
||||||
border: 1px solid #444;
|
border: 1px solid #404040;
|
||||||
border-radius: 3px;
|
border-radius: 4px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
QProgressBar::chunk {
|
QProgressBar::chunk {
|
||||||
background-color: #4caf50;
|
background-color: #4caf50;
|
||||||
}
|
}
|
||||||
QTextEdit {
|
QTextEdit {
|
||||||
background-color: #222;
|
background-color: #252525;
|
||||||
border: 1px solid #444;
|
border: 1px solid #404040;
|
||||||
|
}
|
||||||
|
QCheckBox {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
QLabel {
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue