Lemontropia-Suite/ui/gallery_dialog.py

842 lines
28 KiB
Python

"""
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()
# Block signals to prevent recursion
self.session_filter.blockSignals(True)
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
finally:
# Re-enable signals
self.session_filter.blockSignals(False)
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, "")