""" Entropia Universe Icon Extractor A standalone tool for extracting game icons from cache. Usage: python icon_extractor.py Developer: ImpulsiveFPS Discord: impulsivefps GitHub: https://github.com/ImpulsiveFPS/EU-Icon-Extractor """ import sys import logging from pathlib import Path from typing import Optional, List import ctypes try: from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QListWidget, QListWidgetItem, QFileDialog, QProgressBar, QGroupBox, QMessageBox, QTextEdit, QDialog, QScrollArea ) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings from PyQt6.QtGui import QIcon, QPixmap, QImage except ImportError: print("PyQt6 not available. Install with: pip install PyQt6") sys.exit(1) try: from PIL import Image except ImportError: print("Pillow not available. Install with: pip install Pillow") sys.exit(1) # Setup logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) # Application metadata APP_NAME = "Entropia Universe Icon Extractor" APP_VERSION = "1.0.0" DEVELOPER = "ImpulsiveFPS" DISCORD = "impulsivefps" class TGAHeader: """TGA file header structure.""" def __init__(self, data: bytes): self.id_length = data[0] self.color_map_type = data[1] self.image_type = data[2] self.color_map_origin = int.from_bytes(data[3:5], 'little') self.color_map_length = int.from_bytes(data[5:7], 'little') self.color_map_depth = data[7] self.x_origin = int.from_bytes(data[8:10], 'little') self.y_origin = int.from_bytes(data[10:12], 'little') self.width = int.from_bytes(data[12:14], 'little') self.height = int.from_bytes(data[14:16], 'little') self.pixel_depth = data[16] self.image_descriptor = data[17] def __str__(self): return f"{self.width}x{self.height}, {self.pixel_depth}bpp" class TGAConverter: """Converter for TGA files to PNG with 320x320 canvas.""" CANVAS_SIZE = (320, 320) # Hardcoded to 320x320 def __init__(self, output_dir: Optional[Path] = None): # Default to user's Documents/Entropia Universe/Icons/ # This works on any Windows username if output_dir is None: self.output_dir = Path.home() / "Documents" / "Entropia Universe" / "Icons" else: self.output_dir = output_dir self.output_dir.mkdir(parents=True, exist_ok=True) self._cache_path: Optional[Path] = None def find_cache_folder(self) -> Optional[Path]: """Find the Entropia Universe icon cache folder.""" # Hardcoded path - works on any system with EU installed cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon") if cache_path.exists(): self._cache_path = cache_path return cache_path return None def read_tga_header(self, filepath: Path) -> Optional[TGAHeader]: """Read TGA header from file.""" try: with open(filepath, 'rb') as f: header_data = f.read(18) if len(header_data) < 18: return None return TGAHeader(header_data) except Exception as e: logger.error(f"Error reading TGA header: {e}") return None def 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. Args: tga_path: Path to TGA file output_name: Optional custom output name Returns: Path to output PNG file or None if failed """ try: # Load TGA image = self.load_tga_image(tga_path) if not image: return None # Apply 320x320 canvas (centered, no upscaling) image = self._apply_canvas(image) # Save if output_name is None: output_name = tga_path.stem output_path = self.output_dir / f"{output_name}.png" image.save(output_path, 'PNG') return output_path except Exception as e: logger.error(f"Conversion failed: {e}") return None def _apply_canvas(self, image: Image.Image) -> Image.Image: """ Place image centered on a 320x320 canvas. No upscaling - original size centered on canvas. """ canvas_w, canvas_h = self.CANVAS_SIZE img_w, img_h = image.size # Create transparent canvas canvas = Image.new('RGBA', self.CANVAS_SIZE, (0, 0, 0, 0)) # Center on canvas (no scaling) x = (canvas_w - img_w) // 2 y = (canvas_h - img_h) // 2 canvas.paste(image, (x, y), image if image.mode == 'RGBA' else None) return canvas class ConversionWorker(QThread): """Background worker for batch conversion.""" progress = pyqtSignal(str) file_done = pyqtSignal(str, str) # filename, output_path finished = pyqtSignal(int, int) # success, total error = pyqtSignal(str) def __init__(self, files: List[Path], converter: TGAConverter): super().__init__() self.files = files self.converter = converter self._running = True def run(self): """Run conversion.""" try: success = 0 total = len(self.files) for i, filepath in enumerate(self.files): if not self._running: break self.progress.emit(f"[{i+1}/{total}] {filepath.name}") output = self.converter.convert_tga_to_png(filepath) if output: success += 1 self.file_done.emit(filepath.name, str(output)) self.finished.emit(success, total) except Exception as e: self.error.emit(str(e)) def stop(self): self._running = False class PreviewDialog(QDialog): """Dialog to preview a TGA file at original resolution.""" def __init__(self, tga_path: Path, converter: TGAConverter, parent=None): super().__init__(parent) self.setWindowTitle(f"Preview: {tga_path.name}") layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) # Info info = converter.read_tga_header(tga_path) if info: info_label = QLabel(f"Original Resolution: {info.width}x{info.height} pixels, {info.pixel_depth}bpp") info_label.setStyleSheet("color: #888; font-size: 13px; font-weight: bold;") layout.addWidget(info_label) # Load and display TGA at original size image = converter.load_tga_image(tga_path) if image: # Convert to QPixmap at original size img_data = image.tobytes("raw", "RGBA") qimage = QImage(img_data, image.width, image.height, QImage.Format.Format_RGBA8888) pixmap = QPixmap.fromImage(qimage) # Show at original size img_label = QLabel() img_label.setPixmap(pixmap) img_label.setAlignment(Qt.AlignmentFlag.AlignCenter) img_label.setStyleSheet("background-color: #2a2a2a; border: 1px solid #444; padding: 5px;") # Add to scroll area for large images scroll = QScrollArea() scroll.setWidget(img_label) scroll.setWidgetResizable(True) scroll.setMinimumSize(min(image.width + 40, 800), min(image.height + 40, 600)) scroll.setMaximumSize(800, 600) layout.addWidget(scroll) # Show actual size info size_label = QLabel(f"Displayed at: {image.width}x{image.height} (Original Size)") size_label.setStyleSheet("color: #4caf50; font-size: 12px;") 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) # Resize dialog to fit image (with max limits) if image: dialog_width = min(image.width + 50, 820) dialog_height = min(image.height + 150, 700) self.resize(dialog_width, dialog_height) class IconExtractorWindow(QMainWindow): """Main window for the standalone icon extractor.""" def __init__(self): super().__init__() self.setWindowTitle(APP_NAME) self.setMinimumSize(1050, 850) self.resize(1150, 900) self.converter = TGAConverter() self.worker: Optional[ConversionWorker] = None self.found_files: List[Path] = [] # Hardcoded base cache path self.base_cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon") self.settings = QSettings("ImpulsiveFPS", "EUIconExtractor") self._setup_ui() self._load_icon() self._load_settings() self._detect_subfolders() def _setup_ui(self): """Setup the UI.""" central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) # Header with icon header_layout = QHBoxLayout() header_layout.setSpacing(15) # Icon label (will be set if icon exists) self.header_icon = QLabel() self.header_icon.setFixedSize(48, 48) self.header_icon.setStyleSheet("background: transparent;") header_layout.addWidget(self.header_icon) # Title header = QLabel("Entropia Universe Icon Extractor") header.setStyleSheet("font-size: 22px; font-weight: bold; color: #4caf50;") header_layout.addWidget(header, 1) # Theme toggle button self.theme_btn = QPushButton("☀️ Light") self.theme_btn.setMaximumWidth(80) self.theme_btn.setStyleSheet("font-size: 11px; padding: 5px;") self.theme_btn.setCheckable(True) self.theme_btn.clicked.connect(self._toggle_theme) header_layout.addWidget(self.theme_btn) header_layout.addStretch() layout.addLayout(header_layout) # Description - two lines with clickable link desc_widget = QWidget() desc_layout = QVBoxLayout(desc_widget) desc_layout.setContentsMargins(5, 5, 5, 5) desc_layout.setSpacing(4) desc_line1 = QLabel("Extract the item icons from Entropia Universe cache and convert them to PNG.") desc_line1.setStyleSheet("color: #aaaaaa; font-size: 13px;") desc_layout.addWidget(desc_line1) desc_line2 = QLabel("You can submit these to ") desc_line2.setStyleSheet("color: #aaaaaa; font-size: 13px;") desc_line2.setOpenExternalLinks(True) # Clickable link - Entropia Nexus text links to entropianexus.com link_label = QLabel('Entropia Nexus to help complete the item database.') link_label.setStyleSheet("font-size: 13px; color: #aaaaaa;") link_label.setOpenExternalLinks(True) desc_line2_layout = QHBoxLayout() desc_line2_layout.setContentsMargins(0, 0, 0, 0) desc_line2_layout.setSpacing(0) desc_line2_layout.addWidget(desc_line2) desc_line2_layout.addWidget(link_label) desc_line2_layout.addStretch() desc_layout.addLayout(desc_line2_layout) layout.addWidget(desc_widget) # Cache and Output side by side top_row = QWidget() top_row_layout = QHBoxLayout(top_row) top_row_layout.setContentsMargins(0, 0, 0, 0) top_row_layout.setSpacing(15) # 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(12, 18, 12, 12) cache_layout.setSpacing(10) # Base path (hardcoded) - show just the end part path_display = "...\\Entropia Universe\\public_users_data\\cache\\icon" self.cache_path_full = str(self.base_cache_path).replace("/", "\\") self.cache_label = QLabel(path_display) self.cache_label.setStyleSheet( "font-family: Consolas; font-size: 10px; color: #aaa; " "padding: 6px 8px; background: #252525; border-radius: 3px;" ) self.cache_label.setToolTip(self.cache_path_full) cache_layout.addWidget(self.cache_label) # Subfolder selector subfolder_layout = QHBoxLayout() subfolder_layout.setSpacing(8) 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.setStyleSheet("font-size: 12px; padding: 3px;") self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed) subfolder_layout.addWidget(self.subfolder_combo, 1) refresh_btn = QPushButton("🔄 Refresh") refresh_btn.setMaximumWidth(80) refresh_btn.setStyleSheet("font-size: 11px; padding: 4px;") refresh_btn.clicked.connect(self._detect_subfolders) subfolder_layout.addWidget(refresh_btn) cache_layout.addLayout(subfolder_layout) top_row_layout.addWidget(cache_group, 1) # 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(12, 18, 12, 12) output_layout.setSpacing(10) output_info = QLabel("📁 Icons saved to your Documents folder (same location as chat.log)") output_info.setStyleSheet("color: #666666; font-size: 12px;") output_info.setWordWrap(True) output_layout.addWidget(output_info) rel_path = "Documents\\Entropia Universe\\Icons\\" self.output_label = QLabel(f"📂 {rel_path}") self.output_label.setStyleSheet( "font-family: Consolas; font-size: 10px; color: #aaa; " "padding: 6px 8px; background: #252525; border-radius: 3px;" ) output_layout.addWidget(self.output_label) change_btn = QPushButton("📂 Change Output Folder...") change_btn.setStyleSheet("font-size: 11px; padding: 5px;") change_btn.clicked.connect(self._browse_output) output_layout.addWidget(change_btn) top_row_layout.addWidget(output_group, 1) layout.addWidget(top_row) # Available Icons (full width below) files_group = QGroupBox("📄 Available Icons") files_group.setStyleSheet("QGroupBox { font-size: 13px; font-weight: bold; }") files_layout = QVBoxLayout(files_group) files_layout.setContentsMargins(12, 18, 12, 12) files_layout.setSpacing(10) files_info = QLabel("💡 Double-click an icon to preview. Select icons to extract (or leave blank for all).") files_info.setStyleSheet("color: #666666; font-size: 12px;") files_layout.addWidget(files_info) self.files_count_label = QLabel("❓ No files found") self.files_count_label.setStyleSheet("font-weight: bold; font-size: 12px; padding: 3px 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: 2px;") self.files_list.doubleClicked.connect(self._on_file_double_clicked) files_layout.addWidget(self.files_list, 1) layout.addWidget(files_group, 1) # Bottom buttons area bottom_widget = QWidget() bottom_layout = QHBoxLayout(bottom_widget) bottom_layout.setContentsMargins(0, 0, 0, 0) bottom_layout.setSpacing(15) # Left side: Select buttons select_layout = QHBoxLayout() select_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) select_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) select_layout.addWidget(select_none_btn) bottom_layout.addLayout(select_layout) bottom_layout.addStretch() # Right side: Main buttons stacked vertically right_buttons = QVBoxLayout() right_buttons.setSpacing(8) right_buttons.setAlignment(Qt.AlignmentFlag.AlignRight) # Main action button - GREEN self.convert_btn = QPushButton("▶️ Start Extracting Icons") self.convert_btn.setFixedSize(200, 45) self.convert_btn.setStyleSheet(""" QPushButton { background-color: #4caf50; font-weight: bold; font-size: 13px; border-radius: 5px; padding: 10px; color: white; } QPushButton:hover { background-color: #66bb6a; } QPushButton:disabled { background-color: #757575; color: #bdbdbd; } """) self.convert_btn.clicked.connect(self._start_conversion) right_buttons.addWidget(self.convert_btn) # Open Output Folder button - same size open_folder_btn = QPushButton("📂 Open Output Folder") open_folder_btn.setFixedSize(200, 35) open_folder_btn.setStyleSheet("font-size: 11px; padding: 5px;") open_folder_btn.clicked.connect(self._open_output_folder) right_buttons.addWidget(open_folder_btn) bottom_layout.addLayout(right_buttons) layout.addWidget(bottom_widget) # Progress self.progress_bar = QProgressBar() self.progress_bar.setTextVisible(True) self.progress_bar.setStyleSheet("font-size: 11px;") self.progress_bar.setVisible(False) layout.addWidget(self.progress_bar) self.status_label = QLabel("✅ Ready") self.status_label.setStyleSheet("color: #888; font-size: 12px; padding: 5px;") self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) layout.addWidget(self.status_label) # 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(10, 15, 10, 10) notice_text = QTextEdit() notice_text.setReadOnly(True) notice_text.setMaximumHeight(70) notice_text.setStyleSheet(""" QTextEdit { background-color: #2d2818; color: #ffc107; border: 1px solid #5d4e37; border-radius: 3px; font-size: 12px; padding: 8px; line-height: 1.4; } """) notice_text.setText( "REQUIREMENT: Items must be seen in-game before they appear in the cache! " "If an icon is missing, view the item in your inventory or the auction first. " "Output: Documents/Entropia Universe/Icons/" ) notice_layout.addWidget(notice_text) layout.addWidget(notice_group) # Footer with clickable links (no emojis) footer_widget = QWidget() footer_layout = QVBoxLayout(footer_widget) footer_layout.setContentsMargins(10, 10, 10, 10) footer_layout.setSpacing(5) # First line - developer info (no emojis) footer_line1 = QLabel(f"Developed by {DEVELOPER} | Discord: {DISCORD} | GitHub: (coming soon)") footer_line1.setStyleSheet("color: #888; font-size: 11px;") footer_line1.setAlignment(Qt.AlignmentFlag.AlignCenter) footer_layout.addWidget(footer_line1) # Support me line support_label = QLabel('☕ Support me on Ko-fi') support_label.setStyleSheet("color: #888; font-size: 11px;") support_label.setAlignment(Qt.AlignmentFlag.AlignCenter) support_label.setOpenExternalLinks(True) footer_layout.addWidget(support_label) # Second line - disclaimer with links (no emojis) disclaimer_widget = QWidget() disclaimer_layout = QHBoxLayout(disclaimer_widget) disclaimer_layout.setContentsMargins(0, 0, 0, 0) disclaimer_layout.setSpacing(0) label1 = QLabel("Entropia Universe Icon Extractor is a fan-made resource and is not affiliated with ") label1.setStyleSheet("color: #666; font-size: 10px;") label1.setOpenExternalLinks(True) mindark_link = QLabel('MindArk PE AB. ') mindark_link.setStyleSheet("color: #666; font-size: 10px;") mindark_link.setOpenExternalLinks(True) eu_link = QLabel('Entropia Universe') eu_link.setStyleSheet("color: #666; font-size: 10px;") eu_link.setOpenExternalLinks(True) label3 = QLabel(" is a trademark of MindArk PE AB.") label3.setStyleSheet("color: #666; font-size: 10px;") disclaimer_layout.addStretch() disclaimer_layout.addWidget(label1) disclaimer_layout.addWidget(mindark_link) disclaimer_layout.addWidget(eu_link) disclaimer_layout.addWidget(label3) disclaimer_layout.addStretch() footer_layout.addWidget(disclaimer_widget) layout.addWidget(footer_widget) 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 webbrowser.open(url) def _load_icon(self): """Load and set the 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(): pixmap = QPixmap(str(icon_path)) if not pixmap.isNull(): # Set window icon self.setWindowIcon(QIcon(pixmap)) # Set header icon (scaled to 48x48) header_pixmap = pixmap.scaled(48, 48, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) self.header_icon.setPixmap(header_pixmap) return True # Hide header icon if no icon found self.header_icon.hide() return False def _toggle_theme(self): """Toggle between light and dark theme.""" if self.theme_btn.isChecked(): self._apply_light_theme() self.theme_btn.setText("🌙 Dark") else: self._apply_dark_theme() self.theme_btn.setText("☀️ Light") def _apply_dark_theme(self): """Apply dark theme.""" self.setStyleSheet(""" QMainWindow, QDialog { background-color: #1e1e1e; } QWidget { background-color: #1e1e1e; color: #e0e0e0; } QGroupBox { font-weight: bold; border: 1px solid #404040; border-radius: 6px; margin-top: 12px; padding-top: 12px; } QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 8px; } QPushButton { background-color: #3d3d3d; border: 1px solid #555; padding: 6px 12px; border-radius: 4px; font-size: 12px; } QPushButton:hover { background-color: #4d4d4d; } QComboBox { background-color: #2d2d2d; border: 1px solid #555; padding: 5px; border-radius: 4px; } QListWidget { background-color: #252525; border: 1px solid #404040; border-radius: 4px; } QListWidget::item { padding: 6px; } QListWidget::item:selected { background-color: #1565c0; } QListWidget::item:hover { background-color: #2a4d6e; } QProgressBar { border: 1px solid #404040; border-radius: 4px; text-align: center; } QProgressBar::chunk { background-color: #4caf50; } QTextEdit { background-color: #252525; border: 1px solid #404040; } QCheckBox { font-size: 12px; } QLabel { font-size: 12px; } """) def _apply_light_theme(self): """Apply light theme.""" self.setStyleSheet(""" QMainWindow, QDialog { background-color: #f5f5f5; } QWidget { background-color: #f5f5f5; color: #333333; } QGroupBox { font-weight: bold; border: 1px solid #cccccc; border-radius: 6px; margin-top: 12px; padding-top: 12px; color: #333333; } QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 8px; color: #333333; } QPushButton { background-color: #e0e0e0; border: 1px solid #bbbbbb; padding: 6px 12px; border-radius: 4px; font-size: 12px; color: #333333; } QPushButton:hover { background-color: #d0d0d0; } QComboBox { background-color: #ffffff; border: 1px solid #bbbbbb; padding: 5px; border-radius: 4px; color: #333333; } QListWidget { background-color: #ffffff; border: 1px solid #cccccc; border-radius: 4px; } QListWidget::item { padding: 6px; color: #333333; } QListWidget::item:selected { background-color: #1976d2; color: #ffffff; } QListWidget::item:hover { background-color: #e3f2fd; } QProgressBar { border: 1px solid #cccccc; border-radius: 4px; text-align: center; } QProgressBar::chunk { background-color: #4caf50; } QTextEdit { background-color: #2d2818; border: 1px solid #5d4e37; color: #ffc107; } QCheckBox { font-size: 12px; color: #333333; } QLabel { font-size: 12px; color: #333333; } """) def _load_settings(self): """Load saved settings.""" # Output folder saved_output = self.settings.value("output_dir", str(self.converter.output_dir)) self.converter.output_dir = Path(saved_output) self.output_label.setText("Documents\\Entropia Universe\\Icons\\") def _save_settings(self): """Save current settings.""" self.settings.setValue("output_dir", str(self.converter.output_dir)) def _detect_subfolders(self): """Detect version subfolders in the cache directory.""" self.subfolder_combo.clear() if not self.base_cache_path.exists(): self.cache_label.setText(f"Not found: {self.base_cache_path}") self.status_label.setText("Cache folder not found - is Entropia Universe installed?") return # Find all subfolders that contain TGA files subfolders = [] for item in self.base_cache_path.iterdir(): if item.is_dir(): # Check if this folder has TGA files tga_count = len(list(item.glob("*.tga"))) if tga_count > 0: subfolders.append((item.name, tga_count, item)) if not subfolders: self.cache_label.setText(f"No subfolders with icons in {self.base_cache_path}") self.status_label.setText("No version folders found") return # Sort by name (version) subfolders.sort(key=lambda x: x[0]) # Add to combo for name, count, path in subfolders: self.subfolder_combo.addItem(f"{name} ({count} icons)", str(path)) # Add "All folders" option at top total_icons = sum(s[1] for s in subfolders) self.subfolder_combo.insertItem(0, f"All Folders ({total_icons} icons)", "all") self.subfolder_combo.setCurrentIndex(0) self.cache_label.setText(f"{self.base_cache_path}") self.status_label.setText(f"Found {len(subfolders)} version folders") # Load files self._refresh_file_list() def _on_subfolder_changed(self): """Handle subfolder selection change.""" self._refresh_file_list() def _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) rel_path = "Documents/Entropia Universe/Icons/" self.output_label.setText(rel_path) self._save_settings() def _refresh_file_list(self): """Refresh the list of found files based on current selection.""" self.files_list.clear() self.found_files = [] if not self.base_cache_path.exists(): return # Determine which folders to scan based on dropdown selection path_data = self.subfolder_combo.currentData() if path_data == "all" or path_data is None: # Scan all subfolders folders_to_scan = [d for d in self.base_cache_path.iterdir() if d.is_dir()] else: # Scan selected subfolder folders_to_scan = [Path(path_data)] # Collect TGA files tga_files = [] for folder in folders_to_scan: if folder.exists(): tga_files.extend(folder.glob("*.tga")) self.files_count_label.setText(f"Found {len(tga_files)} icon files") self.status_label.setText(f"Found {len(tga_files)} files") for tga_file in sorted(tga_files): # Show folder prefix for clarity try: rel_path = f"{tga_file.parent.name}/{tga_file.name}" except: rel_path = tga_file.name item = QListWidgetItem(rel_path) item.setData(Qt.ItemDataRole.UserRole, str(tga_file)) # Get info header = self.converter.read_tga_header(tga_file) if header: item.setToolTip(f"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) self.convert_btn.setEnabled(len(tga_files) > 0) def _start_conversion(self): """Start batch conversion.""" # Get selected files or all files selected_items = self.files_list.selectedItems() if selected_items: files_to_convert = [ Path(item.data(Qt.ItemDataRole.UserRole)) for item in selected_items ] else: files_to_convert = self.found_files if not files_to_convert: QMessageBox.warning(self, "No Files", "No files selected for extraction.") return # Save settings self._save_settings() # Setup UI self.convert_btn.setEnabled(False) self.convert_btn.setText("Extracting...") self.progress_bar.setRange(0, len(files_to_convert)) self.progress_bar.setValue(0) self.progress_bar.setVisible(True) # Start worker self.worker = ConversionWorker(files_to_convert, self.converter) self.worker.progress.connect(self._on_progress) self.worker.file_done.connect(self._on_file_done) self.worker.finished.connect(self._on_finished) self.worker.error.connect(self._on_error) self.worker.start() def _on_progress(self, msg: str): self.status_label.setText(msg) self.progress_bar.setValue(self.progress_bar.value() + 1) def _on_file_done(self, filename: str, output_path: str): logger.info(f"Extracted: {filename} -> {output_path}") def _on_finished(self, success: int, total: int): self.convert_btn.setEnabled(True) self.convert_btn.setText("Start Extracting Icons") self.progress_bar.setVisible(False) self.status_label.setText(f"Extracted {success}/{total} icons") QMessageBox.information( self, "Extraction Complete", f"Successfully extracted {success} of {total} icons.\n\n" f"Output location:\n{self.converter.output_dir}\n\n" f"Submit these icons to EntropiaNexus.com to help the community!" ) def _on_error(self, error_msg: str): self.convert_btn.setEnabled(True) self.convert_btn.setText("Start Extracting Icons") self.progress_bar.setVisible(False) self.status_label.setText(f"Error: {error_msg}") QMessageBox.critical(self, "Error", f"Extraction failed:\n{error_msg}") def _open_output_folder(self): """Open output folder in file manager.""" import os import subprocess path = str(self.converter.output_dir) if os.name == 'nt': os.startfile(path) elif sys.platform == 'darwin': subprocess.run(['open', path]) else: subprocess.run(['xdg-open', path]) def closeEvent(self, event): """Save settings on close.""" self._save_settings() if self.worker and self.worker.isRunning(): self.worker.stop() self.worker.wait(1000) event.accept() def set_app_icon(app: QApplication): """Set application icon for window and taskbar.""" # 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 True return False def set_windows_taskbar_icon(): """Set Windows taskbar icon properly.""" if sys.platform == 'win32': # Set the AppUserModelID to ensure taskbar shows our icon try: import ctypes myappid = 'impulsivefps.euiconextractor.1.0' ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid) except Exception: pass def main(): """Main entry point.""" # Set Windows taskbar icon before creating QApplication set_windows_taskbar_icon() app = QApplication(sys.argv) app.setStyle('Fusion') # Set application info for proper Windows taskbar icon app.setApplicationName(APP_NAME) app.setApplicationVersion(APP_VERSION) app.setOrganizationName("ImpulsiveFPS") # Set app icon (for taskbar and window) set_app_icon(app) # Dark theme by default app.setStyleSheet(""" QMainWindow, QDialog { background-color: #1e1e1e; } QWidget { background-color: #1e1e1e; color: #e0e0e0; } QGroupBox { font-weight: bold; border: 1px solid #404040; border-radius: 6px; margin-top: 12px; padding-top: 12px; } QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 8px; } QPushButton { background-color: #3d3d3d; border: 1px solid #555; padding: 6px 12px; border-radius: 4px; font-size: 12px; } QPushButton:hover { background-color: #4d4d4d; } QComboBox { background-color: #2d2d2d; border: 1px solid #555; padding: 5px; border-radius: 4px; } QListWidget { background-color: #252525; border: 1px solid #404040; border-radius: 4px; } QListWidget::item { padding: 6px; } QListWidget::item:selected { background-color: #1565c0; } QListWidget::item:hover { background-color: #2a4d6e; } QProgressBar { border: 1px solid #404040; border-radius: 4px; text-align: center; } QProgressBar::chunk { background-color: #4caf50; } QTextEdit { background-color: #252525; border: 1px solid #404040; } QCheckBox { font-size: 12px; } QLabel { font-size: 12px; } """) window = IconExtractorWindow() window.show() sys.exit(app.exec()) if __name__ == "__main__": main()