diff --git a/standalone_icon_extractor.py b/standalone_icon_extractor.py index 4f0944e..032bd08 100644 --- a/standalone_icon_extractor.py +++ b/standalone_icon_extractor.py @@ -19,8 +19,9 @@ Discord: impulsivefps Website: https://EntropiaNexus.com Disclaimer: - Entropia Nexus is a fan-made resource and is not affiliated with MindArk PE AB. - Entropia Universe is a trademark of MindArk PE AB. + Entropia Universe Icon Extractor is a fan-made resource and is not + affiliated with MindArk PE AB. Entropia Universe is a trademark of + MindArk PE AB. """ import sys @@ -34,10 +35,10 @@ try: QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QListWidget, QListWidgetItem, QFileDialog, QProgressBar, QGroupBox, QMessageBox, QCheckBox, - QSplitter, QTextEdit + QSplitter, QTextEdit, QDialog, QScrollArea, QFrame ) - from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings - from PyQt6.QtGui import QIcon, QPixmap, QFont + from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings, QSize + from PyQt6.QtGui import QIcon, QPixmap, QFont, QImage PYQT_AVAILABLE = True except ImportError: PYQT_AVAILABLE = False @@ -52,6 +53,8 @@ except ImportError: print("Pillow not available. Install with: pip install Pillow") sys.exit(1) +import numpy as np + # Setup logging logging.basicConfig( level=logging.INFO, @@ -126,6 +129,17 @@ class TGAConverter: logger.error(f"Error reading TGA header: {e}") 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]: """ Convert a TGA file to PNG with 320x320 canvas. @@ -139,9 +153,9 @@ class TGAConverter: """ try: # Load TGA - image = Image.open(tga_path) - if image.mode != 'RGBA': - image = image.convert('RGBA') + image = self.load_tga_image(tga_path) + if not image: + return None # Apply 320x320 canvas (centered, no upscaling) image = self._apply_canvas(image) @@ -218,14 +232,64 @@ class ConversionWorker(QThread): 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): """Main window for the standalone icon extractor.""" def __init__(self): super().__init__() self.setWindowTitle(APP_NAME) - self.setMinimumSize(1000, 800) - self.resize(1100, 850) + self.setMinimumSize(1050, 850) + self.resize(1150, 900) self.converter = TGAConverter() self.worker: Optional[ConversionWorker] = None @@ -246,11 +310,11 @@ class IconExtractorWindow(QMainWindow): self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setContentsMargins(15, 15, 15, 15) - layout.setSpacing(12) + layout.setSpacing(10) # Header 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) # Description @@ -258,35 +322,10 @@ class IconExtractorWindow(QMainWindow): "Extract item icons from Entropia Universe cache and convert them to PNG. " "Submit these to EntropiaNexus.com to help complete the item database." ) - desc.setStyleSheet("color: #aaaaaa; padding: 5px;") + desc.setStyleSheet("color: #cccccc; font-size: 13px; padding: 5px;") desc.setWordWrap(True) layout.addWidget(desc) - # Important notice - notice_group = QGroupBox("Important Information") - notice_layout = QVBoxLayout(notice_group) - notice_layout.setContentsMargins(10, 15, 10, 10) - - notice_text = QTextEdit() - notice_text.setReadOnly(True) - notice_text.setStyleSheet(""" - QTextEdit { - background-color: #2a2520; - color: #ffcc80; - border: 1px solid #5d4037; - border-radius: 3px; - font-size: 12px; - padding: 5px; - } - """) - notice_text.setText( - "REQUIREMENT: Items must be seen/rendered in-game before they appear in the cache!\n" - "If an item icon is missing, view it in your inventory or see it dropped as loot first.\n\n" - f"Output: Documents/Entropia Universe/Icons/ (same folder as chat.log)" - ) - notice_layout.addWidget(notice_text) - layout.addWidget(notice_group) - # Main splitter splitter = QSplitter(Qt.Orientation.Horizontal) @@ -294,35 +333,42 @@ class IconExtractorWindow(QMainWindow): left_panel = QWidget() left_layout = QVBoxLayout(left_panel) left_layout.setContentsMargins(0, 0, 0, 0) + left_layout.setSpacing(10) # Cache folder cache_group = QGroupBox("Cache Source") + cache_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }") cache_layout = QVBoxLayout(cache_group) - cache_layout.setContentsMargins(10, 15, 10, 10) - cache_layout.setSpacing(8) + cache_layout.setContentsMargins(12, 18, 12, 12) + cache_layout.setSpacing(10) - # Base path (hardcoded) - use a shorter display + # Base path (hardcoded) path_display = str(self.base_cache_path).replace("/", "\\") self.cache_label = QLabel(path_display) self.cache_label.setStyleSheet( - "font-family: Consolas; font-size: 10px; color: #888; " - "padding: 8px; background: #1a1a1a; border-radius: 3px;" + "font-family: Consolas; font-size: 11px; color: #aaa; " + "padding: 10px; background: #252525; border-radius: 4px;" ) self.cache_label.setWordWrap(True) - self.cache_label.setMinimumHeight(40) + self.cache_label.setMinimumHeight(35) cache_layout.addWidget(self.cache_label) # Subfolder selector subfolder_layout = QHBoxLayout() - subfolder_layout.setSpacing(8) - subfolder_layout.addWidget(QLabel("Version:")) + subfolder_layout.setSpacing(10) + subfolder_label = QLabel("Version:") + subfolder_label.setStyleSheet("font-size: 12px;") + subfolder_layout.addWidget(subfolder_label) + 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) subfolder_layout.addWidget(self.subfolder_combo, 1) 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) subfolder_layout.addWidget(refresh_btn) @@ -330,6 +376,7 @@ class IconExtractorWindow(QMainWindow): # All subfolders checkbox 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.stateChanged.connect(self._on_all_subfolders_changed) cache_layout.addWidget(self.all_subfolders_check) @@ -338,25 +385,26 @@ class IconExtractorWindow(QMainWindow): # Output folder output_group = QGroupBox("Output Location") + output_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }") output_layout = QVBoxLayout(output_group) - output_layout.setContentsMargins(10, 15, 10, 10) - output_layout.setSpacing(8) + output_layout.setContentsMargins(12, 18, 12, 12) + output_layout.setSpacing(10) 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_layout.addWidget(output_info) - # Show relative path instead of full path rel_path = "Documents/Entropia Universe/Icons/" self.output_label = QLabel(rel_path) self.output_label.setStyleSheet( - "font-family: Consolas; font-size: 10px; color: #888; " - "padding: 8px; background: #1a1a1a; border-radius: 3px;" + "font-family: Consolas; font-size: 11px; color: #aaa; " + "padding: 10px; background: #252525; border-radius: 4px;" ) output_layout.addWidget(self.output_label) change_btn = QPushButton("Change Output Folder...") + change_btn.setStyleSheet("font-size: 11px; padding: 6px;") change_btn.clicked.connect(self._browse_output) output_layout.addWidget(change_btn) @@ -364,31 +412,34 @@ class IconExtractorWindow(QMainWindow): # Settings (simplified - just 320x320) settings_group = QGroupBox("Export Settings") + settings_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }") settings_layout = QVBoxLayout(settings_group) - settings_layout.setContentsMargins(10, 15, 10, 10) + settings_layout.setContentsMargins(12, 18, 12, 12) settings_info = QLabel( "Format: PNG with transparency\n" "Canvas: 320x320 pixels (centered)\n" "Size: Original icon size (no upscaling)" ) - settings_info.setStyleSheet("color: #888; font-size: 11px;") + settings_info.setStyleSheet("color: #aaaaaa; font-size: 12px; line-height: 1.5;") settings_layout.addWidget(settings_info) left_layout.addWidget(settings_group) # Nexus link nexus_group = QGroupBox("EntropiaNexus.com") + nexus_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; color: #4caf50; }") 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.setStyleSheet("color: #4caf50; font-size: 11px;") + nexus_info.setStyleSheet("color: #cccccc; font-size: 12px;") nexus_info.setWordWrap(True) nexus_layout.addWidget(nexus_info) 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_layout.addWidget(nexus_btn) @@ -396,17 +447,18 @@ class IconExtractorWindow(QMainWindow): # Convert button self.convert_btn = QPushButton("Start Extracting Icons") - self.convert_btn.setMinimumHeight(50) + self.convert_btn.setMinimumHeight(55) self.convert_btn.setStyleSheet(""" QPushButton { - background-color: #0d47a1; + background-color: #1565c0; font-weight: bold; font-size: 14px; - border-radius: 5px; - padding: 10px; + border-radius: 6px; + padding: 12px; + color: white; } - QPushButton:hover { background-color: #1565c0; } - QPushButton:disabled { background-color: #333; color: #666; } + QPushButton:hover { background-color: #1976d2; } + QPushButton:disabled { background-color: #424242; color: #888; } """) self.convert_btn.clicked.connect(self._start_conversion) left_layout.addWidget(self.convert_btn) @@ -414,11 +466,12 @@ class IconExtractorWindow(QMainWindow): # Progress self.progress_bar = QProgressBar() self.progress_bar.setTextVisible(True) + self.progress_bar.setStyleSheet("font-size: 11px;") self.progress_bar.setVisible(False) left_layout.addWidget(self.progress_bar) self.status_label = QLabel("Ready") - self.status_label.setStyleSheet("color: #888; padding: 5px;") + self.status_label.setStyleSheet("color: #888; font-size: 12px; padding: 5px;") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) left_layout.addWidget(self.status_label) @@ -432,32 +485,38 @@ class IconExtractorWindow(QMainWindow): right_layout.setContentsMargins(0, 0, 0, 0) files_group = QGroupBox("Available Icons") + files_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }") 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.setStyleSheet("color: #888; font-size: 11px;") + files_info = QLabel("Double-click an icon to preview. Select icons to extract (or leave blank for all).") + files_info.setStyleSheet("color: #aaaaaa; font-size: 12px;") files_layout.addWidget(files_info) 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) self.files_list = QListWidget() 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) # Selection buttons sel_layout = QHBoxLayout() - sel_layout.setSpacing(8) + sel_layout.setSpacing(10) select_all_btn = QPushButton("Select All") select_all_btn.setMaximumWidth(100) + select_all_btn.setStyleSheet("font-size: 11px; padding: 5px;") select_all_btn.clicked.connect(self.files_list.selectAll) sel_layout.addWidget(select_all_btn) select_none_btn = QPushButton("Select None") select_none_btn.setMaximumWidth(100) + select_none_btn.setStyleSheet("font-size: 11px; padding: 5px;") select_none_btn.clicked.connect(self.files_list.clearSelection) sel_layout.addWidget(select_none_btn) @@ -465,6 +524,7 @@ class IconExtractorWindow(QMainWindow): open_folder_btn = QPushButton("Open Output Folder") open_folder_btn.setMaximumWidth(130) + open_folder_btn.setStyleSheet("font-size: 11px; padding: 5px;") open_folder_btn.clicked.connect(self._open_output_folder) sel_layout.addWidget(open_folder_btn) @@ -472,23 +532,68 @@ class IconExtractorWindow(QMainWindow): right_layout.addWidget(files_group) splitter.addWidget(right_panel) - splitter.setSizes([350, 550]) + splitter.setSizes([360, 560]) 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 = QLabel( - f"Developed by {DEVELOPER} | Discord: {DISCORD}\n" - f"{WEBSITE}\n" - "Entropia Nexus is a fan-made resource and is not affiliated with MindArk PE AB. " + f"Developed by {DEVELOPER} | Discord: {DISCORD} | {WEBSITE}\n" + "Entropia Universe Icon Extractor is a fan-made resource and is not affiliated with MindArk PE AB. " "Entropia Universe is a trademark of MindArk PE AB." ) - footer.setStyleSheet("color: #555; font-size: 9px; padding: 8px;") + footer.setStyleSheet("color: #666; font-size: 10px; padding: 10px;") footer.setAlignment(Qt.AlignmentFlag.AlignCenter) footer.setWordWrap(True) - footer.setMinimumHeight(60) + footer.setMinimumHeight(55) 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): """Open URL in default browser.""" import webbrowser @@ -499,7 +604,7 @@ class IconExtractorWindow(QMainWindow): # 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) + self.output_label.setText("Documents/Entropia Universe/Icons/") def _save_settings(self): """Save current settings.""" @@ -565,7 +670,8 @@ class IconExtractorWindow(QMainWindow): if 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() def _refresh_file_list(self): @@ -610,7 +716,9 @@ class IconExtractorWindow(QMainWindow): # Get info header = self.converter.read_tga_header(tga_file) 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.found_files.append(tga_file) @@ -717,9 +825,6 @@ def set_app_icon(app: QApplication): 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(): @@ -735,64 +840,74 @@ def main(): # Try to set icon set_app_icon(app) - # Dark theme + # Dark theme with better readability app.setStyleSheet(""" QMainWindow, QDialog { - background-color: #1a1a1a; + background-color: #1e1e1e; } QWidget { - background-color: #1a1a1a; + background-color: #1e1e1e; color: #e0e0e0; } QGroupBox { font-weight: bold; - border: 1px solid #333; - border-radius: 5px; - margin-top: 10px; - padding-top: 10px; + border: 1px solid #404040; + border-radius: 6px; + margin-top: 12px; + padding-top: 12px; } QGroupBox::title { subcontrol-origin: margin; - left: 10px; - padding: 0 5px; + left: 12px; + padding: 0 8px; } QPushButton { - background-color: #333; + background-color: #3d3d3d; border: 1px solid #555; - padding: 8px 15px; - border-radius: 3px; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; } QPushButton:hover { - background-color: #444; + background-color: #4d4d4d; } QComboBox { - background-color: #2a2a2a; + background-color: #2d2d2d; border: 1px solid #555; padding: 5px; - min-width: 150px; + border-radius: 4px; } QListWidget { - background-color: #222; - border: 1px solid #444; - border-radius: 3px; + background-color: #252525; + border: 1px solid #404040; + border-radius: 4px; } QListWidget::item { - padding: 5px; + padding: 6px; } QListWidget::item:selected { - background-color: #0d47a1; + background-color: #1565c0; + } + QListWidget::item:hover { + background-color: #2a4d6e; } QProgressBar { - border: 1px solid #444; - border-radius: 3px; + border: 1px solid #404040; + border-radius: 4px; text-align: center; } QProgressBar::chunk { background-color: #4caf50; } QTextEdit { - background-color: #222; - border: 1px solid #444; + background-color: #252525; + border: 1px solid #404040; + } + QCheckBox { + font-size: 12px; + } + QLabel { + font-size: 12px; } """)