mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:17:58 +02:00
209 lines
9.5 KiB
Python
209 lines
9.5 KiB
Python
"""Visual card representing a single modlist."""
|
|
from PySide6.QtWidgets import QFrame, QVBoxLayout, QHBoxLayout, QLabel, QSizePolicy
|
|
from PySide6.QtCore import Qt, Signal, QSize
|
|
from PySide6.QtGui import QPixmap, QPainter, QColor, QFont
|
|
from jackify.backend.models.modlist_metadata import ModlistMetadata
|
|
from ..shared_theme import JACKIFY_COLOR_BLUE
|
|
from .modlist_gallery_image_manager import ImageManager
|
|
|
|
class ModlistCard(QFrame):
|
|
"""Visual card representing a single modlist"""
|
|
clicked = Signal(ModlistMetadata)
|
|
|
|
def __init__(self, metadata: ModlistMetadata, image_manager: ImageManager, is_steamdeck: bool = False):
|
|
super().__init__()
|
|
self.metadata = metadata
|
|
self.image_manager = image_manager
|
|
self.is_steamdeck = is_steamdeck
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Set up the card UI"""
|
|
self.setFrameShape(QFrame.StyledPanel)
|
|
self.setFrameShadow(QFrame.Raised)
|
|
self.setCursor(Qt.PointingHandCursor)
|
|
|
|
# Steam Deck-specific sizing (1280x800 screen)
|
|
if self.is_steamdeck:
|
|
self.setFixedSize(250, 270) # Smaller cards for Steam Deck
|
|
image_width, image_height = 230, 130 # Smaller images, maintaining 16:9 ratio
|
|
else:
|
|
self.setFixedSize(300, 320) # Standard size
|
|
image_width, image_height = 280, 158 # Standard image size
|
|
|
|
self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
|
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(10, 8, 10, 8) # Reduced vertical margins
|
|
layout.setSpacing(6) # Reduced spacing between elements
|
|
|
|
# Image (widescreen aspect ratio like Wabbajack)
|
|
self.image_label = QLabel()
|
|
self.image_label.setFixedSize(image_width, image_height) # 16:9 aspect ratio
|
|
self.image_label.setStyleSheet("background: #333; border-radius: 4px;")
|
|
self.image_label.setAlignment(Qt.AlignCenter)
|
|
self.image_label.setScaledContents(True) # Use Qt's automatic scaling - this works best
|
|
self.image_label.setText("")
|
|
layout.addWidget(self.image_label)
|
|
|
|
# Title row with badges (Official, NSFW, UNAVAILABLE)
|
|
title_row = QHBoxLayout()
|
|
title_row.setSpacing(4)
|
|
|
|
title = QLabel(self.metadata.title)
|
|
title.setWordWrap(True)
|
|
title.setFont(QFont("Sans", 12, QFont.Bold))
|
|
title.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE};")
|
|
title.setMaximumHeight(40)
|
|
title_row.addWidget(title, stretch=1)
|
|
|
|
# Store reference to unavailable badge for dynamic updates
|
|
self.unavailable_badge = None
|
|
if not self.metadata.is_available():
|
|
self.unavailable_badge = QLabel("UNAVAILABLE")
|
|
self.unavailable_badge.setStyleSheet("background: #666; color: white; padding: 2px 6px; font-size: 9px; border-radius: 3px;")
|
|
self.unavailable_badge.setFixedHeight(20)
|
|
title_row.addWidget(self.unavailable_badge, alignment=Qt.AlignTop | Qt.AlignRight)
|
|
|
|
if self.metadata.official:
|
|
official_badge = QLabel("OFFICIAL")
|
|
official_badge.setStyleSheet("background: #2a5; color: white; padding: 2px 6px; font-size: 9px; border-radius: 3px;")
|
|
official_badge.setFixedHeight(20)
|
|
title_row.addWidget(official_badge, alignment=Qt.AlignTop | Qt.AlignRight)
|
|
|
|
if self.metadata.nsfw:
|
|
nsfw_badge = QLabel("NSFW")
|
|
nsfw_badge.setStyleSheet("background: #d44; color: white; padding: 2px 6px; font-size: 9px; border-radius: 3px;")
|
|
nsfw_badge.setFixedHeight(20)
|
|
title_row.addWidget(nsfw_badge, alignment=Qt.AlignTop | Qt.AlignRight)
|
|
|
|
layout.addLayout(title_row)
|
|
|
|
# Author
|
|
author = QLabel(f"by {self.metadata.author}")
|
|
author.setStyleSheet("color: #aaa; font-size: 11px;")
|
|
layout.addWidget(author)
|
|
|
|
# Game
|
|
game = QLabel(self.metadata.gameHumanFriendly)
|
|
game.setStyleSheet("color: #ccc; font-size: 10px;")
|
|
layout.addWidget(game)
|
|
|
|
# Sizes (Download, Install, Total)
|
|
if self.metadata.sizes:
|
|
size_info = QLabel(
|
|
f"Download: {self.metadata.sizes.downloadSizeFormatted} | "
|
|
f"Install: {self.metadata.sizes.installSizeFormatted} | "
|
|
f"Total: {self.metadata.sizes.totalSizeFormatted}"
|
|
)
|
|
size_info.setStyleSheet("color: #999; font-size: 10px;")
|
|
size_info.setWordWrap(True) # Allow wrapping if text is too long
|
|
layout.addWidget(size_info)
|
|
|
|
# Removed addStretch() to eliminate wasted space
|
|
self.setLayout(layout)
|
|
|
|
# Load image
|
|
self._load_image()
|
|
|
|
def _create_placeholder(self):
|
|
"""Create a placeholder pixmap for cards without images"""
|
|
# Create placeholder matching the image label size (Steam Deck or standard)
|
|
image_size = self.image_label.size()
|
|
placeholder = QPixmap(image_size)
|
|
placeholder.fill(QColor("#333"))
|
|
|
|
# Draw a simple icon/text on the placeholder
|
|
painter = QPainter(placeholder)
|
|
painter.setPen(QColor("#666"))
|
|
painter.setFont(QFont("Sans", 10))
|
|
painter.drawText(placeholder.rect(), Qt.AlignCenter, "No Image")
|
|
painter.end()
|
|
|
|
# Show placeholder immediately
|
|
self.image_label.setPixmap(placeholder)
|
|
|
|
def _load_image(self):
|
|
"""Load image using centralized image manager - use large images and scale down for quality"""
|
|
# Get large image for card - scale down for better quality than small images
|
|
pixmap = self.image_manager.get_image(self.metadata, self._on_image_loaded, size="large")
|
|
|
|
if pixmap and not pixmap.isNull():
|
|
# Image was in cache - display immediately (should be instant)
|
|
self._display_image(pixmap)
|
|
else:
|
|
# Image needs to be downloaded - show placeholder
|
|
self._create_placeholder()
|
|
|
|
def _on_image_loaded(self, pixmap: QPixmap):
|
|
"""Callback when image is loaded from network"""
|
|
if pixmap and not pixmap.isNull():
|
|
self._display_image(pixmap)
|
|
|
|
def _display_image(self, pixmap: QPixmap):
|
|
"""Display image - use best method based on aspect ratio"""
|
|
if pixmap.isNull():
|
|
return
|
|
|
|
label_size = self.image_label.size()
|
|
label_aspect = label_size.width() / label_size.height() # 16:9 = ~1.778
|
|
|
|
# Calculate image aspect ratio
|
|
image_aspect = pixmap.width() / pixmap.height() if pixmap.height() > 0 else label_aspect
|
|
|
|
# If aspect ratios are close (within 5%), use Qt's automatic scaling for best quality
|
|
# Otherwise, manually scale with cropping to avoid stretching
|
|
aspect_diff = abs(image_aspect - label_aspect) / label_aspect
|
|
|
|
if aspect_diff < 0.05: # Within 5% of 16:9
|
|
# Close to correct aspect - use Qt's automatic scaling (best quality)
|
|
self.image_label.setScaledContents(True)
|
|
self.image_label.setPixmap(pixmap)
|
|
else:
|
|
# Different aspect - manually scale with cropping (no stretching)
|
|
self.image_label.setScaledContents(False)
|
|
scaled_pixmap = pixmap.scaled(
|
|
label_size.width(),
|
|
label_size.height(),
|
|
Qt.KeepAspectRatioByExpanding, # Crop instead of stretch
|
|
Qt.SmoothTransformation # High quality
|
|
)
|
|
self.image_label.setPixmap(scaled_pixmap)
|
|
|
|
def _update_availability_badge(self):
|
|
"""Update unavailable badge visibility based on current availability status"""
|
|
is_unavailable = not self.metadata.is_available()
|
|
|
|
# Find title row layout (it's the 2nd layout item: image at 0, title_row at 1)
|
|
main_layout = self.layout()
|
|
if main_layout and main_layout.count() >= 2:
|
|
title_row = main_layout.itemAt(1).layout()
|
|
if title_row:
|
|
if is_unavailable and self.unavailable_badge is None:
|
|
# Need to add badge to title row (before Official/NSFW badges)
|
|
self.unavailable_badge = QLabel("UNAVAILABLE")
|
|
self.unavailable_badge.setStyleSheet("background: #666; color: white; padding: 2px 6px; font-size: 9px; border-radius: 3px;")
|
|
self.unavailable_badge.setFixedHeight(20)
|
|
# Insert after title (index 1) but before other badges
|
|
# Find first badge position (if any exist)
|
|
insert_index = 1 # After title widget
|
|
for i in range(title_row.count()):
|
|
item = title_row.itemAt(i)
|
|
if item and item.widget() and isinstance(item.widget(), QLabel):
|
|
widget_text = item.widget().text()
|
|
if widget_text in ("OFFICIAL", "NSFW"):
|
|
insert_index = i
|
|
break
|
|
title_row.insertWidget(insert_index, self.unavailable_badge, alignment=Qt.AlignTop | Qt.AlignRight)
|
|
elif not is_unavailable and self.unavailable_badge is not None:
|
|
# Need to remove badge from title row
|
|
title_row.removeWidget(self.unavailable_badge)
|
|
self.unavailable_badge.setParent(None)
|
|
self.unavailable_badge = None
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Handle click on card"""
|
|
if event.button() == Qt.LeftButton:
|
|
self.clicked.emit(self.metadata)
|
|
super().mousePressEvent(event)
|