2094 lines
79 KiB
Python
2094 lines
79 KiB
Python
"""
|
|
Lemontropia Suite - Main Application Window (Session-Focused Redesign)
|
|
PyQt6 GUI for managing game automation sessions and activities.
|
|
"""
|
|
|
|
# Fix PyTorch DLL loading on Windows - MUST be before PyQt imports
|
|
import sys
|
|
import platform
|
|
if platform.system() == "Windows":
|
|
try:
|
|
import torch
|
|
# Force torch to load its DLLs before PyQt
|
|
_ = torch.__version__
|
|
except Exception:
|
|
pass # Torch not available, will be handled later
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from enum import Enum, auto
|
|
from typing import Optional, List, Callable, Any
|
|
from dataclasses import dataclass
|
|
|
|
# Setup logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
from PyQt6.QtWidgets import (
|
|
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QSplitter, QPushButton, QListWidget, QListWidgetItem,
|
|
QTextEdit, QLabel, QStatusBar, QMenuBar, QMenu,
|
|
QDialog, QLineEdit, QFormLayout, QDialogButtonBox,
|
|
QMessageBox, QGroupBox, QFrame, QApplication,
|
|
QTreeWidget, QTreeWidgetItem, QHeaderView, QComboBox,
|
|
QGridLayout, QToolButton, QCheckBox
|
|
)
|
|
from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QSize, QSettings
|
|
from PyQt6.QtGui import QAction, QFont, QColor, QPalette, QIcon
|
|
|
|
|
|
# ============================================================================
|
|
# Data Models
|
|
# ============================================================================
|
|
|
|
class SessionState(Enum):
|
|
"""Session state enumeration."""
|
|
IDLE = "Idle"
|
|
RUNNING = "Running"
|
|
PAUSED = "Paused"
|
|
ERROR = "Error"
|
|
STOPPING = "Stopping"
|
|
|
|
|
|
class ActivityType(Enum):
|
|
"""Activity type enumeration."""
|
|
HUNTING = ("hunt", "🎯 Hunting", "#4caf50")
|
|
MINING = ("mine", "⛏️ Mining", "#ff9800")
|
|
CRAFTING = ("craft", "⚒️ Crafting", "#2196f3")
|
|
|
|
def __init__(self, value, display_name, color):
|
|
self._value_ = value
|
|
self.display_name = display_name
|
|
self.color = color
|
|
|
|
@classmethod
|
|
def from_string(cls, value: str) -> "ActivityType":
|
|
for item in cls:
|
|
if item.value == value.lower():
|
|
return item
|
|
return cls.HUNTING
|
|
|
|
|
|
@dataclass
|
|
class SessionTemplate:
|
|
"""Session template data model (replaces Project)."""
|
|
id: int
|
|
name: str
|
|
activity_type: ActivityType
|
|
description: str = ""
|
|
created_at: Optional[datetime] = None
|
|
session_count: int = 0
|
|
last_session: Optional[datetime] = None
|
|
|
|
|
|
@dataclass
|
|
class RecentSession:
|
|
"""Recent session data model."""
|
|
id: int
|
|
template_name: str
|
|
activity_type: ActivityType
|
|
started_at: datetime
|
|
duration_minutes: int
|
|
total_cost: float
|
|
total_return: float
|
|
status: str
|
|
|
|
|
|
@dataclass
|
|
class LogEvent:
|
|
"""Log event data model."""
|
|
timestamp: datetime
|
|
level: str
|
|
source: str
|
|
message: str
|
|
|
|
def __str__(self) -> str:
|
|
time_str = self.timestamp.strftime("%H:%M:%S.%f")[:-3]
|
|
return f"[{time_str}] [{self.level}] [{self.source}] {self.message}"
|
|
|
|
|
|
# ============================================================================
|
|
# HUD Overlay
|
|
# ============================================================================
|
|
|
|
from ui.hud_overlay_clean import HUDOverlay
|
|
|
|
|
|
# ============================================================================
|
|
# Session History & Gallery Integration
|
|
# ============================================================================
|
|
|
|
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
|
|
# ============================================================================
|
|
|
|
try:
|
|
from modules.screenshot_hotkey import ScreenshotHotkeyManager, ScreenshotHotkeyWidget
|
|
SCREENSHOT_HOTKEYS_AVAILABLE = True
|
|
except ImportError:
|
|
SCREENSHOT_HOTKEYS_AVAILABLE = False
|
|
ScreenshotHotkeyManager = None
|
|
ScreenshotHotkeyWidget = None
|
|
|
|
|
|
# ============================================================================
|
|
# Core Integration
|
|
# ============================================================================
|
|
|
|
import os
|
|
import asyncio
|
|
from pathlib import Path
|
|
from decimal import Decimal
|
|
|
|
# Add core to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "core"))
|
|
|
|
from core.log_watcher import LogWatcher
|
|
from core.database import DatabaseManager
|
|
|
|
|
|
# ============================================================================
|
|
# Custom Dialogs
|
|
# ============================================================================
|
|
|
|
class NewSessionTemplateDialog(QDialog):
|
|
"""Dialog for creating a new session template."""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.setWindowTitle("New Session Template")
|
|
self.setMinimumWidth(400)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Form layout for inputs
|
|
form_layout = QFormLayout()
|
|
|
|
self.name_input = QLineEdit()
|
|
self.name_input.setPlaceholderText("Enter template name...")
|
|
form_layout.addRow("Name:", self.name_input)
|
|
|
|
self.activity_combo = QComboBox()
|
|
for activity in ActivityType:
|
|
self.activity_combo.addItem(activity.display_name, activity)
|
|
form_layout.addRow("Activity Type:", self.activity_combo)
|
|
|
|
self.desc_input = QLineEdit()
|
|
self.desc_input.setPlaceholderText("Enter description (optional)...")
|
|
form_layout.addRow("Description:", self.desc_input)
|
|
|
|
layout.addLayout(form_layout)
|
|
layout.addSpacing(10)
|
|
|
|
# Button box
|
|
button_box = QDialogButtonBox(
|
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
)
|
|
button_box.accepted.connect(self.accept)
|
|
button_box.rejected.connect(self.reject)
|
|
layout.addWidget(button_box)
|
|
|
|
def get_template_data(self) -> tuple:
|
|
"""Get the entered template data."""
|
|
activity = self.activity_combo.currentData()
|
|
return (
|
|
self.name_input.text().strip(),
|
|
activity,
|
|
self.desc_input.text().strip()
|
|
)
|
|
|
|
def accept(self):
|
|
"""Validate before accepting."""
|
|
name = self.name_input.text().strip()
|
|
if not name:
|
|
QMessageBox.warning(self, "Validation Error", "Template name is required.")
|
|
return
|
|
super().accept()
|
|
|
|
|
|
class TemplateStatsDialog(QDialog):
|
|
"""Dialog for displaying session template statistics."""
|
|
|
|
def __init__(self, template: SessionTemplate, parent=None):
|
|
super().__init__(parent)
|
|
self.template = template
|
|
self.setWindowTitle(f"Template Statistics - {template.name}")
|
|
self.setMinimumWidth(350)
|
|
self.setup_ui()
|
|
|
|
def setup_ui(self):
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Stats display
|
|
stats_group = QGroupBox("Template Information")
|
|
stats_layout = QFormLayout(stats_group)
|
|
|
|
stats_layout.addRow("ID:", QLabel(str(self.template.id)))
|
|
stats_layout.addRow("Name:", QLabel(self.template.name))
|
|
stats_layout.addRow("Activity Type:", QLabel(self.template.activity_type.display_name))
|
|
|
|
created = self.template.created_at.strftime("%Y-%m-%d %H:%M") if self.template.created_at else "N/A"
|
|
stats_layout.addRow("Created:", QLabel(created))
|
|
|
|
stats_layout.addRow("Total Sessions:", QLabel(str(self.template.session_count)))
|
|
|
|
last = self.template.last_session.strftime("%Y-%m-%d %H:%M") if self.template.last_session else "Never"
|
|
stats_layout.addRow("Last Session:", QLabel(last))
|
|
|
|
description = self.template.description or "N/A"
|
|
stats_layout.addRow("Description:", QLabel(description))
|
|
|
|
layout.addWidget(stats_group)
|
|
|
|
# Close button
|
|
button_box = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
|
button_box.rejected.connect(self.reject)
|
|
layout.addWidget(button_box)
|
|
|
|
|
|
# ============================================================================
|
|
# Main Window
|
|
# ============================================================================
|
|
|
|
class MainWindow(QMainWindow):
|
|
"""
|
|
Main application window for Lemontropia Suite.
|
|
|
|
Session-focused UI with:
|
|
- Top: Activity Type selector + Loadout selector
|
|
- Middle: Session Control (start/stop/pause) with Loadout Manager button
|
|
- Bottom: Recent Sessions list
|
|
"""
|
|
|
|
# Signals
|
|
session_started = pyqtSignal(int)
|
|
session_stopped = pyqtSignal()
|
|
session_paused = pyqtSignal()
|
|
session_resumed = pyqtSignal()
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Window configuration
|
|
self.setWindowTitle("Lemontropia Suite")
|
|
self.setMinimumSize(1200, 800)
|
|
self.resize(1400, 900)
|
|
|
|
# Initialize database
|
|
self.db = DatabaseManager()
|
|
if not self.db.initialize():
|
|
QMessageBox.critical(self, "Error", "Failed to initialize database!")
|
|
sys.exit(1)
|
|
|
|
# Initialize HUD
|
|
self.hud = HUDOverlay()
|
|
|
|
# Log watcher - created when session starts
|
|
self.log_watcher: Optional[LogWatcher] = None
|
|
self._log_watcher_task = None
|
|
|
|
# Thread-safe queue for cross-thread communication
|
|
from queue import Queue
|
|
self._event_queue = Queue()
|
|
|
|
# Timer to process queued events in main thread
|
|
self._queue_timer = QTimer(self)
|
|
self._queue_timer.timeout.connect(self._process_queued_events)
|
|
self._queue_timer.start(100)
|
|
|
|
# State
|
|
self.current_template: Optional[SessionTemplate] = None
|
|
self.current_activity: ActivityType = ActivityType.HUNTING
|
|
self.session_state = SessionState.IDLE
|
|
self.current_session_id: Optional[int] = None
|
|
self._current_db_session_id: Optional[int] = None
|
|
|
|
# Player settings
|
|
self.player_name: str = ""
|
|
self.log_path: str = ""
|
|
self.auto_detect_log: bool = True
|
|
|
|
# Selected gear/loadout
|
|
self._selected_loadout: Optional[Any] = None
|
|
self._selected_loadout_name: str = "No Loadout"
|
|
|
|
# Session cost tracking
|
|
self._session_costs: dict = {
|
|
'cost_per_shot': Decimal('0'),
|
|
'cost_per_hit': Decimal('0'),
|
|
'cost_per_heal': Decimal('0'),
|
|
}
|
|
self._session_display: dict = {
|
|
'weapon_name': 'None',
|
|
'armor_name': 'None',
|
|
'healing_name': 'None',
|
|
}
|
|
|
|
# Screenshot capture
|
|
self._screenshot_capture = ScreenshotCapture(self.db)
|
|
|
|
# Screenshot hotkey manager (global hotkeys for manual capture)
|
|
# Note: Initialized after UI setup because it uses log_output
|
|
self._screenshot_hotkeys = None
|
|
|
|
# Setup UI
|
|
self.setup_ui()
|
|
self.apply_dark_theme()
|
|
self.create_menu_bar()
|
|
self.create_status_bar()
|
|
|
|
# Initialize screenshot hotkeys AFTER UI is setup
|
|
self._init_screenshot_hotkeys()
|
|
|
|
# Load persistent settings
|
|
self._load_settings()
|
|
|
|
# Load initial data
|
|
self.refresh_session_templates()
|
|
self.refresh_recent_sessions()
|
|
|
|
# Welcome message
|
|
self.log_info("Application", "Lemontropia Suite initialized")
|
|
self.log_info("Database", f"Database ready: {self.db.db_path}")
|
|
|
|
# ========================================================================
|
|
# UI Setup - New Session-Focused Layout
|
|
# ========================================================================
|
|
|
|
def setup_ui(self):
|
|
"""Setup the main UI layout with session-focused design."""
|
|
# Central widget
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
|
|
# Main layout
|
|
main_layout = QVBoxLayout(central_widget)
|
|
main_layout.setContentsMargins(10, 10, 10, 10)
|
|
main_layout.setSpacing(10)
|
|
|
|
# Main splitter (horizontal: left panels | log panel)
|
|
self.main_splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
main_layout.addWidget(self.main_splitter)
|
|
|
|
# Left side container - Session Control Focus
|
|
left_container = QWidget()
|
|
left_layout = QVBoxLayout(left_container)
|
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
|
left_layout.setSpacing(10)
|
|
|
|
# === TOP: Activity Type Selector + Loadout Selector ===
|
|
self.activity_panel = self.create_activity_panel()
|
|
left_layout.addWidget(self.activity_panel)
|
|
|
|
# === MIDDLE: Session Control (with Loadout Manager button) ===
|
|
self.session_panel = self.create_session_panel()
|
|
left_layout.addWidget(self.session_panel)
|
|
|
|
# === BOTTOM: Recent Sessions List ===
|
|
self.recent_sessions_panel = self.create_recent_sessions_panel()
|
|
left_layout.addWidget(self.recent_sessions_panel, 1) # Stretch factor
|
|
|
|
# Add left container to main splitter
|
|
self.main_splitter.addWidget(left_container)
|
|
|
|
# Log output panel (right side)
|
|
self.log_panel = self.create_log_panel()
|
|
self.main_splitter.addWidget(self.log_panel)
|
|
|
|
# Set main splitter proportions (35% left, 65% log)
|
|
self.main_splitter.setSizes([450, 850])
|
|
|
|
def create_activity_panel(self) -> QGroupBox:
|
|
"""Create the activity type and loadout selection panel (TOP)."""
|
|
panel = QGroupBox("Activity Setup")
|
|
layout = QVBoxLayout(panel)
|
|
layout.setSpacing(10)
|
|
|
|
# Activity Type selector
|
|
activity_layout = QHBoxLayout()
|
|
activity_layout.addWidget(QLabel("🎯 Activity Type:"))
|
|
|
|
self.activity_combo = QComboBox()
|
|
for activity in ActivityType:
|
|
self.activity_combo.addItem(activity.display_name, activity)
|
|
self.activity_combo.currentIndexChanged.connect(self.on_activity_changed)
|
|
activity_layout.addWidget(self.activity_combo, 1)
|
|
|
|
layout.addLayout(activity_layout)
|
|
|
|
# Session Template selector
|
|
template_layout = QHBoxLayout()
|
|
template_layout.addWidget(QLabel("📋 Template:"))
|
|
|
|
self.template_combo = QComboBox()
|
|
self.template_combo.setPlaceholderText("Select a template...")
|
|
self.template_combo.currentIndexChanged.connect(self.on_template_changed)
|
|
template_layout.addWidget(self.template_combo, 1)
|
|
|
|
# New template button
|
|
self.new_template_btn = QPushButton("+")
|
|
self.new_template_btn.setMaximumWidth(40)
|
|
self.new_template_btn.setToolTip("Create new template")
|
|
self.new_template_btn.clicked.connect(self.on_new_template)
|
|
template_layout.addWidget(self.new_template_btn)
|
|
|
|
layout.addLayout(template_layout)
|
|
|
|
# Loadout selector (prominent)
|
|
loadout_layout = QHBoxLayout()
|
|
loadout_layout.addWidget(QLabel("🎛️ Loadout:"))
|
|
|
|
self.loadout_display = QLabel("No Loadout Selected")
|
|
self.loadout_display.setStyleSheet("font-weight: bold; color: #ff9800;")
|
|
loadout_layout.addWidget(self.loadout_display, 1)
|
|
|
|
# Prominent Loadout Manager button
|
|
self.loadout_manager_btn = QPushButton("🔧 Open Loadout Manager")
|
|
self.loadout_manager_btn.setToolTip("Configure your gear loadout")
|
|
self.loadout_manager_btn.setMinimumHeight(32)
|
|
self.loadout_manager_btn.clicked.connect(self.on_loadout_manager)
|
|
loadout_layout.addWidget(self.loadout_manager_btn)
|
|
|
|
layout.addLayout(loadout_layout)
|
|
|
|
return panel
|
|
|
|
def create_session_panel(self) -> QGroupBox:
|
|
"""Create the session control panel (MIDDLE)."""
|
|
panel = QGroupBox("Session Control")
|
|
layout = QVBoxLayout(panel)
|
|
layout.setSpacing(12)
|
|
|
|
# Current session info
|
|
info_layout = QGridLayout()
|
|
|
|
self.current_activity_label = QLabel("No Activity")
|
|
self.current_activity_label.setStyleSheet("font-weight: bold; color: #888;")
|
|
info_layout.addWidget(QLabel("Activity:"), 0, 0)
|
|
info_layout.addWidget(self.current_activity_label, 0, 1)
|
|
|
|
self.current_template_label = QLabel("No Template")
|
|
self.current_template_label.setStyleSheet("font-weight: bold; color: #888;")
|
|
info_layout.addWidget(QLabel("Template:"), 1, 0)
|
|
info_layout.addWidget(self.current_template_label, 1, 1)
|
|
|
|
layout.addLayout(info_layout)
|
|
|
|
# Separator line
|
|
separator = QFrame()
|
|
separator.setFrameShape(QFrame.Shape.HLine)
|
|
separator.setStyleSheet("background-color: #444;")
|
|
layout.addWidget(separator)
|
|
|
|
# Session status
|
|
status_layout = QHBoxLayout()
|
|
status_layout.addWidget(QLabel("Status:"))
|
|
self.session_status_label = QLabel("Idle")
|
|
self.session_status_label.setStyleSheet("""
|
|
QLabel {
|
|
font-weight: bold;
|
|
color: #888;
|
|
padding: 5px 15px;
|
|
background-color: #2a2a2a;
|
|
border-radius: 4px;
|
|
border: 1px solid #444;
|
|
}
|
|
""")
|
|
status_layout.addWidget(self.session_status_label)
|
|
status_layout.addStretch()
|
|
layout.addLayout(status_layout)
|
|
|
|
# Control buttons - Large and prominent
|
|
button_layout = QHBoxLayout()
|
|
button_layout.setSpacing(10)
|
|
|
|
self.start_session_btn = QPushButton("▶️ START SESSION")
|
|
self.start_session_btn.setToolTip("Start a new session")
|
|
self.start_session_btn.setMinimumHeight(50)
|
|
self.start_session_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #1b5e20;
|
|
border: 2px solid #2e7d32;
|
|
border-radius: 6px;
|
|
padding: 10px 20px;
|
|
font-weight: bold;
|
|
font-size: 12pt;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #2e7d32;
|
|
}
|
|
QPushButton:disabled {
|
|
background-color: #1a3a1a;
|
|
color: #666;
|
|
border-color: #333;
|
|
}
|
|
""")
|
|
self.start_session_btn.clicked.connect(self.on_start_session)
|
|
button_layout.addWidget(self.start_session_btn, 2)
|
|
|
|
self.stop_session_btn = QPushButton("⏹️ STOP")
|
|
self.stop_session_btn.setToolTip("Stop current session")
|
|
self.stop_session_btn.setMinimumHeight(50)
|
|
self.stop_session_btn.setEnabled(False)
|
|
self.stop_session_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #b71c1c;
|
|
border: 2px solid #c62828;
|
|
border-radius: 6px;
|
|
padding: 10px 20px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #c62828;
|
|
}
|
|
""")
|
|
self.stop_session_btn.clicked.connect(self.on_stop_session)
|
|
button_layout.addWidget(self.stop_session_btn, 1)
|
|
|
|
self.pause_session_btn = QPushButton("⏸️ PAUSE")
|
|
self.pause_session_btn.setToolTip("Pause/Resume session")
|
|
self.pause_session_btn.setMinimumHeight(50)
|
|
self.pause_session_btn.setEnabled(False)
|
|
self.pause_session_btn.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #e65100;
|
|
border: 2px solid #f57c00;
|
|
border-radius: 6px;
|
|
padding: 10px 20px;
|
|
font-weight: bold;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #f57c00;
|
|
}
|
|
""")
|
|
self.pause_session_btn.clicked.connect(self.on_pause_session)
|
|
button_layout.addWidget(self.pause_session_btn, 1)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
# Session stats summary
|
|
self.session_info_label = QLabel("Ready to start - Select activity and loadout first")
|
|
self.session_info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
self.session_info_label.setStyleSheet("color: #666; padding: 10px;")
|
|
layout.addWidget(self.session_info_label)
|
|
|
|
layout.addStretch()
|
|
|
|
return panel
|
|
|
|
def create_recent_sessions_panel(self) -> QGroupBox:
|
|
"""Create the recent sessions panel (BOTTOM)."""
|
|
panel = QGroupBox("Recent Sessions")
|
|
layout = QVBoxLayout(panel)
|
|
layout.setSpacing(8)
|
|
|
|
# Recent sessions list
|
|
self.recent_sessions_list = QTreeWidget()
|
|
self.recent_sessions_list.setHeaderLabels([
|
|
"Activity", "Template", "Started", "Duration", "Cost", "Return", "Status"
|
|
])
|
|
self.recent_sessions_list.setAlternatingRowColors(True)
|
|
self.recent_sessions_list.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection)
|
|
self.recent_sessions_list.setRootIsDecorated(False)
|
|
self.recent_sessions_list.itemDoubleClicked.connect(self.on_session_double_clicked)
|
|
|
|
# Adjust column widths
|
|
header = self.recent_sessions_list.header()
|
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed)
|
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Fixed)
|
|
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed)
|
|
header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed)
|
|
header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed)
|
|
header.setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed)
|
|
header.resizeSection(0, 100)
|
|
header.resizeSection(2, 130)
|
|
header.resizeSection(3, 70)
|
|
header.resizeSection(4, 70)
|
|
header.resizeSection(5, 70)
|
|
header.resizeSection(6, 80)
|
|
|
|
layout.addWidget(self.recent_sessions_list)
|
|
|
|
# Button row
|
|
button_layout = QHBoxLayout()
|
|
|
|
self.view_history_btn = QPushButton("📊 View Full History")
|
|
self.view_history_btn.setToolTip("View complete session history")
|
|
self.view_history_btn.clicked.connect(self.on_view_full_history)
|
|
button_layout.addWidget(self.view_history_btn)
|
|
|
|
self.refresh_sessions_btn = QPushButton("🔄 Refresh")
|
|
self.refresh_sessions_btn.setToolTip("Refresh recent sessions")
|
|
self.refresh_sessions_btn.clicked.connect(self.refresh_recent_sessions)
|
|
button_layout.addWidget(self.refresh_sessions_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
return panel
|
|
|
|
def create_log_panel(self) -> QGroupBox:
|
|
"""Create the log output panel."""
|
|
panel = QGroupBox("Event Log")
|
|
layout = QVBoxLayout(panel)
|
|
layout.setSpacing(8)
|
|
|
|
# Log text edit
|
|
self.log_output = QTextEdit()
|
|
self.log_output.setReadOnly(True)
|
|
self.log_output.setFont(QFont("Consolas", 10))
|
|
self.log_output.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap)
|
|
layout.addWidget(self.log_output)
|
|
|
|
# Log controls
|
|
controls_layout = QHBoxLayout()
|
|
|
|
self.clear_log_btn = QPushButton("🗑️ Clear")
|
|
self.clear_log_btn.setToolTip("Clear log output")
|
|
self.clear_log_btn.clicked.connect(self.log_output.clear)
|
|
controls_layout.addWidget(self.clear_log_btn)
|
|
|
|
controls_layout.addStretch()
|
|
|
|
self.auto_scroll_check = QLabel("✓ Auto-scroll")
|
|
self.auto_scroll_check.setStyleSheet("color: #888;")
|
|
controls_layout.addWidget(self.auto_scroll_check)
|
|
|
|
layout.addLayout(controls_layout)
|
|
|
|
return panel
|
|
|
|
def create_menu_bar(self):
|
|
"""Create the application menu bar."""
|
|
menubar = self.menuBar()
|
|
|
|
# File menu
|
|
file_menu = menubar.addMenu("&File")
|
|
|
|
new_template_action = QAction("&New Session Template", self)
|
|
new_template_action.setShortcut("Ctrl+N")
|
|
new_template_action.triggered.connect(self.on_new_template)
|
|
file_menu.addAction(new_template_action)
|
|
|
|
file_menu.addSeparator()
|
|
|
|
exit_action = QAction("E&xit", self)
|
|
exit_action.setShortcut("Alt+F4")
|
|
exit_action.triggered.connect(self.close)
|
|
file_menu.addAction(exit_action)
|
|
|
|
# Session menu
|
|
session_menu = menubar.addMenu("&Session")
|
|
|
|
start_action = QAction("&Start", self)
|
|
start_action.setShortcut("F5")
|
|
start_action.triggered.connect(self.on_start_session)
|
|
session_menu.addAction(start_action)
|
|
self.start_action = start_action
|
|
|
|
stop_action = QAction("St&op", self)
|
|
stop_action.setShortcut("Shift+F5")
|
|
stop_action.triggered.connect(self.on_stop_session)
|
|
session_menu.addAction(stop_action)
|
|
self.stop_action = stop_action
|
|
|
|
pause_action = QAction("&Pause", self)
|
|
pause_action.setShortcut("F6")
|
|
pause_action.triggered.connect(self.on_pause_session)
|
|
session_menu.addAction(pause_action)
|
|
self.pause_action = pause_action
|
|
|
|
# Tools menu
|
|
tools_menu = menubar.addMenu("&Tools")
|
|
|
|
loadout_action = QAction("&Loadout Manager", self)
|
|
loadout_action.setShortcut("Ctrl+L")
|
|
loadout_action.triggered.connect(self.on_loadout_manager)
|
|
tools_menu.addAction(loadout_action)
|
|
|
|
tools_menu.addSeparator()
|
|
|
|
select_gear_menu = tools_menu.addMenu("Select &Gear")
|
|
|
|
select_weapon_action = QAction("&Weapon", self)
|
|
select_weapon_action.setShortcut("Ctrl+W")
|
|
select_weapon_action.triggered.connect(lambda: self.on_select_gear("weapon"))
|
|
select_gear_menu.addAction(select_weapon_action)
|
|
|
|
select_armor_action = QAction("&Armor", self)
|
|
select_armor_action.setShortcut("Ctrl+Shift+A")
|
|
select_armor_action.triggered.connect(lambda: self.on_select_gear("armor"))
|
|
select_gear_menu.addAction(select_armor_action)
|
|
|
|
select_finder_action = QAction("&Finder", self)
|
|
select_finder_action.setShortcut("Ctrl+Shift+F")
|
|
select_finder_action.triggered.connect(lambda: self.on_select_gear("finder"))
|
|
select_gear_menu.addAction(select_finder_action)
|
|
|
|
select_medical_action = QAction("&Medical Tool", self)
|
|
select_medical_action.setShortcut("Ctrl+M")
|
|
select_medical_action.triggered.connect(lambda: self.on_select_gear("medical_tool"))
|
|
select_gear_menu.addAction(select_medical_action)
|
|
|
|
tools_menu.addSeparator()
|
|
|
|
# Computer Vision submenu
|
|
vision_menu = tools_menu.addMenu("👁️ &Computer Vision")
|
|
|
|
vision_settings_action = QAction("Vision &Settings", self)
|
|
vision_settings_action.triggered.connect(self.on_vision_settings)
|
|
vision_menu.addAction(vision_settings_action)
|
|
|
|
vision_calibrate_action = QAction("&Calibrate Vision", self)
|
|
vision_calibrate_action.triggered.connect(self.on_vision_calibrate)
|
|
vision_menu.addAction(vision_calibrate_action)
|
|
|
|
vision_test_action = QAction("&Test Vision", self)
|
|
vision_test_action.triggered.connect(self.on_vision_test)
|
|
vision_menu.addAction(vision_test_action)
|
|
|
|
vision_menu.addSeparator()
|
|
|
|
# Inventory Scanner
|
|
inventory_scan_action = QAction("🔍 &Inventory Scanner", self)
|
|
inventory_scan_action.setShortcut("Ctrl+I")
|
|
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")
|
|
|
|
show_hud_action = QAction("Show &HUD", self)
|
|
show_hud_action.setShortcut("F9")
|
|
show_hud_action.triggered.connect(self.on_show_hud)
|
|
view_menu.addAction(show_hud_action)
|
|
|
|
hide_hud_action = QAction("&Hide HUD", self)
|
|
hide_hud_action.setShortcut("F10")
|
|
hide_hud_action.triggered.connect(self.on_hide_hud)
|
|
view_menu.addAction(hide_hud_action)
|
|
|
|
view_menu.addSeparator()
|
|
|
|
settings_action = QAction("&Settings", self)
|
|
settings_action.setShortcut("Ctrl+,")
|
|
settings_action.triggered.connect(self.on_settings)
|
|
view_menu.addAction(settings_action)
|
|
|
|
view_menu.addSeparator()
|
|
|
|
# Session History
|
|
session_history_action = QAction("📊 Session &History", self)
|
|
session_history_action.setShortcut("Ctrl+H")
|
|
session_history_action.triggered.connect(self.on_session_history)
|
|
view_menu.addAction(session_history_action)
|
|
|
|
# Gallery
|
|
gallery_action = QAction("🖼️ Screenshot &Gallery", self)
|
|
gallery_action.setShortcut("Ctrl+G")
|
|
gallery_action.triggered.connect(self.on_gallery)
|
|
view_menu.addAction(gallery_action)
|
|
|
|
# Help menu
|
|
help_menu = menubar.addMenu("&Help")
|
|
|
|
run_wizard_action = QAction("&Run Setup Wizard Again", self)
|
|
run_wizard_action.setShortcut("Ctrl+Shift+W")
|
|
run_wizard_action.triggered.connect(self.on_run_setup_wizard)
|
|
help_menu.addAction(run_wizard_action)
|
|
|
|
help_menu.addSeparator()
|
|
|
|
about_action = QAction("&About", self)
|
|
about_action.triggered.connect(self.on_about)
|
|
help_menu.addAction(about_action)
|
|
|
|
def create_status_bar(self):
|
|
"""Create the status bar."""
|
|
self.status_bar = QStatusBar()
|
|
self.setStatusBar(self.status_bar)
|
|
|
|
# Permanent widgets
|
|
self.status_state_label = QLabel("● Idle")
|
|
self.status_state_label.setStyleSheet("color: #888; padding: 0 10px;")
|
|
self.status_bar.addPermanentWidget(self.status_state_label)
|
|
|
|
self.status_activity_label = QLabel("No Activity")
|
|
self.status_activity_label.setStyleSheet("color: #888; padding: 0 10px;")
|
|
self.status_bar.addPermanentWidget(self.status_activity_label)
|
|
|
|
# Message area
|
|
self.status_bar.showMessage("Ready")
|
|
|
|
# ========================================================================
|
|
# Theme
|
|
# ========================================================================
|
|
|
|
def apply_dark_theme(self):
|
|
"""Apply dark theme styling."""
|
|
dark_stylesheet = """
|
|
QMainWindow {
|
|
background-color: #1e1e1e;
|
|
}
|
|
|
|
QWidget {
|
|
background-color: #1e1e1e;
|
|
color: #e0e0e0;
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
font-size: 10pt;
|
|
}
|
|
|
|
QGroupBox {
|
|
font-weight: bold;
|
|
border: 1px solid #444;
|
|
border-radius: 6px;
|
|
margin-top: 10px;
|
|
padding-top: 10px;
|
|
padding: 10px;
|
|
}
|
|
|
|
QGroupBox::title {
|
|
subcontrol-origin: margin;
|
|
left: 10px;
|
|
padding: 0 5px;
|
|
color: #888;
|
|
}
|
|
|
|
QPushButton {
|
|
background-color: #2d2d2d;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 8px 16px;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
QPushButton:hover {
|
|
background-color: #3d3d3d;
|
|
border-color: #555;
|
|
}
|
|
|
|
QPushButton:pressed {
|
|
background-color: #4d4d4d;
|
|
}
|
|
|
|
QPushButton:disabled {
|
|
background-color: #252525;
|
|
color: #666;
|
|
border-color: #333;
|
|
}
|
|
|
|
QTreeWidget {
|
|
background-color: #252525;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
outline: none;
|
|
}
|
|
|
|
QTreeWidget::item {
|
|
padding: 6px;
|
|
border-bottom: 1px solid #333;
|
|
}
|
|
|
|
QTreeWidget::item:selected {
|
|
background-color: #0d47a1;
|
|
color: white;
|
|
}
|
|
|
|
QTreeWidget::item:alternate {
|
|
background-color: #2a2a2a;
|
|
}
|
|
|
|
QHeaderView::section {
|
|
background-color: #2d2d2d;
|
|
padding: 6px;
|
|
border: none;
|
|
border-right: 1px solid #444;
|
|
font-weight: bold;
|
|
}
|
|
|
|
QTextEdit {
|
|
background-color: #151515;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 8px;
|
|
color: #d0d0d0;
|
|
}
|
|
|
|
QLineEdit {
|
|
background-color: #252525;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 6px;
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
QLineEdit:focus {
|
|
border-color: #0d47a1;
|
|
}
|
|
|
|
QComboBox {
|
|
background-color: #252525;
|
|
border: 1px solid #444;
|
|
border-radius: 4px;
|
|
padding: 6px;
|
|
color: #e0e0e0;
|
|
min-width: 120px;
|
|
}
|
|
|
|
QComboBox:focus {
|
|
border-color: #0d47a1;
|
|
}
|
|
|
|
QComboBox::drop-down {
|
|
border: none;
|
|
padding-right: 10px;
|
|
}
|
|
|
|
QMenuBar {
|
|
background-color: #1e1e1e;
|
|
border-bottom: 1px solid #444;
|
|
}
|
|
|
|
QMenuBar::item {
|
|
background-color: transparent;
|
|
padding: 6px 12px;
|
|
}
|
|
|
|
QMenuBar::item:selected {
|
|
background-color: #2d2d2d;
|
|
}
|
|
|
|
QMenu {
|
|
background-color: #2d2d2d;
|
|
border: 1px solid #444;
|
|
padding: 4px;
|
|
}
|
|
|
|
QMenu::item {
|
|
padding: 6px 24px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
QMenu::item:selected {
|
|
background-color: #0d47a1;
|
|
}
|
|
|
|
QMenu::separator {
|
|
height: 1px;
|
|
background-color: #444;
|
|
margin: 4px 8px;
|
|
}
|
|
|
|
QStatusBar {
|
|
background-color: #1e1e1e;
|
|
border-top: 1px solid #444;
|
|
}
|
|
|
|
QSplitter::handle {
|
|
background-color: #444;
|
|
}
|
|
|
|
QSplitter::handle:horizontal {
|
|
width: 2px;
|
|
}
|
|
|
|
QSplitter::handle:vertical {
|
|
height: 2px;
|
|
}
|
|
|
|
QDialog {
|
|
background-color: #1e1e1e;
|
|
}
|
|
|
|
QLabel {
|
|
color: #e0e0e0;
|
|
}
|
|
|
|
QFormLayout QLabel {
|
|
color: #888;
|
|
}
|
|
"""
|
|
self.setStyleSheet(dark_stylesheet)
|
|
|
|
# ========================================================================
|
|
# Session Template Management
|
|
# ========================================================================
|
|
|
|
def refresh_session_templates(self):
|
|
"""Refresh the session template list."""
|
|
# Block signals while updating to prevent triggering on_template_changed
|
|
self.template_combo.blockSignals(True)
|
|
|
|
self.template_combo.clear()
|
|
|
|
# Load templates from database
|
|
templates = self._load_templates_from_db()
|
|
|
|
# Add "Select a template..." as first item
|
|
self.template_combo.addItem("-- Select a template --", None)
|
|
|
|
for template in templates:
|
|
display_text = f"{template.activity_type.display_name} - {template.name}"
|
|
self.template_combo.addItem(display_text, template)
|
|
|
|
# Restore signals
|
|
self.template_combo.blockSignals(False)
|
|
|
|
self.log_debug("Templates", f"Loaded {len(templates)} session templates")
|
|
|
|
# If we have templates, enable the combo box
|
|
if len(templates) > 0:
|
|
self.template_combo.setEnabled(True)
|
|
else:
|
|
self.template_combo.setEnabled(False)
|
|
|
|
def _load_templates_from_db(self) -> List[SessionTemplate]:
|
|
"""Load session templates from database."""
|
|
templates = []
|
|
try:
|
|
# Query database for projects (using existing project table)
|
|
projects = self.db.fetchall(
|
|
"SELECT id, name, type, created_at, description FROM projects ORDER BY name"
|
|
)
|
|
|
|
self.log_debug("Templates", f"Raw query returned {len(projects)} rows")
|
|
|
|
for i, proj in enumerate(projects):
|
|
try:
|
|
# Log what we're working with for debugging
|
|
self.log_debug("Templates", f"Row {i}: type={type(proj)}, len={len(proj) if hasattr(proj, '__len__') else 'N/A'}")
|
|
|
|
# Convert row to tuple/list if it's a sqlite3.Row
|
|
if hasattr(proj, '__iter__') and not isinstance(proj, (str, bytes)):
|
|
row_data = list(proj)
|
|
else:
|
|
row_data = [proj]
|
|
|
|
self.log_debug("Templates", f"Row {i} data: {row_data}")
|
|
|
|
# Safely extract values
|
|
proj_id = int(row_data[0]) if len(row_data) > 0 else 0
|
|
proj_name = str(row_data[1]) if len(row_data) > 1 else "Unnamed"
|
|
proj_type = str(row_data[2]) if len(row_data) > 2 else 'hunt'
|
|
proj_created = row_data[3] if len(row_data) > 3 else None
|
|
proj_desc = str(row_data[4]) if len(row_data) > 4 else ''
|
|
|
|
self.log_debug("Templates", f"Extracted: id={proj_id}, name={proj_name}, type={proj_type}")
|
|
|
|
# Convert activity type
|
|
activity = ActivityType.from_string(proj_type)
|
|
|
|
# Get session count
|
|
session_count = 0
|
|
try:
|
|
count_result = self.db.fetchone(
|
|
"SELECT COUNT(*) FROM sessions WHERE project_id = ?",
|
|
(proj_id,)
|
|
)
|
|
if count_result:
|
|
count_data = list(count_result)
|
|
session_count = int(count_data[0]) if len(count_data) > 0 else 0
|
|
except Exception as e:
|
|
self.log_debug("Templates", f"Count query failed: {e}")
|
|
|
|
# Get last session
|
|
last_session = None
|
|
try:
|
|
last_result = self.db.fetchone(
|
|
"SELECT MAX(started_at) FROM sessions WHERE project_id = ?",
|
|
(proj_id,)
|
|
)
|
|
if last_result:
|
|
last_data = list(last_result)
|
|
if len(last_data) > 0 and last_data[0]:
|
|
last_session = datetime.fromisoformat(str(last_data[0]))
|
|
except Exception as e:
|
|
self.log_debug("Templates", f"Last session query failed: {e}")
|
|
|
|
# Parse created_at
|
|
created_at = None
|
|
if proj_created:
|
|
try:
|
|
created_at = datetime.fromisoformat(str(proj_created))
|
|
except:
|
|
pass
|
|
|
|
template = SessionTemplate(
|
|
id=proj_id,
|
|
name=proj_name,
|
|
activity_type=activity,
|
|
description=proj_desc,
|
|
created_at=created_at,
|
|
session_count=session_count,
|
|
last_session=last_session
|
|
)
|
|
templates.append(template)
|
|
self.log_debug("Templates", f"Added template: {proj_name}")
|
|
|
|
except Exception as row_error:
|
|
self.log_warning("Templates", f"Skipping row {i}: {row_error}")
|
|
import traceback
|
|
logger.error(traceback.format_exc())
|
|
continue
|
|
|
|
except Exception as e:
|
|
self.log_error("Templates", f"Failed to load templates: {e}")
|
|
import traceback
|
|
logger.error(traceback.format_exc())
|
|
|
|
return templates
|
|
|
|
def on_new_template(self):
|
|
"""Handle new session template creation."""
|
|
dialog = NewSessionTemplateDialog(self)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
name, activity_type, description = dialog.get_template_data()
|
|
|
|
try:
|
|
# Save to database
|
|
result = self.db.execute(
|
|
"""INSERT INTO projects (name, type, description, created_at)
|
|
VALUES (?, ?, ?, ?)""",
|
|
(name, activity_type.value, description, datetime.now().isoformat())
|
|
)
|
|
self.db.commit()
|
|
|
|
# Get the new template ID
|
|
new_id = result.lastrowid
|
|
|
|
self.refresh_session_templates()
|
|
|
|
# Select the newly created template
|
|
for i in range(self.template_combo.count()):
|
|
template = self.template_combo.itemData(i)
|
|
if template and template.id == new_id:
|
|
self.template_combo.setCurrentIndex(i)
|
|
break
|
|
|
|
self.log_info("Templates", f"Created template: {name} ({activity_type.display_name})")
|
|
self.status_bar.showMessage(f"Template '{name}' created", 3000)
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Error", f"Failed to create template: {e}")
|
|
|
|
def on_template_changed(self, index: int):
|
|
"""Handle template selection change."""
|
|
if index > 0: # Skip first item which is "-- Select a template --"
|
|
self.current_template = self.template_combo.currentData()
|
|
if self.current_template:
|
|
self.current_template_label.setText(self.current_template.name)
|
|
self.current_template_label.setStyleSheet(f"font-weight: bold; color: {self.current_template.activity_type.color};")
|
|
self.log_info("Templates", f"Selected template: {self.current_template.name}")
|
|
self._update_start_button()
|
|
else:
|
|
self.current_template = None
|
|
self.current_template_label.setText("No Template")
|
|
self.current_template_label.setStyleSheet("font-weight: bold; color: #888;")
|
|
self._update_start_button()
|
|
|
|
def on_activity_changed(self, index: int):
|
|
"""Handle activity type change."""
|
|
self.current_activity = self.activity_combo.currentData()
|
|
if self.current_activity:
|
|
self.current_activity_label.setText(self.current_activity.display_name)
|
|
self.current_activity_label.setStyleSheet(f"font-weight: bold; color: {self.current_activity.color};")
|
|
self.status_activity_label.setText(self.current_activity.display_name)
|
|
self.log_debug("Activity", f"Changed to: {self.current_activity.display_name}")
|
|
|
|
# ========================================================================
|
|
# Recent Sessions
|
|
# ========================================================================
|
|
|
|
def refresh_recent_sessions(self):
|
|
"""Refresh the recent sessions list."""
|
|
self.recent_sessions_list.clear()
|
|
|
|
sessions = self._load_recent_sessions_from_db()
|
|
|
|
for session in sessions:
|
|
item = QTreeWidgetItem([
|
|
session.activity_type.display_name,
|
|
session.template_name,
|
|
session.started_at.strftime("%m-%d %H:%M"),
|
|
f"{session.duration_minutes}m",
|
|
f"{session.total_cost:.2f}",
|
|
f"{session.total_return:.2f}",
|
|
session.status
|
|
])
|
|
item.setData(0, Qt.ItemDataRole.UserRole, session.id)
|
|
|
|
# Color code status
|
|
if session.status == "completed":
|
|
item.setForeground(6, QColor("#4caf50"))
|
|
elif session.status == "running":
|
|
item.setForeground(6, QColor("#ff9800"))
|
|
|
|
self.recent_sessions_list.addTopLevelItem(item)
|
|
|
|
self.log_debug("Sessions", f"Loaded {len(sessions)} recent sessions")
|
|
|
|
def _load_recent_sessions_from_db(self) -> List[RecentSession]:
|
|
"""Load recent sessions from database."""
|
|
sessions = []
|
|
try:
|
|
rows = self.db.fetchall("""
|
|
SELECT s.id, p.name as template_name, p.type,
|
|
s.started_at, s.ended_at, s.status
|
|
FROM sessions s
|
|
JOIN projects p ON s.project_id = p.id
|
|
ORDER BY s.started_at DESC
|
|
LIMIT 20
|
|
""")
|
|
|
|
for row in rows:
|
|
try:
|
|
# Convert row to list for safe access
|
|
row_data = list(row)
|
|
|
|
# Safely extract values by index
|
|
session_id = int(row_data[0]) if len(row_data) > 0 else 0
|
|
template_name = str(row_data[1]) if len(row_data) > 1 else "Unknown"
|
|
row_type = str(row_data[2]) if len(row_data) > 2 else 'hunt'
|
|
started_at_str = row_data[3] if len(row_data) > 3 else None
|
|
ended_at_str = row_data[4] if len(row_data) > 4 else None
|
|
status = str(row_data[5]) if len(row_data) > 5 else 'unknown'
|
|
|
|
activity = ActivityType.from_string(row_type)
|
|
|
|
# Parse started_at
|
|
started = None
|
|
if started_at_str:
|
|
started = datetime.fromisoformat(str(started_at_str))
|
|
else:
|
|
started = datetime.now()
|
|
|
|
# Calculate duration
|
|
duration = 0
|
|
if ended_at_str:
|
|
ended = datetime.fromisoformat(str(ended_at_str))
|
|
duration = int((ended - started).total_seconds() / 60)
|
|
elif status == 'running':
|
|
duration = int((datetime.now() - started).total_seconds() / 60)
|
|
|
|
# Get costs (placeholder - would need actual cost tracking)
|
|
total_cost = 0.0
|
|
total_return = 0.0
|
|
|
|
session = RecentSession(
|
|
id=session_id,
|
|
template_name=template_name,
|
|
activity_type=activity,
|
|
started_at=started,
|
|
duration_minutes=duration,
|
|
total_cost=total_cost,
|
|
total_return=total_return,
|
|
status=status
|
|
)
|
|
sessions.append(session)
|
|
|
|
except Exception as row_error:
|
|
self.log_warning("Sessions", f"Skipping malformed session row: {row_error}")
|
|
continue
|
|
|
|
except Exception as e:
|
|
self.log_error("Sessions", f"Failed to load sessions: {e}")
|
|
import traceback
|
|
logger.error(traceback.format_exc())
|
|
|
|
return sessions
|
|
|
|
def on_session_double_clicked(self, item: QTreeWidgetItem, column: int):
|
|
"""Handle double-click on session."""
|
|
session_id = item.data(0, Qt.ItemDataRole.UserRole)
|
|
self.log_info("Sessions", f"Viewing session details: {session_id}")
|
|
# TODO: Open session detail dialog
|
|
|
|
def on_view_full_history(self):
|
|
"""Open full session history view."""
|
|
self.log_info("Sessions", "Opening full session history")
|
|
dialog = SessionHistoryDialog(self)
|
|
dialog.exec()
|
|
|
|
# ========================================================================
|
|
# Session Control
|
|
# ========================================================================
|
|
|
|
def _update_start_button(self):
|
|
"""Update start button state based on selections."""
|
|
can_start = (
|
|
self.session_state == SessionState.IDLE and
|
|
self.current_template is not None and
|
|
self._selected_loadout is not None
|
|
)
|
|
self.start_session_btn.setEnabled(can_start)
|
|
self.start_action.setEnabled(can_start)
|
|
|
|
def on_start_session(self):
|
|
"""Handle start session button."""
|
|
if self.session_state != SessionState.IDLE:
|
|
self.log_warning("Session", "Cannot start: session already active")
|
|
return
|
|
|
|
if not self.current_template:
|
|
QMessageBox.warning(self, "No Template", "Please select a session template first.")
|
|
return
|
|
|
|
if not self._selected_loadout:
|
|
QMessageBox.warning(self, "No Loadout", "Please configure a loadout before starting.")
|
|
return
|
|
|
|
# Start the session
|
|
self.start_session_with_template(self.current_template)
|
|
|
|
def start_session_with_template(self, template: SessionTemplate):
|
|
"""Start a new session with the given template."""
|
|
if self.session_state != SessionState.IDLE:
|
|
return
|
|
|
|
# Update state
|
|
self.set_session_state(SessionState.RUNNING)
|
|
self.current_session_id = template.id
|
|
|
|
# Emit signal
|
|
self.session_started.emit(template.id)
|
|
|
|
# Log
|
|
self.log_info("Session", f"Started {template.activity_type.display_name} session: {template.name}")
|
|
self.session_info_label.setText(f"Session active: {template.name}")
|
|
|
|
# Start session in database
|
|
try:
|
|
result = self.db.execute(
|
|
"INSERT INTO sessions (project_id, started_at, status) VALUES (?, ?, ?)",
|
|
(template.id, datetime.now().isoformat(), 'running')
|
|
)
|
|
self.db.commit()
|
|
self._current_db_session_id = result.lastrowid
|
|
except Exception as e:
|
|
self.log_error("Session", f"Failed to record session: {e}")
|
|
|
|
# Setup LogWatcher
|
|
self._setup_log_watcher()
|
|
|
|
# Show HUD
|
|
self.hud.show()
|
|
|
|
# Start HUD session
|
|
session_display = getattr(self, '_session_display', {})
|
|
session_costs = getattr(self, '_session_costs', {})
|
|
|
|
self.hud.start_session(
|
|
weapon=session_display.get('weapon_name', 'Unknown'),
|
|
armor=session_display.get('armor_name', 'None'),
|
|
fap=session_display.get('healing_name', 'None'),
|
|
loadout=self._selected_loadout_name,
|
|
weapon_dpp=Decimal('0'),
|
|
weapon_cost_per_hour=Decimal('0'),
|
|
cost_per_shot=session_costs.get('cost_per_shot', Decimal('0')),
|
|
cost_per_hit=session_costs.get('cost_per_hit', Decimal('0')),
|
|
cost_per_heal=session_costs.get('cost_per_heal', Decimal('0'))
|
|
)
|
|
|
|
def _setup_log_watcher(self):
|
|
"""Setup and start the log watcher."""
|
|
use_mock = os.getenv('USE_MOCK_DATA', 'false').lower() in ('true', '1', 'yes')
|
|
log_path = self.log_path or os.getenv('EU_CHAT_LOG_PATH', '')
|
|
|
|
if use_mock or not log_path:
|
|
# Use mock log for testing
|
|
test_data_dir = Path(__file__).parent.parent / "test-data"
|
|
test_data_dir.mkdir(exist_ok=True)
|
|
mock_log = test_data_dir / "mock-chat.log"
|
|
if not mock_log.exists():
|
|
from core.log_watcher import MockLogGenerator
|
|
MockLogGenerator.create_mock_file(mock_log, lines=50)
|
|
self.log_watcher = LogWatcher(str(mock_log), poll_interval=2.0, mock_mode=True)
|
|
self.log_info("LogWatcher", "Using MOCK data for testing")
|
|
else:
|
|
self.log_watcher = LogWatcher(log_path, poll_interval=1.0, mock_mode=False)
|
|
self.log_info("LogWatcher", f"Using REAL log: {log_path}")
|
|
|
|
# Subscribe to events
|
|
self._subscribe_to_log_events()
|
|
|
|
# Start LogWatcher
|
|
self._start_log_watcher()
|
|
|
|
def _subscribe_to_log_events(self):
|
|
"""Subscribe to log watcher events."""
|
|
if not self.log_watcher:
|
|
return
|
|
|
|
def on_loot(event):
|
|
from decimal import Decimal
|
|
item_name = event.data.get('item_name', 'Unknown')
|
|
value_ped = event.data.get('value_ped', Decimal('0.0'))
|
|
|
|
if item_name == 'Universal Ammo':
|
|
return
|
|
|
|
is_shrapnel = 'shrapnel' in item_name.lower()
|
|
self.hud.update_loot(value_ped, is_shrapnel=is_shrapnel)
|
|
|
|
# Queue for database
|
|
if self._current_db_session_id:
|
|
self._event_queue.put({
|
|
'type': 'loot',
|
|
'session_id': self._current_db_session_id,
|
|
'item_name': item_name,
|
|
'value_ped': value_ped,
|
|
'raw_line': event.raw_line
|
|
})
|
|
|
|
def on_damage_dealt(event):
|
|
from decimal import Decimal
|
|
damage = event.data.get('damage', 0)
|
|
if damage:
|
|
self.hud.on_damage_dealt(Decimal(str(damage)))
|
|
if self._session_costs.get('cost_per_shot'):
|
|
self.hud.update_weapon_cost(self._session_costs['cost_per_shot'])
|
|
|
|
def on_damage_taken(event):
|
|
from decimal import Decimal
|
|
damage = event.data.get('damage', 0)
|
|
if damage:
|
|
self.hud.on_damage_taken(Decimal(str(damage)))
|
|
decay_per_damage = Decimal('0.00025')
|
|
cost_ped = Decimal(str(damage)) * decay_per_damage
|
|
if cost_ped > 0:
|
|
self.hud.update_armor_cost(cost_ped)
|
|
|
|
def on_heal(event):
|
|
from decimal import Decimal
|
|
heal_amount = event.data.get('heal_amount', Decimal('0'))
|
|
if self._session_costs.get('cost_per_heal'):
|
|
self.hud.update_healing_cost(self._session_costs['cost_per_heal'])
|
|
self.hud.on_heal_event({'heal_amount': heal_amount})
|
|
|
|
def on_personal_global(event):
|
|
value_ped = event.data.get('value_ped', Decimal('0.0'))
|
|
player = event.data.get('player_name', 'Unknown')
|
|
creature = event.data.get('creature', 'Unknown')
|
|
if self.player_name and player.lower() == self.player_name.lower():
|
|
self.hud.on_personal_global(value_ped)
|
|
self.log_info("Global", f"🎉 YOUR GLOBAL: {value_ped} PED!!!")
|
|
|
|
# Capture screenshot
|
|
if self._current_db_session_id:
|
|
screenshot_path = self._screenshot_capture.capture_global(
|
|
self._current_db_session_id,
|
|
float(value_ped),
|
|
creature
|
|
)
|
|
if screenshot_path:
|
|
self.log_info("Screenshot", f"📷 Global captured: {screenshot_path}")
|
|
|
|
def on_hof(event):
|
|
value_ped = event.data.get('value_ped', Decimal('0.0'))
|
|
creature = event.data.get('creature', 'Unknown')
|
|
self.hud.on_hof(value_ped)
|
|
self.log_info("HoF", f"🏆 HALL OF FAME: {value_ped} PED!")
|
|
|
|
# Capture screenshot
|
|
if self._current_db_session_id:
|
|
screenshot_path = self._screenshot_capture.capture_hof(
|
|
self._current_db_session_id,
|
|
float(value_ped),
|
|
creature
|
|
)
|
|
if screenshot_path:
|
|
self.log_info("Screenshot", f"📷 HoF captured: {screenshot_path}")
|
|
|
|
# Subscribe
|
|
self.log_watcher.subscribe('loot', on_loot)
|
|
self.log_watcher.subscribe('damage_dealt', on_damage_dealt)
|
|
self.log_watcher.subscribe('damage_taken', on_damage_taken)
|
|
self.log_watcher.subscribe('heal', on_heal)
|
|
self.log_watcher.subscribe('personal_global', on_personal_global)
|
|
self.log_watcher.subscribe('hof', on_hof)
|
|
|
|
def _start_log_watcher(self):
|
|
"""Start LogWatcher in background thread."""
|
|
import asyncio
|
|
from PyQt6.QtCore import QThread
|
|
|
|
class LogWatcherThread(QThread):
|
|
def __init__(self, watcher):
|
|
super().__init__()
|
|
self.watcher = watcher
|
|
self._running = True
|
|
|
|
def run(self):
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
try:
|
|
loop.run_until_complete(self.watcher.start())
|
|
while self._running:
|
|
loop.run_until_complete(asyncio.sleep(0.1))
|
|
except Exception as e:
|
|
print(f"LogWatcher error: {e}")
|
|
finally:
|
|
loop.run_until_complete(self.watcher.stop())
|
|
loop.close()
|
|
|
|
def stop(self):
|
|
self._running = False
|
|
|
|
self._log_watcher_thread = LogWatcherThread(self.log_watcher)
|
|
self._log_watcher_thread.start()
|
|
self.log_info("LogWatcher", "Started watching for events")
|
|
|
|
def _stop_log_watcher(self):
|
|
"""Stop LogWatcher."""
|
|
if hasattr(self, '_log_watcher_thread') and self._log_watcher_thread:
|
|
self._log_watcher_thread.stop()
|
|
self._log_watcher_thread.wait(2000)
|
|
self._log_watcher_thread = None
|
|
self.log_info("LogWatcher", "Stopped")
|
|
|
|
def _process_queued_events(self):
|
|
"""Process events from the queue in the main thread."""
|
|
processed = 0
|
|
while not self._event_queue.empty() and processed < 10:
|
|
try:
|
|
event = self._event_queue.get_nowait()
|
|
if event['type'] == 'loot':
|
|
self.log_info("Loot", f"{event['item_name']} ({event['value_ped']} PED)")
|
|
processed += 1
|
|
except Exception as e:
|
|
self.log_error("EventQueue", f"Error processing event: {e}")
|
|
|
|
def on_stop_session(self):
|
|
"""Handle stop session button."""
|
|
if self.session_state in (SessionState.RUNNING, SessionState.PAUSED):
|
|
self._stop_log_watcher()
|
|
|
|
# End session in database
|
|
if self._current_db_session_id:
|
|
try:
|
|
self.db.execute(
|
|
"UPDATE sessions SET ended_at = ?, status = ? WHERE id = ?",
|
|
(datetime.now().isoformat(), 'completed', self._current_db_session_id)
|
|
)
|
|
self.db.commit()
|
|
except Exception as e:
|
|
self.log_error("Session", f"Failed to end session: {e}")
|
|
self._current_db_session_id = None
|
|
|
|
self.set_session_state(SessionState.IDLE)
|
|
self.current_session_id = None
|
|
self.session_stopped.emit()
|
|
|
|
self.log_info("Session", "Session stopped")
|
|
self.session_info_label.setText("Session stopped")
|
|
|
|
self.hud.end_session()
|
|
self.hud.hide()
|
|
|
|
# Refresh recent sessions
|
|
self.refresh_recent_sessions()
|
|
|
|
def on_pause_session(self):
|
|
"""Handle pause/resume session button."""
|
|
if self.session_state == SessionState.RUNNING:
|
|
self.set_session_state(SessionState.PAUSED)
|
|
self.session_paused.emit()
|
|
if self.hud:
|
|
self.hud.session_active = False
|
|
self.hud.status_label.setText("● Paused")
|
|
self.hud.status_label.setStyleSheet("color: #FF9800; font-weight: bold;")
|
|
self.log_info("Session", "Session paused")
|
|
self.session_info_label.setText("Session paused")
|
|
self.pause_session_btn.setText("▶️ RESUME")
|
|
elif self.session_state == SessionState.PAUSED:
|
|
self.set_session_state(SessionState.RUNNING)
|
|
self.session_resumed.emit()
|
|
if self.hud:
|
|
self.hud.session_active = True
|
|
self.hud.status_label.setText("● Live")
|
|
self.hud.status_label.setStyleSheet("color: #7FFF7F; font-weight: bold;")
|
|
self.log_info("Session", "Session resumed")
|
|
self.session_info_label.setText("Session resumed")
|
|
self.pause_session_btn.setText("⏸️ PAUSE")
|
|
|
|
def set_session_state(self, state: SessionState):
|
|
"""Update the session state and UI."""
|
|
self.session_state = state
|
|
|
|
colors = {
|
|
SessionState.IDLE: "#888",
|
|
SessionState.RUNNING: "#4caf50",
|
|
SessionState.PAUSED: "#ff9800",
|
|
SessionState.ERROR: "#f44336",
|
|
SessionState.STOPPING: "#ff5722"
|
|
}
|
|
|
|
self.session_status_label.setText(state.value)
|
|
self.session_status_label.setStyleSheet(f"""
|
|
QLabel {{
|
|
font-weight: bold;
|
|
color: {colors.get(state, '#888')};
|
|
padding: 5px 15px;
|
|
background-color: #2a2a2a;
|
|
border-radius: 4px;
|
|
border: 1px solid #444;
|
|
}}
|
|
""")
|
|
|
|
self.status_state_label.setText(f"● {state.value}")
|
|
self.status_state_label.setStyleSheet(f"color: {colors.get(state, '#888')}; padding: 0 10px;")
|
|
|
|
# Update buttons
|
|
self._update_start_button()
|
|
self.stop_session_btn.setEnabled(state in (SessionState.RUNNING, SessionState.PAUSED))
|
|
self.pause_session_btn.setEnabled(state in (SessionState.RUNNING, SessionState.PAUSED))
|
|
|
|
# Update menu actions
|
|
self.stop_action.setEnabled(self.stop_session_btn.isEnabled())
|
|
self.pause_action.setEnabled(self.pause_session_btn.isEnabled())
|
|
|
|
if state == SessionState.IDLE:
|
|
self.pause_session_btn.setText("⏸️ PAUSE")
|
|
|
|
# ========================================================================
|
|
# Loadout Manager
|
|
# ========================================================================
|
|
|
|
def on_loadout_manager(self):
|
|
"""Open Loadout Manager dialog."""
|
|
from ui.loadout_manager_simple import LoadoutManagerDialog
|
|
dialog = LoadoutManagerDialog(self)
|
|
dialog.loadout_saved.connect(self._on_loadout_selected_for_session)
|
|
dialog.exec()
|
|
|
|
def _on_loadout_selected_for_session(self, loadout_config):
|
|
"""Handle loadout selection from LoadoutManagerDialog.
|
|
|
|
Args:
|
|
loadout_config: LoadoutConfig object with full gear support
|
|
"""
|
|
from ui.loadout_manager_simple import LoadoutConfig
|
|
|
|
if isinstance(loadout_config, LoadoutConfig):
|
|
# New LoadoutConfig format with full gear support
|
|
loadout_name = loadout_config.name
|
|
|
|
# Get total costs including amplifiers, platings, and mindforce implants
|
|
self._session_costs = {
|
|
'cost_per_shot': loadout_config.get_total_weapon_cost_per_shot(),
|
|
'cost_per_hit': loadout_config.get_total_armor_cost_per_hit(),
|
|
'cost_per_heal': loadout_config.get_total_healing_cost_per_heal(),
|
|
}
|
|
|
|
# Display includes all gear types
|
|
self._session_display = {
|
|
'weapon_name': loadout_config.weapon_name,
|
|
'weapon_amp_name': loadout_config.weapon_amp_name if loadout_config.weapon_amp_id else None,
|
|
'armor_name': loadout_config.armor_name,
|
|
'plating_name': loadout_config.plating_name if loadout_config.plating_id else None,
|
|
'healing_name': loadout_config.healing_name,
|
|
'mindforce_name': loadout_config.mindforce_implant_name if loadout_config.mindforce_implant_id else None,
|
|
}
|
|
|
|
# Log full gear details
|
|
gear_details = f"Weapon: {loadout_config.weapon_name}"
|
|
if loadout_config.weapon_amp_id:
|
|
gear_details += f" + {loadout_config.weapon_amp_name}"
|
|
gear_details += f" | Armor: {loadout_config.armor_name}"
|
|
if loadout_config.plating_id:
|
|
gear_details += f" + {loadout_config.plating_name}"
|
|
gear_details += f" | Healing: {loadout_config.healing_name}"
|
|
if loadout_config.mindforce_implant_id:
|
|
gear_details += f" + {loadout_config.mindforce_implant_name}"
|
|
|
|
self.log_info("Loadout", f"Selected: {loadout_name}")
|
|
self.log_info("Loadout", f"Gear: {gear_details}")
|
|
self.log_info("Loadout",
|
|
f"Costs - Shot: {self._session_costs['cost_per_shot']:.4f} PED, "
|
|
f"Hit: {self._session_costs['cost_per_hit']:.4f} PED, "
|
|
f"Heal: {self._session_costs['cost_per_heal']:.4f} PED")
|
|
else:
|
|
# Legacy dict format (fallback)
|
|
loadout_name = loadout_config.get('name', 'No Loadout') if isinstance(loadout_config, dict) else 'No Loadout'
|
|
costs = loadout_config.get('costs', {}) if isinstance(loadout_config, dict) else {}
|
|
display = loadout_config.get('display', {}) if isinstance(loadout_config, dict) else {}
|
|
|
|
from decimal import Decimal
|
|
self._session_costs = {
|
|
'cost_per_shot': costs.get('cost_per_shot', Decimal('0')),
|
|
'cost_per_hit': costs.get('cost_per_hit', Decimal('0')),
|
|
'cost_per_heal': costs.get('cost_per_heal', Decimal('0')),
|
|
}
|
|
|
|
self._session_display = {
|
|
'weapon_name': display.get('weapon_name', 'None'),
|
|
'armor_name': display.get('armor_name', 'None'),
|
|
'healing_name': display.get('healing_name', 'None'),
|
|
}
|
|
|
|
self.log_info("Loadout", f"Selected (legacy): {loadout_name}")
|
|
|
|
self._selected_loadout = loadout_config
|
|
self._selected_loadout_name = loadout_name
|
|
self.loadout_display.setText(loadout_name)
|
|
self.loadout_display.setStyleSheet("font-weight: bold; color: #4caf50;")
|
|
|
|
self._update_start_button()
|
|
|
|
def on_select_gear(self, gear_type: str = "weapon"):
|
|
"""Open Gear Selector dialog."""
|
|
from ui.gear_selector import GearSelectorDialog
|
|
dialog = GearSelectorDialog(gear_type, self)
|
|
dialog.gear_selected.connect(self.on_gear_selected)
|
|
dialog.exec()
|
|
|
|
def on_gear_selected(self, gear_type: str, name: str, stats: dict):
|
|
"""Handle gear selection."""
|
|
self.log_info("Gear", f"Selected {gear_type}: {name}")
|
|
|
|
# ========================================================================
|
|
# Log Handling
|
|
# ========================================================================
|
|
|
|
def log_debug(self, source: str, message: str):
|
|
"""Log a debug message."""
|
|
self._append_log("DEBUG", source, message)
|
|
|
|
def log_info(self, source: str, message: str):
|
|
"""Log an info message."""
|
|
self._append_log("INFO", source, message)
|
|
|
|
def log_warning(self, source: str, message: str):
|
|
"""Log a warning message."""
|
|
self._append_log("WARNING", source, message)
|
|
|
|
def log_error(self, source: str, message: str):
|
|
"""Log an error message."""
|
|
self._append_log("ERROR", source, message)
|
|
|
|
def _append_log(self, level: str, source: str, message: str):
|
|
"""Append log message to output."""
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
color = {
|
|
"DEBUG": "#888888",
|
|
"INFO": "#4caf50",
|
|
"WARNING": "#ff9800",
|
|
"ERROR": "#f44336"
|
|
}.get(level, "#ffffff")
|
|
|
|
log_entry = f'<span style="color: #666;">[{timestamp}]</span> <span style="color: {color};">[{level}]</span> <span style="color: #aaa;">[{source}]</span> {self.escape_html(message)}'
|
|
self.log_output.append(log_entry)
|
|
|
|
scrollbar = self.log_output.verticalScrollBar()
|
|
scrollbar.setValue(scrollbar.maximum())
|
|
|
|
def escape_html(self, text: str) -> str:
|
|
"""Escape HTML special characters."""
|
|
return (text
|
|
.replace("&", "&")
|
|
.replace("<", "<")
|
|
.replace(">", ">"))
|
|
|
|
# ========================================================================
|
|
# Menu Actions
|
|
# ========================================================================
|
|
|
|
def on_show_hud(self):
|
|
"""Show the HUD overlay."""
|
|
self.hud.show()
|
|
self.log_info("HUD", "HUD overlay shown")
|
|
|
|
def on_hide_hud(self):
|
|
"""Hide the HUD overlay."""
|
|
self.hud.hide()
|
|
self.log_info("HUD", "HUD overlay hidden")
|
|
|
|
def on_settings(self):
|
|
"""Open comprehensive settings dialog."""
|
|
dialog = SettingsDialog(self, self.db)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
# Reload settings from QSettings
|
|
self._load_settings()
|
|
self.log_info("Settings", "Settings updated successfully")
|
|
|
|
def on_run_setup_wizard(self):
|
|
"""Run the setup wizard again."""
|
|
from ui.setup_wizard import SetupWizard
|
|
wizard = SetupWizard(self, first_run=False)
|
|
if wizard.exec() == QDialog.DialogCode.Accepted:
|
|
settings = wizard.get_settings()
|
|
self.player_name = settings.get('avatar_name', '')
|
|
self.log_path = settings.get('log_path', '')
|
|
self.auto_detect_log = settings.get('auto_detect_log', True)
|
|
self.current_activity = ActivityType.from_string(settings.get('default_activity', 'hunting'))
|
|
self._save_settings()
|
|
self._load_settings() # Refresh UI
|
|
self.log_info("Setup", "Settings updated from wizard")
|
|
|
|
def on_about(self):
|
|
"""Show about dialog."""
|
|
QMessageBox.about(
|
|
self,
|
|
"About Lemontropia Suite",
|
|
"""<h2>Lemontropia Suite</h2>
|
|
<p>Version 1.0.0</p>
|
|
<p>A PyQt6-based GUI for Entropia Universe session tracking.</p>
|
|
<p>Features:</p>
|
|
<ul>
|
|
<li>Session management</li>
|
|
<li>Activity tracking (Hunting, Mining, Crafting)</li>
|
|
<li>Loadout management</li>
|
|
<li>Real-time HUD overlay</li>
|
|
<li>Cost tracking</li>
|
|
<li>Session history and statistics</li>
|
|
<li>Screenshot gallery for globals/HoFs</li>
|
|
</ul>
|
|
"""
|
|
)
|
|
|
|
def on_session_history(self):
|
|
"""Open Session History dialog."""
|
|
dialog = SessionHistoryDialog(self)
|
|
dialog.session_selected.connect(self._on_history_session_selected)
|
|
dialog.exec()
|
|
|
|
def _on_history_session_selected(self, session_id: int):
|
|
"""Handle session selection from history dialog."""
|
|
self.log_info("History", f"Selected session {session_id}")
|
|
# Could load this session's details or compare with current
|
|
|
|
def on_gallery(self):
|
|
"""Open Screenshot Gallery dialog."""
|
|
dialog = GalleryDialog(self)
|
|
dialog.screenshot_deleted.connect(self._on_gallery_screenshot_deleted)
|
|
dialog.exec()
|
|
|
|
def _on_gallery_screenshot_deleted(self, screenshot_id: int):
|
|
"""Handle screenshot deletion from gallery."""
|
|
self.log_info("Gallery", f"Screenshot {screenshot_id} deleted")
|
|
|
|
def on_vision_settings(self):
|
|
"""Open Computer Vision Settings dialog."""
|
|
try:
|
|
from ui.vision_settings_dialog import VisionSettingsDialog
|
|
dialog = VisionSettingsDialog(self)
|
|
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
self.log_info("Vision", "Settings updated")
|
|
except Exception as e:
|
|
self.log_error("Vision", f"Failed to open settings: {e}")
|
|
QMessageBox.warning(self, "Error", f"Could not open Vision Settings: {e}")
|
|
|
|
def on_vision_calibrate(self):
|
|
"""Open Computer Vision Calibration wizard."""
|
|
try:
|
|
from ui.vision_calibration_dialog import VisionCalibrationWizard
|
|
wizard = VisionCalibrationWizard(self)
|
|
if wizard.exec() == QDialog.DialogCode.Accepted:
|
|
self.log_info("Vision", "Calibration completed")
|
|
except Exception as e:
|
|
self.log_error("Vision", f"Failed to open calibration: {e}")
|
|
QMessageBox.warning(self, "Error", f"Could not open Vision Calibration: {e}")
|
|
|
|
def on_vision_test(self):
|
|
"""Open Computer Vision Test dialog."""
|
|
try:
|
|
from ui.vision_test_dialog import VisionTestDialog
|
|
dialog = VisionTestDialog(self)
|
|
dialog.exec()
|
|
except Exception as e:
|
|
self.log_error("Vision", f"Failed to open test dialog: {e}")
|
|
QMessageBox.warning(self, "Error", f"Could not open Vision Test: {e}")
|
|
|
|
def on_inventory_scan(self):
|
|
"""Open Inventory Scanner dialog."""
|
|
try:
|
|
dialog = InventoryScannerDialog(self)
|
|
dialog.exec()
|
|
except Exception as e:
|
|
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
|
|
# ========================================================================
|
|
|
|
def _init_screenshot_hotkeys(self):
|
|
"""Initialize global screenshot hotkey manager."""
|
|
if not SCREENSHOT_HOTKEYS_AVAILABLE:
|
|
logger.warning("Screenshot hotkeys not available (pynput not installed)")
|
|
self.log_info("Hotkeys", "Screenshot hotkeys disabled - install pynput")
|
|
return
|
|
|
|
try:
|
|
from modules.auto_screenshot import AutoScreenshot
|
|
|
|
# Create screenshot manager - AutoScreenshot now uses Documents/Entropia Universe/Screenshots by default
|
|
auto_screenshot = AutoScreenshot()
|
|
|
|
# Create hotkey manager (pass parent window for Qt shortcuts)
|
|
self._screenshot_hotkeys = ScreenshotHotkeyManager(self, auto_screenshot)
|
|
|
|
# Connect signals
|
|
self._screenshot_hotkeys.screenshot_captured.connect(self._on_screenshot_captured)
|
|
self._screenshot_hotkeys.hotkey_pressed.connect(self._on_hotkey_pressed)
|
|
|
|
# Log status
|
|
if self._screenshot_hotkeys.is_global_available():
|
|
self.log_info("Hotkeys", "Global hotkeys active (work even when game focused)")
|
|
self.log_info("Hotkeys", "F12=Full, Shift+F12=Region, Ctrl+F12=Loot, Alt+F12=HUD")
|
|
else:
|
|
self.log_info("Hotkeys", "Qt shortcuts active (app must be focused)")
|
|
self.log_info("Hotkeys", "F12=Full, Shift+F12=Region, Ctrl+F12=Loot, Alt+F12=HUD")
|
|
self.log_info("Hotkeys", "For global hotkeys, run as Administrator or install 'keyboard' library")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize screenshot hotkeys: {e}")
|
|
self._screenshot_hotkeys = None
|
|
|
|
def _on_screenshot_captured(self, filepath: str):
|
|
"""Handle screenshot captured signal."""
|
|
self.log_info("Screenshot", f"Saved: {filepath}")
|
|
|
|
# Show notification
|
|
self.statusBar().showMessage(f"📸 Screenshot saved: {filepath}", 3000)
|
|
|
|
# Refresh gallery if open
|
|
# (Gallery dialog would need to be updated to refresh)
|
|
|
|
def _on_hotkey_pressed(self, action: str):
|
|
"""Handle hotkey pressed signal."""
|
|
logger.debug(f"Screenshot hotkey pressed: {action}")
|
|
|
|
def _show_screenshot_hotkey_settings(self):
|
|
"""Show screenshot hotkey settings dialog."""
|
|
if not SCREENSHOT_HOTKEYS_AVAILABLE or not self._screenshot_hotkeys:
|
|
QMessageBox.information(
|
|
self,
|
|
"Screenshot Hotkeys",
|
|
"Screenshot hotkeys are not available.\n\n"
|
|
"Install pynput to enable:\n"
|
|
" pip install pynput\n\n"
|
|
"Default hotkeys:\n"
|
|
" F12 - Full screen\n"
|
|
" Shift+F12 - Center region\n"
|
|
" Ctrl+F12 - Loot window\n"
|
|
" Alt+F12 - HUD area"
|
|
)
|
|
return
|
|
|
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout
|
|
|
|
dialog = QDialog(self)
|
|
dialog.setWindowTitle("Screenshot Hotkey Settings")
|
|
dialog.setMinimumWidth(400)
|
|
|
|
layout = QVBoxLayout(dialog)
|
|
|
|
# Create settings widget
|
|
widget = ScreenshotHotkeyWidget(self._screenshot_hotkeys)
|
|
layout.addWidget(widget.create_settings_widget())
|
|
|
|
dialog.exec()
|
|
|
|
def _load_settings(self):
|
|
"""Load persistent settings from QSettings."""
|
|
settings = QSettings("Lemontropia", "Suite")
|
|
self.player_name = settings.value("player/name", "", type=str)
|
|
self.log_path = settings.value("log/path", "", type=str)
|
|
self.auto_detect_log = settings.value("log/auto_detect", True, type=bool)
|
|
|
|
default_activity = settings.value("activity/default", "hunting", type=str)
|
|
self.current_activity = ActivityType.from_string(default_activity)
|
|
|
|
# Update UI
|
|
index = self.activity_combo.findData(self.current_activity)
|
|
if index >= 0:
|
|
self.activity_combo.setCurrentIndex(index)
|
|
|
|
if self.player_name:
|
|
self.log_info("Settings", f"Loaded avatar name: {self.player_name}")
|
|
|
|
def _save_settings(self):
|
|
"""Save persistent settings to QSettings."""
|
|
settings = QSettings("Lemontropia", "Suite")
|
|
settings.setValue("player/name", self.player_name)
|
|
settings.setValue("log/path", self.log_path)
|
|
settings.setValue("log/auto_detect", self.auto_detect_log)
|
|
settings.setValue("activity/default", self.current_activity.value)
|
|
settings.sync()
|
|
|
|
# ========================================================================
|
|
# Event Overrides
|
|
# ========================================================================
|
|
|
|
def closeEvent(self, event):
|
|
"""Handle window close event."""
|
|
# Stop screenshot hotkey listener
|
|
if self._screenshot_hotkeys:
|
|
try:
|
|
self._screenshot_hotkeys.stop()
|
|
logger.info("Screenshot hotkeys stopped")
|
|
except Exception as e:
|
|
logger.error(f"Error stopping screenshot hotkeys: {e}")
|
|
|
|
if self.session_state == SessionState.RUNNING:
|
|
reply = QMessageBox.question(
|
|
self,
|
|
"Confirm Exit",
|
|
"A session is currently running. Are you sure you want to exit?",
|
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
|
QMessageBox.StandardButton.No
|
|
)
|
|
|
|
if reply == QMessageBox.StandardButton.Yes:
|
|
self.on_stop_session()
|
|
event.accept()
|
|
else:
|
|
event.ignore()
|
|
else:
|
|
event.accept()
|
|
|
|
|
|
# ============================================================================
|
|
# Test Entry Point
|
|
# ============================================================================
|
|
|
|
def main():
|
|
"""Main entry point for testing."""
|
|
app = QApplication(sys.argv)
|
|
|
|
# Set application-wide font
|
|
font = QFont("Segoe UI", 10)
|
|
app.setFont(font)
|
|
|
|
# Create and show main window
|
|
window = MainWindow()
|
|
window.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|