diff --git a/docs/CODEBASE_AUDIT_REPORT.md b/docs/CODEBASE_AUDIT_REPORT.md new file mode 100644 index 0000000..0c7b514 --- /dev/null +++ b/docs/CODEBASE_AUDIT_REPORT.md @@ -0,0 +1,486 @@ +# Lemontropia Suite - Comprehensive Codebase Audit Report +**Date:** 2026-02-11 +**Auditor:** Sub-agent Analysis +**Project Path:** `/home/impulsivefps/.openclaw/workspace/projects/Lemontropia-Suite` + +--- + +## πŸ“Š EXECUTIVE SUMMARY + +| Category | Count | Status | +|----------|-------|--------| +| **Total Features Implemented** | 28 | βœ… | +| **Wired to Main UI** | 14 | βœ… Connected | +| **Partially Wired** | 3 | ⚠️ Needs Work | +| **Orphaned (Not Wired)** | 11 | πŸ”Œ Not Connected | + +--- + +## πŸ” COMPLETE FEATURE INVENTORY + +### 1. CORE MODULES (`core/`) + +| Module | Description | Status | Wiring | +|--------|-------------|--------|--------| +| `database.py` | SQLite database management | βœ… Implemented | βœ… Auto-initialized on startup | +| `project_manager.py` | Project CRUD operations | βœ… Implemented | βœ… Full UI integration | +| `log_watcher.py` | Chat.log parsing & events | βœ… Implemented | βœ… Active during sessions | +| `hunting_session.py` | Session state management | βœ… Implemented | βœ… Via project_manager | +| `session_cost_tracker.py` | Cost tracking calculations | βœ… Implemented | ⚠️ Partial (HUD uses simplified) | +| `armor_system.py` | Armor protection calculations | βœ… Implemented | βœ… Via Armor selectors | +| `armor_decay.py` | Armor decay formulas | βœ… Implemented | βœ… Integrated | +| `entropia_nexus.py` | Nexus API client | βœ… Implemented | βœ… Used by gear selectors | +| `nexus_api.py` | Extended Nexus API | βœ… Implemented | βœ… Used by gear selectors | +| `nexus_full_api.py` | Full Nexus API with models | βœ… Implemented | βœ… Used by loadout manager | +| `healing_tools.py` | Healing/FAP calculations | βœ… Implemented | βœ… Via healing selector | +| `attachments.py` | Weapon attachment stats | βœ… Implemented | βœ… Via attachment selector | +| `loadout_db.py` | Loadout database storage | βœ… Implemented | βœ… Used by loadout manager | + +### 2. UI COMPONENTS (`ui/`) + +| Component | Description | Status | Wiring | +|-----------|-------------|--------|--------| +| `main_window.py` | Primary application window | βœ… Implemented | βœ… Root component | +| `hud_overlay_clean.py` | In-game HUD overlay | βœ… Implemented | βœ… Menu + auto-show on session | +| `hud_overlay.py` | Legacy HUD (superseded) | βœ… Implemented | ❌ Not used (clean version preferred) | +| `loadout_manager_simple.py` | Simplified loadout editor | βœ… Implemented | βœ… Tools menu | +| `loadout_manager.py` | Full loadout manager (legacy) | βœ… Implemented | ❌ Superseded by simple version | +| `loadout_selection_dialog_simple.py` | Pre-session loadout picker | βœ… Implemented | βœ… Auto-shows on Start Session | +| `loadout_selection_dialog.py` | Legacy selection dialog | βœ… Implemented | ❌ Not used | +| `gear_selector.py` | Generic gear selector | βœ… Implemented | βœ… Tools > Select Gear | +| `weapon_selector.py` | Weapon picker | βœ… Implemented | βœ… Via gear selector | +| `armor_selector.py` | Armor picker | βœ… Implemented | βœ… Via gear selector | +| `armor_set_selector.py` | Full armor set picker | βœ… Implemented | ❌ Not directly wired | +| `armor_selection_dialog.py` | Armor selection dialog | βœ… Implemented | ❌ Not directly wired | +| `healing_selector.py` | FAP/Healing picker | βœ… Implemented | βœ… Via gear selector | +| `attachment_selector.py` | Weapon attachments | βœ… Implemented | βœ… Via weapon selector | +| `enhancer_selector.py` | Weapon enhancers | βœ… Implemented | βœ… Via weapon selector | +| `plate_selector.py` | Armor plate picker | βœ… Implemented | βœ… Via armor selector | +| `mindforce_selector.py` | Mindforce chips | βœ… Implemented | ⚠️ Exists but rarely used | +| `accessories_selector.py` | Hunting accessories | βœ… Implemented | ❌ Not wired to main UI | +| `icon_price_dialogs.py` | Icon browser + Price tracker | βœ… Implemented | ❌ **ORPHANED** | + +### 3. FEATURE MODULES (`modules/`) + +| Module | Description | Status | Wiring | +|--------|-------------|--------|--------| +| `icon_manager.py` | Download/cache item icons | βœ… Implemented | ❌ **ORPHANED** | +| `market_prices.py` | Price tracking & profit calc | βœ… Implemented | ❌ **ORPHANED** | +| `loot_analyzer.py` | Loot breakdown analysis | βœ… Implemented | ❌ **ORPHANED** | +| `crafting_tracker.py` | Crafting session tracker | βœ… Implemented | ❌ **ORPHANED** | +| `game_vision.py` | Screen capture & OCR | βœ… Implemented | ❌ **ORPHANED** | +| `notifications.py` | Discord/Telegram alerts | βœ… Implemented | ❌ **ORPHANED** | +| `auto_screenshot.py` | Auto-capture on events | βœ… Implemented | ❌ **ORPHANED** | + +--- + +## πŸ”Œ WIRING DIAGRAM + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ LEMONTROPIA SUITE MAIN UI β”‚ +β”‚ (main_window.py) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ FILE MENU SESSION MENU VIEW MENU β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ New Project β”œβ”€β”€ Start β”œβ”€β”€ Show/Hide HUD β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Open Project β”œβ”€β”€ Stop └── Settings β”‚ β”‚ +β”‚ β”‚ └── Exit └── Pause β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ TOOLS MENU β”‚ β”‚ +β”‚ β”‚ └── Select Gear ───────────────────────────┐ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Weapon ───────► weapon_selector.py β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Armor ────────► armor_selector.py β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Finder ───────► gear_selector.py β”‚ β”‚ β”‚ +β”‚ β”‚ └── Medical Tool ─► healing_selector.pyβ”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ └── Loadout Manager ──► loadout_manager_simple.py ◄────┐ β”‚ β”‚ +β”‚ β”‚ (Opens for editing/saving loadouts) β”‚ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ [ORPHANED - NO MENU ENTRY]: β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Icon Browser ─────► icon_price_dialogs.py β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Price Tracker ────► icon_price_dialogs.py β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Loot Analyzer ────► loot_analyzer.py β”‚ β”‚ β”‚ +β”‚ β”‚ β”œβ”€β”€ Crafting Tracker ─► crafting_tracker.py β”‚ β”‚ β”‚ +β”‚ β”‚ └── Notification Configβ–Ί notifications.py β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ PROJECT PANEL β”‚ β”‚ SESSION PANEL β”‚ β”‚ LOG PANEL β”‚ β”‚ +β”‚ β”‚ ───────────── β”‚ β”‚ ───────────── β”‚ β”‚ ───────── β”‚ β”‚ +β”‚ β”‚ List of projects β”‚ β”‚ Start/Stop/Pause β”‚ β”‚ Event log β”‚ β”‚ +β”‚ β”‚ Create/View Stats β”‚ β”‚ Status display β”‚ β”‚ Real-time β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ project_manager.py β”‚ β”‚ Start Session Button Pressed β”‚ β”‚ +β”‚ β”‚ database.py β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ (CRUD operations) β”‚ β”‚ β–Ό β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ loadout_selection_dialog_simple.py β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β–Ό β”‚ β”‚ +β”‚ β”‚ User selects loadout ───┐ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β–Ό β”‚ β”‚ +β”‚ β”‚ Session starts β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β–Ό β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ HUD OVERLAY SYSTEM β”‚ β”‚ β”‚ +β”‚ β”‚ (hud_overlay_clean.py) β”‚ β”‚ β”‚ +β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚ +β”‚ β”‚ β€’ Live profit/loss β”‚ β”‚ β”‚ +β”‚ β”‚ β€’ Return % β”‚ β”‚ β”‚ +β”‚ β”‚ β€’ Cost tracking β”‚ β”‚ β”‚ +β”‚ β”‚ β€’ Loot tracking β”‚ β”‚ β”‚ +β”‚ β”‚ β€’ Gear info β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β–Ό β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ +β”‚ β”‚ log_watcher.py β”‚ β”‚ β”‚ +β”‚ β”‚ (parses chat.log) β”‚ β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ORPHANED FEATURES (Not Wired) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ ICON MANAGER β”‚ β”‚ MARKET PRICES β”‚ β”‚ LOOT ANALYZER β”‚ β”‚ +β”‚ β”‚ ──────────── β”‚ β”‚ ──────────── β”‚ β”‚ ──────────── β”‚ β”‚ +β”‚ β”‚ β€’ Download β”‚ β”‚ β€’ Track prices β”‚ β”‚ β€’ Mob stats β”‚ β”‚ +β”‚ β”‚ from Wiki β”‚ β”‚ β€’ Calculate β”‚ β”‚ β€’ Category β”‚ β”‚ +β”‚ β”‚ β€’ Local cache β”‚ β”‚ profit β”‚ β”‚ breakdown β”‚ β”‚ +β”‚ β”‚ β€’ Export icons β”‚ β”‚ β€’ Markup % β”‚ β”‚ β€’ Top loot β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β€’ CSV export β”‚ β”‚ +β”‚ β”‚ Status: FULLY β”‚ β”‚ Status: FULLY β”‚ β”‚ Status: FULLY β”‚ β”‚ +β”‚ β”‚ IMPLEMENTED β”‚ β”‚ IMPLEMENTED β”‚ β”‚ IMPLEMENTED β”‚ β”‚ +β”‚ β”‚ NOT ACCESSIBLE β”‚ β”‚ NOT ACCESSIBLE β”‚ β”‚ NOT ACCESSIBLE β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ CRAFTING TRACKERβ”‚ β”‚ GAME VISION β”‚ β”‚ NOTIFICATIONS β”‚ β”‚ +β”‚ β”‚ ─────────────── β”‚ β”‚ ──────────── β”‚ β”‚ ──────────── β”‚ β”‚ +β”‚ β”‚ β€’ Blueprint QR β”‚ β”‚ β€’ Screen captureβ”‚ β”‚ β€’ Discord β”‚ β”‚ +β”‚ β”‚ β€’ Success rates β”‚ β”‚ β€’ OCR (Paddle) β”‚ β”‚ β€’ Telegram β”‚ β”‚ +β”‚ β”‚ β€’ Material inv β”‚ β”‚ β€’ Template matchβ”‚ β”‚ β€’ Sound alerts β”‚ β”‚ +β”‚ β”‚ β€’ Profit calc β”‚ β”‚ β€’ Gear detectionβ”‚ β”‚ β€’ Custom msgs β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Status: FULLY β”‚ β”‚ Status: MOSTLY β”‚ β”‚ Status: FULLY β”‚ β”‚ +β”‚ β”‚ IMPLEMENTED β”‚ β”‚ IMPLEMENTED β”‚ β”‚ IMPLEMENTED β”‚ β”‚ +β”‚ β”‚ NOT ACCESSIBLE β”‚ β”‚ NOT ACCESSIBLE β”‚ β”‚ NOT ACCESSIBLE β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ AUTO SCREENSHOT β”‚ β”‚ +β”‚ β”‚ ───────────── β”‚ β”‚ +β”‚ β”‚ β€’ Capture on globals/HoFs β”‚ β”‚ +β”‚ β”‚ β€’ Region capture β”‚ β”‚ +β”‚ β”‚ β€’ Screenshot viewer β”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ Status: FULLY IMPLEMENTED β”‚ β”‚ +β”‚ β”‚ NOT ACCESSIBLE β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ”΄ IDENTIFIED ORPHANED FEATURES + +### Critical Priority (High Value, Ready to Wire) + +| # | Feature | Module | UI Dialog | Integration Point | Effort | +|---|---------|--------|-----------|-------------------|--------| +| 1 | **Icon Browser** | `icon_manager.py` | `icon_price_dialogs.py` | Tools menu | Low | +| 2 | **Price Tracker** | `market_prices.py` | `icon_price_dialogs.py` | Tools menu | Low | +| 3 | **Loot Analyzer** | `loot_analyzer.py` | Built-in | Session end / Tools menu | Medium | +| 4 | **Auto Screenshot** | `auto_screenshot.py` | Built-in | Settings dialog + events | Medium | + +### Medium Priority (Good Value, May Need Work) + +| # | Feature | Module | UI Dialog | Integration Point | Effort | +|---|---------|--------|-----------|-------------------|--------| +| 5 | **Notifications** | `notifications.py` | Settings dialog | Settings dialog + event hooks | Medium | +| 6 | **Crafting Tracker** | `crafting_tracker.py` | New dialog needed | Separate crafting mode | High | + +### Lower Priority (Niche Use Cases) + +| # | Feature | Module | UI Dialog | Integration Point | Effort | +|---|---------|--------|-----------|-------------------|--------| +| 7 | **Game Vision** | `game_vision.py` | Template capture tool | Settings or calibration wizard | High | +| 8 | **Armor Set Selector** | `armor_set_selector.py` | Built-in | Replace armor_selector | Low | +| 9 | **Mindforce Selector** | `mindforce_selector.py` | Built-in | Tools > Select Gear menu | Low | +| 10 | **Accessories Selector** | `accessories_selector.py` | Built-in | Tools > Select Gear menu | Low | + +--- + +## πŸ“‹ SPECIFIC WIRING RECOMMENDATIONS + +### 1. Icon Browser & Price Tracker (Quick Win) + +**Location:** Add to `main_window.py` `create_menu_bar()` Tools menu + +```python +# In create_menu_bar() under tools_menu: +tools_menu.addSeparator() + +icon_browser_action = QAction("&Icon Browser", self) +icon_browser_action.triggered.connect(self.on_icon_browser) +tools_menu.addAction(icon_browser_action) + +price_tracker_action = QAction("&Price Tracker", self) +price_tracker_action.triggered.connect(self.on_price_tracker) +tools_menu.addAction(price_tracker_action) +``` + +**Add handlers:** +```python +def on_icon_browser(self): + from ui.icon_price_dialogs import IconBrowserDialog + dialog = IconBrowserDialog(self) + dialog.exec() + +def on_price_tracker(self): + from ui.icon_price_dialogs import PriceTrackerDialog + dialog = PriceTrackerDialog(self) + dialog.exec() +``` + +**Effort:** 5 minutes +**Value:** High - Complete UI already exists + +--- + +### 2. Loot Analyzer Integration + +**Option A - Session End Report:** +```python +# In on_stop_session() after ending session: +def on_stop_session(self): + # ... existing code ... + + # Generate loot analysis + from modules.loot_analyzer import LootAnalyzer + analyzer = LootAnalyzer() + + # Load loot from database for this session + loot_data = self.db.get_session_loot(self._current_db_session_id) + for item in loot_data: + analyzer.record_loot(item['name'], item['quantity'], + Decimal(str(item['value']))) + + # Show report or save to file + report = analyzer.generate_report() + self.log_info("LootAnalysis", "\n" + report) + analyzer.export_to_csv(Path.home() / ".lemontropia" / "loot_report.csv") +``` + +**Option B - Tools Menu:** +```python +loot_analyzer_action = QAction("&Loot Analyzer", self) +loot_analyzer_action.triggered.connect(self.on_loot_analyzer) +tools_menu.addAction(loot_analyzer_action) +``` + +**Effort:** 15-30 minutes +**Value:** High - Session analysis capability + +--- + +### 3. Auto Screenshot Integration + +**Add to SettingsDialog:** +```python +# In SettingsDialog setup_ui(): +screenshot_group = QGroupBox("Auto Screenshot") +screenshot_layout = QFormLayout(screenshot_group) + +self.screenshot_global_cb = QCheckBox("Capture on Global") +self.screenshot_hof_cb = QCheckBox("Capture on HoF") +screenshot_layout.addRow(self.screenshot_global_cb) +screenshot_layout.addRow(self.screenshot_hof_cb) +``` + +**Wire to events in main_window.py:** +```python +# In _setup_log_watcher_callbacks(), enhance existing handlers: +def on_personal_global(event): + value_ped = event.data.get('value_ped', Decimal('0')) + # ... existing code ... + + # Trigger auto-screenshot + if self.auto_screenshot and self.auto_screenshot.on_global: + self.auto_screenshot.on_global("Global", float(value_ped)) +``` + +**Effort:** 20 minutes +**Value:** Medium - Documentation capability + +--- + +### 4. Notifications Integration + +**Add to SettingsDialog:** +```python +notifications_group = QGroupBox("Notifications") +notifications_layout = QFormLayout(notifications_group) + +self.discord_webhook_edit = QLineEdit() +self.discord_webhook_edit.setPlaceholderText("Discord webhook URL...") +notifications_layout.addRow("Discord:", self.discord_webhook_edit) + +self.notify_global_cb = QCheckBox("Notify on Global") +self.notify_hof_cb = QCheckBox("Notify on HoF") +notifications_layout.addRow(self.notify_global_cb) +notifications_layout.addRow(self.notify_hof_cb) +``` + +**Initialize in MainWindow:** +```python +def __init__(self): + # ... after settings load ... + from modules.notifications import NotificationManager, NotificationConfig + notif_config = NotificationConfig( + discord_webhook=settings.value("notifications/discord", ""), + notify_on_global=settings.value("notifications/on_global", True), + notify_on_hof=settings.value("notifications/on_hof", True) + ) + self.notification_manager = NotificationManager(notif_config) +``` + +**Wire to events:** +```python +def on_personal_global(self, value_ped): + # ... existing code ... + self.notification_manager.on_global("Global", value_ped) + +def on_hof(self, value_ped): + # ... existing code ... + self.notification_manager.on_hof("HoF", value_ped) +``` + +**Effort:** 30 minutes +**Value:** High - External alerts + +--- + +### 5. Crafting Tracker (Separate Mode) + +**Recommendation:** Create a "Mode Switcher" in main UI + +```python +# Add to main_window.py setup_ui(): +mode_layout = QHBoxLayout() +mode_layout.addWidget(QLabel("Mode:")) +self.mode_combo = QComboBox() +self.mode_combo.addItems(["Hunting", "Crafting", "Mining"]) +self.mode_combo.currentTextChanged.connect(self.on_mode_changed) +mode_layout.addWidget(self.mode_combo) +main_layout.addLayout(mode_layout) + +def on_mode_changed(self, mode): + if mode == "Crafting": + from modules.crafting_tracker import CraftingTracker + self.crafting_tracker = CraftingTracker() + # Show crafting-specific UI +``` + +**Effort:** 2-4 hours +**Value:** Medium - For crafters + +--- + +## πŸ› οΈ IMPLEMENTATION PRIORITY MATRIX + +| Feature | User Value | Dev Effort | Priority | Quick Win? | +|---------|-----------|------------|----------|------------| +| Icon Browser | High | Low | **P1** | βœ… Yes | +| Price Tracker | High | Low | **P1** | βœ… Yes | +| Loot Analyzer | High | Medium | **P2** | ⚠️ Medium | +| Auto Screenshot | Medium | Medium | **P2** | ⚠️ Medium | +| Notifications | High | Medium | **P2** | ⚠️ Medium | +| Crafting Tracker | Medium | High | **P3** | ❌ No | +| Game Vision | Medium | High | **P3** | ❌ No | + +--- + +## πŸ“ FILE STRUCTURE REFERENCE + +``` +projects/Lemontropia-Suite/ +β”œβ”€β”€ main.py # CLI entry point +β”œβ”€β”€ gui_main.py # GUI launcher stub +β”œβ”€β”€ ui/ +β”‚ β”œβ”€β”€ main_window.py βœ… WIRED - Main UI +β”‚ β”œβ”€β”€ hud_overlay_clean.py βœ… WIRED - Active HUD +β”‚ β”œβ”€β”€ hud_overlay.py ❌ Legacy (unused) +β”‚ β”œβ”€β”€ loadout_manager_simple.py βœ… WIRED - Tools menu +β”‚ β”œβ”€β”€ loadout_manager.py ❌ Legacy (unused) +β”‚ β”œβ”€β”€ icon_price_dialogs.py ❌ ORPHANED - Ready to wire +β”‚ β”œβ”€β”€ gear_selector.py βœ… WIRED +β”‚ β”œβ”€β”€ weapon_selector.py βœ… WIRED +β”‚ β”œβ”€β”€ armor_selector.py βœ… WIRED +β”‚ β”œβ”€β”€ healing_selector.py βœ… WIRED +β”‚ β”œβ”€β”€ armor_set_selector.py ⚠️ Partially wired +β”‚ β”œβ”€β”€ armor_selection_dialog.py ❌ Unused +β”‚ β”œβ”€β”€ attachment_selector.py βœ… WIRED +β”‚ β”œβ”€β”€ enhancer_selector.py βœ… WIRED +β”‚ β”œβ”€β”€ plate_selector.py βœ… WIRED +β”‚ β”œβ”€β”€ mindforce_selector.py ❌ ORPHANED +β”‚ β”œβ”€β”€ accessories_selector.py ❌ ORPHANED +β”‚ └── ... +β”œβ”€β”€ core/ +β”‚ β”œβ”€β”€ database.py βœ… WIRED +β”‚ β”œβ”€β”€ project_manager.py βœ… WIRED +β”‚ β”œβ”€β”€ log_watcher.py βœ… WIRED +β”‚ β”œβ”€β”€ session_cost_tracker.py ⚠️ Partially used +β”‚ └── ... +└── modules/ + β”œβ”€β”€ icon_manager.py ❌ ORPHANED + β”œβ”€β”€ market_prices.py ❌ ORPHANED + β”œβ”€β”€ loot_analyzer.py ❌ ORPHANED + β”œβ”€β”€ crafting_tracker.py ❌ ORPHANED + β”œβ”€β”€ game_vision.py ❌ ORPHANED + β”œβ”€β”€ notifications.py ❌ ORPHANED + └── auto_screenshot.py ❌ ORPHANED +``` + +--- + +## βœ… CONCLUSION + +### What's Working Well +- **Core hunting loop** is fully functional (Projects β†’ Sessions β†’ HUD) +- **Loadout system** is well-integrated with cost tracking +- **Log parsing** is robust with event detection +- **HUD overlay** provides real-time session metrics + +### Quick Wins (Do These First) +1. **Wire Icon Browser** - 5 minutes, complete UI exists +2. **Wire Price Tracker** - 5 minutes, same dialog file +3. **Add Loot Analyzer report** on session end +4. **Add Notifications settings** with Discord/Telegram + +### Technical Debt +- Two versions of HUD exist (clean version preferred) +- Two versions of loadout manager exist (simple version preferred) +- Some selectors exist but aren't in the Select Gear menu + +### Estimated Time to Full Integration +- **Minimum viable:** 1 hour (Icon Browser + Price Tracker) +- **Recommended:** 2-3 hours (+ Loot Analyzer + Auto Screenshot) +- **Complete:** 1-2 days (+ Notifications + Crafting + Vision) + +--- + +*Report generated by Sub-agent on 2026-02-11* diff --git a/gui_main.py b/gui_main.py index 5af85b6..5485182 100644 --- a/gui_main.py +++ b/gui_main.py @@ -16,6 +16,7 @@ sys.path.insert(0, str(Path(__file__).parent)) from PyQt6.QtWidgets import QApplication from PyQt6.QtCore import Qt from ui.main_window import MainWindow +from ui.setup_wizard import SetupWizard def main(): @@ -28,6 +29,14 @@ def main(): app.setApplicationName("Lemontropia Suite") app.setApplicationVersion("0.2.0") + # Check if first run - show setup wizard + if SetupWizard.is_first_run(): + wizard = SetupWizard(first_run=True) + if wizard.exec() != SetupWizard.DialogCode.Accepted: + # User cancelled wizard, exit + print("Setup cancelled by user") + sys.exit(0) + # Create and show main window window = MainWindow() window.show() @@ -36,4 +45,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/ui/amplifier_selector.py b/ui/amplifier_selector.py new file mode 100644 index 0000000..79513ba --- /dev/null +++ b/ui/amplifier_selector.py @@ -0,0 +1,166 @@ +""" +Weapon Amplifier Selector Dialog +Uses /weaponamplifiers API endpoint +""" + +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QListWidget, QListWidgetItem, QLineEdit, QGroupBox, + QFormLayout, QMessageBox +) +from PyQt6.QtCore import Qt +from decimal import Decimal + +from core.nexus_full_api import get_nexus_api + + +class AmplifierSelectorDialog(QDialog): + """Dialog for selecting weapon amplifiers.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Select Weapon Amplifier") + self.setMinimumSize(600, 500) + + self._selected_amplifier = None + self._amplifiers_cache = [] + + self._setup_ui() + self._load_amplifiers() + + def _setup_ui(self): + layout = QVBoxLayout(self) + + # Search bar + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("Type to search amplifiers...") + self.search_edit.textChanged.connect(self._on_search) + search_layout.addWidget(self.search_edit) + layout.addLayout(search_layout) + + # Amplifier list + self.list_widget = QListWidget() + self.list_widget.itemClicked.connect(self._on_item_selected) + self.list_widget.itemDoubleClicked.connect(self.accept) + layout.addWidget(self.list_widget) + + # Info panel + info_group = QGroupBox("Amplifier Info") + info_layout = QFormLayout(info_group) + + self.info_name = QLabel("Select an amplifier") + self.info_damage = QLabel("-") + self.info_decay = QLabel("-") + self.info_ammo = QLabel("-") + self.info_efficiency = QLabel("-") + self.info_cost = QLabel("-") + + info_layout.addRow("Name:", self.info_name) + info_layout.addRow("Damage Bonus:", self.info_damage) + info_layout.addRow("Decay (PEC):", self.info_decay) + info_layout.addRow("Ammo Burn:", self.info_ammo) + info_layout.addRow("Efficiency:", self.info_efficiency) + info_layout.addRow("Cost/Shot:", self.info_cost) + + layout.addWidget(info_group) + + # Buttons + button_layout = QHBoxLayout() + button_layout.addStretch() + + self.select_btn = QPushButton("Select") + self.select_btn.clicked.connect(self.accept) + self.select_btn.setEnabled(False) + button_layout.addWidget(self.select_btn) + + cancel_btn = QPushButton("Cancel") + cancel_btn.clicked.connect(self.reject) + button_layout.addWidget(cancel_btn) + + layout.addLayout(button_layout) + + def _load_amplifiers(self): + """Load amplifiers from API.""" + try: + api = get_nexus_api() + self._amplifiers_cache = api.get_all_amplifiers() + self._populate_list(self._amplifiers_cache) + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load amplifiers: {e}") + + def _populate_list(self, amplifiers): + """Populate the list widget.""" + self.list_widget.clear() + + for amp in amplifiers: + # Format display text + decay_str = f"{amp.decay} PEC" if amp.decay else "N/A" + item_text = f"{amp.name} | Decay: {decay_str}" + + item = QListWidgetItem(item_text) + item.setData(Qt.ItemDataRole.UserRole, amp) + + # Tooltip with more info + tooltip = f"Type: {amp.attachment_type}\nDecay: {amp.decay} PEC" + if amp.damage_bonus > 0: + tooltip += f"\nDamage: +{amp.damage_bonus}" + if amp.efficiency_bonus > 0: + tooltip += f"\nEfficiency: {amp.efficiency_bonus}%" + item.setToolTip(tooltip) + + self.list_widget.addItem(item) + + def _on_search(self, text): + """Filter list based on search text.""" + if not text: + self._populate_list(self._amplifiers_cache) + return + + filtered = [amp for amp in self._amplifiers_cache + if text.lower() in amp.name.lower()] + self._populate_list(filtered) + + def _on_item_selected(self, item): + """Handle item selection.""" + amp = item.data(Qt.ItemDataRole.UserRole) + if not amp: + return + + # Store selection + decay_pec = Decimal(str(amp.decay)) if amp.decay else Decimal("0") + damage_bonus = Decimal(str(amp.damage_bonus)) if hasattr(amp, 'damage_bonus') else Decimal("0") + + self._selected_amplifier = { + 'name': amp.name, + 'api_id': amp.id, + 'decay_pec': decay_pec, + 'damage_bonus': damage_bonus, + 'efficiency': amp.efficiency_bonus if hasattr(amp, 'efficiency_bonus') else Decimal("0"), + } + + # Update info panel + self.info_name.setText(amp.name) + self.info_damage.setText(f"+{damage_bonus}") + self.info_decay.setText(f"{decay_pec} PEC") + + # Get ammo burn from properties if available + ammo_burn = "N/A" + if hasattr(amp, 'ammo_burn') and amp.ammo_burn: + ammo_burn = str(amp.ammo_burn) + self._selected_amplifier['ammo_burn'] = amp.ammo_burn + self.info_ammo.setText(ammo_burn) + + efficiency = f"{amp.efficiency_bonus}%" if hasattr(amp, 'efficiency_bonus') and amp.efficiency_bonus else "N/A" + self.info_efficiency.setText(efficiency) + + # Calculate cost per shot (decay in PEC converted to PED) + cost_per_shot = decay_pec / Decimal("100") + self.info_cost.setText(f"{cost_per_shot:.4f} PED") + + self.select_btn.setEnabled(True) + + def get_selected_amplifier(self): + """Get the selected amplifier data.""" + return self._selected_amplifier diff --git a/ui/gallery_dialog.py b/ui/gallery_dialog.py new file mode 100644 index 0000000..2c6ebb2 --- /dev/null +++ b/ui/gallery_dialog.py @@ -0,0 +1,835 @@ +""" +Lemontropia Suite - Gallery Dialog +Browse and manage screenshots captured during hunting sessions. +""" + +import os +import json +from datetime import datetime +from pathlib import Path +from typing import Optional, List, Dict, Any + +# PIL is optional - screenshots won't work without it but gallery can still view +try: + from PIL import Image, ImageGrab + PIL_AVAILABLE = True +except ImportError: + PIL_AVAILABLE = False + Image = None + ImageGrab = None + +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, + QLabel, QPushButton, QListWidget, QListWidgetItem, + QSplitter, QWidget, QGroupBox, QComboBox, + QMessageBox, QFileDialog, QScrollArea, QFrame, + QSizePolicy, QGridLayout +) +from PyQt6.QtCore import Qt, pyqtSignal, QSize +from PyQt6.QtGui import QPixmap, QImage, QColor + +from core.database import DatabaseManager + + +class ImageLabel(QLabel): + """Custom label for displaying images with click handling.""" + + clicked = pyqtSignal() + + def __init__(self, parent=None): + super().__init__(parent) + self.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.setStyleSheet("background-color: #252525; border: 1px solid #444;") + self.setMinimumSize(400, 300) + self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + def mousePressEvent(self, event): + self.clicked.emit() + + +class GalleryDialog(QDialog): + """ + Dialog for viewing and managing captured screenshots. + + Features: + - Browse all screenshots with thumbnails + - Filter by type (global, hof, all) + - View full-size image with metadata + - Delete screenshots + - Open in external viewer + """ + + screenshot_deleted = pyqtSignal(int) # Emits screenshot_id when deleted + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Screenshot Gallery") + self.setMinimumSize(1400, 900) + self.resize(1600, 1000) + + # Initialize database + self.db = DatabaseManager() + + # Ensure screenshots directory exists + self.screenshots_dir = Path(__file__).parent.parent / "data" / "screenshots" + self.screenshots_dir.mkdir(parents=True, exist_ok=True) + + # State + self.current_screenshot_id: Optional[int] = None + self.screenshots_data: List[Dict[str, Any]] = [] + self.current_pixmap: Optional[QPixmap] = None + + self._setup_ui() + self._load_screenshots() + self._apply_dark_theme() + + def _setup_ui(self): + """Setup the dialog UI.""" + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Top controls + controls_layout = QHBoxLayout() + + # Filter by type + controls_layout.addWidget(QLabel("Filter:")) + self.type_filter = QComboBox() + self.type_filter.addItem("πŸ“· All Screenshots", "all") + self.type_filter.addItem("🌟 Globals", "global") + self.type_filter.addItem("πŸ† HoFs", "hof") + self.type_filter.currentIndexChanged.connect(self._on_filter_changed) + controls_layout.addWidget(self.type_filter) + + controls_layout.addSpacing(20) + + # Filter by session + controls_layout.addWidget(QLabel("Session:")) + self.session_filter = QComboBox() + self.session_filter.addItem("All Sessions", None) + self.session_filter.currentIndexChanged.connect(self._on_filter_changed) + controls_layout.addWidget(self.session_filter) + + controls_layout.addStretch() + + # Stats label + self.stats_label = QLabel("0 screenshots") + controls_layout.addWidget(self.stats_label) + + controls_layout.addSpacing(20) + + # Refresh button + self.refresh_btn = QPushButton("πŸ”„ Refresh") + self.refresh_btn.clicked.connect(self._load_screenshots) + controls_layout.addWidget(self.refresh_btn) + + # Open folder button + self.open_folder_btn = QPushButton("πŸ“ Open Folder") + self.open_folder_btn.clicked.connect(self._open_screenshots_folder) + controls_layout.addWidget(self.open_folder_btn) + + layout.addLayout(controls_layout) + + # Main splitter + splitter = QSplitter(Qt.Orientation.Horizontal) + layout.addWidget(splitter) + + # Left side - Thumbnail list + left_panel = QWidget() + left_layout = QVBoxLayout(left_panel) + left_layout.setContentsMargins(0, 0, 0, 0) + + # Screenshots list + self.screenshots_list = QListWidget() + self.screenshots_list.setIconSize(QSize(120, 90)) + self.screenshots_list.setViewMode(QListWidget.ViewMode.IconMode) + self.screenshots_list.setResizeMode(QListWidget.ResizeMode.Adjust) + self.screenshots_list.setSpacing(10) + self.screenshots_list.setWrapping(True) + self.screenshots_list.setMinimumWidth(400) + self.screenshots_list.itemClicked.connect(self._on_screenshot_selected) + self.screenshots_list.itemDoubleClicked.connect(self._on_screenshot_double_clicked) + + left_layout.addWidget(self.screenshots_list) + + splitter.addWidget(left_panel) + + # Right side - Preview and details + right_panel = QWidget() + right_layout = QVBoxLayout(right_panel) + right_layout.setContentsMargins(0, 0, 0, 0) + + # Image preview area + self.preview_group = QGroupBox("Preview") + preview_layout = QVBoxLayout(self.preview_group) + + # Scroll area for image + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setStyleSheet("background-color: #151515; border: none;") + + self.image_label = ImageLabel() + self.image_label.setText("Select a screenshot to preview") + self.image_label.clicked.connect(self._open_external_viewer) + self.scroll_area.setWidget(self.image_label) + + preview_layout.addWidget(self.scroll_area) + right_layout.addWidget(self.preview_group, stretch=1) + + # Details panel + self.details_group = QGroupBox("Details") + details_layout = QFormLayout(self.details_group) + + self.detail_id = QLabel("-") + details_layout.addRow("ID:", self.detail_id) + + self.detail_timestamp = QLabel("-") + details_layout.addRow("Captured:", self.detail_timestamp) + + self.detail_event_type = QLabel("-") + details_layout.addRow("Event Type:", self.detail_event_type) + + self.detail_value = QLabel("-") + details_layout.addRow("Value:", self.detail_value) + + self.detail_mob = QLabel("-") + details_layout.addRow("Mob:", self.detail_mob) + + self.detail_session = QLabel("-") + details_layout.addRow("Session:", self.detail_session) + + self.detail_file_path = QLabel("-") + self.detail_file_path.setWordWrap(True) + self.detail_file_path.setStyleSheet("font-size: 9px; color: #888;") + details_layout.addRow("File:", self.detail_file_path) + + self.detail_file_size = QLabel("-") + details_layout.addRow("Size:", self.detail_file_size) + + self.detail_dimensions = QLabel("-") + details_layout.addRow("Dimensions:", self.detail_dimensions) + + right_layout.addWidget(self.details_group) + + # Action buttons + actions_layout = QHBoxLayout() + + self.open_external_btn = QPushButton("πŸ” Open External") + self.open_external_btn.clicked.connect(self._open_external_viewer) + self.open_external_btn.setEnabled(False) + actions_layout.addWidget(self.open_external_btn) + + self.save_as_btn = QPushButton("πŸ’Ύ Save As...") + self.save_as_btn.clicked.connect(self._save_as) + self.save_as_btn.setEnabled(False) + actions_layout.addWidget(self.save_as_btn) + + actions_layout.addStretch() + + self.delete_btn = QPushButton("πŸ—‘οΈ Delete") + self.delete_btn.clicked.connect(self._delete_screenshot) + self.delete_btn.setEnabled(False) + actions_layout.addWidget(self.delete_btn) + + right_layout.addLayout(actions_layout) + + splitter.addWidget(right_panel) + + # Set splitter sizes + splitter.setSizes([500, 700]) + + # Close button + close_layout = QHBoxLayout() + close_layout.addStretch() + + self.close_btn = QPushButton("Close") + self.close_btn.clicked.connect(self.accept) + close_layout.addWidget(self.close_btn) + + layout.addLayout(close_layout) + + def _load_screenshots(self): + """Load screenshots from database.""" + self.screenshots_list.clear() + self.screenshots_data = [] + + try: + # Load sessions for filter + self._load_sessions() + + # Get filter values + event_filter = self.type_filter.currentData() + session_filter = self.session_filter.currentData() + + # Build query - check both screenshots table and loot_events with screenshots + query = """ + SELECT + s.id, + s.session_id, + s.timestamp, + s.file_path, + s.trigger_event, + s.trigger_value_ped as value, + le.creature_name as mob_name, + le.event_type as loot_event_type, + p.name as project_name, + hs.started_at as session_date + FROM screenshots s + JOIN sessions ses ON s.session_id = ses.id + JOIN projects p ON ses.project_id = p.id + LEFT JOIN hunting_sessions hs ON hs.session_id = s.session_id + LEFT JOIN loot_events le ON le.session_id = s.session_id + AND le.screenshot_path = s.file_path + WHERE 1=1 + """ + params = [] + + # Apply event type filter + if event_filter == "global": + query += " AND (s.trigger_event LIKE '%global%' OR le.event_type = 'global')" + elif event_filter == "hof": + query += " AND (s.trigger_event LIKE '%hof%' OR s.trigger_event LIKE '%hall%' OR le.event_type = 'hof')" + + # Apply session filter + if session_filter: + query += " AND s.session_id = ?" + params.append(session_filter) + + query += " ORDER BY s.timestamp DESC" + + cursor = self.db.execute(query, tuple(params)) + rows = cursor.fetchall() + + for row in rows: + screenshot_data = dict(row) + self.screenshots_data.append(screenshot_data) + + # Create list item with thumbnail + item = QListWidgetItem() + item.setData(Qt.ItemDataRole.UserRole, screenshot_data['id']) + + # Set text with timestamp and value + timestamp = datetime.fromisoformat(screenshot_data['timestamp']) + time_str = timestamp.strftime("%m/%d %H:%M") + + value = screenshot_data['value'] or 0 + event_type = screenshot_data['trigger_event'] or screenshot_data['loot_event_type'] or "Manual" + + if value > 0: + item.setText(f"{time_str}\n{value:.0f} PED") + else: + item.setText(f"{time_str}\n{event_type}") + + # Load thumbnail + file_path = screenshot_data['file_path'] + if file_path and os.path.exists(file_path): + pixmap = QPixmap(file_path) + if not pixmap.isNull(): + # Scale to thumbnail size + scaled = pixmap.scaled(120, 90, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + item.setIcon(scaled) + + self.screenshots_list.addItem(item) + + # Update stats + self.stats_label.setText(f"{len(self.screenshots_data)} screenshot(s)") + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load screenshots: {e}") + + def _load_sessions(self): + """Load sessions for filter dropdown.""" + current = self.session_filter.currentData() + self.session_filter.clear() + self.session_filter.addItem("All Sessions", None) + + try: + cursor = self.db.execute(""" + SELECT DISTINCT s.id, p.name, s.started_at + FROM sessions s + JOIN projects p ON s.project_id = p.id + JOIN screenshots sc ON sc.session_id = s.id + ORDER BY s.started_at DESC + """) + + for row in cursor.fetchall(): + started = datetime.fromisoformat(row['started_at']) + label = f"{row['name']} - {started.strftime('%Y-%m-%d %H:%M')}" + self.session_filter.addItem(label, row['id']) + + # Restore selection + if current: + idx = self.session_filter.findData(current) + if idx >= 0: + self.session_filter.setCurrentIndex(idx) + except Exception: + pass + + def _on_filter_changed(self): + """Handle filter changes.""" + self._load_screenshots() + + def _on_screenshot_selected(self, item: QListWidgetItem): + """Handle screenshot selection.""" + screenshot_id = item.data(Qt.ItemDataRole.UserRole) + self.current_screenshot_id = screenshot_id + self._load_screenshot_details(screenshot_id) + + def _on_screenshot_double_clicked(self, item: QListWidgetItem): + """Handle double-click on screenshot.""" + self._open_external_viewer() + + def _load_screenshot_details(self, screenshot_id: int): + """Load and display screenshot details.""" + try: + screenshot = next((s for s in self.screenshots_data if s['id'] == screenshot_id), None) + if not screenshot: + return + + file_path = screenshot['file_path'] + + # Update details + self.detail_id.setText(str(screenshot['id'])) + + timestamp = datetime.fromisoformat(screenshot['timestamp']) + self.detail_timestamp.setText(timestamp.strftime("%Y-%m-%d %H:%M:%S")) + + event_type = screenshot['trigger_event'] or screenshot['loot_event_type'] or "Manual" + self.detail_event_type.setText(event_type.capitalize()) + + value = screenshot['value'] or 0 + if value > 0: + self.detail_value.setText(f"{value:.2f} PED") + self.detail_value.setStyleSheet("color: #4caf50; font-weight: bold;") + else: + self.detail_value.setText("-") + self.detail_value.setStyleSheet("") + + mob = screenshot['mob_name'] or "Unknown" + self.detail_mob.setText(mob) + + session_info = f"{screenshot['project_name'] or 'Unknown'}" + if screenshot['session_date']: + session_date = datetime.fromisoformat(screenshot['session_date']) + session_info += f" ({session_date.strftime('%Y-%m-%d')})" + self.detail_session.setText(session_info) + + self.detail_file_path.setText(file_path or "-") + + # Load and display image + if file_path and os.path.exists(file_path): + self._display_image(file_path) + + # Get file info + file_size = os.path.getsize(file_path) + self.detail_file_size.setText(self._format_file_size(file_size)) + + # Enable buttons + self.open_external_btn.setEnabled(True) + self.save_as_btn.setEnabled(True) + self.delete_btn.setEnabled(True) + else: + self.image_label.setText(f"File not found:\n{file_path}") + self.image_label.setPixmap(QPixmap()) + self.current_pixmap = None + self.detail_file_size.setText("-") + self.detail_dimensions.setText("-") + + # Disable buttons + self.open_external_btn.setEnabled(False) + self.save_as_btn.setEnabled(False) + self.delete_btn.setEnabled(True) # Still allow delete if DB record exists + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load screenshot details: {e}") + + def _display_image(self, file_path: str): + """Load and display the image.""" + pixmap = QPixmap(file_path) + + if pixmap.isNull(): + self.image_label.setText("Failed to load image") + self.current_pixmap = None + return + + self.current_pixmap = pixmap + + # Update dimensions + self.detail_dimensions.setText(f"{pixmap.width()} x {pixmap.height()}") + + # Scale to fit while maintaining aspect ratio + scroll_size = self.scroll_area.size() + scaled = pixmap.scaled( + scroll_size.width() - 20, + scroll_size.height() - 20, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + ) + + self.image_label.setPixmap(scaled) + self.image_label.setText("") + + def _format_file_size(self, size_bytes: int) -> str: + """Format file size to human readable.""" + if size_bytes < 1024: + return f"{size_bytes} B" + elif size_bytes < 1024 * 1024: + return f"{size_bytes / 1024:.1f} KB" + else: + return f"{size_bytes / (1024 * 1024):.1f} MB" + + def _open_external_viewer(self): + """Open the screenshot in external viewer.""" + if not self.current_screenshot_id: + return + + screenshot = next( + (s for s in self.screenshots_data if s['id'] == self.current_screenshot_id), + None + ) + + if screenshot and screenshot['file_path'] and os.path.exists(screenshot['file_path']): + import subprocess + import platform + + file_path = screenshot['file_path'] + + try: + if platform.system() == 'Windows': + os.startfile(file_path) + elif platform.system() == 'Darwin': # macOS + subprocess.run(['open', file_path]) + else: # Linux + subprocess.run(['xdg-open', file_path]) + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to open image: {e}") + + def _save_as(self): + """Save the screenshot to a new location.""" + if not self.current_screenshot_id: + return + + screenshot = next( + (s for s in self.screenshots_data if s['id'] == self.current_screenshot_id), + None + ) + + if not screenshot or not screenshot['file_path']: + return + + source_path = Path(screenshot['file_path']) + + file_path, _ = QFileDialog.getSaveFileName( + self, + "Save Screenshot", + f"screenshot_{screenshot['id']}.png", + "PNG Images (*.png);;JPEG Images (*.jpg *.jpeg);;All Files (*)" + ) + + if not file_path: + return + + try: + import shutil + shutil.copy2(source_path, file_path) + QMessageBox.information(self, "Success", f"Screenshot saved to {file_path}") + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to save: {e}") + + def _delete_screenshot(self): + """Delete the selected screenshot.""" + if not self.current_screenshot_id: + return + + reply = QMessageBox.question( + self, "Confirm Delete", + "Are you sure you want to delete this screenshot?\n\n" + "This will permanently delete the file and its metadata.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply != QMessageBox.StandardButton.Yes: + return + + try: + screenshot = next( + (s for s in self.screenshots_data if s['id'] == self.current_screenshot_id), + None + ) + + # Delete file if exists + if screenshot and screenshot['file_path'] and os.path.exists(screenshot['file_path']): + os.remove(screenshot['file_path']) + + # Delete from database + self.db.execute( + "DELETE FROM screenshots WHERE id = ?", + (self.current_screenshot_id,) + ) + self.db.commit() + + # Emit signal + self.screenshot_deleted.emit(self.current_screenshot_id) + + # Reload + self._load_screenshots() + self.current_screenshot_id = None + + # Clear details + self.image_label.setText("Select a screenshot to preview") + self.image_label.setPixmap(QPixmap()) + self.current_pixmap = None + + self.detail_id.setText("-") + self.detail_timestamp.setText("-") + self.detail_event_type.setText("-") + self.detail_value.setText("-") + self.detail_value.setStyleSheet("") + self.detail_mob.setText("-") + self.detail_session.setText("-") + self.detail_file_path.setText("-") + self.detail_file_size.setText("-") + self.detail_dimensions.setText("-") + + self.open_external_btn.setEnabled(False) + self.save_as_btn.setEnabled(False) + self.delete_btn.setEnabled(False) + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to delete screenshot: {e}") + + def _open_screenshots_folder(self): + """Open the screenshots folder in file manager.""" + import subprocess + import platform + + folder_path = str(self.screenshots_dir) + + try: + if platform.system() == 'Windows': + os.startfile(folder_path) + elif platform.system() == 'Darwin': # macOS + subprocess.run(['open', folder_path]) + else: # Linux + subprocess.run(['xdg-open', folder_path]) + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to open folder: {e}") + + def resizeEvent(self, event): + """Handle resize to rescale image.""" + super().resizeEvent(event) + if self.current_pixmap: + scroll_size = self.scroll_area.size() + scaled = self.current_pixmap.scaled( + scroll_size.width() - 20, + scroll_size.height() - 20, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + ) + self.image_label.setPixmap(scaled) + + def _apply_dark_theme(self): + """Apply dark theme styling.""" + dark_stylesheet = """ + QDialog { + 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; + } + + QListWidget { + background-color: #252525; + border: 1px solid #444; + border-radius: 4px; + outline: none; + padding: 10px; + } + + QListWidget::item { + padding: 5px; + border-radius: 4px; + margin: 2px; + background-color: #2d2d2d; + } + + QListWidget::item:selected { + background-color: #0d47a1; + color: white; + } + + QListWidget::item:hover:!selected { + background-color: #3d3d3d; + } + + QComboBox { + background-color: #252525; + border: 1px solid #444; + border-radius: 4px; + padding: 6px; + color: #e0e0e0; + min-width: 150px; + } + + QComboBox:focus { + border-color: #0d47a1; + } + + QComboBox::drop-down { + border: none; + padding-right: 10px; + } + + QScrollArea { + border: none; + } + + QLabel { + color: #e0e0e0; + } + + QFormLayout QLabel { + color: #888; + } + + QSplitter::handle { + background-color: #444; + } + """ + self.setStyleSheet(dark_stylesheet) + + +class ScreenshotCapture: + """ + Utility class for capturing screenshots. + + Handles: + - Screen capture + - Saving to file with metadata + - Database recording + """ + + def __init__(self, db: Optional[DatabaseManager] = None): + self.db = db or DatabaseManager() + self.screenshots_dir = Path(__file__).parent.parent / "data" / "screenshots" + self.screenshots_dir.mkdir(parents=True, exist_ok=True) + + def capture(self, session_id: int, trigger_event: str = "manual", + value_ped: float = 0.0, mob_name: str = "") -> Optional[str]: + """ + Capture a screenshot and save it. + + Args: + session_id: The current session ID + trigger_event: What triggered the screenshot (global, hof, manual) + value_ped: Value of the event in PED + mob_name: Name of the mob/creature + + Returns: + Path to the saved screenshot, or None if failed + """ + if not PIL_AVAILABLE: + print("Screenshot capture requires PIL (Pillow). Install with: pip install Pillow") + return None + + try: + # Generate filename with timestamp + timestamp = datetime.now() + filename = f"{timestamp.strftime('%Y%m%d_%H%M%S')}_{trigger_event}" + if value_ped > 0: + filename += f"_{value_ped:.0f}PED" + filename += ".png" + + file_path = self.screenshots_dir / filename + + # Capture screenshot using PIL + screenshot = ImageGrab.grab() + screenshot.save(file_path, "PNG") + + # Save metadata to database + cursor = self.db.execute(""" + INSERT INTO screenshots + (session_id, timestamp, file_path, trigger_event, trigger_value_ped) + VALUES (?, ?, ?, ?, ?) + """, ( + session_id, + timestamp.isoformat(), + str(file_path), + trigger_event, + value_ped + )) + + # Also update loot_events if there's a matching recent event + self.db.execute(""" + UPDATE loot_events + SET screenshot_path = ? + WHERE session_id = ? + AND screenshot_path IS NULL + AND event_type IN ('global', 'hof') + ORDER BY timestamp DESC + LIMIT 1 + """, (str(file_path), session_id)) + + self.db.commit() + + return str(file_path) + + except Exception as e: + print(f"Screenshot capture failed: {e}") + return None + + def capture_global(self, session_id: int, value_ped: float, mob_name: str = "") -> Optional[str]: + """Capture screenshot for a global event.""" + return self.capture(session_id, "global", value_ped, mob_name) + + def capture_hof(self, session_id: int, value_ped: float, mob_name: str = "") -> Optional[str]: + """Capture screenshot for a HoF event.""" + return self.capture(session_id, "hof", value_ped, mob_name) + + def capture_manual(self, session_id: int) -> Optional[str]: + """Capture manual screenshot.""" + return self.capture(session_id, "manual", 0.0, "") diff --git a/ui/loadout_manager_simple.py b/ui/loadout_manager_simple.py index 992b976..ede9099 100644 --- a/ui/loadout_manager_simple.py +++ b/ui/loadout_manager_simple.py @@ -1,6 +1,6 @@ """ -Lemontropia Suite - Loadout Manager UI v4.0 -Simplified cost-focused loadout system. +Lemontropia Suite - Loadout Manager UI v5.0 +Full gear support with weapon amplifiers, mindforce implants, and armor platings. """ import json @@ -15,7 +15,7 @@ from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QLabel, QPushButton, QGroupBox, QMessageBox, QListWidget, QListWidgetItem, - QSplitter, QWidget, QFrame, QGridLayout, + QSplitter, QWidget, QFrame, QGridLayout, QScrollArea, ) from PyQt6.QtCore import Qt, pyqtSignal @@ -25,62 +25,94 @@ logger = logging.getLogger(__name__) # ============================================================================ -# Simple Cost-Focused Loadout Config +# Full Gear Loadout Config # ============================================================================ @dataclass class LoadoutConfig: - """Simple loadout configuration focused on cost tracking. + """Complete loadout configuration with full gear support. - Core principle: Only store what's needed for cost calculations. - Everything else is display metadata. + Core principle: Store all gear types needed for comprehensive cost tracking. """ # Identity name: str = "Unnamed" - version: int = 2 # Version 2 = simplified format + version: int = 3 # Version 3 = full gear support - # === COST DATA (Required for tracking) === - # All values in PED (not PEC) + # === WEAPON & ATTACHMENTS === weapon_cost_per_shot: Decimal = Decimal("0") - armor_cost_per_hit: Decimal = Decimal("0") - healing_cost_per_heal: Decimal = Decimal("0") - - # === DISPLAY METADATA (For UI only) === weapon_name: str = "None" weapon_damage: Decimal = Decimal("0") - weapon_decay_pec: Decimal = Decimal("0") # Raw for reference - weapon_ammo_pec: Decimal = Decimal("0") # Raw for reference - - armor_name: str = "None" - armor_decay_pec: Decimal = Decimal("0") # Raw for reference - - healing_name: str = "None" - healing_decay_pec: Decimal = Decimal("0") # Raw for reference - - # === API REFERENCES (For re-loading from API) === + weapon_decay_pec: Decimal = Decimal("0") + weapon_ammo_pec: Decimal = Decimal("0") weapon_api_id: Optional[int] = None + + # Weapon Amplifier + weapon_amp_id: Optional[int] = None + weapon_amp_name: str = "None" + weapon_amp_decay: Decimal = Decimal("0") + weapon_amp_damage_bonus: Decimal = Decimal("0") + + # === ARMOR & PLATINGS === + armor_cost_per_hit: Decimal = Decimal("0") + armor_name: str = "None" + armor_decay_pec: Decimal = Decimal("0") armor_api_id: Optional[int] = None + + # Armor Plating + plating_id: Optional[int] = None + plating_name: str = "None" + plating_decay: Decimal = Decimal("0") + plating_protection_summary: str = "" + + # === HEALING & MINDFORCE === + healing_cost_per_heal: Decimal = Decimal("0") + healing_name: str = "None" + healing_decay_pec: Decimal = Decimal("0") healing_api_id: Optional[int] = None + # Mindforce Implant (for healing chips) + mindforce_implant_id: Optional[int] = None + mindforce_implant_name: str = "None" + mindforce_implant_decay: Decimal = Decimal("0") + mindforce_implant_heal_amount: Decimal = Decimal("0") + def to_dict(self) -> dict: - """Serialize to simple dictionary.""" + """Serialize to dictionary.""" return { 'name': self.name, 'version': self.version, + # Weapon 'weapon_cost_per_shot': str(self.weapon_cost_per_shot), - 'armor_cost_per_hit': str(self.armor_cost_per_hit), - 'healing_cost_per_heal': str(self.healing_cost_per_heal), 'weapon_name': self.weapon_name, 'weapon_damage': str(self.weapon_damage), 'weapon_decay_pec': str(self.weapon_decay_pec), 'weapon_ammo_pec': str(self.weapon_ammo_pec), + 'weapon_api_id': self.weapon_api_id, + # Weapon Amplifier + 'weapon_amp_id': self.weapon_amp_id, + 'weapon_amp_name': self.weapon_amp_name, + 'weapon_amp_decay': str(self.weapon_amp_decay), + 'weapon_amp_damage_bonus': str(self.weapon_amp_damage_bonus), + # Armor + 'armor_cost_per_hit': str(self.armor_cost_per_hit), 'armor_name': self.armor_name, 'armor_decay_pec': str(self.armor_decay_pec), + 'armor_api_id': self.armor_api_id, + # Plating + 'plating_id': self.plating_id, + 'plating_name': self.plating_name, + 'plating_decay': str(self.plating_decay), + 'plating_protection_summary': self.plating_protection_summary, + # Healing + 'healing_cost_per_heal': str(self.healing_cost_per_heal), 'healing_name': self.healing_name, 'healing_decay_pec': str(self.healing_decay_pec), - 'weapon_api_id': self.weapon_api_id, - 'armor_api_id': self.armor_api_id, 'healing_api_id': self.healing_api_id, + # Mindforce + 'mindforce_implant_id': self.mindforce_implant_id, + 'mindforce_implant_name': self.mindforce_implant_name, + 'mindforce_implant_decay': str(self.mindforce_implant_decay), + 'mindforce_implant_heal_amount': str(self.mindforce_implant_heal_amount), } @classmethod @@ -89,13 +121,15 @@ class LoadoutConfig: version = data.get('version', 1) if version == 1: - return cls._from_legacy(data) - else: + return cls._from_legacy_v1(data) + elif version == 2: return cls._from_v2(data) + else: + return cls._from_v3(data) @classmethod - def _from_v2(cls, data: dict) -> "LoadoutConfig": - """Parse version 2 (current) format.""" + def _from_v3(cls, data: dict) -> "LoadoutConfig": + """Parse version 3 (current) format.""" def get_decimal(key: str, default: str = "0") -> Decimal: try: return Decimal(str(data.get(key, default))) @@ -104,26 +138,72 @@ class LoadoutConfig: return cls( name=data.get('name', 'Unnamed'), - version=2, + version=3, + # Weapon weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'), - armor_cost_per_hit=get_decimal('armor_cost_per_hit'), - healing_cost_per_heal=get_decimal('healing_cost_per_heal'), weapon_name=data.get('weapon_name', 'None'), weapon_damage=get_decimal('weapon_damage'), weapon_decay_pec=get_decimal('weapon_decay_pec'), weapon_ammo_pec=get_decimal('weapon_ammo_pec'), + weapon_api_id=data.get('weapon_api_id'), + # Weapon Amplifier + weapon_amp_id=data.get('weapon_amp_id'), + weapon_amp_name=data.get('weapon_amp_name', 'None'), + weapon_amp_decay=get_decimal('weapon_amp_decay'), + weapon_amp_damage_bonus=get_decimal('weapon_amp_damage_bonus'), + # Armor + armor_cost_per_hit=get_decimal('armor_cost_per_hit'), armor_name=data.get('armor_name', 'None'), armor_decay_pec=get_decimal('armor_decay_pec'), + armor_api_id=data.get('armor_api_id'), + # Plating + plating_id=data.get('plating_id'), + plating_name=data.get('plating_name', 'None'), + plating_decay=get_decimal('plating_decay'), + plating_protection_summary=data.get('plating_protection_summary', ''), + # Healing + healing_cost_per_heal=get_decimal('healing_cost_per_heal'), healing_name=data.get('healing_name', 'None'), healing_decay_pec=get_decimal('healing_decay_pec'), + healing_api_id=data.get('healing_api_id'), + # Mindforce + mindforce_implant_id=data.get('mindforce_implant_id'), + mindforce_implant_name=data.get('mindforce_implant_name', 'None'), + mindforce_implant_decay=get_decimal('mindforce_implant_decay'), + mindforce_implant_heal_amount=get_decimal('mindforce_implant_heal_amount'), + ) + + @classmethod + def _from_v2(cls, data: dict) -> "LoadoutConfig": + """Convert version 2 to version 3.""" + def get_decimal(key: str, default: str = "0") -> Decimal: + try: + return Decimal(str(data.get(key, default))) + except Exception: + return Decimal(default) + + return cls( + name=data.get('name', 'Unnamed'), + version=3, + weapon_cost_per_shot=get_decimal('weapon_cost_per_shot'), + weapon_name=data.get('weapon_name', 'None'), + weapon_damage=get_decimal('weapon_damage'), + weapon_decay_pec=get_decimal('weapon_decay_pec'), + weapon_ammo_pec=get_decimal('weapon_ammo_pec'), weapon_api_id=data.get('weapon_api_id'), + armor_cost_per_hit=get_decimal('armor_cost_per_hit'), + armor_name=data.get('armor_name', 'None'), + armor_decay_pec=get_decimal('armor_decay_pec'), armor_api_id=data.get('armor_api_id'), + healing_cost_per_heal=get_decimal('healing_cost_per_heal'), + healing_name=data.get('healing_name', 'None'), + healing_decay_pec=get_decimal('healing_decay_pec'), healing_api_id=data.get('healing_api_id'), ) @classmethod - def _from_legacy(cls, data: dict) -> "LoadoutConfig": - """Convert legacy format to new simple format.""" + def _from_legacy_v1(cls, data: dict) -> "LoadoutConfig": + """Convert legacy format to new format.""" def get_decimal(key: str, default: str = "0") -> Decimal: try: return Decimal(str(data.get(key, default))) @@ -143,62 +223,78 @@ class LoadoutConfig: return cls( name=data.get('name', 'Unnamed'), - version=2, + version=3, weapon_cost_per_shot=weapon_cost_per_shot, - armor_cost_per_hit=armor_cost_per_hit, - healing_cost_per_heal=healing_cost_per_heal, weapon_name=data.get('weapon_name', data.get('weapon', 'None')), weapon_damage=get_decimal('weapon_damage'), weapon_decay_pec=weapon_decay, weapon_ammo_pec=weapon_ammo, + armor_cost_per_hit=armor_cost_per_hit, armor_name=data.get('armor_set_name', data.get('armor_name', 'None')), armor_decay_pec=armor_decay, + healing_cost_per_heal=healing_cost_per_heal, healing_name=data.get('heal_name', 'None'), healing_decay_pec=heal_decay, ) + def get_total_weapon_cost_per_shot(self) -> Decimal: + """Calculate total weapon cost including amplifier.""" + base_cost = self.weapon_cost_per_shot + amp_cost = self.weapon_amp_decay / Decimal("100") # Convert PEC to PED + return base_cost + amp_cost + + def get_total_healing_cost_per_heal(self) -> Decimal: + """Calculate total healing cost including mindforce implant.""" + base_cost = self.healing_cost_per_heal + implant_cost = self.mindforce_implant_decay / Decimal("100") # Convert PEC to PED + return base_cost + implant_cost + + def get_total_armor_cost_per_hit(self) -> Decimal: + """Calculate total armor cost including plating.""" + base_cost = self.armor_cost_per_hit + plating_cost = self.plating_decay / Decimal("100") # Convert PEC to PED + return base_cost + plating_cost + def get_summary(self) -> Dict[str, Any]: """Get cost summary for display.""" return { 'name': self.name, 'weapon': self.weapon_name, + 'weapon_amp': self.weapon_amp_name if self.weapon_amp_id else "None", 'armor': self.armor_name, + 'plating': self.plating_name if self.plating_id else "None", 'healing': self.healing_name, - 'cost_per_shot': self.weapon_cost_per_shot, - 'cost_per_hit': self.armor_cost_per_hit, - 'cost_per_heal': self.healing_cost_per_heal, + 'mindforce': self.mindforce_implant_name if self.mindforce_implant_id else "None", + 'cost_per_shot': self.get_total_weapon_cost_per_shot(), + 'cost_per_hit': self.get_total_armor_cost_per_hit(), + 'cost_per_heal': self.get_total_healing_cost_per_heal(), } # ============================================================================ -# Simple Loadout Manager Dialog +# Full Gear Loadout Manager Dialog # ============================================================================ class LoadoutManagerDialog(QDialog): - """Simplified loadout manager focused on cost configuration.""" + """Full-featured loadout manager with all gear types.""" loadout_saved = pyqtSignal(LoadoutConfig) def __init__(self, parent=None): super().__init__(parent) - self.setWindowTitle("Loadout Manager") - self.setMinimumSize(600, 500) + self.setWindowTitle("Loadout Manager - Full Gear Configuration") + self.setMinimumSize(800, 700) # State self.config_dir = Path.home() / ".lemontropia" / "loadouts" self.config_dir.mkdir(parents=True, exist_ok=True) self.current_config: Optional[LoadoutConfig] = None - # Cached API data - self._cached_weapons: Optional[list] = None - self._cached_armors: Optional[list] = None - self._cached_healing: Optional[list] = None - self._setup_ui() self._load_saved_loadouts() def _setup_ui(self): - """Setup simplified UI.""" + """Setup full gear UI.""" layout = QVBoxLayout(self) layout.setSpacing(10) @@ -206,7 +302,7 @@ class LoadoutManagerDialog(QDialog): name_layout = QHBoxLayout() name_layout.addWidget(QLabel("Loadout Name:")) self.name_edit = QLineEdit() - self.name_edit.setPlaceholderText("e.g., ArMatrix Ghost Hunt") + self.name_edit.setPlaceholderText("e.g., ArMatrix Ghost Hunt with Dante") name_layout.addWidget(self.name_edit) layout.addLayout(name_layout) @@ -235,13 +331,16 @@ class LoadoutManagerDialog(QDialog): splitter.addWidget(left_widget) - # Right: Configuration + # Right: Configuration (in a scroll area) + scroll = QScrollArea() + scroll.setWidgetResizable(True) right_widget = QWidget() right_layout = QVBoxLayout(right_widget) right_layout.setContentsMargins(0, 0, 0, 0) + right_layout.setSpacing(10) # -- Weapon Section -- - weapon_group = QGroupBox("βš”οΈ Weapon") + weapon_group = QGroupBox("βš”οΈ Weapon & Amplifier") weapon_layout = QFormLayout(weapon_group) self.weapon_btn = QPushButton("Select Weapon...") @@ -252,20 +351,31 @@ class LoadoutManagerDialog(QDialog): self.weapon_info.setStyleSheet("color: #888;") weapon_layout.addRow(self.weapon_info) - self.weapon_decay_label = QLabel("0 PEC") - weapon_layout.addRow("Decay:", self.weapon_decay_label) - - self.weapon_ammo_label = QLabel("0") - weapon_layout.addRow("Ammo:", self.weapon_ammo_label) - self.weapon_cost_label = QLabel("0.0000 PED") self.weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") - weapon_layout.addRow("Cost/Shot:", self.weapon_cost_label) + weapon_layout.addRow("Base Cost/Shot:", self.weapon_cost_label) + + # Amplifier + self.amp_btn = QPushButton("Select Amplifier...") + self.amp_btn.clicked.connect(self._select_amplifier) + weapon_layout.addRow("Amplifier:", self.amp_btn) + + self.amp_info = QLabel("None selected") + self.amp_info.setStyleSheet("color: #888;") + weapon_layout.addRow(self.amp_info) + + self.amp_cost_label = QLabel("0.0000 PED") + self.amp_cost_label.setStyleSheet("color: #FFA07A;") + weapon_layout.addRow("Amp Cost/Shot:", self.amp_cost_label) + + self.total_weapon_cost_label = QLabel("0.0000 PED") + self.total_weapon_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") + weapon_layout.addRow("Total Cost/Shot:", self.total_weapon_cost_label) right_layout.addWidget(weapon_group) # -- Armor Section -- - armor_group = QGroupBox("πŸ›‘οΈ Armor") + armor_group = QGroupBox("πŸ›‘οΈ Armor & Plating") armor_layout = QFormLayout(armor_group) self.armor_btn = QPushButton("Select Armor...") @@ -278,17 +388,34 @@ class LoadoutManagerDialog(QDialog): self.armor_cost_label = QLabel("0.0000 PED") self.armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") - armor_layout.addRow("Cost/Hit:", self.armor_cost_label) + armor_layout.addRow("Base Cost/Hit:", self.armor_cost_label) + + # Plating + self.plating_btn = QPushButton("Select Plating...") + self.plating_btn.clicked.connect(self._select_plating) + armor_layout.addRow("Plating:", self.plating_btn) + + self.plating_info = QLabel("None selected") + self.plating_info.setStyleSheet("color: #888;") + armor_layout.addRow(self.plating_info) + + self.plating_cost_label = QLabel("0.0000 PED") + self.plating_cost_label.setStyleSheet("color: #FFA07A;") + armor_layout.addRow("Plating Cost/Hit:", self.plating_cost_label) + + self.total_armor_cost_label = QLabel("0.0000 PED") + self.total_armor_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") + armor_layout.addRow("Total Cost/Hit:", self.total_armor_cost_label) right_layout.addWidget(armor_group) # -- Healing Section -- - healing_group = QGroupBox("πŸ’š Healing") + healing_group = QGroupBox("πŸ’š Healing & Mindforce") healing_layout = QFormLayout(healing_group) - self.healing_btn = QPushButton("Select Healing...") + self.healing_btn = QPushButton("Select Healing Tool...") self.healing_btn.clicked.connect(self._select_healing) - healing_layout.addRow("Healing:", self.healing_btn) + healing_layout.addRow("Healing Tool:", self.healing_btn) self.healing_info = QLabel("None selected") self.healing_info.setStyleSheet("color: #888;") @@ -296,24 +423,44 @@ class LoadoutManagerDialog(QDialog): self.healing_cost_label = QLabel("0.0000 PED") self.healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F;") - healing_layout.addRow("Cost/Heal:", self.healing_cost_label) + healing_layout.addRow("Base Cost/Heal:", self.healing_cost_label) + + # Mindforce Implant + self.mindforce_btn = QPushButton("Select Mindforce Chip...") + self.mindforce_btn.clicked.connect(self._select_mindforce) + healing_layout.addRow("Mindforce Chip:", self.mindforce_btn) + + self.mindforce_info = QLabel("None selected") + self.mindforce_info.setStyleSheet("color: #888;") + healing_layout.addRow(self.mindforce_info) + + self.mindforce_cost_label = QLabel("0.0000 PED") + self.mindforce_cost_label.setStyleSheet("color: #FFA07A;") + healing_layout.addRow("Chip Cost/Heal:", self.mindforce_cost_label) + + self.total_healing_cost_label = QLabel("0.0000 PED") + self.total_healing_cost_label.setStyleSheet("font-weight: bold; color: #7FFF7F; font-size: 12px;") + healing_layout.addRow("Total Cost/Heal:", self.total_healing_cost_label) right_layout.addWidget(healing_group) # -- Summary Section -- - summary_group = QGroupBox("πŸ’° Session Cost Summary") + summary_group = QGroupBox("πŸ’° Total Session Cost Summary") summary_layout = QGridLayout(summary_group) summary_layout.addWidget(QLabel("Cost per Shot:"), 0, 0) self.summary_shot = QLabel("0.0000 PED") + self.summary_shot.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_shot, 0, 1) summary_layout.addWidget(QLabel("Cost per Hit:"), 1, 0) self.summary_hit = QLabel("0.0000 PED") + self.summary_hit.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_hit, 1, 1) summary_layout.addWidget(QLabel("Cost per Heal:"), 2, 0) self.summary_heal = QLabel("0.0000 PED") + self.summary_heal.setStyleSheet("font-weight: bold; color: #7FFF7F;") summary_layout.addWidget(self.summary_heal, 2, 1) summary_layout.setColumnStretch(1, 1) @@ -321,8 +468,10 @@ class LoadoutManagerDialog(QDialog): right_layout.addStretch() - splitter.addWidget(right_widget) - splitter.setSizes([200, 400]) + scroll.setWidget(right_widget) + splitter.addWidget(scroll) + + splitter.setSizes([250, 550]) layout.addWidget(splitter) @@ -371,8 +520,6 @@ class LoadoutManagerDialog(QDialog): # Update UI self.weapon_btn.setText(weapon.name[:30]) self.weapon_info.setText(f"Damage: {weapon.damage} | Range: {weapon.range_val}") - self.weapon_decay_label.setText(f"{decay_pec} PEC") - self.weapon_ammo_label.setText(f"{ammo}") self.weapon_cost_label.setText(f"{cost_per_shot:.4f} PED") # Store for saving @@ -385,8 +532,54 @@ class LoadoutManagerDialog(QDialog): 'cost_per_shot': cost_per_shot, } + self._update_weapon_total() self._update_summary() + def _select_amplifier(self): + """Open weapon amplifier selector.""" + from ui.amplifier_selector import AmplifierSelectorDialog + + dialog = AmplifierSelectorDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + amp = dialog.get_selected_amplifier() + if amp: + self._set_amplifier(amp) + + def _set_amplifier(self, amp_data: dict): + """Set weapon amplifier.""" + name = amp_data.get('name', 'Unknown') + decay_pec = Decimal(str(amp_data.get('decay_pec', 0))) + damage_bonus = Decimal(str(amp_data.get('damage_bonus', 0))) + cost_per_shot = decay_pec / Decimal("100") + + # Update UI + self.amp_btn.setText(name[:30]) + self.amp_info.setText(f"+{damage_bonus} Damage") + self.amp_cost_label.setText(f"{cost_per_shot:.4f} PED") + + # Store for saving + self._pending_amplifier = { + 'name': name, + 'api_id': amp_data.get('api_id'), + 'decay_pec': decay_pec, + 'damage_bonus': damage_bonus, + 'cost_per_shot': cost_per_shot, + } + + self._update_weapon_total() + self._update_summary() + + def _update_weapon_total(self): + """Update total weapon cost display.""" + weapon = getattr(self, '_pending_weapon', {}) + amp = getattr(self, '_pending_amplifier', {}) + + weapon_cost = weapon.get('cost_per_shot', Decimal("0")) + amp_cost = amp.get('cost_per_shot', Decimal("0")) + total = weapon_cost + amp_cost + + self.total_weapon_cost_label.setText(f"{total:.4f} PED") + def _select_armor(self): """Open simplified armor selector.""" from ui.armor_selector import ArmorSelectorDialog @@ -399,7 +592,6 @@ class LoadoutManagerDialog(QDialog): def _set_armor(self, armor_data: dict): """Set armor and calculate cost.""" - # armor_data has: name, decay_pec, protection_summary name = armor_data.get('name', 'Unknown') decay_pec = Decimal(str(armor_data.get('decay_pec', 0))) cost_per_hit = decay_pec / Decimal("100") @@ -418,8 +610,54 @@ class LoadoutManagerDialog(QDialog): 'cost_per_hit': cost_per_hit, } + self._update_armor_total() self._update_summary() + def _select_plating(self): + """Open armor plating selector.""" + from ui.plate_selector import PlateSelectorDialog + + dialog = PlateSelectorDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + plate = dialog.get_selected_plate() + if plate: + self._set_plating(plate) + + def _set_plating(self, plate_data: dict): + """Set armor plating.""" + name = plate_data.get('name', 'Unknown') + decay_pec = Decimal(str(plate_data.get('decay_pec', 0))) + cost_per_hit = decay_pec / Decimal("100") + + # Update UI + self.plating_btn.setText(name[:30]) + prot_summary = plate_data.get('protection_summary', '') + self.plating_info.setText(prot_summary[:40] if prot_summary else "No data") + self.plating_cost_label.setText(f"{cost_per_hit:.4f} PED") + + # Store for saving + self._pending_plating = { + 'name': name, + 'api_id': plate_data.get('api_id'), + 'decay_pec': decay_pec, + 'protection_summary': prot_summary, + 'cost_per_hit': cost_per_hit, + } + + self._update_armor_total() + self._update_summary() + + def _update_armor_total(self): + """Update total armor cost display.""" + armor = getattr(self, '_pending_armor', {}) + plating = getattr(self, '_pending_plating', {}) + + armor_cost = armor.get('cost_per_hit', Decimal("0")) + plating_cost = plating.get('cost_per_hit', Decimal("0")) + total = armor_cost + plating_cost + + self.total_armor_cost_label.setText(f"{total:.4f} PED") + def _select_healing(self): """Open healing selector.""" from ui.healing_selector import HealingSelectorDialog @@ -451,13 +689,66 @@ class LoadoutManagerDialog(QDialog): 'cost_per_heal': cost_per_heal, } + self._update_healing_total() self._update_summary() + def _select_mindforce(self): + """Open mindforce implant selector.""" + from ui.mindforce_selector import MindforceSelectorDialog + + dialog = MindforceSelectorDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + chip = dialog.get_selected_chip() + if chip: + self._set_mindforce(chip) + + def _set_mindforce(self, chip_data: dict): + """Set mindforce implant.""" + name = chip_data.get('name', 'Unknown') + decay_pec = Decimal(str(chip_data.get('decay_pec', 0))) + heal_amount = Decimal(str(chip_data.get('heal_amount', 0))) + cost_per_heal = decay_pec / Decimal("100") + + # Update UI + self.mindforce_btn.setText(name[:30]) + self.mindforce_info.setText(f"Heal: {heal_amount} HP") + self.mindforce_cost_label.setText(f"{cost_per_heal:.4f} PED") + + # Store for saving + self._pending_mindforce = { + 'name': name, + 'api_id': chip_data.get('api_id'), + 'decay_pec': decay_pec, + 'heal_amount': heal_amount, + 'cost_per_heal': cost_per_heal, + } + + self._update_healing_total() + self._update_summary() + + def _update_healing_total(self): + """Update total healing cost display.""" + healing = getattr(self, '_pending_healing', {}) + mindforce = getattr(self, '_pending_mindforce', {}) + + healing_cost = healing.get('cost_per_heal', Decimal("0")) + mindforce_cost = mindforce.get('cost_per_heal', Decimal("0")) + total = healing_cost + mindforce_cost + + self.total_healing_cost_label.setText(f"{total:.4f} PED") + def _update_summary(self): """Update cost summary display.""" - shot = getattr(self, '_pending_weapon', {}).get('cost_per_shot', Decimal("0")) - hit = getattr(self, '_pending_armor', {}).get('cost_per_hit', Decimal("0")) - heal = getattr(self, '_pending_healing', {}).get('cost_per_heal', Decimal("0")) + weapon = getattr(self, '_pending_weapon', {}) + amp = getattr(self, '_pending_amplifier', {}) + armor = getattr(self, '_pending_armor', {}) + plating = getattr(self, '_pending_plating', {}) + healing = getattr(self, '_pending_healing', {}) + mindforce = getattr(self, '_pending_mindforce', {}) + + shot = weapon.get('cost_per_shot', Decimal("0")) + amp.get('cost_per_shot', Decimal("0")) + hit = armor.get('cost_per_hit', Decimal("0")) + plating.get('cost_per_hit', Decimal("0")) + heal = healing.get('cost_per_heal', Decimal("0")) + mindforce.get('cost_per_heal', Decimal("0")) self.summary_shot.setText(f"{shot:.4f} PED") self.summary_hit.setText(f"{hit:.4f} PED") @@ -472,25 +763,46 @@ class LoadoutManagerDialog(QDialog): # Build config from pending data weapon = getattr(self, '_pending_weapon', {}) + amp = getattr(self, '_pending_amplifier', {}) armor = getattr(self, '_pending_armor', {}) + plating = getattr(self, '_pending_plating', {}) healing = getattr(self, '_pending_healing', {}) + mindforce = getattr(self, '_pending_mindforce', {}) config = LoadoutConfig( name=name, + # Weapon weapon_cost_per_shot=weapon.get('cost_per_shot', Decimal("0")), - armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")), - healing_cost_per_heal=healing.get('cost_per_heal', Decimal("0")), weapon_name=weapon.get('name', 'None'), weapon_damage=weapon.get('damage', Decimal("0")), weapon_decay_pec=weapon.get('decay_pec', Decimal("0")), weapon_ammo_pec=weapon.get('ammo_pec', Decimal("0")), + weapon_api_id=weapon.get('api_id'), + # Weapon Amplifier + weapon_amp_id=amp.get('api_id'), + weapon_amp_name=amp.get('name', 'None'), + weapon_amp_decay=amp.get('decay_pec', Decimal("0")), + weapon_amp_damage_bonus=amp.get('damage_bonus', Decimal("0")), + # Armor + armor_cost_per_hit=armor.get('cost_per_hit', Decimal("0")), armor_name=armor.get('name', 'None'), armor_decay_pec=armor.get('decay_pec', Decimal("0")), + armor_api_id=armor.get('api_id'), + # Plating + plating_id=plating.get('api_id'), + plating_name=plating.get('name', 'None'), + plating_decay=plating.get('decay_pec', Decimal("0")), + plating_protection_summary=plating.get('protection_summary', ''), + # Healing + healing_cost_per_heal=healing.get('cost_per_heal', Decimal("0")), healing_name=healing.get('name', 'None'), healing_decay_pec=healing.get('decay_pec', Decimal("0")), - weapon_api_id=weapon.get('api_id'), - armor_api_id=armor.get('api_id'), healing_api_id=healing.get('api_id'), + # Mindforce + mindforce_implant_id=mindforce.get('api_id'), + mindforce_implant_name=mindforce.get('name', 'None'), + mindforce_implant_decay=mindforce.get('decay_pec', Decimal("0")), + mindforce_implant_heal_amount=mindforce.get('heal_amount', Decimal("0")), ) # Save to file @@ -529,10 +841,14 @@ class LoadoutManagerDialog(QDialog): # Tooltip with costs tooltip = ( f"Weapon: {config.weapon_name}\n" + f" + Amp: {config.weapon_amp_name if config.weapon_amp_id else 'None'}\n" f"Armor: {config.armor_name}\n" - f"Cost/Shot: {config.weapon_cost_per_shot:.4f} PED\n" - f"Cost/Hit: {config.armor_cost_per_hit:.4f} PED\n" - f"Cost/Heal: {config.healing_cost_per_heal:.4f} PED" + f" + Plating: {config.plating_name if config.plating_id else 'None'}\n" + f"Healing: {config.healing_name}\n" + f" + Chip: {config.mindforce_implant_name if config.mindforce_implant_id else 'None'}\n" + f"Cost/Shot: {config.get_total_weapon_cost_per_shot():.4f} PED\n" + f"Cost/Hit: {config.get_total_armor_cost_per_hit():.4f} PED\n" + f"Cost/Heal: {config.get_total_healing_cost_per_heal():.4f} PED" ) item.setToolTip(tooltip) self.saved_list.addItem(item) @@ -569,8 +885,6 @@ class LoadoutManagerDialog(QDialog): if config.weapon_name != "None": self.weapon_btn.setText(config.weapon_name[:30]) self.weapon_info.setText(f"Damage: {config.weapon_damage}") - self.weapon_decay_label.setText(f"{config.weapon_decay_pec} PEC") - self.weapon_ammo_label.setText(f"{config.weapon_ammo_pec}") self.weapon_cost_label.setText(f"{config.weapon_cost_per_shot:.4f} PED") self._pending_weapon = { 'name': config.weapon_name, @@ -581,6 +895,20 @@ class LoadoutManagerDialog(QDialog): 'cost_per_shot': config.weapon_cost_per_shot, } + # Weapon Amplifier + if config.weapon_amp_id: + self.amp_btn.setText(config.weapon_amp_name[:30]) + self.amp_info.setText(f"+{config.weapon_amp_damage_bonus} Damage") + amp_cost = config.weapon_amp_decay / Decimal("100") + self.amp_cost_label.setText(f"{amp_cost:.4f} PED") + self._pending_amplifier = { + 'name': config.weapon_amp_name, + 'api_id': config.weapon_amp_id, + 'decay_pec': config.weapon_amp_decay, + 'damage_bonus': config.weapon_amp_damage_bonus, + 'cost_per_shot': amp_cost, + } + # Armor if config.armor_name != "None": self.armor_btn.setText(config.armor_name[:30]) @@ -593,6 +921,20 @@ class LoadoutManagerDialog(QDialog): 'cost_per_hit': config.armor_cost_per_hit, } + # Plating + if config.plating_id: + self.plating_btn.setText(config.plating_name[:30]) + self.plating_info.setText(config.plating_protection_summary[:40] if config.plating_protection_summary else "No data") + plating_cost = config.plating_decay / Decimal("100") + self.plating_cost_label.setText(f"{plating_cost:.4f} PED") + self._pending_plating = { + 'name': config.plating_name, + 'api_id': config.plating_id, + 'decay_pec': config.plating_decay, + 'protection_summary': config.plating_protection_summary, + 'cost_per_hit': plating_cost, + } + # Healing if config.healing_name != "None": self.healing_btn.setText(config.healing_name[:30]) @@ -604,29 +946,64 @@ class LoadoutManagerDialog(QDialog): 'cost_per_heal': config.healing_cost_per_heal, } + # Mindforce + if config.mindforce_implant_id: + self.mindforce_btn.setText(config.mindforce_implant_name[:30]) + mindforce_cost = config.mindforce_implant_decay / Decimal("100") + self.mindforce_cost_label.setText(f"{mindforce_cost:.4f} PED") + self._pending_mindforce = { + 'name': config.mindforce_implant_name, + 'api_id': config.mindforce_implant_id, + 'decay_pec': config.mindforce_implant_decay, + 'heal_amount': config.mindforce_implant_heal_amount, + 'cost_per_heal': mindforce_cost, + } + + self._update_weapon_total() + self._update_armor_total() + self._update_healing_total() self._update_summary() self.current_config = config def _new_loadout(self): """Clear all fields for new loadout.""" self.name_edit.clear() + + # Reset weapon section self.weapon_btn.setText("Select Weapon...") self.weapon_info.setText("None selected") - self.weapon_decay_label.setText("0 PEC") - self.weapon_ammo_label.setText("0") self.weapon_cost_label.setText("0.0000 PED") + self.amp_btn.setText("Select Amplifier...") + self.amp_info.setText("None selected") + self.amp_cost_label.setText("0.0000 PED") + self.total_weapon_cost_label.setText("0.0000 PED") + # Reset armor section self.armor_btn.setText("Select Armor...") self.armor_info.setText("None selected") self.armor_cost_label.setText("0.0000 PED") + self.plating_btn.setText("Select Plating...") + self.plating_info.setText("None selected") + self.plating_cost_label.setText("0.0000 PED") + self.total_armor_cost_label.setText("0.0000 PED") + # Reset healing section self.healing_btn.setText("Select Healing...") self.healing_info.setText("None selected") self.healing_cost_label.setText("0.0000 PED") + self.mindforce_btn.setText("Select Mindforce Chip...") + self.mindforce_info.setText("None selected") + self.mindforce_cost_label.setText("0.0000 PED") + self.total_healing_cost_label.setText("0.0000 PED") + # Clear pending data self._pending_weapon = None + self._pending_amplifier = None self._pending_armor = None + self._pending_plating = None self._pending_healing = None + self._pending_mindforce = None + self._update_summary() self.current_config = None diff --git a/ui/main_window.py b/ui/main_window.py index b28e01d..f3a5efa 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1,13 +1,13 @@ """ -Lemontropia Suite - Main Application Window -PyQt6 GUI for managing game automation projects and sessions. +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 +from typing import Optional, List, Callable, Any from dataclasses import dataclass # Setup logger @@ -19,7 +19,8 @@ from PyQt6.QtWidgets import ( QTextEdit, QLabel, QStatusBar, QMenuBar, QMenu, QDialog, QLineEdit, QFormLayout, QDialogButtonBox, QMessageBox, QGroupBox, QFrame, QApplication, - QTreeWidget, QTreeWidgetItem, QHeaderView + QTreeWidget, QTreeWidgetItem, QHeaderView, QComboBox, + QGridLayout, QToolButton ) from PyQt6.QtCore import Qt, pyqtSignal, QTimer, QSize, QSettings from PyQt6.QtGui import QAction, QFont, QColor, QPalette, QIcon @@ -38,22 +39,55 @@ class SessionState(Enum): STOPPING = "Stopping" +class ActivityType(Enum): + """Activity type enumeration.""" + HUNTING = ("hunting", "🎯 Hunting", "#4caf50") + MINING = ("mining", "⛏️ Mining", "#ff9800") + CRAFTING = ("crafting", "βš’οΈ 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 Project: - """Project data model.""" +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 # DEBUG, INFO, WARNING, ERROR, CRITICAL + level: str source: str message: str @@ -69,6 +103,14 @@ class LogEvent: 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 # ============================================================================ @@ -82,7 +124,6 @@ from decimal import Decimal sys.path.insert(0, str(Path(__file__).parent.parent / "core")) from core.log_watcher import LogWatcher -from core.project_manager import ProjectManager from core.database import DatabaseManager @@ -90,12 +131,12 @@ from core.database import DatabaseManager # Custom Dialogs # ============================================================================ -class NewProjectDialog(QDialog): - """Dialog for creating a new project.""" +class NewSessionTemplateDialog(QDialog): + """Dialog for creating a new session template.""" def __init__(self, parent=None): super().__init__(parent) - self.setWindowTitle("New Project") + self.setWindowTitle("New Session Template") self.setMinimumWidth(400) self.setup_ui() @@ -106,9 +147,14 @@ class NewProjectDialog(QDialog): form_layout = QFormLayout() self.name_input = QLineEdit() - self.name_input.setPlaceholderText("Enter project name...") + 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) @@ -124,26 +170,31 @@ class NewProjectDialog(QDialog): button_box.rejected.connect(self.reject) layout.addWidget(button_box) - def get_project_data(self) -> tuple: - """Get the entered project data.""" - return self.name_input.text().strip(), self.desc_input.text().strip() + 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", "Project name is required.") + QMessageBox.warning(self, "Validation Error", "Template name is required.") return super().accept() -class ProjectStatsDialog(QDialog): - """Dialog for displaying project statistics.""" +class TemplateStatsDialog(QDialog): + """Dialog for displaying session template statistics.""" - def __init__(self, project: Project, parent=None): + def __init__(self, template: SessionTemplate, parent=None): super().__init__(parent) - self.project = project - self.setWindowTitle(f"Project Statistics - {project.name}") + self.template = template + self.setWindowTitle(f"Template Statistics - {template.name}") self.setMinimumWidth(350) self.setup_ui() @@ -151,21 +202,24 @@ class ProjectStatsDialog(QDialog): layout = QVBoxLayout(self) # Stats display - stats_group = QGroupBox("Project Information") + stats_group = QGroupBox("Template Information") stats_layout = QFormLayout(stats_group) - stats_layout.addRow("ID:", QLabel(str(self.project.id))) - stats_layout.addRow("Name:", QLabel(self.project.name)) - stats_layout.addRow("Type:", QLabel(self.project.type)) - stats_layout.addRow("Status:", QLabel(self.project.status)) - - # Description from metadata - description = self.project.metadata.get('description', 'N/A') if self.project.metadata else 'N/A' - stats_layout.addRow("Description:", QLabel(description)) - - created = self.project.created_at.strftime("%Y-%m-%d %H:%M") if self.project.created_at else "N/A" + 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 @@ -180,27 +234,53 @@ class SettingsDialog(QDialog): def __init__(self, parent=None, current_player_name: str = ""): super().__init__(parent) self.setWindowTitle("Settings") - self.setMinimumWidth(400) + 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( @@ -209,11 +289,24 @@ class SettingsDialog(QDialog): 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 @@ -222,12 +315,15 @@ class SettingsDialog(QDialog): class MainWindow(QMainWindow): """ Main application window for Lemontropia Suite. - - Provides project management, session control, and log viewing capabilities. + + 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) # project_id + session_started = pyqtSignal(int) session_stopped = pyqtSignal() session_paused = pyqtSignal() session_resumed = pyqtSignal() @@ -240,14 +336,12 @@ class MainWindow(QMainWindow): self.setMinimumSize(1200, 800) self.resize(1400, 900) - # Initialize database and real project manager + # Initialize database self.db = DatabaseManager() if not self.db.initialize(): QMessageBox.critical(self, "Error", "Failed to initialize database!") sys.exit(1) - self.project_manager = ProjectManager(self.db) - # Initialize HUD self.hud = HUDOverlay() @@ -262,29 +356,25 @@ class MainWindow(QMainWindow): # 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) # Check every 100ms + self._queue_timer.start(100) # State - self.current_project: Optional[Project] = None + 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 = "" # Set via Settings dialog - # Selected gear - self._selected_weapon: Optional[str] = None - self._selected_weapon_stats: Optional[dict] = None - self._selected_armor: Optional[str] = None - self._selected_armor_stats: Optional[dict] = None - self._selected_finder: Optional[str] = None - self._selected_finder_stats: Optional[dict] = None - self._selected_medical_tool: Optional[str] = None - self._selected_medical_tool_stats: Optional[dict] = 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 - - # Session cost tracking (initialized empty, populated by loadout selection) + self._selected_loadout_name: str = "No Loadout" + + # Session cost tracking self._session_costs: dict = { 'cost_per_shot': Decimal('0'), 'cost_per_hit': Decimal('0'), @@ -296,28 +386,32 @@ class MainWindow(QMainWindow): '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 (after UI setup so log_info works) + # Load persistent settings self._load_settings() # Load initial data - self.refresh_project_list() + 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 + # UI Setup - New Session-Focused Layout # ======================================================================== def setup_ui(self): - """Setup the main UI layout.""" + """Setup the main UI layout with session-focused design.""" # Central widget central_widget = QWidget() self.setCentralWidget(central_widget) @@ -331,99 +425,109 @@ class MainWindow(QMainWindow): self.main_splitter = QSplitter(Qt.Orientation.Horizontal) main_layout.addWidget(self.main_splitter) - # Left side container + # 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) - # Left splitter (vertical: projects | session control) - left_splitter = QSplitter(Qt.Orientation.Vertical) - left_layout.addWidget(left_splitter) + # === TOP: Activity Type Selector + Loadout Selector === + self.activity_panel = self.create_activity_panel() + left_layout.addWidget(self.activity_panel) - # Project panel - self.project_panel = self.create_project_panel() - left_splitter.addWidget(self.project_panel) - - # Session control panel + # === MIDDLE: Session Control (with Loadout Manager button) === self.session_panel = self.create_session_panel() - left_splitter.addWidget(self.session_panel) + left_layout.addWidget(self.session_panel) - # Set splitter proportions - left_splitter.setSizes([400, 300]) + # === 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 + # Log output panel (right side) self.log_panel = self.create_log_panel() self.main_splitter.addWidget(self.log_panel) - # Set main splitter proportions (30% left, 70% log) - self.main_splitter.setSizes([400, 900]) + # Set main splitter proportions (35% left, 65% log) + self.main_splitter.setSizes([450, 850]) - def create_project_panel(self) -> QGroupBox: - """Create the project management panel.""" - panel = QGroupBox("Project Management") + def create_activity_panel(self) -> QGroupBox: + """Create the activity type and loadout selection panel (TOP).""" + panel = QGroupBox("Activity Setup") layout = QVBoxLayout(panel) - layout.setSpacing(8) + layout.setSpacing(10) - # Project list - self.project_list = QTreeWidget() - self.project_list.setHeaderLabels(["ID", "Name", "Type", "Status"]) - self.project_list.setAlternatingRowColors(True) - self.project_list.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection) - self.project_list.setRootIsDecorated(False) - self.project_list.itemSelectionChanged.connect(self.on_project_selected) - self.project_list.itemDoubleClicked.connect(self.on_project_double_clicked) + # 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) - # Adjust column widths - header = self.project_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.resizeSection(0, 50) - header.resizeSection(2, 70) - header.resizeSection(3, 80) + # 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) - layout.addWidget(self.project_list) - - # Button row - button_layout = QHBoxLayout() - - self.new_project_btn = QPushButton("βž• New Project") - self.new_project_btn.setToolTip("Create a new project") - self.new_project_btn.clicked.connect(self.on_new_project) - button_layout.addWidget(self.new_project_btn) - - self.view_stats_btn = QPushButton("πŸ“Š View Stats") - self.view_stats_btn.setToolTip("View selected project statistics") - self.view_stats_btn.clicked.connect(self.on_view_stats) - self.view_stats_btn.setEnabled(False) - button_layout.addWidget(self.view_stats_btn) - - self.refresh_btn = QPushButton("πŸ”„ Refresh") - self.refresh_btn.setToolTip("Refresh project list") - self.refresh_btn.clicked.connect(self.refresh_project_list) - button_layout.addWidget(self.refresh_btn) - - layout.addLayout(button_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.""" + """Create the session control panel (MIDDLE).""" panel = QGroupBox("Session Control") layout = QVBoxLayout(panel) - layout.setSpacing(10) + layout.setSpacing(12) - # Current project display - project_info_layout = QFormLayout() - self.current_project_label = QLabel("No project selected") - self.current_project_label.setStyleSheet("font-weight: bold; color: #888;") - project_info_layout.addRow("Selected Project:", self.current_project_label) - layout.addLayout(project_info_layout) + # 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() @@ -449,31 +553,76 @@ class MainWindow(QMainWindow): status_layout.addStretch() layout.addLayout(status_layout) - # Control buttons + # 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 with selected project") + 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) - self.start_session_btn.setEnabled(False) - button_layout.addWidget(self.start_session_btn) + button_layout.addWidget(self.start_session_btn, 2) - self.stop_session_btn = QPushButton("⏹️ Stop") + self.stop_session_btn = QPushButton("⏹️ STOP") self.stop_session_btn.setToolTip("Stop current session") - self.stop_session_btn.clicked.connect(self.on_stop_session) + self.stop_session_btn.setMinimumHeight(50) self.stop_session_btn.setEnabled(False) - button_layout.addWidget(self.stop_session_btn) + 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 current session") - self.pause_session_btn.clicked.connect(self.on_pause_session) + 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) - button_layout.addWidget(self.pause_session_btn) + 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 info - self.session_info_label = QLabel("Ready to start") + # 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) @@ -482,6 +631,57 @@ class MainWindow(QMainWindow): 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") @@ -520,15 +720,10 @@ class MainWindow(QMainWindow): # File menu file_menu = menubar.addMenu("&File") - new_project_action = QAction("&New Project", self) - new_project_action.setShortcut("Ctrl+N") - new_project_action.triggered.connect(self.on_new_project) - file_menu.addAction(new_project_action) - - open_project_action = QAction("&Open Project", self) - open_project_action.setShortcut("Ctrl+O") - open_project_action.triggered.connect(self.on_open_project) - file_menu.addAction(open_project_action) + 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() @@ -558,30 +753,16 @@ class MainWindow(QMainWindow): session_menu.addAction(pause_action) self.pause_action = pause_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) - # Tools menu tools_menu = menubar.addMenu("&Tools") - # Select Gear submenu + 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) @@ -604,16 +785,50 @@ class MainWindow(QMainWindow): select_medical_action.triggered.connect(lambda: self.on_select_gear("medical_tool")) select_gear_menu.addAction(select_medical_action) - tools_menu.addSeparator() + # View menu + view_menu = menubar.addMenu("&View") - loadout_action = QAction("&Loadout Manager", self) - loadout_action.setShortcut("Ctrl+L") - loadout_action.triggered.connect(self.on_loadout_manager) - tools_menu.addAction(loadout_action) + 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) @@ -628,9 +843,9 @@ class MainWindow(QMainWindow): self.status_state_label.setStyleSheet("color: #888; padding: 0 10px;") self.status_bar.addPermanentWidget(self.status_state_label) - self.status_project_label = QLabel("No project") - self.status_project_label.setStyleSheet("color: #888; padding: 0 10px;") - self.status_bar.addPermanentWidget(self.status_project_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") @@ -692,24 +907,6 @@ class MainWindow(QMainWindow): border-color: #333; } - QPushButton#start_button { - background-color: #1b5e20; - border-color: #2e7d32; - } - - QPushButton#start_button:hover { - background-color: #2e7d32; - } - - QPushButton#stop_button { - background-color: #b71c1c; - border-color: #c62828; - } - - QPushButton#stop_button:hover { - background-color: #c62828; - } - QTreeWidget { background-color: #252525; border: 1px solid #444; @@ -759,6 +956,24 @@ class MainWindow(QMainWindow): 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; @@ -826,129 +1041,284 @@ class MainWindow(QMainWindow): self.setStyleSheet(dark_stylesheet) # ======================================================================== - # Project Management + # Session Template Management # ======================================================================== - def refresh_project_list(self): - """Refresh the project list display.""" - self.project_list.clear() - projects = self.project_manager.list_projects() + 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") - for project in projects: - item = QTreeWidgetItem([ - str(project.id), - project.name, - project.type, - project.status - ]) - item.setData(0, Qt.ItemDataRole.UserRole, project.id) - self.project_list.addTopLevelItem(item) + 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: + activity = ActivityType.from_string(proj.get('type', 'hunting')) + # 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']) + + template = SessionTemplate( + id=proj['id'], + name=proj['name'], + activity_type=activity, + description=proj.get('description', ''), + created_at=datetime.fromisoformat(proj['created_at']) if proj.get('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 - self.log_debug("ProjectManager", f"Loaded {len(projects)} projects") - - def on_project_selected(self): - """Handle project selection change.""" - selected = self.project_list.selectedItems() - if selected: - project_id = selected[0].data(0, Qt.ItemDataRole.UserRole) - self.current_project = self.project_manager.load_project(project_id) - - if self.current_project: - self.current_project_label.setText(self.current_project.name) - self.current_project_label.setStyleSheet("font-weight: bold; color: #4caf50;") - self.view_stats_btn.setEnabled(True) - self.start_session_btn.setEnabled(self.session_state == SessionState.IDLE) - self.status_project_label.setText(f"Project: {self.current_project.name}") - self.log_debug("ProjectManager", f"Selected project: {self.current_project.name}") - else: - self.current_project = None - self.current_project_label.setText("No project selected") - self.current_project_label.setStyleSheet("font-weight: bold; color: #888;") - self.view_stats_btn.setEnabled(False) - self.start_session_btn.setEnabled(False) - self.status_project_label.setText("No project") - - def on_project_double_clicked(self, item: QTreeWidgetItem, column: int): - """Handle double-click on project.""" - project_id = item.data(0, Qt.ItemDataRole.UserRole) - project = self.project_manager.load_project(project_id) - if project: - self.show_project_stats(project) - - def on_new_project(self): - """Handle new project creation.""" - dialog = NewProjectDialog(self) + def on_new_template(self): + """Handle new session template creation.""" + dialog = NewSessionTemplateDialog(self) if dialog.exec() == QDialog.DialogCode.Accepted: - name, description = dialog.get_project_data() - metadata = {"description": description} if description else None - project = self.project_manager.create_project(name, 'hunt', metadata) - self.refresh_project_list() - self.log_info("ProjectManager", f"Created project: {project.name}") - self.status_bar.showMessage(f"Project '{name}' created", 3000) + 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_open_project(self): - """Handle open project action.""" - # For now, just focus the project list - self.project_list.setFocus() - self.status_bar.showMessage("Select a project from the list", 3000) + 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_view_stats(self): - """Handle view stats button.""" - if self.current_project: - self.show_project_stats(self.current_project) + 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}") - def show_project_stats(self, project: Project): - """Show project statistics dialog.""" - dialog = ProjectStatsDialog(project, self) + # ======================================================================== + # 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: + activity = ActivityType.from_string(row.get('type', 'hunting')) + + # Calculate duration + started = datetime.fromisoformat(row['started_at']) + duration = 0 + if row.get('ended_at'): + ended = datetime.fromisoformat(row['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.get('status', '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 start_session(self, project_id: int): - """ - Start a new session with the given project. - - Args: - project_id: The ID of the project to start session for - """ - from core.project_manager import ProjectData - - # Get real project from database - projects = self.project_manager.list_projects() - project = None - for p in projects: - if p.id == project_id: - project = p - break - - if not project: - self.log_error("Session", f"Project {project_id} not found") - return + 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 = project_id + self.current_session_id = template.id # Emit signal - self.session_started.emit(project_id) + self.session_started.emit(template.id) # Log - self.log_info("Session", f"Started session for project: {project.name}") - self.session_info_label.setText(f"Session active: {project.name}") + 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 real session in database - session = self.project_manager.start_session(project_id) - self._current_db_session_id = session.id if session else None + # 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 = os.getenv('EU_CHAT_LOG_PATH', '') + log_path = self.log_path or os.getenv('EU_CHAT_LOG_PATH', '') if use_mock or not log_path: # Use mock log for testing @@ -965,275 +1335,103 @@ class MainWindow(QMainWindow): self.log_info("LogWatcher", f"Using REAL log: {log_path}") # Subscribe to events - self._setup_log_watcher_callbacks() + self._subscribe_to_log_events() - # Start LogWatcher in background + # Start LogWatcher self._start_log_watcher() - # Show HUD and start session tracking - self.hud.show() - - # Get gear names and costs from simplified loadout structure - session_display = getattr(self, '_session_display', {}) - session_costs = getattr(self, '_session_costs', {}) - - weapon_name = session_display.get('weapon_name', self._selected_weapon or "Unknown") - armor_name = session_display.get('armor_name', "None") - healing_name = session_display.get('healing_name', "None") - loadout_name = "Loadout" if session_costs else "Default" - - weapon_stats = self._selected_weapon_stats or {} - weapon_dpp = Decimal(str(weapon_stats.get('dpp', 0))) - weapon_cost_per_hour = Decimal(str(weapon_stats.get('cost_per_hour', 0))) - - # Get cost data from simplified structure - 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')) - - self.hud.start_session( - weapon=weapon_name, - armor=armor_name, - fap=healing_name, - loadout=loadout_name, - weapon_dpp=weapon_dpp, - weapon_cost_per_hour=weapon_cost_per_hour, - cost_per_shot=cost_per_shot, - cost_per_hit=cost_per_hit, - cost_per_heal=cost_per_heal - ) - - # Simple cost tracking - no database required - self.log_info("CostTracker", "Cost tracking enabled with pre-calculated values") - - self.log_info("HUD", f"HUD shown - Weapon: {weapon_name}, Armor: {armor_name}, Healing: {healing_name}, Loadout: {loadout_name}") - - def _setup_log_watcher_callbacks(self): - """Setup LogWatcher event callbacks.""" + def _subscribe_to_log_events(self): + """Subscribe to log watcher events.""" if not self.log_watcher: return - from core.project_manager import LootEvent - from decimal import Decimal - - def on_heal(event): - """Handle heal events from chat.log. - - Pattern: "You healed yourself X points" - Calculates healing cost based on FAP decay and updates HUD. - """ - heal_amount = event.data.get('heal_amount', Decimal('0')) - logger.debug(f"[EVENT] on_heal: heal_amount={heal_amount}") - - # Calculate heal cost based on selected medical tool decay - # Get decay per heal from loadout or use default - decay_cost = Decimal('0') - if self._selected_loadout and hasattr(self._selected_loadout, 'heal_cost_pec'): - # heal_cost_pec is the decay per heal in PEC - # Convert to PED for cost calculation - decay_cost = self._selected_loadout.heal_cost_pec / Decimal('100') - elif self._selected_medical_tool_stats and 'decay' in self._selected_medical_tool_stats: - decay_pec = Decimal(str(self._selected_medical_tool_stats['decay'])) - decay_cost = decay_pec / Decimal('100') - else: - # Default estimate: 2 PEC per heal - decay_cost = Decimal('0.02') - - # Update HUD with heal event - try: - logger.debug(f"[DEBUG] _session_costs={self._session_costs}") - # Track healing cost - if self._session_costs: - cost_per_heal = self._session_costs.get('cost_per_heal', Decimal('0')) - logger.debug(f"[DEBUG] cost_per_heal={cost_per_heal}") - if cost_per_heal > 0: - logger.debug(f"[HUD] update_healing_cost({cost_per_heal})") - self.hud.update_healing_cost(cost_per_heal) - - # Track heal amount (as event dict for new HUD) - logger.debug(f"[HUD] on_heal_event({{'heal_amount': {heal_amount}}})") - self.hud.on_heal_event({'heal_amount': heal_amount}) - logger.debug(f"[HUD] Heal update successful") - except Exception as e: - logger.error(f"[ERROR] Error updating HUD heal: {e}") - import traceback - logger.error(traceback.format_exc()) - - # Log to UI - self.log_info("Heal", f"Healed {heal_amount} HP (Cost: {decay_cost:.4f} PED)") - def on_loot(event): - """Handle loot events.""" from decimal import Decimal - item_name = event.data.get('item_name', 'Unknown') value_ped = event.data.get('value_ped', Decimal('0.0')) - quantity = event.data.get('quantity', 1) - logger.debug(f"[EVENT] on_loot: item={item_name}, value={value_ped}, qty={quantity}") - - # Skip Universal Ammo if item_name == 'Universal Ammo': - logger.debug("[EVENT] on_loot: skipped Universal Ammo") return - try: - # Update loot value - this also handles kill counting internally - is_shrapnel = 'shrapnel' in item_name.lower() - logger.debug(f"[HUD] update_loot({value_ped}, is_shrapnel={is_shrapnel})") - self.hud.update_loot(value_ped, is_shrapnel=is_shrapnel) - logger.debug(f"[HUD] Loot update successful") - except Exception as e: - logger.error(f"[ERROR] Error updating HUD loot: {e}") - import traceback - logger.error(traceback.format_exc()) - - # Queue database write for main thread (SQLite thread safety) + 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, - 'quantity': quantity, 'value_ped': value_ped, 'raw_line': event.raw_line }) - # Log to UI (main thread only - use signal/slot or queue) - # We'll log this in _process_queued_events instead - - def on_global(event): - """Handle global events (other players).""" - value_ped = event.data.get('value_ped', Decimal('0.0')) - player = event.data.get('player_name', 'Unknown') - # Don't count other players' globals in HUD - self.log_info("Global", f"{player} found {value_ped} PED!") - - def on_personal_global(event): - """Handle personal global events (only your globals).""" - value_ped = event.data.get('value_ped', Decimal('0.0')) - creature = event.data.get('creature', 'Unknown') - player = event.data.get('player_name', 'Unknown') - - # Only count if it matches our configured player name - if self.player_name and player.lower() == self.player_name.lower(): - # Only count personal globals in HUD stats - self.hud.on_personal_global(value_ped) - self.log_info("Global", f"πŸŽ‰ YOUR GLOBAL: {creature} for {value_ped} PED!!!") - else: - # Log but don't count - might be another player if names are similar - self.log_info("Global", f"{player} got global: {creature} for {value_ped} PED") - - # If no player name set, warn user - if not self.player_name: - self.log_info("Config", "⚠️ Set your avatar name in Settings to track your globals correctly") - - def on_hof(event): - """Handle HoF events.""" - value_ped = event.data.get('value_ped', Decimal('0.0')) - self.hud.on_hof(value_ped) - self.log_info("HoF", f"πŸ† HALL OF FAME: {value_ped} PED!") - - def on_skill(event): - """Handle skill events.""" - from decimal import Decimal - skill_name = event.data.get('skill_name', 'Unknown') - gained = event.data.get('gained', 0) - self.log_info("Skill", f"{skill_name} +{gained}") - - # Update HUD skill tracking - try: - gained_decimal = Decimal(str(gained)) - self.hud.update_skill(skill_name, gained_decimal) - except Exception as e: - logger.error(f"[ERROR] Error updating HUD skill: {e}") - def on_damage_dealt(event): - """Handle damage dealt - track damage stats and weapon cost.""" from decimal import Decimal - try: - damage = event.data.get('damage', 0) - logger.debug(f"[EVENT] on_damage_dealt: damage={damage}") - if damage: - # Track damage amount - logger.debug(f"[HUD] on_damage_dealt({damage})") - self.hud.on_damage_dealt(Decimal(str(damage))) - - # Track weapon cost per shot - logger.debug(f"[DEBUG] _session_costs={self._session_costs}") - if self._session_costs: - cost_per_shot = self._session_costs.get('cost_per_shot', Decimal('0')) - logger.debug(f"[DEBUG] cost_per_shot={cost_per_shot}") - if cost_per_shot > 0: - logger.debug(f"[HUD] update_weapon_cost({cost_per_shot})") - self.hud.update_weapon_cost(cost_per_shot) - except Exception as e: - logger.error(f"[ERROR] Error in on_damage_dealt: {e}") - import traceback - logger.error(traceback.format_exc()) - - def on_critical_hit(event): - """Handle critical hit - same as damage dealt.""" - try: - on_damage_dealt(event) - except Exception as e: - logger.error(f"Error in on_critical_hit: {e}") + 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): - """Handle damage taken - track damage stats and armor cost.""" from decimal import Decimal - try: - damage = event.data.get('damage', 0) - logger.debug(f"[EVENT] on_damage_taken: damage={damage}") - if damage: - # Track damage amount - logger.debug(f"[HUD] on_damage_taken({damage})") - self.hud.on_damage_taken(Decimal(str(damage))) - - # Calculate armor decay based on actual damage absorbed - # EU Formula: Decay (PEC) = damage_absorbed * 0.05 * (1 - durability/100000) - # For (L) armor at full TT: ~0.05 PEC per damage point - # For adjusted armor: decay varies by piece - # Based on user feedback: ~0.00025 PED per damage (0.025 PEC) - decay_per_damage = Decimal('0.00025') # ~0.025 PEC per damage - cost_ped = Decimal(str(damage)) * decay_per_damage - if cost_ped > 0: - logger.debug(f"[HUD] update_armor_cost({cost_ped:.4f}) based on damage {damage}") - self.hud.update_armor_cost(cost_ped) - except Exception as e: - logger.error(f"[ERROR] Error in on_damage_taken: {e}") - import traceback - logger.error(traceback.format_exc()) + 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_evade(event): - """Handle evade - count weapon cost when target dodges your attack.""" - evade_type = event.data.get('type', 'Evade') - self.log_info("Evade", evade_type) + 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!") - # If target evaded/dodged YOUR attack, you still pay weapon cost - target_evaded = 'target Evaded' in evade_type or 'target Dodged' in evade_type - if target_evaded: - try: - from decimal import Decimal - if self._session_costs: - cost_per_shot = self._session_costs.get('cost_per_shot', Decimal('0')) - if cost_per_shot > 0: - logger.debug(f"[HUD] update_weapon_cost({cost_per_shot}) [target evaded]") - self.hud.update_weapon_cost(cost_per_shot) - except Exception as e: - logger.error(f"[ERROR] Error tracking weapon cost on evade: {e}") + # 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 to all event types + # Subscribe self.log_watcher.subscribe('loot', on_loot) - self.log_watcher.subscribe('global', on_global) + 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) - self.log_watcher.subscribe('skill', on_skill) - self.log_watcher.subscribe('damage_dealt', on_damage_dealt) - self.log_watcher.subscribe('critical_hit', on_critical_hit) - self.log_watcher.subscribe('damage_taken', on_damage_taken) - self.log_watcher.subscribe('evade', on_evade) - self.log_watcher.subscribe('heal', on_heal) # NEW: Heal event tracking def _start_log_watcher(self): """Start LogWatcher in background thread.""" @@ -1270,227 +1468,79 @@ class MainWindow(QMainWindow): """Stop LogWatcher.""" if hasattr(self, '_log_watcher_thread') and self._log_watcher_thread: self._log_watcher_thread.stop() - self._log_watcher_thread.wait(2000) # Wait up to 2 seconds + self._log_watcher_thread.wait(2000) self._log_watcher_thread = None self.log_info("LogWatcher", "Stopped") - def _setup_session_cost_tracker(self, loadout_info: dict): - """Set up session cost tracker with selected loadout.""" - from core.session_cost_tracker import SessionCostTracker - from core.database import DatabaseManager - - loadout_id = loadout_info.get('id') - loadout_source = loadout_info.get('source') - - # If JSON-based loadout, we need to save it to database first or handle differently - if loadout_source == 'json': - # For now, skip cost tracking for JSON loadouts - # TODO: Save JSON loadout to database first, then use its ID - self.log_warning("CostTracker", "Cost tracking not available for file-based loadouts. Save to database first.") - return - - try: - db = DatabaseManager() - self._cost_tracker = SessionCostTracker( - session_id=self._current_db_session_id, - loadout_id=loadout_id, - db_manager=db - ) - self._cost_tracker.register_callback(self._on_cost_update) - self.log_info("CostTracker", f"Cost tracking enabled for loadout ID: {loadout_id}") - except Exception as e: - self.log_error("CostTracker", f"Failed to set up cost tracker: {e}") - - def _on_cost_update(self, state): - """Handle cost update from SessionCostTracker.""" - # Update HUD with new cost state - if hasattr(self, 'hud') and self.hud: - summary = { - 'weapon_cost': state.weapon_cost, - 'armor_cost': state.armor_cost, - 'healing_cost': state.healing_cost, - 'enhancer_cost': state.enhancer_cost, - 'mindforce_cost': state.mindforce_cost, - 'shots_fired': state.shots_fired, - 'hits_taken': state.hits_taken, - 'heals_used': state.heals_used, - } - self.hud._stats.update_from_cost_tracker(summary) - self.hud._refresh_display() - def _process_queued_events(self): - """Process events from the queue in the main thread (SQLite thread safety).""" - from core.project_manager import LootEvent - from decimal import Decimal - + """Process events from the queue in the main thread.""" processed = 0 - while not self._event_queue.empty() and processed < 10: # Process max 10 per tick + while not self._event_queue.empty() and processed < 10: try: event = self._event_queue.get_nowait() - if event['type'] == 'loot': - # Record to database (now in main thread - safe) - loot = LootEvent( - item_name=event['item_name'], - quantity=event['quantity'], - value_ped=event['value_ped'], - event_type='regular', - raw_log_line=event['raw_line'] - ) - self.project_manager.record_loot(loot) - - # Log to UI - self.log_info("Loot", f"{event['item_name']} x{event['quantity']} ({event['value_ped']} PED)") - + 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_start_session(self): - """Handle start session button - shows loadout selection first.""" - if self.current_project and self.session_state == SessionState.IDLE: - # Show loadout selection dialog - from ui.loadout_selection_dialog_simple import LoadoutSelectionDialog - dialog = LoadoutSelectionDialog(self) - dialog.loadout_selected.connect(self._on_loadout_selected_for_session) - dialog.rejected.connect(lambda: self.log_info("Session", "Session start cancelled - no loadout selected")) - dialog.exec() - - def _on_loadout_selected_for_session(self, loadout_info: dict): - """Handle loadout selection and start session - simplified cost-focused version.""" - loadout_name = loadout_info.get('name', 'No Loadout') - costs = loadout_info.get('costs', {}) - display = loadout_info.get('display', {}) - - # Store cost data for session tracking - 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')), - } - - # Store display data for HUD - self._session_display = { - 'weapon_name': display.get('weapon_name', 'None'), - 'armor_name': display.get('armor_name', 'None'), - 'healing_name': display.get('healing_name', 'None'), - } - - if any(self._session_costs.values()): - self.log_info("Session", f"Starting with loadout: {loadout_name}") - self.log_info("SessionCosts", - f"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: - self.log_info("Session", f"Starting with loadout: {loadout_name} (no costs configured)") - - # Now start the session - if self.current_project: - self.start_session(self.current_project.id) - def on_stop_session(self): """Handle stop session button.""" if self.session_state in (SessionState.RUNNING, SessionState.PAUSED): - # Stop LogWatcher self._stop_log_watcher() - # Save HUD costs to database before ending session - if self._current_db_session_id and self.hud: - from decimal import Decimal - from datetime import datetime - - # Save weapon decay - if self.hud._stats.weapon_cost_total > 0: - self.db.execute( - """INSERT INTO decay_events - (session_id, item_name, decay_amount_ped, timestamp) - VALUES (?, ?, ?, ?)""", - (self._current_db_session_id, 'Weapon', - float(self.hud._stats.weapon_cost_total), datetime.now()) - ) - - # Save armor decay - if self.hud._stats.armor_cost_total > 0: - self.db.execute( - """INSERT INTO decay_events - (session_id, item_name, decay_amount_ped, timestamp) - VALUES (?, ?, ?, ?)""", - (self._current_db_session_id, 'Armor', - float(self.hud._stats.armor_cost_total), datetime.now()) - ) - - # Save healing decay - if self.hud._stats.healing_cost_total > 0: - self.db.execute( - """INSERT INTO decay_events - (session_id, item_name, decay_amount_ped, timestamp) - VALUES (?, ?, ?, ?)""", - (self._current_db_session_id, 'Healing', - float(self.hud._stats.healing_cost_total), datetime.now()) - ) - - self.db.commit() - self.log_info("Session", - f"Saved costs to DB: Weapon={self.hud._stats.weapon_cost_total:.2f}, " - f"Armor={self.hud._stats.armor_cost_total:.2f}, " - f"Healing={self.hud._stats.healing_cost_total:.2f}") - # End session in database if self._current_db_session_id: - self.project_manager.end_session(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") - # End HUD session self.hud.end_session() - - # Hide HUD 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() - # Pause HUD tracking 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 - tracking stopped") + self.log_info("Session", "Session paused") self.session_info_label.setText("Session paused") - self.pause_session_btn.setText("▢️ Resume") + self.pause_session_btn.setText("▢️ RESUME") elif self.session_state == SessionState.PAUSED: self.set_session_state(SessionState.RUNNING) self.session_resumed.emit() - # Resume HUD tracking 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 - tracking started") + self.log_info("Session", "Session resumed") self.session_info_label.setText("Session resumed") - self.pause_session_btn.setText("⏸️ Pause") + self.pause_session_btn.setText("⏸️ PAUSE") def set_session_state(self, state: SessionState): - """ - Update the session state and UI. - - Args: - state: New session state - """ + """Update the session state and UI.""" self.session_state = state - # Update status label colors = { SessionState.IDLE: "#888", SessionState.RUNNING: "#4caf50", @@ -1511,54 +1561,121 @@ class MainWindow(QMainWindow): }} """) - # Update status bar 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.start_session_btn.setEnabled( - state == SessionState.IDLE and self.current_project is not None - ) + 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.start_action.setEnabled(self.start_session_btn.isEnabled()) 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") + 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 on_log_event(self, event: LogEvent): - """ - Handle incoming log events. - - Args: - event: The log event to display - """ - # Color mapping - colors = { - "DEBUG": "#888", - "INFO": "#4fc3f7", - "WARNING": "#ff9800", - "ERROR": "#f44336", - "CRITICAL": "#e91e63" - } - - color = colors.get(event.level, "#e0e0e0") - html = f'{self.escape_html(str(event))}' - - self.log_output.append(html) - - # Auto-scroll to bottom - scrollbar = self.log_output.verticalScrollBar() - scrollbar.setValue(scrollbar.maximum()) - def log_debug(self, source: str, message: str): """Log a debug message.""" self._append_log("DEBUG", source, message) @@ -1588,7 +1705,6 @@ class MainWindow(QMainWindow): log_entry = f'[{timestamp}] [{level}] [{source}] {self.escape_html(message)}' self.log_output.append(log_entry) - # Auto-scroll to bottom scrollbar = self.log_output.verticalScrollBar() scrollbar.setValue(scrollbar.maximum()) @@ -1618,88 +1734,24 @@ class MainWindow(QMainWindow): dialog = SettingsDialog(self, self.player_name) if dialog.exec() == QDialog.DialogCode.Accepted: self.player_name = dialog.get_player_name() - self._save_settings() # Save to persistent storage - if self.player_name: - self.log_info("Settings", f"Avatar name set to: {self.player_name}") - else: - self.log_info("Settings", "Warning: No avatar name set. Global tracking may not work correctly.") + 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 _load_settings(self): - """Load persistent settings from QSettings.""" - settings = QSettings("Lemontropia", "Suite") - self.player_name = settings.value("player/name", "", type=str) - 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.sync() # Ensure settings are written immediately - - 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) - dialog.exec() - - def on_loadout_selected(self, loadout): - """Handle loadout selection from Loadout Manager - simplified version.""" - self._selected_loadout = loadout - self.log_info("Loadout", f"Selected loadout: {loadout.name}") - - # Update selected gear from loadout (simplified structure) - if hasattr(loadout, 'weapon_name'): - self._selected_weapon = loadout.weapon_name - if hasattr(loadout, 'healing_name'): - self._selected_medical_tool = loadout.healing_name - self._selected_medical_tool_stats = { - 'decay': float(loadout.healing_decay_pec), - 'cost_per_heal': float(loadout.healing_cost_per_heal), - } - # Store simplified costs for session - if hasattr(loadout, 'weapon_cost_per_shot'): - self._session_costs = { - 'cost_per_shot': loadout.weapon_cost_per_shot, - 'cost_per_hit': loadout.armor_cost_per_hit, - 'cost_per_heal': loadout.healing_cost_per_heal, - } - if hasattr(loadout, 'weapon_name'): - self._session_display = { - 'weapon_name': loadout.weapon_name, - 'armor_name': loadout.armor_name, - 'healing_name': loadout.healing_name, - } - - 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}") - - if gear_type == "weapon": - self._selected_weapon = name - self._selected_weapon_stats = stats - if self.session_state == SessionState.RUNNING: - self.hud.update_stats({'weapon': name}) - elif gear_type == "armor": - self._selected_armor = name - self._selected_armor_stats = stats - elif gear_type == "finder": - self._selected_finder = name - self._selected_finder_stats = stats - elif gear_type == "medical_tool": - self._selected_medical_tool = name - self._selected_medical_tool_stats = stats - if self.session_state == SessionState.RUNNING: - self.hud.update_stats({'medical_tool': 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.""" @@ -1708,17 +1760,72 @@ class MainWindow(QMainWindow): "About Lemontropia Suite", """

Lemontropia Suite

Version 1.0.0

-

A PyQt6-based GUI for game automation and session management.

+

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 # ======================================================================== @@ -1759,35 +1866,6 @@ def main(): window = MainWindow() window.show() - # Simulate some log activity for demonstration - def simulate_logs(): - import random - sources = ["Engine", "Input", "Vision", "Network", "Session"] - levels = ["DEBUG", "INFO", "INFO", "INFO", "WARNING"] - messages = [ - "Initializing component...", - "Connection established", - "Processing frame #1234", - "Waiting for input", - "Buffer cleared", - "Sync complete" - ] - - if window.session_state == SessionState.RUNNING: - if random.random() < 0.3: # 30% chance each tick - event = LogEvent( - timestamp=datetime.now(), - level=random.choice(levels), - source=random.choice(sources), - message=random.choice(messages) - ) - window.log_watcher.emit(event) - - # Timer to simulate log activity - timer = QTimer() - timer.timeout.connect(simulate_logs) - timer.start(1000) # Every second - sys.exit(app.exec()) diff --git a/ui/mindforce_selector.py b/ui/mindforce_selector.py index adfeefc..c64de9a 100644 --- a/ui/mindforce_selector.py +++ b/ui/mindforce_selector.py @@ -1,260 +1,171 @@ """ -Mindforce Implant Selector for Lemontropia Suite -Browse and select mindforce implants from Entropia Nexus API +Mindforce Implant/Chip Selector Dialog +Uses /medicalchips API endpoint for healing chips """ -from decimal import Decimal from PyQt6.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, - QTreeWidget, QTreeWidgetItem, QHeaderView, QLabel, QDialogButtonBox, - QProgressBar, QGroupBox, QFormLayout, QComboBox + QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, + QListWidget, QListWidgetItem, QLineEdit, QGroupBox, + QFormLayout, QMessageBox ) -from PyQt6.QtCore import Qt, QThread, pyqtSignal -from PyQt6.QtGui import QColor -from typing import Optional, List +from PyQt6.QtCore import Qt +from decimal import Decimal -from core.nexus_full_api import get_nexus_api, NexusMindforceImplant +from core.nexus_full_api import get_nexus_api -class MindforceImplantLoaderThread(QThread): - """Background thread for loading mindforce implants from API.""" - implants_loaded = pyqtSignal(list) - error_occurred = pyqtSignal(str) +class MindforceSelectorDialog(QDialog): + """Dialog for selecting mindforce implants (healing chips).""" - def run(self): - try: - api = get_nexus_api() - implants = api.get_all_mindforce_implants() - self.implants_loaded.emit(implants) - except Exception as e: - self.error_occurred.emit(str(e)) - - -class MindforceImplantSelectorDialog(QDialog): - """Dialog for selecting mindforce implants from Entropia Nexus API.""" - - implant_selected = pyqtSignal(NexusMindforceImplant) - - def __init__(self, parent=None, implant_type: str = ""): + def __init__(self, parent=None): super().__init__(parent) - self.preferred_type = implant_type.lower() + self.setWindowTitle("Select Mindforce Healing Chip") + self.setMinimumSize(600, 500) - type_names = { - "healing": "Healing Chip", - "damage": "Damage Chip", - "utility": "Utility Chip" - } - title_type = type_names.get(self.preferred_type, "Mindforce Implant") - - self.setWindowTitle(f"Select {title_type} - Entropia Nexus") - self.setMinimumSize(800, 500) - - self.all_implants: List[NexusMindforceImplant] = [] - self.selected_implant: Optional[NexusMindforceImplant] = None + self._selected_chip = None + self._chips_cache = [] self._setup_ui() - self._load_data() + self._load_chips() def _setup_ui(self): layout = QVBoxLayout(self) - layout.setSpacing(10) - # Status - self.status_label = QLabel("Loading mindforce implants from Entropia Nexus...") - layout.addWidget(self.status_label) + # Search bar + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("Search:")) + self.search_edit = QLineEdit() + self.search_edit.setPlaceholderText("Type to search chips...") + self.search_edit.textChanged.connect(self._on_search) + search_layout.addWidget(self.search_edit) + layout.addLayout(search_layout) - self.progress = QProgressBar() - self.progress.setRange(0, 0) - layout.addWidget(self.progress) + # Chip list + self.list_widget = QListWidget() + self.list_widget.itemClicked.connect(self._on_item_selected) + self.list_widget.itemDoubleClicked.connect(self.accept) + layout.addWidget(self.list_widget) - # Filters - filter_layout = QHBoxLayout() + # Info panel + info_group = QGroupBox("Chip Info") + info_layout = QFormLayout(info_group) - filter_layout.addWidget(QLabel("Type:")) - self.type_combo = QComboBox() - self.type_combo.addItems(["All", "Healing", "Damage", "Utility"]) - if self.preferred_type: - type_map = {"healing": 1, "damage": 2, "utility": 3} - self.type_combo.setCurrentIndex(type_map.get(self.preferred_type, 0)) - self.type_combo.currentTextChanged.connect(self._filter_implants) - filter_layout.addWidget(self.type_combo) + self.info_name = QLabel("Select a chip") + self.info_heal = QLabel("-") + self.info_decay = QLabel("-") + self.info_uses = QLabel("-") + self.info_mindforce_level = QLabel("-") + self.info_cost = QLabel("-") - filter_layout.addWidget(QLabel("Search:")) - self.search_input = QLineEdit() - self.search_input.setPlaceholderText("Search implants...") - self.search_input.textChanged.connect(self._filter_implants) - filter_layout.addWidget(self.search_input) + info_layout.addRow("Name:", self.info_name) + info_layout.addRow("Heal Amount:", self.info_heal) + info_layout.addRow("Decay (PEC):", self.info_decay) + info_layout.addRow("Uses/Min:", self.info_uses) + info_layout.addRow("Mindforce Level:", self.info_mindforce_level) + info_layout.addRow("Cost/Heal:", self.info_cost) - clear_btn = QPushButton("Clear") - clear_btn.clicked.connect(self.search_input.clear) - filter_layout.addWidget(clear_btn) - - layout.addLayout(filter_layout) - - # Results tree - self.results_tree = QTreeWidget() - self.results_tree.setHeaderLabels([ - "Name", "Type", "Chip Type", "Decay (PEC)", "Prof. Level", "Limited" - ]) - header = self.results_tree.header() - header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) - self.results_tree.itemSelectionChanged.connect(self._on_selection_changed) - self.results_tree.itemDoubleClicked.connect(self._on_double_click) - layout.addWidget(self.results_tree) - - # Preview panel - self.preview_group = QGroupBox("Implant Preview") - preview_layout = QFormLayout(self.preview_group) - self.preview_name = QLabel("-") - self.preview_type = QLabel("-") - self.preview_decay = QLabel("-") - preview_layout.addRow("Name:", self.preview_name) - preview_layout.addRow("Type:", self.preview_type) - preview_layout.addRow("Decay/Use:", self.preview_decay) - layout.addWidget(self.preview_group) + layout.addWidget(info_group) # Buttons - buttons = QDialogButtonBox( - QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel - ) - buttons.accepted.connect(self._on_accept) - buttons.rejected.connect(self.reject) - self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) - self.ok_button.setEnabled(False) - layout.addWidget(buttons) - - def _load_data(self): - """Load implants in background thread.""" - self.loader = MindforceImplantLoaderThread() - self.loader.implants_loaded.connect(self._on_implants_loaded) - self.loader.error_occurred.connect(self._on_load_error) - self.loader.start() - - def _on_implants_loaded(self, implants: List[NexusMindforceImplant]): - """Handle loaded implants.""" - self.all_implants = implants - self._filter_implants() - self.status_label.setText(f"Loaded {len(implants)} mindforce implants") - self.progress.setRange(0, 100) - self.progress.setValue(100) - - def _on_load_error(self, error: str): - """Handle load error.""" - self.status_label.setText(f"Error loading implants: {error}") - self.progress.setRange(0, 100) - self.progress.setValue(0) - - def _populate_results(self, implants: List[NexusMindforceImplant]): - """Populate results tree.""" - self.results_tree.clear() + button_layout = QHBoxLayout() + button_layout.addStretch() - if not implants: - item = QTreeWidgetItem() - item.setText(0, "No implants available") - item.setForeground(0, QColor("#888888")) - self.results_tree.addTopLevelItem(item) + self.select_btn = QPushButton("Select") + self.select_btn.clicked.connect(self.accept) + self.select_btn.setEnabled(False) + button_layout.addWidget(self.select_btn) + + cancel_btn = QPushButton("Cancel") + cancel_btn.clicked.connect(self.reject) + button_layout.addWidget(cancel_btn) + + layout.addLayout(button_layout) + + def _load_chips(self): + """Load healing chips from API.""" + try: + api = get_nexus_api() + # Medical chips are healing-focused mindforce implants + self._chips_cache = api.get_all_healing_chips() + self._populate_list(self._chips_cache) + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load chips: {e}") + + def _populate_list(self, chips): + """Populate the list widget.""" + self.list_widget.clear() + + for chip in chips: + # Get heal amount (average of min/max) + heal_amount = getattr(chip, 'heal_amount', Decimal("0")) + decay = getattr(chip, 'decay', Decimal("0")) + + item_text = f"{chip.name} | Heal: {heal_amount}" + if decay > 0: + item_text += f" | Decay: {decay} PEC" + + item = QListWidgetItem(item_text) + item.setData(Qt.ItemDataRole.UserRole, chip) + + # Tooltip + tooltip = f"Name: {chip.name}" + if hasattr(chip, 'heal_amount'): + tooltip += f"\nHeal: {chip.heal_amount} HP" + if decay > 0: + tooltip += f"\nDecay: {decay} PEC" + if hasattr(chip, 'uses_per_minute') and chip.uses_per_minute: + tooltip += f"\nUses/Min: {chip.uses_per_minute}" + item.setToolTip(tooltip) + + self.list_widget.addItem(item) + + def _on_search(self, text): + """Filter list based on search text.""" + if not text: + self._populate_list(self._chips_cache) return - # Sort by decay (lower is better economy) - implants = sorted(implants, key=lambda i: i.decay) + filtered = [chip for chip in self._chips_cache + if text.lower() in chip.name.lower()] + self._populate_list(filtered) + + def _on_item_selected(self, item): + """Handle item selection.""" + chip = item.data(Qt.ItemDataRole.UserRole) + if not chip: + return - for implant in implants: - item = QTreeWidgetItem() - item.setText(0, implant.name) - item.setText(1, implant.implant_type.title()) - item.setText(2, implant.chip_type) - item.setText(3, f"{implant.decay:.2f}") - item.setText(4, str(implant.profession_level) if implant.profession_level > 0 else "-") - item.setText(5, "Yes" if implant.is_limited else "No") - - # Color limited items - if implant.is_limited: - item.setForeground(5, QColor("#ff9800")) - - # Color by type - if implant.implant_type == "healing": - item.setForeground(1, QColor("#4caf50")) - elif implant.implant_type == "damage": - item.setForeground(1, QColor("#f44336")) - - item.setData(0, Qt.ItemDataRole.UserRole, implant) - self.results_tree.addTopLevelItem(item) - - def _filter_implants(self): - """Filter implants based on search and type.""" - type_filter = self.type_combo.currentText().lower() - search = self.search_input.text().lower() + # Extract chip data + heal_amount = getattr(chip, 'heal_amount', Decimal("0")) + decay = Decimal(str(getattr(chip, 'decay', 0))) + uses_per_min = getattr(chip, 'uses_per_minute', None) - filtered = self.all_implants + self._selected_chip = { + 'name': chip.name, + 'api_id': chip.id, + 'decay_pec': decay, + 'heal_amount': heal_amount, + 'uses_per_minute': uses_per_min, + } - # Filter by type - if type_filter != "all": - filtered = [i for i in filtered if i.implant_type == type_filter] + # Update info panel + self.info_name.setText(chip.name) + self.info_heal.setText(f"{heal_amount} HP") + self.info_decay.setText(f"{decay} PEC") + self.info_uses.setText(str(uses_per_min) if uses_per_min else "N/A") - # Filter by search - if search: - filtered = [ - i for i in filtered - if search in i.name.lower() - or search in i.chip_type.lower() - ] + # Mindforce level if available + mindforce_level = "N/A" + if hasattr(chip, 'profession_level') and chip.profession_level: + mindforce_level = str(chip.profession_level) + self.info_mindforce_level.setText(mindforce_level) - self._populate_results(filtered) - - def _on_selection_changed(self): - """Handle selection change.""" - items = self.results_tree.selectedItems() - if items: - self.selected_implant = items[0].data(0, Qt.ItemDataRole.UserRole) - self._update_preview(self.selected_implant) - self.ok_button.setEnabled(True) - else: - self.selected_implant = None - self.ok_button.setEnabled(False) - - def _update_preview(self, implant: NexusMindforceImplant): - """Update preview panel.""" - self.preview_name.setText(implant.name) - self.preview_type.setText(f"{implant.implant_type.title()} ({implant.chip_type})") - self.preview_decay.setText(f"{implant.decay:.4f} PEC per use") + # Calculate cost per heal + cost_per_heal = decay / Decimal("100") + self.info_cost.setText(f"{cost_per_heal:.4f} PED") - # Color by type - if implant.implant_type == "healing": - self.preview_type.setStyleSheet("color: #4caf50;") - elif implant.implant_type == "damage": - self.preview_type.setStyleSheet("color: #f44336;") - else: - self.preview_type.setStyleSheet("color: #4a90d9;") + self.select_btn.setEnabled(True) - def _on_double_click(self, item: QTreeWidgetItem, column: int): - """Handle double click.""" - if item.data(0, Qt.ItemDataRole.UserRole): - self._on_accept() - - def _on_accept(self): - """Handle OK button.""" - if self.selected_implant: - self.implant_selected.emit(self.selected_implant) - self.accept() - - -# Main entry for testing -if __name__ == "__main__": - import sys - import logging - from PyQt6.QtWidgets import QApplication - - logging.basicConfig(level=logging.INFO) - - app = QApplication(sys.argv) - app.setStyle('Fusion') - - dialog = MindforceImplantSelectorDialog() - - # Connect signal for testing - dialog.implant_selected.connect(lambda i: print(f"Selected implant: {i.name}")) - - if dialog.exec() == QDialog.DialogCode.Accepted: - print("Implant selected!") - - sys.exit(0) + def get_selected_chip(self): + """Get the selected chip data.""" + return self._selected_chip diff --git a/ui/session_history.py b/ui/session_history.py new file mode 100644 index 0000000..65456ae --- /dev/null +++ b/ui/session_history.py @@ -0,0 +1,1115 @@ +""" +Lemontropia Suite - Session History Dialog +Displays past hunting sessions with detailed stats and export functionality. +""" + +import json +import csv +from datetime import datetime, timedelta +from decimal import Decimal +from pathlib import Path +from typing import Optional, List, Dict, Any + +from PyQt6.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, + QLabel, QPushButton, QTreeWidget, QTreeWidgetItem, + QHeaderView, QSplitter, QWidget, QGroupBox, + QMessageBox, QFileDialog, QComboBox, QLineEdit, + QTableWidget, QTableWidgetItem, QTabWidget, + QAbstractItemView +) +from PyQt6.QtCore import Qt, pyqtSignal +from PyQt6.QtGui import QColor + +from core.database import DatabaseManager +from core.project_manager import HuntingSessionData + + +class SessionHistoryDialog(QDialog): + """ + Dialog for viewing and managing hunting session history. + + Features: + - List of past sessions with key metrics + - Detailed session statistics view + - Export to JSON/CSV + - Filter by project and date range + """ + + session_selected = pyqtSignal(int) # Emits session_id when selected + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Session History") + self.setMinimumSize(1200, 800) + self.resize(1400, 900) + + # Initialize database + self.db = DatabaseManager() + + # State + self.current_session_id: Optional[int] = None + self.sessions_data: List[Dict[str, Any]] = [] + + self._setup_ui() + self._load_sessions() + self._apply_dark_theme() + + def _setup_ui(self): + """Setup the dialog UI.""" + layout = QVBoxLayout(self) + layout.setContentsMargins(10, 10, 10, 10) + layout.setSpacing(10) + + # Top controls + controls_layout = QHBoxLayout() + + # Project filter + controls_layout.addWidget(QLabel("Project:")) + self.project_filter = QComboBox() + self.project_filter.addItem("All Projects", None) + self.project_filter.currentIndexChanged.connect(self._on_filter_changed) + controls_layout.addWidget(self.project_filter) + + # Date range filter + controls_layout.addWidget(QLabel("From:")) + self.date_from = QLineEdit() + self.date_from.setPlaceholderText("YYYY-MM-DD") + self.date_from.setMaximumWidth(120) + controls_layout.addWidget(self.date_from) + + controls_layout.addWidget(QLabel("To:")) + self.date_to = QLineEdit() + self.date_to.setPlaceholderText("YYYY-MM-DD") + self.date_to.setMaximumWidth(120) + controls_layout.addWidget(self.date_to) + + # Search + controls_layout.addWidget(QLabel("Search:")) + self.search_input = QLineEdit() + self.search_input.setPlaceholderText("Weapon, mob, etc...") + self.search_input.textChanged.connect(self._on_search_changed) + controls_layout.addWidget(self.search_input) + + controls_layout.addStretch() + + # Refresh button + self.refresh_btn = QPushButton("πŸ”„ Refresh") + self.refresh_btn.clicked.connect(self._load_sessions) + controls_layout.addWidget(self.refresh_btn) + + layout.addLayout(controls_layout) + + # Main splitter + splitter = QSplitter(Qt.Orientation.Horizontal) + layout.addWidget(splitter) + + # Left side - Session list + left_panel = QWidget() + left_layout = QVBoxLayout(left_panel) + left_layout.setContentsMargins(0, 0, 0, 0) + + # Sessions table + self.sessions_table = QTreeWidget() + self.sessions_table.setHeaderLabels([ + "ID", "Date", "Project", "Duration", "Kills", + "Cost", "Loot", "Profit/Loss", "Return %", "Globals", "HoFs" + ]) + self.sessions_table.setAlternatingRowColors(True) + self.sessions_table.setSelectionMode(QTreeWidget.SelectionMode.SingleSelection) + self.sessions_table.setRootIsDecorated(False) + self.sessions_table.itemSelectionChanged.connect(self._on_session_selected) + self.sessions_table.itemDoubleClicked.connect(self._on_session_double_clicked) + + # Configure columns + header = self.sessions_table.header() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) # ID + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed) # Date + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Project + header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Duration + header.setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # Kills + header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed) # Cost + header.setSectionResizeMode(6, QHeaderView.ResizeMode.Fixed) # Loot + header.setSectionResizeMode(7, QHeaderView.ResizeMode.Fixed) # Profit + header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed) # Return % + header.setSectionResizeMode(9, QHeaderView.ResizeMode.Fixed) # Globals + header.setSectionResizeMode(10, QHeaderView.ResizeMode.Fixed) # HoFs + + header.resizeSection(0, 50) + header.resizeSection(1, 140) + header.resizeSection(3, 80) + header.resizeSection(4, 50) + header.resizeSection(5, 80) + header.resizeSection(6, 80) + header.resizeSection(7, 90) + header.resizeSection(8, 70) + header.resizeSection(9, 60) + header.resizeSection(10, 50) + + left_layout.addWidget(self.sessions_table) + + # Export buttons + export_layout = QHBoxLayout() + + self.export_json_btn = QPushButton("πŸ“„ Export JSON") + self.export_json_btn.clicked.connect(self._export_json) + self.export_json_btn.setEnabled(False) + export_layout.addWidget(self.export_json_btn) + + self.export_csv_btn = QPushButton("πŸ“Š Export CSV") + self.export_csv_btn.clicked.connect(self._export_csv) + self.export_csv_btn.setEnabled(False) + export_layout.addWidget(self.export_csv_btn) + + self.export_all_btn = QPushButton("πŸ“ Export All") + self.export_all_btn.clicked.connect(self._export_all) + export_layout.addWidget(self.export_all_btn) + + export_layout.addStretch() + + self.delete_btn = QPushButton("πŸ—‘οΈ Delete") + self.delete_btn.clicked.connect(self._delete_session) + self.delete_btn.setEnabled(False) + export_layout.addWidget(self.delete_btn) + + left_layout.addLayout(export_layout) + + splitter.addWidget(left_panel) + + # Right side - Session details + self.details_tabs = QTabWidget() + self.details_tabs.setEnabled(False) + + # Summary tab + self.summary_tab = self._create_summary_tab() + self.details_tabs.addTab(self.summary_tab, "πŸ“‹ Summary") + + # Combat tab + self.combat_tab = self._create_combat_tab() + self.details_tabs.addTab(self.combat_tab, "βš”οΈ Combat") + + # Loot tab + self.loot_tab = self._create_loot_tab() + self.details_tabs.addTab(self.loot_tab, "πŸ’° Loot") + + # Equipment tab + self.equipment_tab = self._create_equipment_tab() + self.details_tabs.addTab(self.equipment_tab, "πŸ›‘οΈ Equipment") + + splitter.addWidget(self.details_tabs) + + # Set splitter sizes + splitter.setSizes([700, 500]) + + # Close button + close_layout = QHBoxLayout() + close_layout.addStretch() + + self.close_btn = QPushButton("Close") + self.close_btn.clicked.connect(self.accept) + close_layout.addWidget(self.close_btn) + + layout.addLayout(close_layout) + + def _create_summary_tab(self) -> QWidget: + """Create the summary details tab.""" + widget = QWidget() + layout = QVBoxLayout(widget) + layout.setContentsMargins(15, 15, 15, 15) + + # Session info group + info_group = QGroupBox("Session Information") + info_layout = QFormLayout(info_group) + + self.detail_session_id = QLabel("-") + info_layout.addRow("Session ID:", self.detail_session_id) + + self.detail_project = QLabel("-") + info_layout.addRow("Project:", self.detail_project) + + self.detail_started = QLabel("-") + info_layout.addRow("Started:", self.detail_started) + + self.detail_ended = QLabel("-") + info_layout.addRow("Ended:", self.detail_ended) + + self.detail_duration = QLabel("-") + info_layout.addRow("Duration:", self.detail_duration) + + layout.addWidget(info_group) + + # Financial summary group + financial_group = QGroupBox("Financial Summary") + financial_layout = QFormLayout(financial_group) + + self.detail_total_cost = QLabel("-") + financial_layout.addRow("Total Cost:", self.detail_total_cost) + + self.detail_loot_value = QLabel("-") + financial_layout.addRow("Loot Value:", self.detail_loot_value) + + self.detail_shrapnel = QLabel("-") + financial_layout.addRow("Shrapnel:", self.detail_shrapnel) + + self.detail_profit_loss = QLabel("-") + financial_layout.addRow("Profit/Loss:", self.detail_profit_loss) + + self.detail_return_pct = QLabel("-") + financial_layout.addRow("Return %:", self.detail_return_pct) + + self.detail_cost_per_hour = QLabel("-") + financial_layout.addRow("Cost/Hour:", self.detail_cost_per_hour) + + self.detail_profit_per_hour = QLabel("-") + financial_layout.addRow("Profit/Hour:", self.detail_profit_per_hour) + + layout.addWidget(financial_group) + + # Efficiency metrics + efficiency_group = QGroupBox("Efficiency Metrics") + efficiency_layout = QFormLayout(efficiency_group) + + self.detail_dpp = QLabel("-") + efficiency_layout.addRow("DPP:", self.detail_dpp) + + self.detail_cost_per_kill = QLabel("-") + efficiency_layout.addRow("Cost/Kill:", self.detail_cost_per_kill) + + self.detail_loot_per_kill = QLabel("-") + efficiency_layout.addRow("Loot/Kill:", self.detail_loot_per_kill) + + layout.addWidget(efficiency_group) + layout.addStretch() + + return widget + + def _create_combat_tab(self) -> QWidget: + """Create the combat statistics tab.""" + widget = QWidget() + layout = QVBoxLayout(widget) + layout.setContentsMargins(15, 15, 15, 15) + + # Combat stats group + combat_group = QGroupBox("Combat Statistics") + combat_layout = QFormLayout(combat_group) + + self.detail_shots_fired = QLabel("-") + combat_layout.addRow("Shots Fired:", self.detail_shots_fired) + + self.detail_shots_missed = QLabel("-") + combat_layout.addRow("Shots Missed:", self.detail_shots_missed) + + self.detail_accuracy = QLabel("-") + combat_layout.addRow("Accuracy:", self.detail_accuracy) + + self.detail_kills = QLabel("-") + combat_layout.addRow("Kills:", self.detail_kills) + + self.detail_kills_per_hour = QLabel("-") + combat_layout.addRow("Kills/Hour:", self.detail_kills_per_hour) + + layout.addWidget(combat_group) + + # Damage group + damage_group = QGroupBox("Damage Statistics") + damage_layout = QFormLayout(damage_group) + + self.detail_damage_dealt = QLabel("-") + damage_layout.addRow("Damage Dealt:", self.detail_damage_dealt) + + self.detail_damage_taken = QLabel("-") + damage_layout.addRow("Damage Taken:", self.detail_damage_taken) + + self.detail_damage_per_kill = QLabel("-") + damage_layout.addRow("Damage/Kill:", self.detail_damage_per_kill) + + self.detail_healing_done = QLabel("-") + damage_layout.addRow("Healing Done:", self.detail_healing_done) + + layout.addWidget(damage_group) + + # Special events group + events_group = QGroupBox("Special Events") + events_layout = QFormLayout(events_group) + + self.detail_globals = QLabel("-") + events_layout.addRow("Globals:", self.detail_globals) + + self.detail_hofs = QLabel("-") + events_layout.addRow("HoFs:", self.detail_hofs) + + self.detail_evades = QLabel("-") + events_layout.addRow("Evades:", self.detail_evades) + + layout.addWidget(events_group) + layout.addStretch() + + return widget + + def _create_loot_tab(self) -> QWidget: + """Create the loot breakdown tab.""" + widget = QWidget() + layout = QVBoxLayout(widget) + layout.setContentsMargins(15, 15, 15, 15) + + # Loot breakdown group + loot_group = QGroupBox("Loot Breakdown") + loot_layout = QFormLayout(loot_group) + + self.detail_total_loot = QLabel("-") + loot_layout.addRow("Total Loot:", self.detail_total_loot) + + self.detail_other_loot = QLabel("-") + loot_layout.addRow("Marketable Loot:", self.detail_other_loot) + + self.detail_shrapnel_loot = QLabel("-") + loot_layout.addRow("Shrapnel:", self.detail_shrapnel_loot) + + self.detail_universal_ammo = QLabel("-") + loot_layout.addRow("Universal Ammo:", self.detail_universal_ammo) + + layout.addWidget(loot_group) + + # Cost breakdown group + cost_group = QGroupBox("Cost Breakdown") + cost_layout = QFormLayout(cost_group) + + self.detail_weapon_cost = QLabel("-") + cost_layout.addRow("Weapon Cost:", self.detail_weapon_cost) + + self.detail_armor_cost = QLabel("-") + cost_layout.addRow("Armor Cost:", self.detail_armor_cost) + + self.detail_healing_cost = QLabel("-") + cost_layout.addRow("Healing Cost:", self.detail_healing_cost) + + self.detail_plates_cost = QLabel("-") + cost_layout.addRow("Plates Cost:", self.detail_plates_cost) + + self.detail_enhancer_cost = QLabel("-") + cost_layout.addRow("Enhancer Cost:", self.detail_enhancer_cost) + + layout.addWidget(cost_group) + layout.addStretch() + + return widget + + def _create_equipment_tab(self) -> QWidget: + """Create the equipment used tab.""" + widget = QWidget() + layout = QVBoxLayout(widget) + layout.setContentsMargins(15, 15, 15, 15) + + # Equipment group + equip_group = QGroupBox("Equipment Used") + equip_layout = QFormLayout(equip_group) + + self.detail_weapon_name = QLabel("-") + equip_layout.addRow("Weapon:", self.detail_weapon_name) + + self.detail_weapon_dpp = QLabel("-") + equip_layout.addRow("Weapon DPP:", self.detail_weapon_dpp) + + self.detail_armor_name = QLabel("-") + equip_layout.addRow("Armor:", self.detail_armor_name) + + self.detail_fap_name = QLabel("-") + equip_layout.addRow("Medical Tool:", self.detail_fap_name) + + layout.addWidget(equip_group) + layout.addStretch() + + return widget + + def _load_sessions(self): + """Load sessions from database.""" + self.sessions_table.clear() + self.sessions_data = [] + + try: + # Load projects for filter + self._load_projects() + + # Build query with filters + query = """ + SELECT + hs.id, hs.session_id, hs.started_at, hs.ended_at, + hs.total_loot_ped, hs.total_shrapnel_ped, hs.total_other_loot_ped, + hs.total_cost_ped, hs.weapon_cost_ped, hs.armor_cost_ped, + hs.healing_cost_ped, hs.plates_cost_ped, hs.enhancer_cost_ped, + hs.damage_dealt, hs.damage_taken, hs.healing_done, + hs.shots_fired, hs.shots_missed, hs.evades, hs.kills, + hs.globals_count, hs.hofs_count, + hs.weapon_name, hs.weapon_dpp, hs.armor_name, hs.fap_name, + p.name as project_name, p.id as project_id + FROM hunting_sessions hs + JOIN sessions s ON hs.session_id = s.id + JOIN projects p ON s.project_id = p.id + WHERE 1=1 + """ + params = [] + + # Apply project filter + project_id = self.project_filter.currentData() + if project_id: + query += " AND p.id = ?" + params.append(project_id) + + # Apply date filters + date_from = self.date_from.text().strip() + if date_from: + query += " AND hs.started_at >= ?" + params.append(date_from) + + date_to = self.date_to.text().strip() + if date_to: + query += " AND hs.started_at <= ?" + params.append(date_to + " 23:59:59") + + query += " ORDER BY hs.started_at DESC" + + cursor = self.db.execute(query, tuple(params)) + rows = cursor.fetchall() + + for row in rows: + session_data = dict(row) + self.sessions_data.append(session_data) + + # Calculate derived values + duration = "-" + if session_data['started_at'] and session_data['ended_at']: + start = datetime.fromisoformat(session_data['started_at']) + end = datetime.fromisoformat(session_data['ended_at']) + duration_secs = (end - start).total_seconds() + duration = self._format_duration(duration_secs) + + total_cost = Decimal(str(session_data['total_cost_ped'] or 0)) + other_loot = Decimal(str(session_data['total_other_loot_ped'] or 0)) + profit_loss = other_loot - total_cost + + return_pct = Decimal('0') + if total_cost > 0: + return_pct = (other_loot / total_cost) * Decimal('100') + + # Format date + started = datetime.fromisoformat(session_data['started_at']) + date_str = started.strftime("%Y-%m-%d %H:%M") + + # Create tree item + item = QTreeWidgetItem([ + str(session_data['id']), + date_str, + session_data['project_name'] or "Unknown", + duration, + str(session_data['kills'] or 0), + f"{total_cost:.2f}", + f"{other_loot:.2f}", + f"{profit_loss:+.2f}", + f"{return_pct:.1f}%", + str(session_data['globals_count'] or 0), + str(session_data['hofs_count'] or 0) + ]) + + # Color coding for profit/loss + if profit_loss > 0: + item.setForeground(7, QColor("#4caf50")) # Green + elif profit_loss < 0: + item.setForeground(7, QColor("#f44336")) # Red + + # Color coding for return % + if return_pct >= 100: + item.setForeground(8, QColor("#4caf50")) + elif return_pct >= 90: + item.setForeground(8, QColor("#ff9800")) + else: + item.setForeground(8, QColor("#f44336")) + + item.setData(0, Qt.ItemDataRole.UserRole, session_data['id']) + self.sessions_table.addTopLevelItem(item) + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load sessions: {e}") + + def _load_projects(self): + """Load projects for filter dropdown.""" + current = self.project_filter.currentData() + self.project_filter.clear() + self.project_filter.addItem("All Projects", None) + + try: + cursor = self.db.execute( + "SELECT id, name FROM projects ORDER BY name" + ) + for row in cursor.fetchall(): + self.project_filter.addItem(row['name'], row['id']) + + # Restore selection + if current: + idx = self.project_filter.findData(current) + if idx >= 0: + self.project_filter.setCurrentIndex(idx) + except Exception: + pass + + def _on_filter_changed(self): + """Handle filter changes.""" + self._load_sessions() + + def _on_search_changed(self): + """Handle search text changes.""" + search = self.search_input.text().lower() + + for i in range(self.sessions_table.topLevelItemCount()): + item = self.sessions_table.topLevelItem(i) + # Get session data for this row + session_id = item.data(0, Qt.ItemDataRole.UserRole) + session_data = next((s for s in self.sessions_data if s['id'] == session_id), None) + + if session_data: + # Search in weapon name, project name + weapon = (session_data.get('weapon_name') or '').lower() + project = (session_data.get('project_name') or '').lower() + + match = search in weapon or search in project + item.setHidden(bool(search) and not match) + + def _on_session_selected(self): + """Handle session selection.""" + selected = self.sessions_table.selectedItems() + if selected: + session_id = selected[0].data(0, Qt.ItemDataRole.UserRole) + self.current_session_id = session_id + self._load_session_details(session_id) + self.details_tabs.setEnabled(True) + self.export_json_btn.setEnabled(True) + self.export_csv_btn.setEnabled(True) + self.delete_btn.setEnabled(True) + else: + self.current_session_id = None + self.details_tabs.setEnabled(False) + self.export_json_btn.setEnabled(False) + self.export_csv_btn.setEnabled(False) + self.delete_btn.setEnabled(False) + + def _on_session_double_clicked(self, item: QTreeWidgetItem, column: int): + """Handle double-click on session.""" + session_id = item.data(0, Qt.ItemDataRole.UserRole) + self.session_selected.emit(session_id) + + def _load_session_details(self, session_id: int): + """Load and display detailed session information.""" + try: + # Get session data + cursor = self.db.execute(""" + SELECT + hs.*, p.name as project_name, s.started_at, s.ended_at + FROM hunting_sessions hs + JOIN sessions s ON hs.session_id = s.id + JOIN projects p ON s.project_id = p.id + WHERE hs.id = ? + """, (session_id,)) + + row = cursor.fetchone() + if not row: + return + + data = dict(row) + + # Calculate derived values + total_cost = Decimal(str(data['total_cost_ped'] or 0)) + total_loot = Decimal(str(data['total_loot_ped'] or 0)) + other_loot = Decimal(str(data['total_other_loot_ped'] or 0)) + shrapnel = Decimal(str(data['total_shrapnel_ped'] or 0)) + profit_loss = other_loot - total_cost + + return_pct = Decimal('0') + if total_cost > 0: + return_pct = (other_loot / total_cost) * Decimal('100') + + # Duration + duration_str = "-" + if data['started_at'] and data['ended_at']: + start = datetime.fromisoformat(data['started_at']) + end = datetime.fromisoformat(data['ended_at']) + duration_secs = (end - start).total_seconds() + duration_str = self._format_duration(duration_secs) + + # Update summary tab + self.detail_session_id.setText(str(data['id'])) + self.detail_project.setText(data['project_name'] or "Unknown") + self.detail_started.setText(data['started_at'] or "-") + self.detail_ended.setText(data['ended_at'] or "-") + self.detail_duration.setText(duration_str) + + self.detail_total_cost.setText(f"{total_cost:.4f} PED") + self.detail_loot_value.setText(f"{other_loot:.4f} PED") + self.detail_shrapnel.setText(f"{shrapnel:.4f} PED") + + # Color code profit/loss + profit_text = f"{profit_loss:+.4f} PED" + if profit_loss > 0: + self.detail_profit_loss.setStyleSheet("color: #4caf50; font-weight: bold;") + elif profit_loss < 0: + self.detail_profit_loss.setStyleSheet("color: #f44336; font-weight: bold;") + else: + self.detail_profit_loss.setStyleSheet("") + self.detail_profit_loss.setText(profit_text) + + self.detail_return_pct.setText(f"{return_pct:.2f}%") + + # Cost/profit per hour + if data['started_at'] and data['ended_at']: + start = datetime.fromisoformat(data['started_at']) + end = datetime.fromisoformat(data['ended_at']) + hours = (end - start).total_seconds() / 3600 + if hours > 0: + cost_per_hour = total_cost / Decimal(str(hours)) + profit_per_hour = profit_loss / Decimal(str(hours)) + self.detail_cost_per_hour.setText(f"{cost_per_hour:.2f} PED/hr") + self.detail_profit_per_hour.setText(f"{profit_per_hour:+.2f} PED/hr") + else: + self.detail_cost_per_hour.setText("-") + self.detail_profit_per_hour.setText("-") + else: + self.detail_cost_per_hour.setText("-") + self.detail_profit_per_hour.setText("-") + + # DPP and efficiency + damage_dealt = Decimal(str(data['damage_dealt'] or 0)) + kills = data['kills'] or 0 + + if total_cost > 0: + dpp = damage_dealt / total_cost + self.detail_dpp.setText(f"{dpp:.4f}") + else: + self.detail_dpp.setText("-") + + if kills > 0: + cost_per_kill = total_cost / kills + loot_per_kill = other_loot / kills + self.detail_cost_per_kill.setText(f"{cost_per_kill:.4f} PED") + self.detail_loot_per_kill.setText(f"{loot_per_kill:.4f} PED") + else: + self.detail_cost_per_kill.setText("-") + self.detail_loot_per_kill.setText("-") + + # Update combat tab + self.detail_shots_fired.setText(str(data['shots_fired'] or 0)) + self.detail_shots_missed.setText(str(data['shots_missed'] or 0)) + + shots_fired = data['shots_fired'] or 0 + shots_missed = data['shots_missed'] or 0 + total_shots = shots_fired + shots_missed + if total_shots > 0: + accuracy = (shots_fired / total_shots) * 100 + self.detail_accuracy.setText(f"{accuracy:.1f}%") + else: + self.detail_accuracy.setText("-") + + self.detail_kills.setText(str(kills)) + + # Kills per hour + if data['started_at'] and data['ended_at']: + hours = (end - start).total_seconds() / 3600 + if hours > 0: + kph = kills / hours + self.detail_kills_per_hour.setText(f"{kph:.1f}") + else: + self.detail_kills_per_hour.setText("-") + else: + self.detail_kills_per_hour.setText("-") + + self.detail_damage_dealt.setText(f"{damage_dealt:.0f}") + self.detail_damage_taken.setText(f"{data['damage_taken'] or 0:.0f}") + + if kills > 0: + dpk = damage_dealt / kills + self.detail_damage_per_kill.setText(f"{dpk:.0f}") + else: + self.detail_damage_per_kill.setText("-") + + self.detail_healing_done.setText(f"{data['healing_done'] or 0:.0f}") + self.detail_globals.setText(str(data['globals_count'] or 0)) + self.detail_hofs.setText(str(data['hofs_count'] or 0)) + self.detail_evades.setText(str(data['evades'] or 0)) + + # Update loot tab + self.detail_total_loot.setText(f"{total_loot:.4f} PED") + self.detail_other_loot.setText(f"{other_loot:.4f} PED") + self.detail_shrapnel_loot.setText(f"{shrapnel:.4f} PED") + self.detail_universal_ammo.setText(f"{data['total_universal_ammo_ped'] or 0:.4f} PED") + + self.detail_weapon_cost.setText(f"{data['weapon_cost_ped'] or 0:.4f} PED") + self.detail_armor_cost.setText(f"{data['armor_cost_ped'] or 0:.4f} PED") + self.detail_healing_cost.setText(f"{data['healing_cost_ped'] or 0:.4f} PED") + self.detail_plates_cost.setText(f"{data['plates_cost_ped'] or 0:.4f} PED") + self.detail_enhancer_cost.setText(f"{data['enhancer_cost_ped'] or 0:.4f} PED") + + # Update equipment tab + self.detail_weapon_name.setText(data['weapon_name'] or "-") + self.detail_weapon_dpp.setText(f"{data['weapon_dpp'] or 0:.4f}") + self.detail_armor_name.setText(data['armor_name'] or "-") + self.detail_fap_name.setText(data['fap_name'] or "-") + + except Exception as e: + QMessageBox.warning(self, "Error", f"Failed to load session details: {e}") + + def _format_duration(self, seconds: float) -> str: + """Format duration in seconds to human readable string.""" + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + + if hours > 0: + return f"{hours}h {minutes}m" + else: + return f"{minutes}m" + + def _export_json(self): + """Export selected session to JSON.""" + if not self.current_session_id: + return + + file_path, _ = QFileDialog.getSaveFileName( + self, "Export Session", + f"session_{self.current_session_id}.json", + "JSON Files (*.json)" + ) + + if not file_path: + return + + try: + # Get full session data + session_data = self._get_full_session_data(self.current_session_id) + + with open(file_path, 'w') as f: + json.dump(session_data, f, indent=2, default=str) + + QMessageBox.information(self, "Success", f"Session exported to {file_path}") + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to export: {e}") + + def _export_csv(self): + """Export selected session to CSV.""" + if not self.current_session_id: + return + + file_path, _ = QFileDialog.getSaveFileName( + self, "Export Session", + f"session_{self.current_session_id}.csv", + "CSV Files (*.csv)" + ) + + if not file_path: + return + + try: + session_data = self._get_full_session_data(self.current_session_id) + + with open(file_path, 'w', newline='') as f: + writer = csv.writer(f) + + # Write header and values + for key, value in session_data.items(): + writer.writerow([key, value]) + + QMessageBox.information(self, "Success", f"Session exported to {file_path}") + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to export: {e}") + + def _export_all(self): + """Export all sessions to CSV.""" + file_path, _ = QFileDialog.getSaveFileName( + self, "Export All Sessions", + "sessions_export.csv", + "CSV Files (*.csv)" + ) + + if not file_path: + return + + try: + with open(file_path, 'w', newline='') as f: + writer = csv.writer(f) + + # Header + writer.writerow([ + 'ID', 'Date', 'Project', 'Duration', 'Kills', 'Cost', + 'Loot', 'Profit/Loss', 'Return %', 'Globals', 'HoFs', + 'Weapon', 'DPP' + ]) + + # Data rows + for session in self.sessions_data: + started = datetime.fromisoformat(session['started_at']) + + duration = "" + if session['ended_at']: + end = datetime.fromisoformat(session['ended_at']) + duration = self._format_duration((end - started).total_seconds()) + + total_cost = Decimal(str(session['total_cost_ped'] or 0)) + other_loot = Decimal(str(session['total_other_loot_ped'] or 0)) + profit_loss = other_loot - total_cost + + return_pct = Decimal('0') + if total_cost > 0: + return_pct = (other_loot / total_cost) * Decimal('100') + + writer.writerow([ + session['id'], + started.strftime("%Y-%m-%d %H:%M"), + session['project_name'], + duration, + session['kills'] or 0, + float(total_cost), + float(other_loot), + float(profit_loss), + float(return_pct), + session['globals_count'] or 0, + session['hofs_count'] or 0, + session['weapon_name'] or '', + session['weapon_dpp'] or 0 + ]) + + QMessageBox.information(self, "Success", f"All sessions exported to {file_path}") + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to export: {e}") + + def _get_full_session_data(self, session_id: int) -> Dict[str, Any]: + """Get complete session data including related records.""" + cursor = self.db.execute(""" + SELECT + hs.*, p.name as project_name, s.started_at, s.ended_at, + s.notes as session_notes + FROM hunting_sessions hs + JOIN sessions s ON hs.session_id = s.id + JOIN projects p ON s.project_id = p.id + WHERE hs.id = ? + """, (session_id,)) + + session = dict(cursor.fetchone()) + + # Get loot events + cursor = self.db.execute(""" + SELECT * FROM loot_events + WHERE session_id = ? + ORDER BY timestamp + """, (session['session_id'],)) + session['loot_events'] = [dict(row) for row in cursor.fetchall()] + + # Get combat events + cursor = self.db.execute(""" + SELECT * FROM combat_events + WHERE session_id = ? + ORDER BY timestamp + """, (session['session_id'],)) + session['combat_events'] = [dict(row) for row in cursor.fetchall()] + + # Get skill gains + cursor = self.db.execute(""" + SELECT * FROM skill_gains + WHERE session_id = ? + ORDER BY timestamp + """, (session['session_id'],)) + session['skill_gains'] = [dict(row) for row in cursor.fetchall()] + + return session + + def _delete_session(self): + """Delete the selected session.""" + if not self.current_session_id: + return + + reply = QMessageBox.question( + self, "Confirm Delete", + "Are you sure you want to delete this session?\n\n" + "This will permanently remove all session data including loot, combat, and skill records.", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, + QMessageBox.StandardButton.No + ) + + if reply != QMessageBox.StandardButton.Yes: + return + + try: + # Get session_id from hunting_sessions + cursor = self.db.execute( + "SELECT session_id FROM hunting_sessions WHERE id = ?", + (self.current_session_id,) + ) + row = cursor.fetchone() + if row: + session_id = row['session_id'] + + # Delete related records first + self.db.execute("DELETE FROM loot_events WHERE session_id = ?", (session_id,)) + self.db.execute("DELETE FROM combat_events WHERE session_id = ?", (session_id,)) + self.db.execute("DELETE FROM skill_gains WHERE session_id = ?", (session_id,)) + self.db.execute("DELETE FROM decay_events WHERE session_id = ?", (session_id,)) + self.db.execute("DELETE FROM screenshots WHERE session_id = ?", (session_id,)) + + # Delete hunting session + self.db.execute("DELETE FROM hunting_sessions WHERE id = ?", (self.current_session_id,)) + + # Delete main session + self.db.execute("DELETE FROM sessions WHERE id = ?", (session_id,)) + + self.db.commit() + + self._load_sessions() + self.details_tabs.setEnabled(False) + self.current_session_id = None + + QMessageBox.information(self, "Success", "Session deleted successfully") + + except Exception as e: + QMessageBox.critical(self, "Error", f"Failed to delete session: {e}") + + def _apply_dark_theme(self): + """Apply dark theme styling.""" + dark_stylesheet = """ + QDialog { + 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; + } + + QTabWidget::pane { + border: 1px solid #444; + background-color: #1e1e1e; + } + + QTabBar::tab { + background-color: #2d2d2d; + border: 1px solid #444; + padding: 8px 16px; + margin-right: 2px; + } + + QTabBar::tab:selected { + background-color: #0d47a1; + } + + QTabBar::tab:hover:!selected { + background-color: #3d3d3d; + } + + 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; + } + + QComboBox:focus { + border-color: #0d47a1; + } + + QComboBox::drop-down { + border: none; + padding-right: 10px; + } + + QLabel { + color: #e0e0e0; + } + + QFormLayout QLabel { + color: #888; + } + + QSplitter::handle { + background-color: #444; + } + """ + self.setStyleSheet(dark_stylesheet) diff --git a/ui/setup_wizard.py b/ui/setup_wizard.py new file mode 100644 index 0000000..11eb7bc --- /dev/null +++ b/ui/setup_wizard.py @@ -0,0 +1,635 @@ +""" +Lemontropia Suite - Setup Wizard +First-run wizard for configuring the application. +""" + +import sys +from pathlib import Path +from typing import Optional + +from PyQt6.QtWidgets import ( + QWizard, QWizardPage, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QFileDialog, + QComboBox, QCheckBox, QProgressBar, QGroupBox, + QFormLayout, QTextEdit, QMessageBox, QWidget +) +from PyQt6.QtCore import Qt, QSettings, QThread, pyqtSignal +from PyQt6.QtGui import QFont, QPixmap + + +class GearDownloadWorker(QThread): + """Background worker for downloading gear database.""" + progress = pyqtSignal(int) + finished_signal = pyqtSignal(bool, str) + + def __init__(self, parent=None): + super().__init__(parent) + self._cancelled = False + + def run(self): + """Download gear database in background.""" + try: + # Simulate download progress + import time + for i in range(0, 101, 10): + if self._cancelled: + self.finished_signal.emit(False, "Download cancelled") + return + self.progress.emit(i) + time.sleep(0.2) + + # TODO: Implement actual gear database download + # For now, just create a placeholder + self.finished_signal.emit(True, "Gear database downloaded successfully") + except Exception as e: + self.finished_signal.emit(False, str(e)) + + def cancel(self): + self._cancelled = True + + +class WelcomePage(QWizardPage): + """Welcome page of the setup wizard.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("Welcome to Lemontropia Suite") + self.setSubTitle("Let's get you set up for tracking your Entropia Universe sessions.") + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Welcome text + welcome_label = QLabel( + "

Welcome!

" + "

Lemontropia Suite helps you track your hunting, mining, and crafting sessions " + "in Entropia Universe. This wizard will guide you through the initial setup.

" + "

You'll need:

" + "" + ) + welcome_label.setWordWrap(True) + layout.addWidget(welcome_label) + + layout.addStretch() + + # Note about re-running wizard + note_label = QLabel( + "You can run this wizard again anytime from the Settings menu." + ) + note_label.setStyleSheet("color: #888;") + layout.addWidget(note_label) + + +class AvatarNamePage(QWizardPage): + """Page for setting the avatar name.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("Avatar Name") + self.setSubTitle("Enter your avatar name exactly as it appears in Entropia Universe.") + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Explanation + info_label = QLabel( + "Your avatar name is used to identify your globals and HoFs in the chat log. " + "Make sure to enter it exactly as it appears in-game, including capitalization." + ) + info_label.setWordWrap(True) + layout.addWidget(info_label) + + layout.addSpacing(20) + + # Name input + form_layout = QFormLayout() + self.name_input = QLineEdit() + self.name_input.setPlaceholderText("e.g., John Doe Mega") + self.name_input.textChanged.connect(self.completeChanged) + form_layout.addRow("Avatar Name:", self.name_input) + layout.addLayout(form_layout) + + layout.addSpacing(10) + + # Example + example_label = QLabel( + "Example: If your avatar is named 'Roberth Noname Rajala', " + "enter it exactly like that." + ) + example_label.setStyleSheet("color: #888;") + layout.addWidget(example_label) + + layout.addStretch() + + def isComplete(self) -> bool: + return len(self.name_input.text().strip()) > 0 + + def get_avatar_name(self) -> str: + return self.name_input.text().strip() + + +class LogPathPage(QWizardPage): + """Page for configuring the log file path.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("Chat Log Path") + self.setSubTitle("Configure the path to your Entropia Universe chat log file.") + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Explanation + info_label = QLabel( + "Lemontropia Suite reads your chat log to track events like loot, globals, " + "damage dealt/taken, and healing. Please select your chat.log file." + ) + info_label.setWordWrap(True) + layout.addWidget(info_label) + + layout.addSpacing(20) + + # Default path info + default_path = self._get_default_log_path() + default_label = QLabel(f"Default location:
{default_path}") + default_label.setStyleSheet("color: #4caf50;") + default_label.setWordWrap(True) + layout.addWidget(default_label) + + layout.addSpacing(10) + + # Path selection + path_layout = QHBoxLayout() + self.path_input = QLineEdit() + self.path_input.setText(default_path) + self.path_input.textChanged.connect(self.completeChanged) + path_layout.addWidget(self.path_input) + + browse_btn = QPushButton("Browse...") + browse_btn.clicked.connect(self.on_browse) + path_layout.addWidget(browse_btn) + + layout.addLayout(path_layout) + + layout.addSpacing(10) + + # Auto-detect checkbox + self.auto_detect_check = QCheckBox("Automatically detect log path on startup") + self.auto_detect_check.setChecked(True) + layout.addWidget(self.auto_detect_check) + + layout.addStretch() + + # Help text + help_label = QLabel( + "Tip: If you can't find your chat.log, make sure you've run Entropia Universe " + "at least once and enabled chat logging in-game." + ) + help_label.setStyleSheet("color: #888;") + layout.addWidget(help_label) + + def _get_default_log_path(self) -> str: + """Get the default chat log path based on OS.""" + # Default paths for different scenarios + import os + + # Check environment variable first + env_path = os.getenv('EU_CHAT_LOG_PATH', '') + if env_path: + return env_path + + # Windows default path + appdata = os.getenv('LOCALAPPDATA', '') + if appdata: + return str(Path(appdata) / "Entropia Universe" / "chat.log") + + # Fallback + return str(Path.home() / "Entropia Universe" / "chat.log") + + def on_browse(self): + """Open file browser to select chat log.""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "Select Chat Log File", + str(Path.home()), + "Log Files (*.log);;All Files (*.*)" + ) + if file_path: + self.path_input.setText(file_path) + + def isComplete(self) -> bool: + return len(self.path_input.text().strip()) > 0 + + def get_log_path(self) -> str: + return self.path_input.text().strip() + + def get_auto_detect(self) -> bool: + return self.auto_detect_check.isChecked() + + +class ActivityTypePage(QWizardPage): + """Page for selecting default activity type.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("Default Activity Type") + self.setSubTitle("Choose your preferred default activity type.") + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Explanation + info_label = QLabel( + "Select the activity type you do most often. This will be the default " + "when starting new sessions. You can always change this later." + ) + info_label.setWordWrap(True) + layout.addWidget(info_label) + + layout.addSpacing(20) + + # Activity type selection + form_layout = QFormLayout() + self.activity_combo = QComboBox() + self.activity_combo.addItem("🎯 Hunting", "hunting") + self.activity_combo.addItem("⛏️ Mining", "mining") + self.activity_combo.addItem("βš’οΈ Crafting", "crafting") + form_layout.addRow("Default Activity:", self.activity_combo) + layout.addLayout(form_layout) + + layout.addSpacing(20) + + # Activity descriptions + descriptions = QGroupBox("Activity Types") + desc_layout = QVBoxLayout(descriptions) + + hunting_desc = QLabel( + "🎯 Hunting
" + "Track weapon decay, armor decay, healing costs, loot, globals, and HoFs." + ) + hunting_desc.setWordWrap(True) + desc_layout.addWidget(hunting_desc) + + mining_desc = QLabel( + "⛏️ Mining
" + "Track finder decay, extractor decay, claim values, and mining runs." + ) + mining_desc.setWordWrap(True) + desc_layout.addWidget(mining_desc) + + crafting_desc = QLabel( + "βš’οΈ Crafting
" + "Track blueprint runs, material costs, success rates, and near-successes." + ) + crafting_desc.setWordWrap(True) + desc_layout.addWidget(crafting_desc) + + layout.addWidget(descriptions) + + layout.addStretch() + + def get_activity_type(self) -> str: + return self.activity_combo.currentData() + + +class GearDatabasePage(QWizardPage): + """Page for downloading initial gear database.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("Gear Database (Optional)") + self.setSubTitle("Download initial gear database for cost tracking.") + self.setup_ui() + self.worker = None + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Explanation + info_label = QLabel( + "You can download an initial gear database with common weapons, armor, and tools. " + "This makes it easier to set up your loadouts for cost tracking.

" + "This step is optional - you can always add gear manually later." + ) + info_label.setWordWrap(True) + layout.addWidget(info_label) + + layout.addSpacing(20) + + # Download option + self.download_check = QCheckBox("Download initial gear database") + self.download_check.setChecked(True) + layout.addWidget(self.download_check) + + layout.addSpacing(10) + + # Progress bar (hidden initially) + self.progress_bar = QProgressBar() + self.progress_bar.setVisible(False) + layout.addWidget(self.progress_bar) + + # Status label + self.status_label = QLabel("") + self.status_label.setStyleSheet("color: #4caf50;") + layout.addWidget(self.status_label) + + layout.addStretch() + + # Note + note_label = QLabel( + "Note: The gear database requires internet connection. " + "If you skip this now, you can download it later from the Settings menu." + ) + note_label.setStyleSheet("color: #888;") + note_label.setWordWrap(True) + layout.addWidget(note_label) + + def initializePage(self): + """Called when page is shown.""" + self.progress_bar.setVisible(False) + self.status_label.setText("") + + def download_gear_database(self): + """Start downloading gear database.""" + if not self.download_check.isChecked(): + return True + + self.progress_bar.setVisible(True) + self.progress_bar.setValue(0) + self.status_label.setText("Downloading gear database...") + + # Start worker thread + self.worker = GearDownloadWorker(self) + self.worker.progress.connect(self.progress_bar.setValue) + self.worker.finished_signal.connect(self.on_download_finished) + self.worker.start() + + # Wait for completion + self.worker.wait() + + return self._download_success + + def on_download_finished(self, success: bool, message: str): + """Handle download completion.""" + self._download_success = success + if success: + self.status_label.setText(f"βœ“ {message}") + self.status_label.setStyleSheet("color: #4caf50;") + else: + self.status_label.setText(f"βœ— {message}") + self.status_label.setStyleSheet("color: #f44336;") + + def get_download_gear(self) -> bool: + return self.download_check.isChecked() + + +class SummaryPage(QWizardPage): + """Final page showing summary of configuration.""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setTitle("Setup Complete") + self.setSubTitle("Review your configuration before finishing.") + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + + # Success message + success_label = QLabel("

βœ“ Setup Complete!

") + success_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(success_label) + + layout.addSpacing(20) + + # Summary text area + self.summary_text = QTextEdit() + self.summary_text.setReadOnly(True) + self.summary_text.setMaximumHeight(200) + layout.addWidget(self.summary_text) + + layout.addSpacing(20) + + # Next steps + next_label = QLabel( + "Next Steps:
" + "1. Create or select a loadout for your gear
" + "2. Start a new session to begin tracking
" + "3. Check the HUD overlay for real-time stats

" + "Click Finish to start using Lemontropia Suite!" + ) + next_label.setWordWrap(True) + layout.addWidget(next_label) + + layout.addStretch() + + def initializePage(self): + """Update summary when page is shown.""" + wizard = self.wizard() + + # Get values from previous pages + avatar_page = wizard.page(1) # AvatarNamePage + log_page = wizard.page(2) # LogPathPage + activity_page = wizard.page(3) # ActivityTypePage + gear_page = wizard.page(4) # GearDatabasePage + + summary = f""" +Configuration Summary:
+
+Avatar Name: {avatar_page.get_avatar_name()}
+Chat Log Path: {log_page.get_log_path()}
+Auto-detect Log: {'Yes' if log_page.get_auto_detect() else 'No'}
+Default Activity: {activity_page.get_activity_type().capitalize()}
+Download Gear DB: {'Yes' if gear_page.get_download_gear() else 'No'}
+ """ + + self.summary_text.setHtml(summary) + + +class SetupWizard(QWizard): + """ + First-run setup wizard for Lemontropia Suite. + + Guides users through: + - Setting avatar name + - Configuring log file path + - Choosing default activity type + - Downloading initial gear database (optional) + """ + + def __init__(self, parent=None, first_run: bool = True): + super().__init__(parent) + + self.setWindowTitle("Lemontropia Suite - Setup Wizard") + self.setMinimumSize(600, 500) + + # Set wizard style + self.setWizardStyle(QWizard.WizardStyle.ModernStyle) + + # Add pages + self.addPage(WelcomePage()) + self.addPage(AvatarNamePage()) + self.addPage(LogPathPage()) + self.addPage(ActivityTypePage()) + self.addPage(GearDatabasePage()) + self.addPage(SummaryPage()) + + # Set button text + self.setButtonText(QWizard.WizardButton.FinishButton, "Start Using Lemontropia Suite") + + self._first_run = first_run + + def accept(self): + """Handle wizard completion.""" + # Collect all settings + settings = { + 'avatar_name': self.field("avatar_name") or self.page(1).get_avatar_name(), + 'log_path': self.page(2).get_log_path(), + 'auto_detect_log': self.page(2).get_auto_detect(), + 'default_activity': self.page(3).get_activity_type(), + 'gear_db_downloaded': self.page(4).get_download_gear(), + } + + # Save to QSettings + qsettings = QSettings("Lemontropia", "Suite") + qsettings.setValue("player/name", settings['avatar_name']) + qsettings.setValue("log/path", settings['log_path']) + qsettings.setValue("log/auto_detect", settings['auto_detect_log']) + qsettings.setValue("activity/default", settings['default_activity']) + qsettings.setValue("setup/first_run_complete", True) + qsettings.setValue("setup/gear_db_downloaded", settings['gear_db_downloaded']) + qsettings.sync() + + # Store settings for access after wizard closes + self._settings = settings + + super().accept() + + def get_settings(self) -> dict: + """Get the configured settings after wizard completion.""" + return getattr(self, '_settings', {}) + + @staticmethod + def is_first_run() -> bool: + """Check if this is the first run of the application.""" + settings = QSettings("Lemontropia", "Suite") + return not settings.value("setup/first_run_complete", False, type=bool) + + @staticmethod + def reset_first_run(): + """Reset the first-run flag (for testing).""" + settings = QSettings("Lemontropia", "Suite") + settings.setValue("setup/first_run_complete", False) + settings.sync() + + +def main(): + """Test entry point for the setup wizard.""" + from PyQt6.QtWidgets import QApplication + + app = QApplication(sys.argv) + + # Apply dark theme + dark_stylesheet = """ + QWizard { + background-color: #1e1e1e; + color: #e0e0e0; + } + + QWizardPage { + background-color: #1e1e1e; + color: #e0e0e0; + } + + QLabel { + color: #e0e0e0; + } + + QLineEdit { + background-color: #252525; + border: 1px solid #444; + border-radius: 4px; + padding: 6px; + color: #e0e0e0; + } + + QPushButton { + background-color: #2d2d2d; + border: 1px solid #444; + border-radius: 4px; + padding: 8px 16px; + color: #e0e0e0; + } + + QPushButton:hover { + background-color: #3d3d3d; + } + + QComboBox { + background-color: #252525; + border: 1px solid #444; + border-radius: 4px; + padding: 6px; + color: #e0e0e0; + } + + QGroupBox { + font-weight: bold; + border: 1px solid #444; + border-radius: 6px; + margin-top: 10px; + padding-top: 10px; + padding: 10px; + color: #e0e0e0; + } + + QTextEdit { + background-color: #252525; + border: 1px solid #444; + border-radius: 4px; + padding: 8px; + color: #e0e0e0; + } + + QCheckBox { + color: #e0e0e0; + } + + QCheckBox::indicator { + width: 18px; + height: 18px; + } + + QProgressBar { + border: 1px solid #444; + border-radius: 4px; + text-align: center; + color: #e0e0e0; + } + + QProgressBar::chunk { + background-color: #4caf50; + border-radius: 3px; + } + """ + app.setStyleSheet(dark_stylesheet) + + wizard = SetupWizard() + if wizard.exec() == QWizard.DialogCode.Accepted: + print("Settings saved:", wizard.get_settings()) + else: + print("Wizard cancelled") + + sys.exit(0) + + +if __name__ == '__main__': + main()