""" Lemontropia Suite - Main Application Window (Session-Focused Redesign) PyQt6 GUI for managing game automation sessions and activities. """ import sys 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 # ============================================================================ # 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) class SettingsDialog(QDialog): """Dialog for application settings.""" def __init__(self, parent=None, current_player_name: str = ""): super().__init__(parent) self.setWindowTitle("Settings") self.setMinimumWidth(450) self.player_name = current_player_name self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Player Settings Group player_group = QGroupBox("Player Settings") player_layout = QFormLayout(player_group) self.player_name_edit = QLineEdit() self.player_name_edit.setText(self.player_name) self.player_name_edit.setPlaceholderText("Your avatar name in Entropia Universe") player_layout.addRow("Avatar Name:", self.player_name_edit) help_label = QLabel("Set your avatar name to track your globals correctly.") help_label.setStyleSheet("color: #888; font-size: 11px;") player_layout.addRow(help_label) layout.addWidget(player_group) # Log Settings Group log_group = QGroupBox("Log File Settings") log_layout = QFormLayout(log_group) self.log_path_edit = QLineEdit() self.log_path_edit.setPlaceholderText("Path to chat.log") log_layout.addRow("Log Path:", self.log_path_edit) self.auto_detect_check = QCheckBox("Auto-detect log path on startup") self.auto_detect_check.setChecked(True) log_layout.addRow(self.auto_detect_check) layout.addWidget(log_group) # Default Activity Group activity_group = QGroupBox("Default Activity") activity_layout = QFormLayout(activity_group) self.default_activity_combo = QComboBox() for activity in ActivityType: self.default_activity_combo.addItem(activity.display_name, activity) activity_layout.addRow("Default:", self.default_activity_combo) layout.addWidget(activity_group) layout.addStretch() 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_player_name(self) -> str: """Get the configured player name.""" return self.player_name_edit.text().strip() def get_log_path(self) -> str: """Get the configured log path.""" return self.log_path_edit.text().strip() def get_auto_detect(self) -> bool: """Get auto-detect setting.""" return self.auto_detect_check.isChecked() def get_default_activity(self) -> str: """Get default activity type.""" activity = self.default_activity_combo.currentData() return activity.value if activity else "hunting" # ============================================================================ # 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) # Setup UI self.setup_ui() self.apply_dark_theme() self.create_menu_bar() self.create_status_bar() # 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) # 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.""" self.template_combo.clear() # Load templates from database templates = self._load_templates_from_db() for template in templates: self.template_combo.addItem( f"{template.activity_type.display_name} - {template.name}", template ) self.log_debug("Templates", f"Loaded {len(templates)} session templates") 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" ) for proj in projects: # sqlite3.Row doesn't have .get() method proj_type = proj['type'] if 'type' in proj.keys() else 'hunt' activity = ActivityType.from_string(proj_type) # Get session count count_result = self.db.fetchone( "SELECT COUNT(*) as count FROM sessions WHERE project_id = ?", (proj['id'],) ) session_count = count_result['count'] if count_result else 0 # Get last session last_result = self.db.fetchone( "SELECT MAX(started_at) as last FROM sessions WHERE project_id = ?", (proj['id'],) ) last_session = None if last_result and last_result['last']: last_session = datetime.fromisoformat(last_result['last']) # Handle description which might not exist in old databases description = proj['description'] if 'description' in proj.keys() else '' created_at = proj['created_at'] if 'created_at' in proj.keys() else None template = SessionTemplate( id=proj['id'], name=proj['name'], activity_type=activity, description=description, created_at=datetime.fromisoformat(created_at) if created_at else None, session_count=session_count, last_session=last_session ) templates.append(template) except Exception as e: self.log_error("Templates", f"Failed to load templates: {e}") 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() self.refresh_session_templates() 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: 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_debug("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: # sqlite3.Row doesn't have .get() method, use direct access row_type = row['type'] if 'type' in row.keys() else 'hunt' activity = ActivityType.from_string(row_type) # Calculate duration started = datetime.fromisoformat(row['started_at']) duration = 0 ended_at = row['ended_at'] if 'ended_at' in row.keys() else None if ended_at: ended = datetime.fromisoformat(ended_at) duration = int((ended - started).total_seconds() / 60) elif row['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=row['id'], template_name=row['template_name'], activity_type=activity, started_at=started, duration_minutes=duration, total_cost=total_cost, total_return=total_return, status=row['status'] if 'status' in row.keys() else 'unknown' ) sessions.append(session) except Exception as e: self.log_error("Sessions", f"Failed to load sessions: {e}") 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'[{timestamp}] [{level}] [{source}] {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 settings dialog.""" dialog = SettingsDialog(self, self.player_name) if dialog.exec() == QDialog.DialogCode.Accepted: self.player_name = dialog.get_player_name() self.log_path = dialog.get_log_path() self.auto_detect_log = dialog.get_auto_detect() self._save_settings() self.log_info("Settings", f"Avatar name: {self.player_name}") 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", """

Lemontropia Suite

Version 1.0.0

A PyQt6-based GUI for Entropia Universe session tracking.

Features:

""" ) 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") # ======================================================================== # Settings Management # ======================================================================== 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.""" 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()