""" Lemontropia Suite - Inventory Scanner Dialog UI for scanning and extracting item data from Entropia Universe inventory. """ import logging from pathlib import Path from typing import Optional from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton, QListWidget, QListWidgetItem, QGroupBox, QSplitter, QTableWidget, QTableWidgetItem, QHeaderView, QTextEdit, QMessageBox, QFileDialog, QProgressBar, QCheckBox, QSpinBox, QTabWidget, QWidget, QScrollArea, QFrame ) from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer from PyQt6.QtGui import QPixmap, QImage from modules.inventory_scanner import InventoryScanner, InventoryScanResult, ItemStats logger = logging.getLogger(__name__) class InventoryScanWorker(QThread): """Background worker for inventory scanning.""" scan_complete = pyqtSignal(object) # InventoryScanResult scan_error = pyqtSignal(str) progress_update = pyqtSignal(str) def __init__(self, scanner: InventoryScanner, extract_icons: bool = True, read_details: bool = True): super().__init__() self.scanner = scanner self.extract_icons = extract_icons self.read_details = read_details self._is_running = True def run(self): """Run the scan.""" try: self.progress_update.emit("Capturing screen...") result = self.scanner.scan_inventory( extract_icons=self.extract_icons, read_details=self.read_details ) if self._is_running: self.scan_complete.emit(result) except Exception as e: if self._is_running: self.scan_error.emit(str(e)) def stop(self): """Stop the scan.""" self._is_running = False class InventoryScannerDialog(QDialog): """ Dialog for scanning Entropia Universe inventory. Features: - Auto-detect inventory window - Extract item icons - Read item stats from details panel - Save results """ def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("🔍 Inventory Scanner") self.setMinimumSize(900, 700) self.resize(1100, 800) self.scanner = InventoryScanner() self.scan_worker: Optional[InventoryScanWorker] = None self.last_result: Optional[InventoryScanResult] = None self._setup_ui() self._apply_dark_theme() def _setup_ui(self): """Setup the dialog UI.""" layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) layout.setSpacing(10) # Header header = QLabel("Inventory Scanner - Extract items and stats from Entropia Universe") header.setStyleSheet("font-size: 16px; font-weight: bold; color: #4caf50;") layout.addWidget(header) # Controls controls_layout = QHBoxLayout() # Scan buttons self.scan_btn = QPushButton("🔍 Scan Inventory") self.scan_btn.setMinimumHeight(40) self.scan_btn.setStyleSheet(""" QPushButton { background-color: #0d47a1; font-weight: bold; font-size: 12px; } QPushButton:hover { background-color: #1565c0; } """) self.scan_btn.clicked.connect(self.on_scan) controls_layout.addWidget(self.scan_btn) self.scan_details_btn = QPushButton("📋 Scan Item Details Only") self.scan_details_btn.setMinimumHeight(40) self.scan_details_btn.clicked.connect(self.on_scan_details_only) controls_layout.addWidget(self.scan_details_btn) # Options options_group = QGroupBox("Options") options_layout = QHBoxLayout(options_group) self.extract_icons_check = QCheckBox("Extract Icons") self.extract_icons_check.setChecked(True) options_layout.addWidget(self.extract_icons_check) self.read_details_check = QCheckBox("Read Item Details") self.read_details_check.setChecked(True) options_layout.addWidget(self.read_details_check) controls_layout.addWidget(options_group) controls_layout.addStretch() # Save button self.save_btn = QPushButton("💾 Save Results") self.save_btn.clicked.connect(self.on_save_results) self.save_btn.setEnabled(False) controls_layout.addWidget(self.save_btn) layout.addLayout(controls_layout) # Progress bar self.progress = QProgressBar() self.progress.setRange(0, 0) # Indeterminate self.progress.setVisible(False) layout.addWidget(self.progress) # Status label self.status_label = QLabel("Ready to scan") self.status_label.setStyleSheet("color: #888; padding: 5px;") layout.addWidget(self.status_label) # Results tabs self.results_tabs = QTabWidget() layout.addWidget(self.results_tabs) # Tab 1: Item Icons self.icons_tab = self._create_icons_tab() self.results_tabs.addTab(self.icons_tab, "🖼️ Item Icons") # Tab 2: Item Details self.details_tab = self._create_details_tab() self.results_tabs.addTab(self.details_tab, "📋 Item Details") # Tab 3: Raw Data self.raw_tab = self._create_raw_tab() self.results_tabs.addTab(self.raw_tab, "📝 Raw Data") # Help text help = QLabel( "Instructions:\n" "1. Open Entropia Universe\n" "2. Open your inventory window\n" "3. Click an item to show its details panel\n" "4. Click 'Scan Inventory' above" ) help.setStyleSheet("color: #888; padding: 10px; background-color: #252525; border-radius: 4px;") help.setWordWrap(True) layout.addWidget(help) # Close button close_btn = QPushButton("Close") close_btn.clicked.connect(self.accept) layout.addWidget(close_btn) def _create_icons_tab(self) -> QWidget: """Create the item icons tab.""" tab = QWidget() layout = QVBoxLayout(tab) # Icons count self.icons_count_label = QLabel("No icons extracted yet") layout.addWidget(self.icons_count_label) # Icons grid scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setStyleSheet("background-color: #1a1a1a;") self.icons_container = QWidget() self.icons_grid = QGridLayout(self.icons_container) self.icons_grid.setSpacing(10) scroll.setWidget(self.icons_container) layout.addWidget(scroll) return tab def _create_details_tab(self) -> QWidget: """Create the item details tab.""" tab = QWidget() layout = QVBoxLayout(tab) # Details table self.details_table = QTableWidget() self.details_table.setColumnCount(2) self.details_table.setHorizontalHeaderLabels(["Property", "Value"]) self.details_table.horizontalHeader().setStretchLastSection(True) self.details_table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) layout.addWidget(self.details_table) return tab def _create_raw_tab(self) -> QWidget: """Create the raw data tab.""" tab = QWidget() layout = QVBoxLayout(tab) self.raw_text = QTextEdit() self.raw_text.setReadOnly(True) self.raw_text.setFontFamily("Consolas") layout.addWidget(self.raw_text) return tab def on_scan(self): """Start full inventory scan.""" self._start_scan( extract_icons=self.extract_icons_check.isChecked(), read_details=self.read_details_check.isChecked() ) def on_scan_details_only(self): """Scan only item details.""" self._start_scan(extract_icons=False, read_details=True) def _start_scan(self, extract_icons: bool, read_details: bool): """Start scan worker.""" if self.scan_worker and self.scan_worker.isRunning(): QMessageBox.warning(self, "Scan in Progress", "A scan is already running.") return # Update UI self.scan_btn.setEnabled(False) self.scan_details_btn.setEnabled(False) self.progress.setVisible(True) self.status_label.setText("Scanning...") self.save_btn.setEnabled(False) # Clear previous results self._clear_icons_grid() self.details_table.setRowCount(0) self.raw_text.clear() # Start worker self.scan_worker = InventoryScanWorker( self.scanner, extract_icons=extract_icons, read_details=read_details ) self.scan_worker.scan_complete.connect(self._on_scan_complete) self.scan_worker.scan_error.connect(self._on_scan_error) self.scan_worker.progress_update.connect(self._on_progress_update) self.scan_worker.start() def _on_scan_complete(self, result: InventoryScanResult): """Handle scan completion.""" self.last_result = result # Update UI self.progress.setVisible(False) self.scan_btn.setEnabled(True) self.scan_details_btn.setEnabled(True) self.save_btn.setEnabled(True) self.status_label.setText( f"Scan complete: {len(result.items)} icons, " f"details: {'Yes' if result.details_item else 'No'}" ) # Update tabs self._update_icons_tab(result) self._update_details_tab(result) self._update_raw_tab(result) QMessageBox.information( self, "Scan Complete", f"Found {len(result.items)} items\n" f"Item details: {'Yes' if result.details_item else 'No'}" ) def _on_scan_error(self, error: str): """Handle scan error.""" self.progress.setVisible(False) self.scan_btn.setEnabled(True) self.scan_details_btn.setEnabled(True) self.status_label.setText(f"Error: {error}") QMessageBox.critical(self, "Scan Error", error) def _on_progress_update(self, message: str): """Handle progress update.""" self.status_label.setText(message) def _clear_icons_grid(self): """Clear icons grid.""" while self.icons_grid.count(): item = self.icons_grid.takeAt(0) if item.widget(): item.widget().deleteLater() def _update_icons_tab(self, result: InventoryScanResult): """Update icons tab with results.""" self.icons_count_label.setText(f"Extracted {len(result.items)} item icons") self._clear_icons_grid() for i, item in enumerate(result.items): if item.icon_path and Path(item.icon_path).exists(): # Create icon widget icon_widget = QFrame() icon_widget.setStyleSheet(""" QFrame { background-color: #2a2a2a; border: 1px solid #444; border-radius: 4px; padding: 5px; } """) icon_layout = QVBoxLayout(icon_widget) # Image pixmap = QPixmap(item.icon_path) if not pixmap.isNull(): scaled = pixmap.scaled(64, 64, Qt.AspectRatioMode.KeepAspectRatio) img_label = QLabel() img_label.setPixmap(scaled) img_label.setAlignment(Qt.AlignmentFlag.AlignCenter) icon_layout.addWidget(img_label) # Info info = QLabel(f"Slot: {item.slot_position}\nHash: {item.icon_hash[:8]}...") info.setStyleSheet("font-size: 10px; color: #888;") icon_layout.addWidget(info) # Add to grid (5 columns) row = i // 5 col = i % 5 self.icons_grid.addWidget(icon_widget, row, col) def _update_details_tab(self, result: InventoryScanResult): """Update details tab with results.""" if not result.details_item: self.details_table.setRowCount(1) self.details_table.setItem(0, 0, QTableWidgetItem("Status")) self.details_table.setItem(0, 1, QTableWidgetItem("No item details detected")) return stats = result.details_item properties = [ ("Item Name", stats.item_name), ("Class", stats.item_class), ("Damage", f"{stats.damage:.2f}" if stats.damage else "N/A"), ("Range", f"{stats.range:.2f}" if stats.range else "N/A"), ("Attacks/Min", str(stats.attacks_per_min) if stats.attacks_per_min else "N/A"), ("Decay (PEC)", f"{stats.decay:.4f}" if stats.decay else "N/A"), ("Ammo Burn (PEC)", f"{stats.ammo_burn:.4f}" if stats.ammo_burn else "N/A"), ("DPP", f"{stats.damage_per_pec:.4f}" if stats.damage_per_pec else "N/A"), ("Weight", f"{stats.weight:.2f}" if stats.weight else "N/A"), ("Level", str(stats.level) if stats.level else "N/A"), ("Durability", f"{stats.durability:.1f}%" if stats.durability else "N/A"), ("Protection Impact", f"{stats.protection_impact:.1f}" if stats.protection_impact else "N/A"), ("Protection Stab", f"{stats.protection_stab:.1f}" if stats.protection_stab else "N/A"), ] self.details_table.setRowCount(len(properties)) for row, (prop, value) in enumerate(properties): self.details_table.setItem(row, 0, QTableWidgetItem(prop)) self.details_table.setItem(row, 1, QTableWidgetItem(value)) def _update_raw_tab(self, result: InventoryScanResult): """Update raw data tab.""" import json text = json.dumps(result.to_dict(), indent=2) self.raw_text.setText(text) def on_save_results(self): """Save scan results to file.""" if not self.last_result: return filepath, _ = QFileDialog.getSaveFileName( self, "Save Inventory Scan", str(Path.home() / f"inventory_scan_{datetime.now():%Y%m%d_%H%M%S}.json"), "JSON Files (*.json)" ) if filepath: try: self.last_result.save(filepath) QMessageBox.information(self, "Saved", f"Results saved to:\n{filepath}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save:\n{e}") def closeEvent(self, event): """Handle dialog close.""" if self.scan_worker and self.scan_worker.isRunning(): self.scan_worker.stop() self.scan_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; } QTableWidget { background-color: #252525; border: 1px solid #444; } QHeaderView::section { background-color: #2d2d2d; padding: 6px; border: none; border-right: 1px solid #444; } QTextEdit { background-color: #1a1a1a; border: 1px solid #444; color: #d0d0d0; } """)