Lemontropia-Suite/ui/inventory_scanner_dialog.py

453 lines
16 KiB
Python

"""
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;
}
""")