feat: add TGA Icon Converter for game cache files
- New module: modules/tga_converter.py - Reads Truevision TGA format from game cache - Converts to PNG with proper color handling (BGR->RGB) - Auto-detects cache folder location - Batch conversion support - Handles alpha channels - New dialog: ui/tga_converter_dialog.py - Visual interface for TGA conversion - Shows list of cached .tga files - Progress tracking during conversion - One-click convert all - Added to Tools → Computer Vision → TGA Icon Converter (Ctrl+T) This is much more efficient than extracting icons from screenshots! The game already has the icons in cache as .tga files.
This commit is contained in:
parent
adc76e42cc
commit
acca0d3491
|
|
@ -0,0 +1,392 @@
|
|||
"""
|
||||
Lemontropia Suite - TGA Icon Converter
|
||||
Convert Entropia Universe cached .tga icons to PNG format.
|
||||
|
||||
The game stores item icons as .tga files in the cache folder.
|
||||
This module converts them to PNG for easier use.
|
||||
"""
|
||||
|
||||
import struct
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Tuple, Dict
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
PIL_AVAILABLE = True
|
||||
except ImportError:
|
||||
PIL_AVAILABLE = False
|
||||
Image = None
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TGAFile:
|
||||
"""Represents a TGA file with metadata."""
|
||||
filepath: Path
|
||||
width: int
|
||||
height: int
|
||||
pixel_depth: int
|
||||
has_alpha: bool
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"TGA({self.width}x{self.height}, {self.pixel_depth}bpp, alpha={self.has_alpha})"
|
||||
|
||||
|
||||
class TGAConverter:
|
||||
"""
|
||||
Converter for Entropia Universe TGA icon files.
|
||||
|
||||
Usage:
|
||||
converter = TGAConverter()
|
||||
|
||||
# Convert single file
|
||||
png_path = converter.convert_tga_to_png("item_icon.tga")
|
||||
|
||||
# Batch convert all cached icons
|
||||
results = converter.convert_cache_folder()
|
||||
"""
|
||||
|
||||
# Common Entropia Universe cache locations
|
||||
DEFAULT_CACHE_PATHS = [
|
||||
Path.home() / "Documents" / "Entropia Universe" / "cache" / "icons",
|
||||
Path.home() / "Documents" / "Entropia Universe" / "cache",
|
||||
Path("C:") / "ProgramData" / "Entropia Universe" / "cache" / "icons",
|
||||
Path("C:") / "Program Files (x86)" / "Entropia Universe" / "cache" / "icons",
|
||||
]
|
||||
|
||||
def __init__(self, output_dir: Optional[Path] = None):
|
||||
"""
|
||||
Initialize TGA converter.
|
||||
|
||||
Args:
|
||||
output_dir: Directory for converted PNG files.
|
||||
Default: Documents/Entropia Universe/Icons/
|
||||
"""
|
||||
if output_dir is None:
|
||||
output_dir = Path.home() / "Documents" / "Entropia Universe" / "Icons"
|
||||
|
||||
self.output_dir = output_dir
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._cache_path: Optional[Path] = None
|
||||
|
||||
if not PIL_AVAILABLE:
|
||||
logger.warning("PIL/Pillow not available. Install with: pip install Pillow")
|
||||
|
||||
def find_cache_folder(self) -> Optional[Path]:
|
||||
"""
|
||||
Find the Entropia Universe icon cache folder.
|
||||
|
||||
Returns:
|
||||
Path to cache folder or None if not found
|
||||
"""
|
||||
# Check default locations
|
||||
for path in self.DEFAULT_CACHE_PATHS:
|
||||
if path.exists():
|
||||
logger.info(f"Found cache folder: {path}")
|
||||
self._cache_path = path
|
||||
return path
|
||||
|
||||
# Try to find by looking for .tga files
|
||||
logger.info("Searching for .tga files...")
|
||||
docs_path = Path.home() / "Documents" / "Entropia Universe"
|
||||
if docs_path.exists():
|
||||
for tga_file in docs_path.rglob("*.tga"):
|
||||
cache_path = tga_file.parent
|
||||
logger.info(f"Found cache folder via search: {cache_path}")
|
||||
self._cache_path = cache_path
|
||||
return cache_path
|
||||
|
||||
logger.warning("Could not find Entropia Universe cache folder")
|
||||
return None
|
||||
|
||||
def read_tga_header(self, filepath: Path) -> Optional[TGAFile]:
|
||||
"""
|
||||
Read TGA file header.
|
||||
|
||||
Args:
|
||||
filepath: Path to .tga file
|
||||
|
||||
Returns:
|
||||
TGAFile with metadata or None if invalid
|
||||
"""
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
# Read TGA header (18 bytes)
|
||||
header = f.read(18)
|
||||
|
||||
if len(header) < 18:
|
||||
logger.warning(f"Invalid TGA file (too small): {filepath}")
|
||||
return None
|
||||
|
||||
# Unpack header
|
||||
# Format: id_length, color_map_type, image_type, color_map_spec(5),
|
||||
# x_origin(2), y_origin(2), width(2), height(2), pixel_depth, descriptor
|
||||
id_length = header[0]
|
||||
color_map_type = header[1]
|
||||
image_type = header[2]
|
||||
|
||||
# Skip color map specification (5 bytes)
|
||||
x_origin = struct.unpack('<H', header[8:10])[0]
|
||||
y_origin = struct.unpack('<H', header[10:12])[0]
|
||||
width = struct.unpack('<H', header[12:14])[0]
|
||||
height = struct.unpack('<H', header[14:16])[0]
|
||||
pixel_depth = header[16]
|
||||
image_descriptor = header[17]
|
||||
|
||||
# Check if valid TGA
|
||||
if width == 0 or height == 0:
|
||||
logger.warning(f"Invalid TGA dimensions: {filepath}")
|
||||
return None
|
||||
|
||||
has_alpha = (pixel_depth == 32) or (image_descriptor & 0x0F > 0)
|
||||
|
||||
return TGAFile(
|
||||
filepath=filepath,
|
||||
width=width,
|
||||
height=height,
|
||||
pixel_depth=pixel_depth,
|
||||
has_alpha=has_alpha
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading TGA header: {e}")
|
||||
return None
|
||||
|
||||
def read_tga_pixels(self, tga_file: TGAFile) -> Optional[np.ndarray]:
|
||||
"""
|
||||
Read pixel data from TGA file.
|
||||
|
||||
Args:
|
||||
tga_file: TGAFile with metadata
|
||||
|
||||
Returns:
|
||||
Numpy array of pixels (H, W, C) or None
|
||||
"""
|
||||
try:
|
||||
with open(tga_file.filepath, 'rb') as f:
|
||||
# Skip header
|
||||
f.seek(18)
|
||||
|
||||
# Skip ID field if present
|
||||
header = f.read(18)
|
||||
id_length = header[0] if len(header) >= 18 else 0
|
||||
if id_length > 0:
|
||||
f.seek(18 + id_length)
|
||||
|
||||
# Calculate pixel data size
|
||||
bytes_per_pixel = tga_file.pixel_depth // 8
|
||||
pixel_data_size = tga_file.width * tga_file.height * bytes_per_pixel
|
||||
|
||||
# Read pixel data
|
||||
pixel_data = f.read(pixel_data_size)
|
||||
|
||||
if len(pixel_data) < pixel_data_size:
|
||||
logger.warning(f"Incomplete TGA pixel data: {tga_file.filepath}")
|
||||
return None
|
||||
|
||||
# Convert to numpy array
|
||||
pixels = np.frombuffer(pixel_data, dtype=np.uint8)
|
||||
|
||||
# Reshape based on format
|
||||
if bytes_per_pixel == 4:
|
||||
# BGRA format
|
||||
pixels = pixels.reshape((tga_file.height, tga_file.width, 4))
|
||||
# Convert BGRA to RGBA
|
||||
pixels = pixels[:, :, [2, 1, 0, 3]]
|
||||
elif bytes_per_pixel == 3:
|
||||
# BGR format
|
||||
pixels = pixels.reshape((tga_file.height, tga_file.width, 3))
|
||||
# Convert BGR to RGB
|
||||
pixels = pixels[:, :, [2, 1, 0]]
|
||||
elif bytes_per_pixel == 2:
|
||||
# 16-bit format - convert to RGB
|
||||
pixels = pixels.view(np.uint16).reshape((tga_file.height, tga_file.width))
|
||||
# Simple conversion (not accurate but works)
|
||||
pixels = np.stack([
|
||||
((pixels >> 10) & 0x1F) << 3,
|
||||
((pixels >> 5) & 0x1F) << 3,
|
||||
(pixels & 0x1F) << 3
|
||||
], axis=-1).astype(np.uint8)
|
||||
else:
|
||||
logger.warning(f"Unsupported pixel depth: {tga_file.pixel_depth}")
|
||||
return None
|
||||
|
||||
# Flip vertically (TGA stores bottom-to-top)
|
||||
pixels = np.flipud(pixels)
|
||||
|
||||
return pixels
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading TGA pixels: {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.
|
||||
|
||||
Args:
|
||||
tga_path: Path to .tga file
|
||||
output_name: Optional output filename (without extension)
|
||||
|
||||
Returns:
|
||||
Path to converted PNG file or None if failed
|
||||
"""
|
||||
if not PIL_AVAILABLE:
|
||||
logger.error("PIL/Pillow required for TGA to PNG conversion")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Read TGA
|
||||
tga_file = self.read_tga_header(tga_path)
|
||||
if not tga_file:
|
||||
return None
|
||||
|
||||
logger.info(f"Converting: {tga_path.name} ({tga_file})")
|
||||
|
||||
pixels = self.read_tga_pixels(tga_file)
|
||||
if pixels is None:
|
||||
return None
|
||||
|
||||
# Create PIL Image
|
||||
if pixels.shape[2] == 4:
|
||||
# RGBA
|
||||
image = Image.fromarray(pixels, 'RGBA')
|
||||
else:
|
||||
# RGB
|
||||
image = Image.fromarray(pixels, 'RGB')
|
||||
|
||||
# Determine output path
|
||||
if output_name is None:
|
||||
output_name = tga_path.stem
|
||||
|
||||
png_path = self.output_dir / f"{output_name}.png"
|
||||
|
||||
# Save as PNG
|
||||
image.save(png_path, 'PNG')
|
||||
|
||||
logger.info(f"Saved: {png_path}")
|
||||
return png_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error converting {tga_path}: {e}")
|
||||
return None
|
||||
|
||||
def convert_cache_folder(self, cache_path: Optional[Path] = None) -> Dict[str, Path]:
|
||||
"""
|
||||
Convert all TGA files in cache folder to PNG.
|
||||
|
||||
Args:
|
||||
cache_path: Path to cache folder (auto-detect if None)
|
||||
|
||||
Returns:
|
||||
Dictionary mapping TGA filenames to PNG paths
|
||||
"""
|
||||
results = {}
|
||||
|
||||
if cache_path is None:
|
||||
cache_path = self.find_cache_folder()
|
||||
|
||||
if not cache_path or not cache_path.exists():
|
||||
logger.error("Cache folder not found")
|
||||
return results
|
||||
|
||||
# Find all TGA files
|
||||
tga_files = list(cache_path.glob("*.tga"))
|
||||
|
||||
if not tga_files:
|
||||
logger.warning(f"No .tga files found in: {cache_path}")
|
||||
return results
|
||||
|
||||
logger.info(f"Found {len(tga_files)} TGA files to convert")
|
||||
|
||||
# Convert each file
|
||||
for i, tga_path in enumerate(tga_files, 1):
|
||||
logger.info(f"[{i}/{len(tga_files)}] Converting: {tga_path.name}")
|
||||
png_path = self.convert_tga_to_png(tga_path)
|
||||
if png_path:
|
||||
results[tga_path.name] = png_path
|
||||
|
||||
logger.info(f"Converted {len(results)}/{len(tga_files)} files successfully")
|
||||
logger.info(f"Output directory: {self.output_dir}")
|
||||
|
||||
return results
|
||||
|
||||
def get_conversion_summary(self) -> str:
|
||||
"""Get a summary of available TGA files and conversion status."""
|
||||
cache_path = self.find_cache_folder()
|
||||
|
||||
if not cache_path:
|
||||
return "❌ Cache folder not found"
|
||||
|
||||
tga_files = list(cache_path.glob("*.tga"))
|
||||
png_files = list(self.output_dir.glob("*.png"))
|
||||
|
||||
lines = [
|
||||
"📁 TGA Icon Cache Summary",
|
||||
"",
|
||||
f"Cache location: {cache_path}",
|
||||
f"TGA files found: {len(tga_files)}",
|
||||
f"PNG files converted: {len(png_files)}",
|
||||
f"Output directory: {self.output_dir}",
|
||||
"",
|
||||
]
|
||||
|
||||
if tga_files:
|
||||
lines.append("Sample TGA files:")
|
||||
for tga in tga_files[:5]:
|
||||
tga_info = self.read_tga_header(tga)
|
||||
if tga_info:
|
||||
lines.append(f" • {tga.name} ({tga_info.width}x{tga_info.height})")
|
||||
else:
|
||||
lines.append(f" • {tga.name} (invalid)")
|
||||
if len(tga_files) > 5:
|
||||
lines.append(f" ... and {len(tga_files) - 5} more")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Convenience functions
|
||||
def convert_tga_to_png(tga_path: str, output_dir: Optional[str] = None) -> Optional[str]:
|
||||
"""Quick convert single TGA file."""
|
||||
converter = TGAConverter(Path(output_dir) if output_dir else None)
|
||||
result = converter.convert_tga_to_png(Path(tga_path))
|
||||
return str(result) if result else None
|
||||
|
||||
|
||||
def convert_all_cache_icons(output_dir: Optional[str] = None) -> List[str]:
|
||||
"""Convert all cached icons."""
|
||||
converter = TGAConverter(Path(output_dir) if output_dir else None)
|
||||
results = converter.convert_cache_folder()
|
||||
return [str(path) for path in results.values()]
|
||||
|
||||
|
||||
def main():
|
||||
"""CLI interface for TGA conversion."""
|
||||
import sys
|
||||
|
||||
print("🔧 Lemontropia Suite - TGA Icon Converter")
|
||||
print("=" * 50)
|
||||
|
||||
converter = TGAConverter()
|
||||
|
||||
# Show summary
|
||||
print(converter.get_conversion_summary())
|
||||
print()
|
||||
|
||||
# Ask to convert
|
||||
response = input("\nConvert all TGA files to PNG? (y/n): ")
|
||||
if response.lower() == 'y':
|
||||
results = converter.convert_cache_folder()
|
||||
print(f"\n✅ Converted {len(results)} files to:")
|
||||
print(converter.output_dir)
|
||||
else:
|
||||
print("Cancelled.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -121,6 +121,7 @@ from ui.session_history import SessionHistoryDialog
|
|||
from ui.gallery_dialog import GalleryDialog, ScreenshotCapture
|
||||
from ui.settings_dialog import SettingsDialog
|
||||
from ui.inventory_scanner_dialog import InventoryScannerDialog
|
||||
from ui.tga_converter_dialog import TGAConverterDialog
|
||||
|
||||
# ============================================================================
|
||||
# Screenshot Hotkey Integration
|
||||
|
|
@ -761,6 +762,12 @@ class MainWindow(QMainWindow):
|
|||
inventory_scan_action.triggered.connect(self.on_inventory_scan)
|
||||
vision_menu.addAction(inventory_scan_action)
|
||||
|
||||
# TGA Icon Converter
|
||||
tga_convert_action = QAction("🔧 &TGA Icon Converter", self)
|
||||
tga_convert_action.setShortcut("Ctrl+T")
|
||||
tga_convert_action.triggered.connect(self.on_tga_convert)
|
||||
vision_menu.addAction(tga_convert_action)
|
||||
|
||||
# View menu
|
||||
view_menu = menubar.addMenu("&View")
|
||||
|
||||
|
|
@ -1913,6 +1920,15 @@ class MainWindow(QMainWindow):
|
|||
self.log_error("Vision", f"Failed to open inventory scanner: {e}")
|
||||
QMessageBox.warning(self, "Error", f"Could not open Inventory Scanner: {e}")
|
||||
|
||||
def on_tga_convert(self):
|
||||
"""Open TGA Icon Converter dialog."""
|
||||
try:
|
||||
dialog = TGAConverterDialog(self)
|
||||
dialog.exec()
|
||||
except Exception as e:
|
||||
self.log_error("Vision", f"Failed to open TGA converter: {e}")
|
||||
QMessageBox.warning(self, "Error", f"Could not open TGA Converter: {e}")
|
||||
|
||||
# ========================================================================
|
||||
# Settings Management
|
||||
# ========================================================================
|
||||
|
|
|
|||
|
|
@ -0,0 +1,374 @@
|
|||
"""
|
||||
Lemontropia Suite - TGA Icon Converter Dialog
|
||||
UI for converting Entropia Universe cached .tga icons to PNG.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QLabel, QPushButton, QListWidget, QListWidgetItem,
|
||||
QGroupBox, QProgressBar, QTextEdit, QFileDialog,
|
||||
QMessageBox, QSpinBox, QCheckBox
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QPixmap
|
||||
|
||||
from modules.tga_converter import TGAConverter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TGAConvertWorker(QThread):
|
||||
"""Background worker for TGA conversion."""
|
||||
|
||||
progress_update = pyqtSignal(str)
|
||||
file_converted = pyqtSignal(str, str) # tga_name, png_path
|
||||
conversion_complete = pyqtSignal(int, int) # success_count, total_count
|
||||
conversion_error = pyqtSignal(str)
|
||||
|
||||
def __init__(self, converter: TGAConverter):
|
||||
super().__init__()
|
||||
self.converter = converter
|
||||
self._is_running = True
|
||||
|
||||
def run(self):
|
||||
"""Run the conversion."""
|
||||
try:
|
||||
# Find cache folder
|
||||
self.progress_update.emit("Finding cache folder...")
|
||||
cache_path = self.converter.find_cache_folder()
|
||||
|
||||
if not cache_path:
|
||||
self.conversion_error.emit("Cache folder not found")
|
||||
return
|
||||
|
||||
# Get list of TGA files
|
||||
tga_files = list(cache_path.glob("*.tga"))
|
||||
total = len(tga_files)
|
||||
success = 0
|
||||
|
||||
self.progress_update.emit(f"Found {total} TGA files")
|
||||
|
||||
for i, tga_path in enumerate(tga_files):
|
||||
if not self._is_running:
|
||||
break
|
||||
|
||||
self.progress_update.emit(f"[{i+1}/{total}] Converting: {tga_path.name}")
|
||||
|
||||
png_path = self.converter.convert_tga_to_png(tga_path)
|
||||
if png_path:
|
||||
success += 1
|
||||
self.file_converted.emit(tga_path.name, str(png_path))
|
||||
|
||||
self.conversion_complete.emit(success, total)
|
||||
|
||||
except Exception as e:
|
||||
self.conversion_error.emit(str(e))
|
||||
|
||||
def stop(self):
|
||||
"""Stop the conversion."""
|
||||
self._is_running = False
|
||||
|
||||
|
||||
class TGAConverterDialog(QDialog):
|
||||
"""
|
||||
Dialog for converting Entropia Universe TGA icons to PNG.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("🔧 TGA Icon Converter")
|
||||
self.setMinimumSize(700, 600)
|
||||
self.resize(800, 700)
|
||||
|
||||
self.converter = TGAConverter()
|
||||
self.convert_worker: Optional[TGAConvertWorker] = None
|
||||
self.converted_files = []
|
||||
|
||||
self._setup_ui()
|
||||
self._apply_dark_theme()
|
||||
|
||||
# Auto-scan on open
|
||||
self._scan_cache()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Setup the dialog UI."""
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(15, 15, 15, 15)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# Header
|
||||
header = QLabel("🔧 TGA Icon Converter")
|
||||
header.setStyleSheet("font-size: 18px; font-weight: bold; color: #4caf50;")
|
||||
layout.addWidget(header)
|
||||
|
||||
desc = QLabel(
|
||||
"Convert Entropia Universe cached .tga icon files to PNG format.\n"
|
||||
"The game stores item icons in the cache folder as .tga files."
|
||||
)
|
||||
desc.setStyleSheet("color: #888; padding: 5px;")
|
||||
desc.setWordWrap(True)
|
||||
layout.addWidget(desc)
|
||||
|
||||
# Cache folder info
|
||||
cache_group = QGroupBox("📁 Cache Folder")
|
||||
cache_layout = QVBoxLayout(cache_group)
|
||||
|
||||
self.cache_path_label = QLabel("Searching...")
|
||||
self.cache_path_label.setStyleSheet("font-family: Consolas; color: #888;")
|
||||
cache_layout.addWidget(self.cache_path_label)
|
||||
|
||||
cache_buttons = QHBoxLayout()
|
||||
|
||||
self.scan_btn = QPushButton("🔍 Scan Cache")
|
||||
self.scan_btn.clicked.connect(self._scan_cache)
|
||||
cache_buttons.addWidget(self.scan_btn)
|
||||
|
||||
browse_cache_btn = QPushButton("Browse...")
|
||||
browse_cache_btn.clicked.connect(self._browse_cache_folder)
|
||||
cache_buttons.addWidget(browse_cache_btn)
|
||||
|
||||
cache_buttons.addStretch()
|
||||
cache_layout.addLayout(cache_buttons)
|
||||
|
||||
layout.addWidget(cache_group)
|
||||
|
||||
# TGA Files list
|
||||
files_group = QGroupBox("📄 TGA Files Found")
|
||||
files_layout = QVBoxLayout(files_group)
|
||||
|
||||
self.files_count_label = QLabel("No files found")
|
||||
files_layout.addWidget(self.files_count_label)
|
||||
|
||||
self.files_list = QListWidget()
|
||||
self.files_list.setMaximumHeight(150)
|
||||
files_layout.addWidget(self.files_list)
|
||||
|
||||
layout.addWidget(files_group)
|
||||
|
||||
# Output settings
|
||||
output_group = QGroupBox("💾 Output Settings")
|
||||
output_layout = QHBoxLayout(output_group)
|
||||
|
||||
output_layout.addWidget(QLabel("Output folder:"))
|
||||
|
||||
self.output_path_label = QLabel(str(self.converter.output_dir))
|
||||
self.output_path_label.setStyleSheet("font-family: Consolas; color: #888;")
|
||||
output_layout.addWidget(self.output_path_label, 1)
|
||||
|
||||
output_browse = QPushButton("Browse...")
|
||||
output_browse.clicked.connect(self._browse_output_folder)
|
||||
output_layout.addWidget(output_browse)
|
||||
|
||||
layout.addWidget(output_group)
|
||||
|
||||
# Convert button
|
||||
self.convert_btn = QPushButton("🚀 Convert All to PNG")
|
||||
self.convert_btn.setMinimumHeight(50)
|
||||
self.convert_btn.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #0d47a1;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
QPushButton:hover { background-color: #1565c0; }
|
||||
QPushButton:disabled { background-color: #333; }
|
||||
""")
|
||||
self.convert_btn.clicked.connect(self._start_conversion)
|
||||
layout.addWidget(self.convert_btn)
|
||||
|
||||
# Progress
|
||||
self.progress_bar = QProgressBar()
|
||||
self.progress_bar.setRange(0, 0) # Indeterminate
|
||||
self.progress_bar.setVisible(False)
|
||||
layout.addWidget(self.progress_bar)
|
||||
|
||||
self.status_label = QLabel("Ready")
|
||||
self.status_label.setStyleSheet("color: #888; padding: 5px;")
|
||||
layout.addWidget(self.status_label)
|
||||
|
||||
# Results
|
||||
results_group = QGroupBox("✅ Converted Files")
|
||||
results_layout = QVBoxLayout(results_group)
|
||||
|
||||
self.results_list = QListWidget()
|
||||
results_layout.addWidget(self.results_list)
|
||||
|
||||
results_buttons = QHBoxLayout()
|
||||
|
||||
open_folder_btn = QPushButton("📂 Open Output Folder")
|
||||
open_folder_btn.clicked.connect(self._open_output_folder)
|
||||
results_buttons.addWidget(open_folder_btn)
|
||||
|
||||
results_buttons.addStretch()
|
||||
results_layout.addLayout(results_buttons)
|
||||
|
||||
layout.addWidget(results_group)
|
||||
|
||||
# Close button
|
||||
close_btn = QPushButton("Close")
|
||||
close_btn.clicked.connect(self.accept)
|
||||
layout.addWidget(close_btn)
|
||||
|
||||
def _scan_cache(self):
|
||||
"""Scan for TGA files in cache."""
|
||||
self.files_list.clear()
|
||||
self.files_count_label.setText("Scanning...")
|
||||
|
||||
cache_path = self.converter.find_cache_folder()
|
||||
|
||||
if cache_path:
|
||||
self.cache_path_label.setText(str(cache_path))
|
||||
|
||||
tga_files = list(cache_path.glob("*.tga"))
|
||||
self.files_count_label.setText(f"Found {len(tga_files)} TGA files")
|
||||
|
||||
for tga_file in tga_files:
|
||||
item = QListWidgetItem(tga_file.name)
|
||||
# Try to get TGA info
|
||||
tga_info = self.converter.read_tga_header(tga_file)
|
||||
if tga_info:
|
||||
item.setToolTip(f"{tga_info.width}x{tga_info.height}, {tga_info.pixel_depth}bpp")
|
||||
self.files_list.addItem(item)
|
||||
|
||||
self.convert_btn.setEnabled(len(tga_files) > 0)
|
||||
else:
|
||||
self.cache_path_label.setText("❌ Not found")
|
||||
self.files_count_label.setText("Cache folder not found")
|
||||
self.convert_btn.setEnabled(False)
|
||||
|
||||
def _browse_cache_folder(self):
|
||||
"""Browse for cache folder."""
|
||||
dir_path = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"Select Entropia Universe Cache Folder",
|
||||
str(Path.home() / "Documents" / "Entropia Universe"),
|
||||
QFileDialog.Option.ShowDirsOnly
|
||||
)
|
||||
|
||||
if dir_path:
|
||||
self.converter._cache_path = Path(dir_path)
|
||||
self._scan_cache()
|
||||
|
||||
def _browse_output_folder(self):
|
||||
"""Browse for output folder."""
|
||||
dir_path = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"Select Output Folder",
|
||||
str(self.converter.output_dir),
|
||||
QFileDialog.Option.ShowDirsOnly
|
||||
)
|
||||
|
||||
if dir_path:
|
||||
self.converter.output_dir = Path(dir_path)
|
||||
self.output_path_label.setText(dir_path)
|
||||
|
||||
def _start_conversion(self):
|
||||
"""Start TGA to PNG conversion."""
|
||||
if self.convert_worker and self.convert_worker.isRunning():
|
||||
QMessageBox.warning(self, "Busy", "Conversion already in progress")
|
||||
return
|
||||
|
||||
self.convert_btn.setEnabled(False)
|
||||
self.progress_bar.setVisible(True)
|
||||
self.results_list.clear()
|
||||
self.converted_files = []
|
||||
|
||||
# Start worker
|
||||
self.convert_worker = TGAConvertWorker(self.converter)
|
||||
self.convert_worker.progress_update.connect(self._on_progress)
|
||||
self.convert_worker.file_converted.connect(self._on_file_converted)
|
||||
self.convert_worker.conversion_complete.connect(self._on_conversion_complete)
|
||||
self.convert_worker.conversion_error.connect(self._on_conversion_error)
|
||||
self.convert_worker.start()
|
||||
|
||||
def _on_progress(self, message: str):
|
||||
"""Handle progress update."""
|
||||
self.status_label.setText(message)
|
||||
|
||||
def _on_file_converted(self, tga_name: str, png_path: str):
|
||||
"""Handle file converted."""
|
||||
self.converted_files.append(png_path)
|
||||
item = QListWidgetItem(f"✅ {tga_name} → {Path(png_path).name}")
|
||||
self.results_list.addItem(item)
|
||||
|
||||
def _on_conversion_complete(self, success: int, total: int):
|
||||
"""Handle conversion complete."""
|
||||
self.progress_bar.setVisible(False)
|
||||
self.convert_btn.setEnabled(True)
|
||||
self.status_label.setText(f"Complete: {success}/{total} files converted")
|
||||
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"Conversion Complete",
|
||||
f"Successfully converted {success} of {total} files\n\n"
|
||||
f"Output: {self.converter.output_dir}"
|
||||
)
|
||||
|
||||
def _on_conversion_error(self, error: str):
|
||||
"""Handle conversion error."""
|
||||
self.progress_bar.setVisible(False)
|
||||
self.convert_btn.setEnabled(True)
|
||||
self.status_label.setText(f"Error: {error}")
|
||||
|
||||
QMessageBox.critical(self, "Error", error)
|
||||
|
||||
def _open_output_folder(self):
|
||||
"""Open output folder in file explorer."""
|
||||
import os
|
||||
import platform
|
||||
|
||||
output_path = str(self.converter.output_dir)
|
||||
|
||||
if platform.system() == "Windows":
|
||||
os.startfile(output_path)
|
||||
elif platform.system() == "Darwin": # macOS
|
||||
import subprocess
|
||||
subprocess.run(["open", output_path])
|
||||
else: # Linux
|
||||
import subprocess
|
||||
subprocess.run(["xdg-open", output_path])
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle dialog close."""
|
||||
if self.convert_worker and self.convert_worker.isRunning():
|
||||
self.convert_worker.stop()
|
||||
self.convert_worker.wait(1000)
|
||||
event.accept()
|
||||
|
||||
def _apply_dark_theme(self):
|
||||
"""Apply dark theme."""
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
QGroupBox {
|
||||
font-weight: bold;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
QListWidget {
|
||||
background-color: #252525;
|
||||
border: 1px solid #444;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #0d47a1;
|
||||
border: 1px solid #1565c0;
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
color: white;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #1565c0;
|
||||
}
|
||||
QLabel {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
""")
|
||||
Loading…
Reference in New Issue