mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Sync from development - prepare for v0.2.0.1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"""
|
||||
|
||||
Reference in New Issue
Block a user