mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 00:07:45 +02:00
402 lines
17 KiB
Python
402 lines
17 KiB
Python
"""Loading and data management for ModlistGalleryDialog (Mixin)."""
|
|
from PySide6.QtCore import Qt, QThread, Signal, QTimer
|
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QApplication
|
|
from PySide6.QtGui import QFont
|
|
from typing import List, Dict
|
|
import random
|
|
import logging
|
|
from jackify.backend.models.modlist_metadata import ModlistMetadata
|
|
from ..shared_theme import JACKIFY_COLOR_BLUE
|
|
from .modlist_gallery_card import ModlistCard
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ModlistGalleryLoadingMixin:
|
|
"""Mixin providing loading and data management for ModlistGalleryDialog."""
|
|
|
|
def _load_modlists_async(self):
|
|
"""Load modlists in background thread for instant dialog appearance"""
|
|
from PySide6.QtCore import QThread, Signal
|
|
from PySide6.QtGui import QFont
|
|
|
|
# Hide status label during loading (popup dialog will show instead)
|
|
self.status_label.setVisible(False)
|
|
|
|
# Show loading overlay directly in content area (simpler than separate dialog)
|
|
self._loading_overlay = QWidget(self.content_area)
|
|
self._loading_overlay.setStyleSheet("""
|
|
QWidget {
|
|
background-color: rgba(35, 35, 35, 240);
|
|
border-radius: 8px;
|
|
}
|
|
""")
|
|
overlay_layout = QVBoxLayout()
|
|
overlay_layout.setContentsMargins(30, 20, 30, 20)
|
|
overlay_layout.setSpacing(12)
|
|
|
|
self._loading_label = QLabel("Loading modlists")
|
|
self._loading_label.setAlignment(Qt.AlignCenter)
|
|
# Set fixed width to prevent text shifting when dots animate
|
|
# Width accommodates "Loading modlists..." (longest version)
|
|
self._loading_label.setFixedWidth(220)
|
|
font = QFont()
|
|
font.setPointSize(14)
|
|
font.setBold(True)
|
|
self._loading_label.setFont(font)
|
|
self._loading_label.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 14px; font-weight: bold;")
|
|
overlay_layout.addWidget(self._loading_label)
|
|
|
|
self._loading_overlay.setLayout(overlay_layout)
|
|
self._loading_overlay.setFixedSize(300, 120)
|
|
|
|
# Animate dots in loading message
|
|
self._loading_dot_count = 0
|
|
self._loading_dot_timer = QTimer()
|
|
self._loading_dot_timer.timeout.connect(self._animate_loading_dots)
|
|
self._loading_dot_timer.start(500) # Update every 500ms
|
|
|
|
# Position overlay in center of content area
|
|
def position_overlay():
|
|
if hasattr(self, 'content_area') and self.content_area.isVisible():
|
|
content_width = self.content_area.width()
|
|
content_height = self.content_area.height()
|
|
x = (content_width - 300) // 2
|
|
y = (content_height - 120) // 2
|
|
self._loading_overlay.move(x, y)
|
|
self._loading_overlay.show()
|
|
self._loading_overlay.raise_()
|
|
|
|
# Delay slightly to ensure content_area is laid out
|
|
QTimer.singleShot(50, position_overlay)
|
|
|
|
class ModlistLoaderThread(QThread):
|
|
"""Background thread to load modlist metadata"""
|
|
finished = Signal(object, object) # metadata_response, error_message
|
|
|
|
def __init__(self, gallery_service):
|
|
super().__init__()
|
|
self.gallery_service = gallery_service
|
|
|
|
def run(self):
|
|
try:
|
|
import time
|
|
start_time = time.time()
|
|
|
|
# Fetch metadata (CPU-intensive work happens here in background)
|
|
# Skip search index initially for faster loading - can be loaded later if user searches
|
|
metadata_response = self.gallery_service.fetch_modlist_metadata(
|
|
include_validation=False,
|
|
include_search_index=False, # Skip for faster initial load
|
|
sort_by="title"
|
|
)
|
|
|
|
elapsed = time.time() - start_time
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
if elapsed < 0.5:
|
|
logger.debug(f"Gallery metadata loaded from cache in {elapsed:.2f}s")
|
|
else:
|
|
logger.info(f"Gallery metadata fetched from engine in {elapsed:.2f}s")
|
|
|
|
self.finished.emit(metadata_response, None)
|
|
except Exception as e:
|
|
self.finished.emit(None, str(e))
|
|
|
|
# Create and start background thread
|
|
self._loader_thread = ModlistLoaderThread(self.gallery_service)
|
|
self._loader_thread.finished.connect(self._on_modlists_loaded)
|
|
self._loader_thread.start()
|
|
|
|
|
|
def _animate_loading_dots(self):
|
|
"""Animate dots in loading message"""
|
|
if hasattr(self, '_loading_label') and self._loading_label:
|
|
self._loading_dot_count = (self._loading_dot_count + 1) % 4
|
|
dots = "." * self._loading_dot_count
|
|
# Pad with spaces to keep text width constant (prevents shifting)
|
|
padding = " " * (3 - self._loading_dot_count)
|
|
self._loading_label.setText(f"Loading modlists{dots}{padding}")
|
|
|
|
|
|
def _on_modlists_loaded(self, metadata_response, error_message):
|
|
"""Handle modlist metadata loaded in background thread (runs in GUI thread)"""
|
|
import random
|
|
from PySide6.QtGui import QFont
|
|
|
|
# Stop animation timer and close loading overlay
|
|
if hasattr(self, '_loading_dot_timer') and self._loading_dot_timer:
|
|
self._loading_dot_timer.stop()
|
|
self._loading_dot_timer = None
|
|
|
|
if hasattr(self, '_loading_overlay') and self._loading_overlay:
|
|
self._loading_overlay.hide()
|
|
self._loading_overlay.deleteLater()
|
|
self._loading_overlay = None
|
|
|
|
self.status_label.setVisible(True)
|
|
|
|
if error_message:
|
|
self.status_label.setText(f"Error loading modlists: {error_message}")
|
|
return
|
|
|
|
if not metadata_response:
|
|
self.status_label.setText("Failed to load modlists")
|
|
return
|
|
|
|
try:
|
|
# Get all modlists
|
|
all_modlists = metadata_response.modlists
|
|
|
|
# RANDOMIZE the order each time gallery opens (like Wabbajack)
|
|
random.shuffle(all_modlists)
|
|
|
|
self.all_modlists = all_modlists
|
|
|
|
# Precompute normalized tags for display/filtering
|
|
for modlist in self.all_modlists:
|
|
normalized_display = self.gallery_service.normalize_tags_for_display(getattr(modlist, 'tags', []))
|
|
modlist.normalized_tags_display = normalized_display
|
|
modlist.normalized_tags_keys = [tag.lower() for tag in normalized_display]
|
|
|
|
# Temporarily disconnect to prevent triggering during setup
|
|
self.game_combo.currentIndexChanged.disconnect(self._apply_filters)
|
|
|
|
# Populate game filter
|
|
games = sorted(set(m.gameHumanFriendly for m in self.all_modlists))
|
|
for game in games:
|
|
self.game_combo.addItem(game, game)
|
|
|
|
# If dialog was opened with a game filter, pre-select it
|
|
if self.game_filter:
|
|
index = self.game_combo.findData(self.game_filter)
|
|
if index >= 0:
|
|
self.game_combo.setCurrentIndex(index)
|
|
|
|
# Populate tag filter (mod filter temporarily disabled)
|
|
self._populate_tag_filter()
|
|
# self._populate_mod_filter() # TEMPORARILY DISABLED
|
|
|
|
# Create cards immediately (will show placeholders for images not in cache)
|
|
self._create_all_cards()
|
|
|
|
# Preload cached images in background (non-blocking)
|
|
self.status_label.setText("Loading images...")
|
|
QTimer.singleShot(0, self._preload_cached_images_async)
|
|
|
|
# Reconnect filter handler
|
|
self.game_combo.currentIndexChanged.connect(self._apply_filters)
|
|
|
|
# Enable filter controls now that data is loaded
|
|
self._set_filter_controls_enabled(True)
|
|
|
|
# Apply filters (will show all modlists for selected game initially)
|
|
self._apply_filters()
|
|
|
|
# Start background validation update (non-blocking)
|
|
self._start_validation_update()
|
|
|
|
except Exception as e:
|
|
self.status_label.setText(f"Error processing modlists: {str(e)}")
|
|
|
|
|
|
def _load_modlists(self):
|
|
"""DEPRECATED: Synchronous loading - replaced by _load_modlists_async()"""
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
self.status_label.setText("Loading modlists...")
|
|
QApplication.processEvents() # Update UI immediately
|
|
|
|
# Fetch metadata (will use cache if valid)
|
|
# Skip validation initially for faster loading - can be added later if needed
|
|
try:
|
|
metadata_response = self.gallery_service.fetch_modlist_metadata(
|
|
include_validation=False, # Skip validation for faster initial load
|
|
include_search_index=True, # Include mod search index for mod filtering
|
|
sort_by="title"
|
|
)
|
|
|
|
if metadata_response:
|
|
# Get all modlists
|
|
all_modlists = metadata_response.modlists
|
|
|
|
# RANDOMIZE the order each time gallery opens (like Wabbajack)
|
|
# Prevent gaming via alphabetical ordering
|
|
random.shuffle(all_modlists)
|
|
|
|
self.all_modlists = all_modlists
|
|
|
|
# Precompute normalized tags for display/filtering (matches upstream Wabbajack)
|
|
for modlist in self.all_modlists:
|
|
normalized_display = self.gallery_service.normalize_tags_for_display(getattr(modlist, 'tags', []))
|
|
modlist.normalized_tags_display = normalized_display
|
|
modlist.normalized_tags_keys = [tag.lower() for tag in normalized_display]
|
|
|
|
# Temporarily disconnect to prevent triggering during setup
|
|
self.game_combo.currentIndexChanged.disconnect(self._apply_filters)
|
|
|
|
# Populate game filter
|
|
games = sorted(set(m.gameHumanFriendly for m in self.all_modlists))
|
|
for game in games:
|
|
self.game_combo.addItem(game, game)
|
|
|
|
# If dialog was opened with a game filter, pre-select it
|
|
if self.game_filter:
|
|
index = self.game_combo.findData(self.game_filter)
|
|
if index >= 0:
|
|
self.game_combo.setCurrentIndex(index)
|
|
|
|
# Populate tag filter (mod filter temporarily disabled)
|
|
self._populate_tag_filter()
|
|
# self._populate_mod_filter() # TEMPORARILY DISABLED
|
|
|
|
# Create cards immediately (will show placeholders for images not in cache)
|
|
self._create_all_cards()
|
|
|
|
# Preload cached images in background (non-blocking)
|
|
# Images will appear as they're loaded
|
|
self.status_label.setText("Loading images...")
|
|
QTimer.singleShot(0, self._preload_cached_images_async)
|
|
|
|
# Reconnect filter handler
|
|
self.game_combo.currentIndexChanged.connect(self._apply_filters)
|
|
|
|
# Apply filters (will show all modlists for selected game initially)
|
|
self._apply_filters()
|
|
|
|
# Start background validation update (non-blocking)
|
|
self._start_validation_update()
|
|
else:
|
|
self.status_label.setText("Failed to load modlists")
|
|
except Exception as e:
|
|
self.status_label.setText(f"Error loading modlists: {str(e)}")
|
|
|
|
|
|
def _preload_cached_images_async(self):
|
|
"""Preload cached images asynchronously - images appear as they load"""
|
|
from PySide6.QtWidgets import QApplication
|
|
|
|
preloaded = 0
|
|
total = len(self.all_modlists)
|
|
|
|
for idx, modlist in enumerate(self.all_modlists):
|
|
cache_key = modlist.machineURL
|
|
|
|
# Skip if already in cache
|
|
if cache_key in self.image_manager.pixmap_cache:
|
|
continue
|
|
|
|
# Preload large images for cards (scale down for better quality)
|
|
cached_path = self.gallery_service.get_cached_image_path(modlist, "large")
|
|
if cached_path and cached_path.exists():
|
|
try:
|
|
pixmap = QPixmap(str(cached_path))
|
|
if not pixmap.isNull():
|
|
cache_key_large = f"{cache_key}_large"
|
|
self.image_manager.pixmap_cache[cache_key_large] = pixmap
|
|
preloaded += 1
|
|
|
|
# Update card immediately if it exists
|
|
card = self.all_cards.get(cache_key)
|
|
if card:
|
|
card._display_image(pixmap)
|
|
except Exception:
|
|
pass
|
|
|
|
# Process events every 10 images to keep UI responsive
|
|
if idx % 10 == 0 and idx > 0:
|
|
QApplication.processEvents()
|
|
|
|
# Update status (subtle, user-friendly)
|
|
modlist_count = len(self.filtered_modlists)
|
|
if modlist_count == 1:
|
|
self.status_label.setText("1 modlist")
|
|
else:
|
|
self.status_label.setText(f"{modlist_count} modlists")
|
|
|
|
def _create_all_cards(self):
|
|
"""Create cards for all modlists and store in dict"""
|
|
# Clear existing cards
|
|
self.all_cards.clear()
|
|
|
|
# Disable updates during card creation to prevent individual renders
|
|
self.grid_widget.setUpdatesEnabled(False)
|
|
self.setUpdatesEnabled(False)
|
|
|
|
try:
|
|
# Create all cards - images should be in memory cache from preload
|
|
# so _load_image() will find them instantly
|
|
for modlist in self.all_modlists:
|
|
card = ModlistCard(modlist, self.image_manager, is_steamdeck=self.is_steamdeck)
|
|
card.clicked.connect(self._on_modlist_clicked)
|
|
self.all_cards[modlist.machineURL] = card
|
|
finally:
|
|
# Re-enable updates - single render for all cards
|
|
self.setUpdatesEnabled(True)
|
|
self.grid_widget.setUpdatesEnabled(True)
|
|
|
|
|
|
def _refresh_metadata(self):
|
|
"""Force refresh metadata from jackify-engine"""
|
|
self.status_label.setText("Refreshing metadata...")
|
|
self.gallery_service.clear_cache()
|
|
self._load_modlists()
|
|
|
|
|
|
def _start_validation_update(self):
|
|
"""Start background validation update to get availability status"""
|
|
# Update validation in background thread to avoid blocking UI
|
|
class ValidationUpdateThread(QThread):
|
|
finished_signal = Signal(object) # Emits updated metadata response
|
|
|
|
def __init__(self, gallery_service):
|
|
super().__init__()
|
|
self.gallery_service = gallery_service
|
|
|
|
def run(self):
|
|
try:
|
|
# Fetch with validation (slower, but in background)
|
|
metadata_response = self.gallery_service.fetch_modlist_metadata(
|
|
include_validation=True,
|
|
include_search_index=False,
|
|
sort_by="title"
|
|
)
|
|
self.finished_signal.emit(metadata_response)
|
|
except Exception:
|
|
self.finished_signal.emit(None)
|
|
|
|
self._validation_thread = ValidationUpdateThread(self.gallery_service)
|
|
self._validation_thread.finished_signal.connect(self._on_validation_updated)
|
|
self._validation_thread.start()
|
|
|
|
|
|
def _on_validation_updated(self, metadata_response):
|
|
"""Update modlists with validation data when background fetch completes"""
|
|
if not metadata_response:
|
|
return
|
|
|
|
# Create lookup dict for validation data
|
|
validation_map = {}
|
|
for modlist in metadata_response.modlists:
|
|
if modlist.validation:
|
|
validation_map[modlist.machineURL] = modlist.validation
|
|
|
|
# Update existing modlists with validation data
|
|
updated_count = 0
|
|
for modlist in self.all_modlists:
|
|
if modlist.machineURL in validation_map:
|
|
modlist.validation = validation_map[modlist.machineURL]
|
|
updated_count += 1
|
|
|
|
# Update card if it exists
|
|
card = self.all_cards.get(modlist.machineURL)
|
|
if card:
|
|
# Update unavailable badge visibility
|
|
card._update_availability_badge()
|
|
|
|
# Re-apply filters to update availability filtering
|
|
if updated_count > 0:
|
|
self._apply_filters()
|
|
|
|
|