Sync from development - prepare for v0.2.0.1

This commit is contained in:
Omni
2025-12-19 19:42:31 +00:00
parent e3dc62fdac
commit 9c52c0434b
57 changed files with 786 additions and 395 deletions

View File

@@ -1,7 +1,7 @@
"""
InstallModlistScreen for Jackify GUI
"""
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QApplication, QCheckBox, QStyledItemDelegate, QStyle, QTableWidget, QTableWidgetItem, QHeaderView
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QApplication, QCheckBox, QStyledItemDelegate, QStyle, QTableWidget, QTableWidgetItem, QHeaderView, QMainWindow
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QProcess, QMetaObject, QUrl
from PySide6.QtGui import QPixmap, QTextCursor, QColor, QPainter, QFont
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
@@ -2533,10 +2533,34 @@ class InstallModlistScreen(QWidget):
debug_print(f"DEBUG: - {fp.filename}: {fp.percent:.1f}% ({fp.operation.value})")
# Pass phase label to update header (e.g., "[Activity - Downloading]")
# Explicitly clear summary_info when showing file list
self.file_progress_list.update_files(progress_state.active_files, current_phase=phase_label, summary_info=None)
try:
self.file_progress_list.update_files(progress_state.active_files, current_phase=phase_label, summary_info=None)
except RuntimeError as e:
# Widget was deleted - ignore to prevent coredump
if "already deleted" in str(e):
if self.debug:
debug_print(f"DEBUG: Ignoring widget deletion error: {e}")
return
raise
except Exception as e:
# Catch any other exceptions to prevent coredump
if self.debug:
debug_print(f"DEBUG: Error updating file progress list: {e}")
import logging
logging.getLogger(__name__).error(f"Error updating file progress list: {e}", exc_info=True)
else:
# Show empty state so widget stays visible even when no files are active
self.file_progress_list.update_files([], current_phase=phase_label)
try:
self.file_progress_list.update_files([], current_phase=phase_label)
except RuntimeError as e:
# Widget was deleted - ignore to prevent coredump
if "already deleted" in str(e):
return
raise
except Exception as e:
# Catch any other exceptions to prevent coredump
import logging
logging.getLogger(__name__).error(f"Error updating file progress list: {e}", exc_info=True)
def _on_show_details_toggled(self, checked: bool):
"""R&D: Toggle console visibility (reuse TTW pattern)"""
@@ -2996,19 +3020,24 @@ class InstallModlistScreen(QWidget):
normalized = text.lower()
total = max(1, self._post_install_total_steps)
matched = False
matched_step = None
for idx, step in enumerate(self._post_install_sequence, start=1):
if any(keyword in normalized for keyword in step['keywords']):
matched = True
matched_step = idx
# Always update to the highest step we've seen (don't go backwards)
if idx >= self._post_install_current_step:
self._post_install_current_step = idx
self._post_install_last_label = step['label']
self._update_post_install_ui(step['label'], idx, total, detail=text)
else:
self._update_post_install_ui(step['label'], idx, total, detail=text)
# CRITICAL: Always use the current step (not the matched step) to ensure consistency
# This prevents Activity window showing different step than progress banner
self._update_post_install_ui(step['label'], self._post_install_current_step, total, detail=text)
break
# If no match but we have a current step, update with that step (not a new one)
if not matched and self._post_install_current_step > 0:
label = self._post_install_last_label or "Post-installation"
# CRITICAL: Use _post_install_current_step (not a new step) to keep displays in sync
self._update_post_install_ui(label, self._post_install_current_step, total, detail=text)
def _strip_timestamp_prefix(self, text: str) -> str:
@@ -3020,11 +3049,14 @@ class InstallModlistScreen(QWidget):
def _update_post_install_ui(self, label: str, step: int, total: int, detail: Optional[str] = None):
"""Update progress indicator + activity summary for post-install steps."""
# Use the label as the primary display, but include step info in Activity window
display_label = label
if detail:
# Remove timestamp prefix from detail messages
clean_detail = self._strip_timestamp_prefix(detail.strip())
if clean_detail:
# For Activity window, show the detail with step counter
# But keep label simple for progress banner
if clean_detail.lower().startswith(label.lower()):
display_label = clean_detail
else:
@@ -3032,18 +3064,24 @@ class InstallModlistScreen(QWidget):
total = max(1, total)
step_clamped = max(0, min(step, total))
overall_percent = (step_clamped / total) * 100.0
# CRITICAL: Ensure both displays use the SAME step counter
# Progress banner uses phase_step/phase_max_steps from progress_state
progress_state = InstallationProgress(
phase=InstallationPhase.FINALIZE,
phase_name=display_label,
phase_step=step_clamped,
phase_name=display_label, # This will show in progress banner
phase_step=step_clamped, # This creates [step/total] in display_text
phase_max_steps=total,
overall_percent=overall_percent
)
self.progress_indicator.update_progress(progress_state)
# Activity window uses summary_info with the SAME step counter
summary_info = {
'current_step': step_clamped,
'max_steps': total,
'current_step': step_clamped, # Must match phase_step above
'max_steps': total, # Must match phase_max_steps above
}
# Use the same label for consistency
self.file_progress_list.update_files([], current_phase=display_label, summary_info=summary_info)
def _end_post_install_feedback(self, success: bool):
@@ -3263,6 +3301,52 @@ class InstallModlistScreen(QWidget):
debug_print(f"DEBUG: steam -foreground failed: {e2}")
except Exception as e:
debug_print(f"DEBUG: Error ensuring Steam GUI visibility: {e}")
# CRITICAL: Bring Jackify window back to focus after Steam restart
# This ensures the user can continue with the installation workflow
debug_print("DEBUG: Bringing Jackify window back to focus")
try:
# Get the main window - use window() to get top-level widget, then find QMainWindow
top_level = self.window()
main_window = None
# Try to find QMainWindow in the widget hierarchy
if isinstance(top_level, QMainWindow):
main_window = top_level
else:
# Walk up the parent chain
current = self
while current:
if isinstance(current, QMainWindow):
main_window = current
break
current = current.parent()
# Last resort: use top-level widget
if not main_window and top_level:
main_window = top_level
if main_window:
# Restore window if minimized
if hasattr(main_window, 'isMinimized') and main_window.isMinimized():
main_window.showNormal()
# Bring to front and activate - use multiple methods for reliability
main_window.raise_()
main_window.activateWindow()
main_window.show()
# Force focus with multiple attempts (some window managers need this)
from PySide6.QtCore import QTimer
QTimer.singleShot(50, lambda: main_window.activateWindow() if main_window else None)
QTimer.singleShot(200, lambda: (main_window.raise_(), main_window.activateWindow()) if main_window else None)
QTimer.singleShot(500, lambda: main_window.activateWindow() if main_window else None)
debug_print(f"DEBUG: Jackify window brought back to focus (type: {type(main_window).__name__})")
else:
debug_print("DEBUG: Could not find main window to bring to focus")
except Exception as e:
debug_print(f"DEBUG: Error bringing Jackify to focus: {e}")
# Save context for later use in configuration
self._manual_steps_retry_count = 0

View File

@@ -472,13 +472,15 @@ class InstallTTWScreen(QWidget):
# Check version against latest
update_available, installed_v, latest_v = ttw_installer_handler.is_ttw_installer_update_available()
if update_available:
self.ttw_installer_status.setText("Out of date")
version_text = f"Out of date (v{installed_v} → v{latest_v})" if installed_v and latest_v else "Out of date"
self.ttw_installer_status.setText(version_text)
self.ttw_installer_status.setStyleSheet("color: #f44336;")
self.ttw_installer_btn.setText("Update now")
self.ttw_installer_btn.setEnabled(True)
self.ttw_installer_btn.setVisible(True)
else:
self.ttw_installer_status.setText("Ready")
version_text = f"Ready (v{installed_v})" if installed_v else "Ready"
self.ttw_installer_status.setText(version_text)
self.ttw_installer_status.setStyleSheet("color: #3fd0ea;")
self.ttw_installer_btn.setText("Update now")
self.ttw_installer_btn.setEnabled(False) # Greyed out when ready
@@ -1418,8 +1420,11 @@ class InstallTTWScreen(QWidget):
is_warning = 'warning:' in lower_cleaned
is_milestone = any(kw in lower_cleaned for kw in ['===', 'complete', 'finished', 'validation', 'configuration valid'])
is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx'])
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op)
# Filter out meaningless standalone messages (just "OK", etc.)
is_noise = cleaned.strip().upper() in ['OK', 'OK.', 'OK!', 'DONE', 'DONE.', 'SUCCESS', 'SUCCESS.']
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op and not is_noise)
if should_show:
if is_error or is_warning:
@@ -1550,7 +1555,10 @@ class InstallTTWScreen(QWidget):
is_milestone = any(kw in lower_cleaned for kw in ['===', 'complete', 'finished', 'validation', 'configuration valid'])
is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx'])
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op)
# Filter out meaningless standalone messages (just "OK", etc.)
is_noise = cleaned.strip().upper() in ['OK', 'OK.', 'OK!', 'DONE', 'DONE.', 'SUCCESS', 'SUCCESS.']
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op and not is_noise)
if should_show:
# Direct console append - no recursion, no complex processing

View File

@@ -10,7 +10,7 @@ from PySide6.QtWidgets import (
QFrame, QSizePolicy, QDialog, QTextEdit, QTextBrowser, QMessageBox, QListWidget
)
from PySide6.QtCore import Qt, Signal, QSize, QThread, QUrl, QTimer, QObject
from PySide6.QtGui import QPixmap, QFont, QDesktopServices, QPainter, QColor, QTextOption, QPalette
from PySide6.QtGui import QPixmap, QFont, QPainter, QColor, QTextOption, QPalette
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from pathlib import Path
from typing import List, Optional, Dict
@@ -536,7 +536,7 @@ class ModlistDetailDialog(QDialog):
background: #3C45A5;
}
""")
discord_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(self.metadata.links.discordURL)))
discord_btn.clicked.connect(lambda: self._open_url(self.metadata.links.discordURL))
links_layout.addWidget(discord_btn)
if self.metadata.links.websiteURL:
@@ -558,7 +558,7 @@ class ModlistDetailDialog(QDialog):
background: #2a2a2a;
}
""")
website_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(self.metadata.links.websiteURL)))
website_btn.clicked.connect(lambda: self._open_url(self.metadata.links.websiteURL))
links_layout.addWidget(website_btn)
if self.metadata.links.readme:
@@ -581,7 +581,7 @@ class ModlistDetailDialog(QDialog):
}
""")
readme_url = self._convert_raw_github_url(self.metadata.links.readme)
readme_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(readme_url)))
readme_btn.clicked.connect(lambda: self._open_url(readme_url))
links_layout.addWidget(readme_btn)
bottom_bar.addLayout(links_layout)
@@ -732,6 +732,35 @@ class ModlistDetailDialog(QDialog):
self.install_requested.emit(self.metadata)
self.accept()
def _open_url(self, url: str):
"""Open URL with clean environment to avoid AppImage library conflicts."""
import subprocess
import os
env = os.environ.copy()
# Remove AppImage-specific environment variables
appimage_vars = [
'LD_LIBRARY_PATH',
'PYTHONPATH',
'PYTHONHOME',
'QT_PLUGIN_PATH',
'QML2_IMPORT_PATH',
]
if 'APPIMAGE' in env or 'APPDIR' in env:
for var in appimage_vars:
if var in env:
del env[var]
subprocess.Popen(
['xdg-open', url],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
class ModlistGalleryDialog(QDialog):
"""Enhanced modlist gallery dialog with visual browsing"""