""" Helper dialog classes for InstallModlistScreen """ from PySide6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem, QPushButton, QLineEdit, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox, QAbstractItemView, QLabel, QWidget, QSizePolicy) from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QColor, QBrush import logging import os logger = logging.getLogger(__name__) class ModlistFetchThread(QThread): result = Signal(list, str) def __init__(self, game_type, log_path, mode='list-modlists'): super().__init__() self.game_type = game_type self.log_path = log_path self.mode = mode def run(self): try: # Use proper backend service - NOT the misnamed CLI class from jackify.backend.services.modlist_service import ModlistService from jackify.backend.models.configuration import SystemInfo # Initialize backend service # Detect if we're on Steam Deck is_steamdeck = False try: if os.path.exists('/etc/os-release'): with open('/etc/os-release') as f: if 'steamdeck' in f.read().lower(): is_steamdeck = True except Exception: pass system_info = SystemInfo(is_steamdeck=is_steamdeck) modlist_service = ModlistService(system_info) # Get modlists using proper backend service modlist_infos = modlist_service.list_modlists(game_type=self.game_type) # Return full modlist objects instead of just IDs to preserve enhanced metadata self.result.emit(modlist_infos, '') except Exception as e: error_msg = f"Backend service error: {str(e)}" # Don't write to log file before workflow starts - just return error self.result.emit([], error_msg) class SelectionDialog(QDialog): def __init__(self, title, items, parent=None, show_search=True, placeholder_text="Search modlists...", show_legend=False): super().__init__(parent) self.setWindowTitle(title) self.setModal(True) self.setMinimumWidth(600) self.setMinimumHeight(300) layout = QVBoxLayout(self) self.show_search = show_search if self.show_search: # Search box with clear button search_layout = QHBoxLayout() self.search_box = QLineEdit() self.search_box.setPlaceholderText(placeholder_text) # Make placeholder text lighter self.search_box.setStyleSheet("QLineEdit { color: #ccc; } QLineEdit:placeholder { color: #aaa; }") self.clear_btn = QPushButton("Clear") self.clear_btn.setFixedWidth(50) search_layout.addWidget(self.search_box) search_layout.addWidget(self.clear_btn) layout.addLayout(search_layout) if show_legend: # Use table for modlist selection with proper columns self.table_widget = QTableWidget() self.table_widget.setColumnCount(4) self.table_widget.setHorizontalHeaderLabels(["Modlist Name", "Download", "Install", "Total"]) # Configure table appearance self.table_widget.setSelectionBehavior(QTableWidget.SelectRows) self.table_widget.setSelectionMode(QTableWidget.SingleSelection) self.table_widget.verticalHeader().setVisible(False) self.table_widget.setAlternatingRowColors(True) # Set column widths header = self.table_widget.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.Stretch) # Modlist name takes remaining space header.setSectionResizeMode(1, QHeaderView.ResizeToContents) # Download size header.setSectionResizeMode(2, QHeaderView.ResizeToContents) # Install size header.setSectionResizeMode(3, QHeaderView.ResizeToContents) # Total size self._all_items = list(items) self._populate_table(self._all_items) layout.addWidget(self.table_widget) # Apply initial NSFW filter since checkbox starts unchecked self._filter_nsfw(False) else: # Use list for non-modlist dialogs (backward compatibility) self.list_widget = QListWidget() self.list_widget.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.list_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self._all_items = list(items) self._populate_list(self._all_items) layout.addWidget(self.list_widget) # Add interactive legend bar only for modlist selection dialogs if show_legend: legend_layout = QHBoxLayout() legend_layout.setContentsMargins(10, 5, 10, 5) # Status indicator explanation (far left) status_label = QLabel('[DOWN] Unavailable') status_label.setStyleSheet("color: #bbb;") legend_layout.addWidget(status_label) # Spacer after DOWN legend legend_layout.addSpacing(15) # No need for size format explanation since we have table headers now # Just add some spacing # Main spacer to push NSFW checkbox to far right legend_layout.addStretch() # NSFW filter checkbox (far right) self.nsfw_checkbox = QCheckBox("Show NSFW") self.nsfw_checkbox.setStyleSheet("color: #bbb; font-size: 11px;") self.nsfw_checkbox.setChecked(False) # Default to hiding NSFW content self.nsfw_checkbox.toggled.connect(self._filter_nsfw) legend_layout.addWidget(self.nsfw_checkbox) # Legend container legend_widget = QWidget() legend_widget.setLayout(legend_layout) legend_widget.setStyleSheet("background-color: #333; border-radius: 3px; margin: 2px;") layout.addWidget(legend_widget) self.selected_item = None # Connect appropriate signals based on widget type if show_legend: self.table_widget.itemClicked.connect(self.on_table_item_clicked) if self.show_search: self.search_box.textChanged.connect(self._filter_table) self.clear_btn.clicked.connect(self._clear_search) self.search_box.returnPressed.connect(self._focus_table) self.search_box.installEventFilter(self) else: self.list_widget.itemClicked.connect(self.on_item_clicked) if self.show_search: self.search_box.textChanged.connect(self._filter_list) self.clear_btn.clicked.connect(self._clear_search) self.search_box.returnPressed.connect(self._focus_list) self.search_box.installEventFilter(self) def _populate_list(self, items): self.list_widget.clear() for item in items: # Create list item - custom delegate handles all styling QListWidgetItem(item, self.list_widget) def _populate_table(self, items): self.table_widget.setRowCount(len(items)) for row, item in enumerate(items): # Parse the item string to extract components # Format: "[STATUS] Modlist Name Download|Install|Total" # Extract status indicators status_down = '[DOWN]' in item status_nsfw = '[NSFW]' in item # Clean the item string clean_item = item.replace('[DOWN]', '').replace('[NSFW]', '').strip() # Split into name and sizes # The format should be "Name Download|Install|Total" parts = clean_item.rsplit(' ', 1) # Split from right to separate name from sizes if len(parts) == 2: name = parts[0].strip() sizes = parts[1].strip() size_parts = sizes.split('|') if len(size_parts) == 3: download_size, install_size, total_size = [s.strip() for s in size_parts] else: # Fallback if format is unexpected download_size = install_size = total_size = sizes else: # Fallback if format is unexpected name = clean_item download_size = install_size = total_size = "" # Create table items name_item = QTableWidgetItem(name) download_item = QTableWidgetItem(download_size) install_item = QTableWidgetItem(install_size) total_item = QTableWidgetItem(total_size) # Apply styling if status_down: # Gray out and strikethrough for DOWN items for item_widget in [name_item, download_item, install_item, total_item]: item_widget.setForeground(QColor('#999999')) font = item_widget.font() font.setStrikeOut(True) item_widget.setFont(font) elif status_nsfw: # Red text for NSFW items - but only the name, sizes stay white name_item.setForeground(QColor('#ff4444')) for item_widget in [download_item, install_item, total_item]: item_widget.setForeground(QColor('#ffffff')) else: # White text for normal items for item_widget in [name_item, download_item, install_item, total_item]: item_widget.setForeground(QColor('#ffffff')) # Add status indicators to name if present if status_nsfw: name_item.setText(f"[NSFW] {name}") if status_down: # For DOWN items, we want [DOWN] normal and the name strikethrough # Since we can't easily mix fonts in a single QTableWidgetItem, # we'll style the whole item but the visual effect will be clear name_item.setText(f"[DOWN] {name_item.text()}") # Right-align size columns download_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) install_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) total_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) # Add items to table self.table_widget.setItem(row, 0, name_item) self.table_widget.setItem(row, 1, download_item) self.table_widget.setItem(row, 2, install_item) self.table_widget.setItem(row, 3, total_item) # Store original item text as data for filtering name_item.setData(Qt.UserRole, item) def _filter_list(self, text): text = text.strip().lower() if not text: filtered = self._all_items else: filtered = [item for item in self._all_items if text in item.lower()] self._populate_list(filtered) if filtered: self.list_widget.setCurrentRow(0) def _clear_search(self): self.search_box.clear() self.search_box.setFocus() def _focus_list(self): self.list_widget.setFocus() self.list_widget.setCurrentRow(0) def _focus_table(self): self.table_widget.setFocus() self.table_widget.setCurrentCell(0, 0) def _filter_table(self, text): text = text.strip().lower() if not text: # Show all rows for row in range(self.table_widget.rowCount()): self.table_widget.setRowHidden(row, False) else: # Filter rows based on modlist name for row in range(self.table_widget.rowCount()): name_item = self.table_widget.item(row, 0) if name_item: # Search in the modlist name match = text in name_item.text().lower() self.table_widget.setRowHidden(row, not match) def on_table_item_clicked(self, item): # Get the original item text from the name column row = item.row() name_item = self.table_widget.item(row, 0) if name_item: original_item = name_item.data(Qt.UserRole) self.selected_item = original_item self.accept() def _filter_nsfw(self, show_nsfw): """Filter NSFW modlists based on checkbox state""" if show_nsfw: # Show all items filtered_items = self._all_items else: # Hide NSFW items filtered_items = [item for item in self._all_items if '[NSFW]' not in item] # Use appropriate populate method based on widget type if hasattr(self, 'table_widget'): self._populate_table(filtered_items) # Apply search filter if there's search text if hasattr(self, 'search_box') and self.search_box.text().strip(): self._filter_table(self.search_box.text()) else: self._populate_list(filtered_items) # Apply search filter if there's search text if hasattr(self, 'search_box') and self.search_box.text().strip(): self._filter_list(self.search_box.text()) def eventFilter(self, obj, event): if self.show_search and obj == self.search_box and event.type() == event.Type.KeyPress: if event.key() in (Qt.Key.Key_Down, Qt.Key.Key_Tab): # Focus appropriate widget if hasattr(self, 'table_widget'): self._focus_table() else: self._focus_list() return True return super().eventFilter(obj, event) def on_item_clicked(self, item): self.selected_item = item.text() self.accept()