diff --git a/standalone_icon_extractor.py b/standalone_icon_extractor.py index 6017d29..e560ba1 100644 --- a/standalone_icon_extractor.py +++ b/standalone_icon_extractor.py @@ -1,45 +1,57 @@ """ Entropia Universe Icon Extractor -A standalone tool for extracting and converting game icons from cache. +A standalone tool for extracting game icons from cache. -Description: Standalone GUI tool to extract TGA icons from Entropia Universe cache, -convert them to PNG, and optionally upscale them for better quality. +Description: Extracts item icons from Entropia Universe game cache and converts +them to PNG format for use with EntropiaNexus.com wiki submissions. + +Important: Items must be seen/rendered in-game before they appear in the cache. Usage: python standalone_icon_extractor.py -Author: LemonNexus +Output Location: + Icons are saved to your Documents/Entropia Universe/Icons/ folder + (same location where chat.log is normally stored) + +Developer: ImpulsiveFPS +Discord: impulsivefps +Website: https://EntropiaNexus.com + +Disclaimer: + This tool is not affiliated with, endorsed by, or connected to + Entropia Universe or Mindark PE AB in any way. + Entropia Universe is a registered trademark of Mindark PE AB. """ import sys import logging from pathlib import Path from typing import Optional, List, Tuple -from dataclasses import dataclass -from enum import Enum +import ctypes try: from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QListWidget, QListWidgetItem, QFileDialog, QProgressBar, QGroupBox, QMessageBox, QCheckBox, - QSpinBox, QSplitter + QSplitter, QTextEdit ) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QSettings - from PyQt6.QtGui import QIcon, QPixmap + from PyQt6.QtGui import QIcon, QPixmap, QFont PYQT_AVAILABLE = True except ImportError: PYQT_AVAILABLE = False print("PyQt6 not available. Install with: pip install PyQt6") + sys.exit(1) try: from PIL import Image, ImageFilter PIL_AVAILABLE = True except ImportError: PIL_AVAILABLE = False - print("PIL not available. Install with: pip install Pillow") - -import numpy as np + print("Pillow not available. Install with: pip install Pillow") + sys.exit(1) # Setup logging logging.basicConfig( @@ -49,6 +61,14 @@ logging.basicConfig( logger = logging.getLogger(__name__) +# Application metadata +APP_NAME = "Entropia Universe Icon Extractor" +APP_VERSION = "1.0.0" +DEVELOPER = "ImpulsiveFPS" +DISCORD = "impulsivefps" +WEBSITE = "https://EntropiaNexus.com" + + class TGAHeader: """TGA file header structure.""" def __init__(self, data: bytes): @@ -70,26 +90,28 @@ class TGAHeader: class TGAConverter: - """Converter for TGA files to PNG with canvas and upscaling options.""" + """Converter for TGA files to PNG with 320x320 canvas.""" + + CANVAS_SIZE = (320, 320) # Hardcoded to 320x320 def __init__(self, output_dir: Optional[Path] = None): - self.output_dir = output_dir or Path.home() / "Documents" / "Entropia Universe" / "Icons" + # Default to user's Documents/Entropia Universe/Icons/ + # This works on any Windows username + if output_dir is None: + self.output_dir = Path.home() / "Documents" / "Entropia Universe" / "Icons" + else: + self.output_dir = output_dir self.output_dir.mkdir(parents=True, exist_ok=True) self._cache_path: Optional[Path] = None def find_cache_folder(self) -> Optional[Path]: """Find the Entropia Universe icon cache folder.""" - possible_paths = [ - Path("C:") / "ProgramData" / "Entropia Universe" / "public_users_data" / "cache" / "icon", - Path.home() / "Documents" / "Entropia Universe" / "cache" / "icons", - ] + # Hardcoded path - works on any system with EU installed + cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon") - for path in possible_paths: - if path.exists(): - # Check if this folder or any subfolder has .tga files - if list(path.rglob("*.tga")): - self._cache_path = path - return path + if cache_path.exists(): + self._cache_path = cache_path + return cache_path return None @@ -105,25 +127,25 @@ class TGAConverter: logger.error(f"Error reading TGA header: {e}") return None - def convert_tga_to_png( - self, - tga_path: Path, - output_name: Optional[str] = None, - canvas_size: Optional[Tuple[int, int]] = None, - upscale: bool = False, - upscale_method: str = 'nearest' - ) -> Optional[Path]: - """Convert a TGA file to PNG.""" + def convert_tga_to_png(self, tga_path: Path, output_name: Optional[str] = None) -> Optional[Path]: + """ + Convert a TGA file to PNG with 320x320 canvas. + + Args: + tga_path: Path to TGA file + output_name: Optional custom output name + + Returns: + Path to output PNG file or None if failed + """ try: - # Try PIL first + # Load TGA image = Image.open(tga_path) if image.mode != 'RGBA': image = image.convert('RGBA') - # Apply canvas sizing if requested - if canvas_size: - do_upscale = upscale and upscale_method != 'none' - image = self._apply_canvas(image, canvas_size, do_upscale, upscale_method) + # Apply 320x320 canvas (centered, no upscaling) + image = self._apply_canvas(image) # Save if output_name is None: @@ -138,45 +160,18 @@ class TGAConverter: logger.error(f"Conversion failed: {e}") return None - def _apply_canvas( - self, - image: Image.Image, - canvas_size: Tuple[int, int], - upscale: bool = False, - upscale_method: str = 'nearest' - ) -> Image.Image: - """Place image centered on a canvas.""" - canvas_w, canvas_h = canvas_size + def _apply_canvas(self, image: Image.Image) -> Image.Image: + """ + Place image centered on a 320x320 canvas. + No upscaling - original size centered on canvas. + """ + canvas_w, canvas_h = self.CANVAS_SIZE img_w, img_h = image.size # Create transparent canvas - canvas = Image.new('RGBA', canvas_size, (0, 0, 0, 0)) + canvas = Image.new('RGBA', self.CANVAS_SIZE, (0, 0, 0, 0)) - # Calculate scaling - if upscale: - max_size = int(min(canvas_w, canvas_h) * 0.85) - scale = min(max_size / img_w, max_size / img_h) - - if scale > 1: - new_w = int(img_w * scale) - new_h = int(img_h * scale) - - if upscale_method == 'nearest': - image = image.resize((new_w, new_h), Image.Resampling.NEAREST) - elif upscale_method == 'hq4x': - int_scale = max(2, int(scale)) - temp_w = img_w * int_scale - temp_h = img_h * int_scale - image = image.resize((temp_w, temp_h), Image.Resampling.NEAREST) - image = image.resize((new_w, new_h), Image.Resampling.LANCZOS) - image = image.filter(ImageFilter.EDGE_ENHANCE_MORE) - else: # lanczos - image = image.resize((new_w, new_h), Image.Resampling.LANCZOS) - - image = image.filter(ImageFilter.UnsharpMask(radius=2, percent=150, threshold=3)) - img_w, img_h = new_w, new_h - - # Center on canvas + # Center on canvas (no scaling) x = (canvas_w - img_w) // 2 y = (canvas_h - img_h) // 2 @@ -191,18 +186,10 @@ class ConversionWorker(QThread): finished = pyqtSignal(int, int) # success, total error = pyqtSignal(str) - def __init__( - self, - files: List[Path], - converter: TGAConverter, - canvas_size: Optional[Tuple[int, int]], - upscale_method: str - ): + def __init__(self, files: List[Path], converter: TGAConverter): super().__init__() self.files = files self.converter = converter - self.canvas_size = canvas_size - self.upscale_method = upscale_method self._running = True def run(self): @@ -217,12 +204,7 @@ class ConversionWorker(QThread): self.progress.emit(f"[{i+1}/{total}] {filepath.name}") - output = self.converter.convert_tga_to_png( - filepath, - canvas_size=self.canvas_size, - upscale=True, - upscale_method=self.upscale_method - ) + output = self.converter.convert_tga_to_png(filepath) if output: success += 1 @@ -242,18 +224,17 @@ class IconExtractorWindow(QMainWindow): def __init__(self): super().__init__() - self.setWindowTitle("🎮 Entropia Universe Icon Extractor") - self.setMinimumSize(900, 700) + self.setWindowTitle(APP_NAME) + self.setMinimumSize(950, 750) self.converter = TGAConverter() self.worker: Optional[ConversionWorker] = None self.found_files: List[Path] = [] - self.current_subfolder: Optional[Path] = None # Hardcoded base cache path self.base_cache_path = Path("C:/ProgramData/Entropia Universe/public_users_data/cache/icon") - self.settings = QSettings("Lemontropia", "IconExtractor") + self.settings = QSettings("ImpulsiveFPS", "EUIconExtractor") self._setup_ui() self._load_settings() @@ -268,18 +249,44 @@ class IconExtractorWindow(QMainWindow): layout.setSpacing(12) # Header - header = QLabel("🎮 Entropia Universe Icon Extractor") - header.setStyleSheet("font-size: 20px; font-weight: bold; color: #4caf50;") + header = QLabel(APP_NAME) + header.setStyleSheet("font-size: 22px; font-weight: bold; color: #4caf50;") layout.addWidget(header) + # Description desc = QLabel( - "Extract and convert item icons from Entropia Universe game cache.\n" - "Icons are saved as PNG files with transparent backgrounds." + "Extract item icons from Entropia Universe game cache and convert them to PNG format.\n" + "These icons can be submitted to EntropiaNexus.com to help complete the item database." ) - desc.setStyleSheet("color: #888; padding: 5px;") + desc.setStyleSheet("color: #aaaaaa; padding: 5px;") desc.setWordWrap(True) layout.addWidget(desc) + # Important notice + notice_group = QGroupBox("Important Information") + notice_layout = QVBoxLayout(notice_group) + + notice_text = QTextEdit() + notice_text.setReadOnly(True) + notice_text.setMaximumHeight(120) + notice_text.setStyleSheet(""" + QTextEdit { + background-color: #2a2520; + color: #ffcc80; + border: 1px solid #5d4037; + border-radius: 3px; + font-size: 12px; + } + """) + notice_text.setText( + "REQUIREMENT: Items must be seen/rendered in-game before they appear in the cache!\n" + "If an item icon is missing, you need to view the item in your inventory or see it dropped as loot first.\n\n" + f"Output Location: Icons are saved to your Documents folder ({self.converter.output_dir})\n" + "in the same location where Entropia Universe normally stores your chat.log file." + ) + notice_layout.addWidget(notice_text) + layout.addWidget(notice_group) + # Main splitter splitter = QSplitter(Qt.Orientation.Horizontal) @@ -289,15 +296,15 @@ class IconExtractorWindow(QMainWindow): left_layout.setContentsMargins(0, 0, 0, 0) # Cache folder - cache_group = QGroupBox("📁 Cache Folder") + cache_group = QGroupBox("Cache Source") cache_layout = QVBoxLayout(cache_group) # Base path (hardcoded) - base_label = QLabel("Base Path:") + base_label = QLabel("Game Cache Location:") cache_layout.addWidget(base_label) self.cache_label = QLabel(str(self.base_cache_path)) - self.cache_label.setStyleSheet("font-family: Consolas; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;") + self.cache_label.setStyleSheet("font-family: Consolas; font-size: 11px; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;") cache_layout.addWidget(self.cache_label) # Subfolder selector @@ -308,7 +315,7 @@ class IconExtractorWindow(QMainWindow): self.subfolder_combo.currentIndexChanged.connect(self._on_subfolder_changed) subfolder_layout.addWidget(self.subfolder_combo) - refresh_btn = QPushButton("🔄 Refresh") + refresh_btn = QPushButton("Refresh") refresh_btn.clicked.connect(self._detect_subfolders) subfolder_layout.addWidget(refresh_btn) @@ -316,81 +323,68 @@ class IconExtractorWindow(QMainWindow): cache_layout.addLayout(subfolder_layout) # All subfolders checkbox - self.all_subfolders_check = QCheckBox("Include ALL subfolders (merge everything)") - self.all_subfolders_check.setToolTip("If checked, will find TGA files from ALL version subfolders") + self.all_subfolders_check = QCheckBox("Include ALL version folders") + self.all_subfolders_check.setToolTip("Merge icons from all game versions") self.all_subfolders_check.stateChanged.connect(self._on_all_subfolders_changed) cache_layout.addWidget(self.all_subfolders_check) - cache_btn_layout = QHBoxLayout() - - scan_btn = QPushButton("🔍 Auto-Detect") - scan_btn.clicked.connect(self._auto_scan) - cache_btn_layout.addWidget(scan_btn) - - browse_btn = QPushButton("Browse...") - browse_btn.clicked.connect(self._browse_cache) - cache_btn_layout.addWidget(browse_btn) - - cache_btn_layout.addStretch() - cache_layout.addLayout(cache_btn_layout) left_layout.addWidget(cache_group) # Output folder - output_group = QGroupBox("💾 Output Folder") + output_group = QGroupBox("Output Location") output_layout = QVBoxLayout(output_group) + output_info = QLabel( + "Icons will be saved to your Documents folder:\n" + "(Same location as chat.log)" + ) + output_info.setStyleSheet("color: #888; font-size: 11px;") + output_layout.addWidget(output_info) + self.output_label = QLabel(str(self.converter.output_dir)) - self.output_label.setStyleSheet("font-family: Consolas; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;") + self.output_label.setStyleSheet("font-family: Consolas; font-size: 11px; color: #888; padding: 5px; background: #1a1a1a; border-radius: 3px;") + self.output_label.setWordWrap(True) output_layout.addWidget(self.output_label) - output_btn = QPushButton("Change Output Folder...") - output_btn.clicked.connect(self._browse_output) - output_layout.addWidget(output_btn) + change_btn = QPushButton("Change Output Folder...") + change_btn.clicked.connect(self._browse_output) + output_layout.addWidget(change_btn) left_layout.addWidget(output_group) - # Settings - settings_group = QGroupBox("⚙️ Conversion Settings") + # Settings (simplified - just 320x320) + settings_group = QGroupBox("Export Settings") settings_layout = QVBoxLayout(settings_group) - # Canvas size - size_layout = QHBoxLayout() - size_layout.addWidget(QLabel("Canvas Size:")) - self.size_combo = QComboBox() - self.size_combo.addItem("Original (no canvas)", None) - self.size_combo.addItem("64x64", (64, 64)) - self.size_combo.addItem("128x128", (128, 128)) - self.size_combo.addItem("256x256", (256, 256)) - self.size_combo.addItem("280x280 (Forum)", (280, 280)) - self.size_combo.addItem("320x320", (320, 320)) - self.size_combo.addItem("512x512", (512, 512)) - self.size_combo.setCurrentIndex(5) # Default 320x320 - size_layout.addWidget(self.size_combo) - size_layout.addStretch() - settings_layout.addLayout(size_layout) - - # Upscale method - method_layout = QHBoxLayout() - method_layout.addWidget(QLabel("Upscale:")) - self.method_combo = QComboBox() - self.method_combo.addItem("❌ No Upscaling", "none") - self.method_combo.addItem("Sharp Pixels (NEAREST)", "nearest") - self.method_combo.addItem("Smooth (HQ4x-style)", "hq4x") - self.method_combo.addItem("Photorealistic (LANCZOS)", "lanczos") - self.method_combo.setToolTip( - "None: Keep original size\n" - "NEAREST: Sharp edges (pixel art)\n" - "HQ4x: Smooth (game icons)\n" - "LANCZOS: Very smooth (photos)" + settings_info = QLabel( + "Export Format: PNG with transparent background\n" + "Canvas Size: 320x320 pixels (centered)\n" + "Upscaling: None (original icon size)" ) - method_layout.addWidget(self.method_combo) - method_layout.addStretch() - settings_layout.addLayout(method_layout) + settings_info.setStyleSheet("color: #888; font-size: 11px;") + settings_layout.addWidget(settings_info) left_layout.addWidget(settings_group) + # Nexus link + nexus_group = QGroupBox("EntropiaNexus.com") + nexus_layout = QVBoxLayout(nexus_group) + + nexus_info = QLabel( + "Submit extracted icons to help complete the\n" + "Entropia Universe item database and wiki!" + ) + nexus_info.setStyleSheet("color: #4caf50;") + nexus_layout.addWidget(nexus_info) + + nexus_btn = QPushButton("Open EntropiaNexus.com") + nexus_btn.clicked.connect(lambda: self._open_url(WEBSITE)) + nexus_layout.addWidget(nexus_btn) + + left_layout.addWidget(nexus_group) + # Convert button - self.convert_btn = QPushButton("🚀 Convert All Icons") + self.convert_btn = QPushButton("Start Extracting Icons") self.convert_btn.setMinimumHeight(60) self.convert_btn.setStyleSheet(""" QPushButton { @@ -423,9 +417,13 @@ class IconExtractorWindow(QMainWindow): right_layout = QVBoxLayout(right_panel) right_layout.setContentsMargins(0, 0, 0, 0) - files_group = QGroupBox("📄 Found Icons") + files_group = QGroupBox("Available Icons") files_layout = QVBoxLayout(files_group) + files_info = QLabel("Select icons to extract (or leave unselected to extract all)") + files_info.setStyleSheet("color: #888; font-size: 11px;") + files_layout.addWidget(files_info) + self.files_count_label = QLabel("No files found") files_layout.addWidget(self.files_count_label) @@ -446,7 +444,7 @@ class IconExtractorWindow(QMainWindow): sel_layout.addStretch() - open_folder_btn = QPushButton("📂 Open Output Folder") + open_folder_btn = QPushButton("Open Output Folder") open_folder_btn.clicked.connect(self._open_output_folder) sel_layout.addWidget(open_folder_btn) @@ -454,44 +452,44 @@ class IconExtractorWindow(QMainWindow): right_layout.addWidget(files_group) splitter.addWidget(right_panel) - splitter.setSizes([350, 550]) + splitter.setSizes([380, 520]) layout.addWidget(splitter, 1) # Footer - footer = QLabel("Entropia Universe Icon Extractor | Standalone Tool") - footer.setStyleSheet("color: #555; font-size: 11px; padding: 5px;") + footer = QLabel( + f"Developed by {DEVELOPER} | Discord: {DISCORD} | {WEBSITE}\n" + "Not affiliated with or endorsed by Entropia Universe or Mindark PE AB." + " Entropia Universe is a trademark of Mindark PE AB." + ) + footer.setStyleSheet("color: #555; font-size: 10px; padding: 5px;") footer.setAlignment(Qt.AlignmentFlag.AlignCenter) + footer.setWordWrap(True) layout.addWidget(footer) + def _open_url(self, url: str): + """Open URL in default browser.""" + import webbrowser + webbrowser.open(url) + def _load_settings(self): """Load saved settings.""" # Output folder saved_output = self.settings.value("output_dir", str(self.converter.output_dir)) self.converter.output_dir = Path(saved_output) self.output_label.setText(saved_output) - - # Canvas size - size_index = int(self.settings.value("canvas_size_index", 5)) - self.size_combo.setCurrentIndex(size_index) - - # Upscale method - method_index = int(self.settings.value("upscale_method_index", 0)) - self.method_combo.setCurrentIndex(method_index) def _save_settings(self): """Save current settings.""" self.settings.setValue("output_dir", str(self.converter.output_dir)) - self.settings.setValue("canvas_size_index", self.size_combo.currentIndex()) - self.settings.setValue("upscale_method_index", self.method_combo.currentIndex()) def _detect_subfolders(self): """Detect version subfolders in the cache directory.""" self.subfolder_combo.clear() if not self.base_cache_path.exists(): - self.cache_label.setText(f"❌ Not found: {self.base_cache_path}") - self.status_label.setText("Cache folder not found - check if path is correct") + self.cache_label.setText(f"Not found: {self.base_cache_path}") + self.status_label.setText("Cache folder not found - is Entropia Universe installed?") return # Find all subfolders that contain TGA files @@ -504,7 +502,7 @@ class IconExtractorWindow(QMainWindow): subfolders.append((item.name, tga_count, item)) if not subfolders: - self.cache_label.setText(f"⚠️ No subfolders with TGA files in {self.base_cache_path}") + self.cache_label.setText(f"No subfolders with icons in {self.base_cache_path}") self.status_label.setText("No version folders found") return @@ -517,10 +515,10 @@ class IconExtractorWindow(QMainWindow): # Add "All folders" option at top total_icons = sum(s[1] for s in subfolders) - self.subfolder_combo.insertItem(0, f"📁 All Folders ({total_icons} icons)", "all") + self.subfolder_combo.insertItem(0, f"All Folders ({total_icons} icons)", "all") self.subfolder_combo.setCurrentIndex(0) - self.cache_label.setText(f"✅ {self.base_cache_path}") + self.cache_label.setText(f"{self.base_cache_path}") self.status_label.setText(f"Found {len(subfolders)} version folders") # Load files @@ -535,23 +533,18 @@ class IconExtractorWindow(QMainWindow): self.subfolder_combo.setEnabled(not self.all_subfolders_check.isChecked()) self._refresh_file_list() - def _auto_scan(self): - """Auto-detect cache folder - just refreshes subfolder list.""" - self.status_label.setText("Scanning for version folders...") - self._detect_subfolders() - - def _browse_cache(self): - """Browse for cache folder.""" + def _browse_output(self): + """Browse for output folder.""" folder = QFileDialog.getExistingDirectory( self, - "Select Entropia Universe Cache Folder", - str(self.base_cache_path.parent) + "Select Output Folder", + str(self.converter.output_dir) ) if folder: - self.base_cache_path = Path(folder) - self.cache_label.setText(str(self.base_cache_path)) - self._detect_subfolders() + self.converter.output_dir = Path(folder) + self.output_label.setText(folder) + self._save_settings() def _refresh_file_list(self): """Refresh the list of found files based on current selection.""" @@ -602,19 +595,6 @@ class IconExtractorWindow(QMainWindow): self.convert_btn.setEnabled(len(tga_files) > 0) - def _browse_output(self): - """Browse for output folder.""" - folder = QFileDialog.getExistingDirectory( - self, - "Select Output Folder", - str(self.converter.output_dir) - ) - - if folder: - self.converter.output_dir = Path(folder) - self.output_label.setText(folder) - self._save_settings() - def _start_conversion(self): """Start batch conversion.""" # Get selected files or all files @@ -628,30 +608,21 @@ class IconExtractorWindow(QMainWindow): files_to_convert = self.found_files if not files_to_convert: - QMessageBox.warning(self, "No Files", "No files selected for conversion.") + QMessageBox.warning(self, "No Files", "No files selected for extraction.") return - # Get settings - canvas_size = self.size_combo.currentData() - upscale_method = self.method_combo.currentData() - # Save settings self._save_settings() # Setup UI self.convert_btn.setEnabled(False) - self.convert_btn.setText("⏳ Converting...") + self.convert_btn.setText("Extracting...") self.progress_bar.setRange(0, len(files_to_convert)) self.progress_bar.setValue(0) self.progress_bar.setVisible(True) # Start worker - self.worker = ConversionWorker( - files_to_convert, - self.converter, - canvas_size, - upscale_method - ) + self.worker = ConversionWorker(files_to_convert, self.converter) self.worker.progress.connect(self._on_progress) self.worker.file_done.connect(self._on_file_done) self.worker.finished.connect(self._on_finished) @@ -663,28 +634,29 @@ class IconExtractorWindow(QMainWindow): self.progress_bar.setValue(self.progress_bar.value() + 1) def _on_file_done(self, filename: str, output_path: str): - logger.info(f"Converted: {filename} -> {output_path}") + logger.info(f"Extracted: {filename} -> {output_path}") def _on_finished(self, success: int, total: int): self.convert_btn.setEnabled(True) - self.convert_btn.setText("🚀 Convert All Icons") + self.convert_btn.setText("Start Extracting Icons") self.progress_bar.setVisible(False) - self.status_label.setText(f"✅ Converted {success}/{total} files") + self.status_label.setText(f"Extracted {success}/{total} icons") QMessageBox.information( self, - "Conversion Complete", - f"Successfully converted {success} of {total} icons.\n\n" - f"Output folder:\n{self.converter.output_dir}" + "Extraction Complete", + f"Successfully extracted {success} of {total} icons.\n\n" + f"Output location:\n{self.converter.output_dir}\n\n" + f"Submit these icons to EntropiaNexus.com to help the community!" ) def _on_error(self, error_msg: str): self.convert_btn.setEnabled(True) - self.convert_btn.setText("🚀 Convert All Icons") + self.convert_btn.setText("Start Extracting Icons") self.progress_bar.setVisible(False) - self.status_label.setText(f"❌ Error: {error_msg}") + self.status_label.setText(f"Error: {error_msg}") - QMessageBox.critical(self, "Error", f"Conversion failed:\n{error_msg}") + QMessageBox.critical(self, "Error", f"Extraction failed:\n{error_msg}") def _open_output_folder(self): """Open output folder in file manager.""" @@ -709,19 +681,38 @@ class IconExtractorWindow(QMainWindow): event.accept() +def set_app_icon(app: QApplication): + """Set application icon.""" + # Try to load icon from various locations + icon_paths = [ + Path(__file__).parent / "assets" / "icon.ico", + Path(__file__).parent / "assets" / "icon.png", + Path(__file__).parent / "icon.ico", + Path(__file__).parent / "icon.png", + ] + + for icon_path in icon_paths: + if icon_path.exists(): + app.setWindowIcon(QIcon(str(icon_path))) + return + + # If no icon file, we can't set one programmatically + # User would need to provide an icon file + + def main(): """Main entry point.""" - if not PYQT_AVAILABLE: - print("ERROR: PyQt6 is required. Install with: pip install PyQt6") - sys.exit(1) - - if not PIL_AVAILABLE: - print("ERROR: Pillow is required. Install with: pip install Pillow") - sys.exit(1) - app = QApplication(sys.argv) app.setStyle('Fusion') + # Set application info for proper Windows taskbar icon + app.setApplicationName(APP_NAME) + app.setApplicationVersion(APP_VERSION) + app.setOrganizationName("ImpulsiveFPS") + + # Try to set icon + set_app_icon(app) + # Dark theme app.setStyleSheet(""" QMainWindow, QDialog { @@ -777,6 +768,10 @@ def main(): QProgressBar::chunk { background-color: #4caf50; } + QTextEdit { + background-color: #222; + border: 1px solid #444; + } """) window = IconExtractorWindow()