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.1
This commit is contained in:
@@ -10,6 +10,7 @@ from .additional_tasks import AdditionalTasksScreen
|
||||
from .install_modlist import InstallModlistScreen
|
||||
from .configure_new_modlist import ConfigureNewModlistScreen
|
||||
from .configure_existing_modlist import ConfigureExistingModlistScreen
|
||||
from .wabbajack_installer import WabbajackInstallerScreen
|
||||
|
||||
__all__ = [
|
||||
'MainMenu',
|
||||
@@ -17,5 +18,6 @@ __all__ = [
|
||||
'AdditionalTasksScreen',
|
||||
'InstallModlistScreen',
|
||||
'ConfigureNewModlistScreen',
|
||||
'ConfigureExistingModlistScreen'
|
||||
'ConfigureExistingModlistScreen',
|
||||
'WabbajackInstallerScreen'
|
||||
]
|
||||
@@ -65,7 +65,7 @@ class AdditionalTasksScreen(QWidget):
|
||||
header_layout.addSpacing(10)
|
||||
|
||||
# Description area with fixed height
|
||||
desc = QLabel("TTW automation and additional tools.")
|
||||
desc = QLabel("TTW automation, Wabbajack installer, and additional tools.")
|
||||
desc.setWordWrap(True)
|
||||
desc.setStyleSheet("color: #ccc; font-size: 13px;")
|
||||
desc.setAlignment(Qt.AlignHCenter)
|
||||
@@ -89,10 +89,10 @@ class AdditionalTasksScreen(QWidget):
|
||||
|
||||
def _setup_menu_buttons(self, layout):
|
||||
"""Set up the menu buttons section"""
|
||||
# Menu options - ONLY TTW and placeholder
|
||||
# Menu options
|
||||
MENU_ITEMS = [
|
||||
("Install TTW", "ttw_install", "Install Tale of Two Wastelands using TTW_Linux_Installer"),
|
||||
("Coming Soon...", "coming_soon", "Additional tools will be added in future updates"),
|
||||
("Install Wabbajack", "wabbajack_install", "Install Wabbajack.exe via Proton (automated setup)"),
|
||||
("Return to Main Menu", "return_main_menu", "Go back to the main menu"),
|
||||
]
|
||||
|
||||
@@ -146,6 +146,8 @@ class AdditionalTasksScreen(QWidget):
|
||||
"""Handle button clicks"""
|
||||
if action_id == "ttw_install":
|
||||
self._show_ttw_info()
|
||||
elif action_id == "wabbajack_install":
|
||||
self._show_wabbajack_installer()
|
||||
elif action_id == "coming_soon":
|
||||
self._show_coming_soon_info()
|
||||
elif action_id == "return_main_menu":
|
||||
@@ -157,6 +159,12 @@ class AdditionalTasksScreen(QWidget):
|
||||
# Navigate to TTW installation screen (index 5)
|
||||
self.stacked_widget.setCurrentIndex(5)
|
||||
|
||||
def _show_wabbajack_installer(self):
|
||||
"""Navigate to Wabbajack installer screen"""
|
||||
if self.stacked_widget:
|
||||
# Navigate to Wabbajack installer screen (index 7)
|
||||
self.stacked_widget.setCurrentIndex(7)
|
||||
|
||||
def _show_coming_soon_info(self):
|
||||
"""Show coming soon info"""
|
||||
from ..services.message_service import MessageService
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
ConfigureNewModlistScreen for Jackify GUI
|
||||
"""
|
||||
import logging
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QCheckBox, QMainWindow
|
||||
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QProcess, QMetaObject
|
||||
from PySide6.QtGui import QPixmap, QTextCursor
|
||||
@@ -28,6 +29,8 @@ from PySide6.QtWidgets import QApplication
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
from jackify.shared.resolution_utils import get_resolution_fallback
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def debug_print(message):
|
||||
"""Print debug message only if debug mode is enabled"""
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
@@ -97,7 +100,6 @@ class SelectionDialog(QDialog):
|
||||
self.accept()
|
||||
|
||||
class ConfigureNewModlistScreen(QWidget):
|
||||
steam_restart_finished = Signal(bool, str)
|
||||
resize_request = Signal(str)
|
||||
def __init__(self, stacked_widget=None, main_menu_index=0):
|
||||
super().__init__()
|
||||
@@ -426,8 +428,6 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
self.top_timer.start(2000)
|
||||
# --- Start Configuration button ---
|
||||
self.start_btn.clicked.connect(self.validate_and_start_configure)
|
||||
# --- Connect steam_restart_finished signal ---
|
||||
self.steam_restart_finished.connect(self._on_steam_restart_finished)
|
||||
|
||||
# Initialize empty controls list - will be populated after UI is built
|
||||
self._actionable_controls = []
|
||||
@@ -852,34 +852,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
MessageService.warning(self, "Missing Name", "Please specify a name for your modlist", safety_level="low")
|
||||
self._enable_controls_after_operation()
|
||||
return
|
||||
# --- Shortcut creation will be handled by automated workflow ---
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
from jackify.backend.services.platform_detection_service import PlatformDetectionService
|
||||
platform_service = PlatformDetectionService.get_instance()
|
||||
steamdeck = platform_service.is_steamdeck
|
||||
shortcut_handler = ShortcutHandler(steamdeck=steamdeck) # Still needed for Steam restart
|
||||
|
||||
# Check if auto-restart is enabled
|
||||
auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked()
|
||||
|
||||
if auto_restart_enabled:
|
||||
# Auto-accept Steam restart - proceed without dialog
|
||||
self._safe_append_text("Auto-accepting Steam restart (unattended mode enabled)")
|
||||
reply = QMessageBox.Yes # Simulate user clicking Yes
|
||||
else:
|
||||
# --- User confirmation before restarting Steam ---
|
||||
reply = MessageService.question(
|
||||
self, "Ready to Configure Modlist",
|
||||
"Would you like to restart Steam and begin post-install configuration now? Restarting Steam could close any games you have open!",
|
||||
safety_level="medium"
|
||||
)
|
||||
|
||||
debug_print(f"DEBUG: Steam restart dialog returned: {reply!r}")
|
||||
if reply not in (QMessageBox.Yes, QMessageBox.Ok, QMessageBox.AcceptRole):
|
||||
self._enable_controls_after_operation()
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
return
|
||||
# Handle resolution saving
|
||||
resolution = self.resolution_combo.currentText()
|
||||
if resolution and resolution != "Leave unchanged":
|
||||
@@ -893,41 +866,9 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
if self.resolution_service.has_saved_resolution():
|
||||
self.resolution_service.clear_saved_resolution()
|
||||
debug_print("DEBUG: Saved resolution cleared")
|
||||
# --- Steam Configuration (progress dialog, thread, and signal) ---
|
||||
progress = QProgressDialog("Steam Configuration...", None, 0, 0, self)
|
||||
progress.setWindowTitle("Steam Configuration")
|
||||
progress.setWindowModality(Qt.WindowModal)
|
||||
progress.setMinimumDuration(0)
|
||||
progress.setValue(0)
|
||||
progress.show()
|
||||
def do_restart():
|
||||
try:
|
||||
ok = shortcut_handler.secure_steam_restart()
|
||||
out = ''
|
||||
except Exception as e:
|
||||
ok = False
|
||||
out = str(e)
|
||||
self._safe_append_text(f"[ERROR] Exception during Steam restart: {e}")
|
||||
self.steam_restart_finished.emit(ok, out)
|
||||
threading.Thread(target=do_restart, daemon=True).start()
|
||||
self._steam_restart_progress = progress
|
||||
|
||||
def _on_steam_restart_finished(self, success, out):
|
||||
if hasattr(self, '_steam_restart_progress'):
|
||||
self._steam_restart_progress.close()
|
||||
del self._steam_restart_progress
|
||||
self._enable_controls_after_operation()
|
||||
if success:
|
||||
self._safe_append_text("Steam restarted successfully.")
|
||||
|
||||
# Start configuration immediately - the CLI will handle any manual steps
|
||||
from jackify import __version__ as jackify_version
|
||||
self._safe_append_text(f"Jackify v{jackify_version}")
|
||||
self._safe_append_text("Starting modlist configuration...")
|
||||
self.configure_modlist()
|
||||
else:
|
||||
self._safe_append_text("Failed to restart Steam.\n" + str(out))
|
||||
MessageService.critical(self, "Steam Restart Failed", "Failed to restart Steam automatically. Please restart Steam manually, then try again.", safety_level="medium")
|
||||
|
||||
# Start configuration - automated workflow handles Steam restart internally
|
||||
self.configure_modlist()
|
||||
|
||||
def configure_modlist(self):
|
||||
# CRITICAL: Reload config from disk to pick up any settings changes from Settings dialog
|
||||
@@ -1061,6 +1002,16 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
"""Handle error from the automated prefix workflow"""
|
||||
self._safe_append_text(f"Error during automated Steam setup: {error_message}")
|
||||
self._safe_append_text("Please check the logs for details.")
|
||||
|
||||
# Show critical error dialog to user (don't silently fail)
|
||||
from jackify.backend.services.message_service import MessageService
|
||||
MessageService.critical(
|
||||
self,
|
||||
"Steam Setup Error",
|
||||
f"Error during automated Steam setup:\n\n{error_message}\n\nPlease check the console output for details.",
|
||||
safety_level="medium"
|
||||
)
|
||||
|
||||
self._enable_controls_after_operation()
|
||||
|
||||
def show_shortcut_conflict_dialog(self, conflicts):
|
||||
@@ -1331,7 +1282,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
# Create new config thread with updated context
|
||||
class ConfigThread(QThread):
|
||||
progress_update = Signal(str)
|
||||
configuration_complete = Signal(bool, str, str)
|
||||
configuration_complete = Signal(bool, str, str, bool)
|
||||
error_occurred = Signal(str)
|
||||
|
||||
def __init__(self, context):
|
||||
@@ -1369,8 +1320,8 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
def progress_callback(message):
|
||||
self.progress_update.emit(message)
|
||||
|
||||
def completion_callback(success, message, modlist_name):
|
||||
self.configuration_complete.emit(success, message, modlist_name)
|
||||
def completion_callback(success, message, modlist_name, enb_detected=False):
|
||||
self.configuration_complete.emit(success, message, modlist_name, enb_detected)
|
||||
|
||||
def manual_steps_callback(modlist_name, retry_count):
|
||||
# This shouldn't happen since automated prefix creation is complete
|
||||
@@ -1432,7 +1383,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
|
||||
class ConfigThread(QThread):
|
||||
progress_update = Signal(str)
|
||||
configuration_complete = Signal(bool, str, str)
|
||||
configuration_complete = Signal(bool, str, str, bool)
|
||||
error_occurred = Signal(str)
|
||||
|
||||
def __init__(self, context):
|
||||
@@ -1471,8 +1422,8 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
def progress_callback(message):
|
||||
self.progress_update.emit(message)
|
||||
|
||||
def completion_callback(success, message, modlist_name):
|
||||
self.configuration_complete.emit(success, message, modlist_name)
|
||||
def completion_callback(success, message, modlist_name, enb_detected=False):
|
||||
self.configuration_complete.emit(success, message, modlist_name, enb_detected)
|
||||
|
||||
def manual_steps_callback(modlist_name, retry_count):
|
||||
# This shouldn't happen since manual steps should be done
|
||||
@@ -1507,7 +1458,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
self._safe_append_text(f"Error continuing configuration: {e}")
|
||||
MessageService.critical(self, "Configuration Error", f"Failed to continue configuration: {e}", safety_level="medium")
|
||||
|
||||
def on_configuration_complete(self, success, message, modlist_name):
|
||||
def on_configuration_complete(self, success, message, modlist_name, enb_detected=False):
|
||||
"""Handle configuration completion (same as Tuxborn)"""
|
||||
# Re-enable all controls when workflow completes
|
||||
self._enable_controls_after_operation()
|
||||
@@ -1528,6 +1479,16 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
parent=self
|
||||
)
|
||||
success_dialog.show()
|
||||
|
||||
# Show ENB Proton dialog if ENB was detected (use stored detection result, no re-detection)
|
||||
if enb_detected:
|
||||
try:
|
||||
from ..dialogs.enb_proton_dialog import ENBProtonDialog
|
||||
enb_dialog = ENBProtonDialog(modlist_name=modlist_name, parent=self)
|
||||
enb_dialog.exec() # Modal dialog - blocks until user clicks OK
|
||||
except Exception as e:
|
||||
# Non-blocking: if dialog fails, just log and continue
|
||||
logger.warning(f"Failed to show ENB dialog: {e}")
|
||||
else:
|
||||
self._safe_append_text(f"Configuration failed: {message}")
|
||||
MessageService.critical(self, "Configuration Failed",
|
||||
|
||||
@@ -30,7 +30,7 @@ from jackify.backend.utils.nexus_premium_detector import is_non_premium_indicato
|
||||
from jackify.backend.handlers.progress_parser import ProgressStateManager
|
||||
from jackify.frontends.gui.widgets.progress_indicator import OverallProgressIndicator
|
||||
from jackify.frontends.gui.widgets.file_progress_list import FileProgressList
|
||||
from jackify.shared.progress_models import InstallationPhase, InstallationProgress
|
||||
from jackify.shared.progress_models import InstallationPhase, InstallationProgress, OperationType
|
||||
# Modlist gallery (imported at module level to avoid import delay when opening dialog)
|
||||
from jackify.frontends.gui.screens.modlist_gallery import ModlistGalleryDialog
|
||||
|
||||
@@ -409,6 +409,8 @@ class InstallModlistScreen(QWidget):
|
||||
self.file_progress_list = FileProgressList() # Shows all active files (scrolls if needed)
|
||||
self._premium_notice_shown = False
|
||||
self._premium_failure_active = False
|
||||
self._stalled_download_start_time = None # Track when downloads stall
|
||||
self._stalled_download_notified = False
|
||||
self._post_install_sequence = self._build_post_install_sequence()
|
||||
self._post_install_total_steps = len(self._post_install_sequence)
|
||||
self._post_install_current_step = 0
|
||||
@@ -2065,6 +2067,9 @@ class InstallModlistScreen(QWidget):
|
||||
self.file_progress_list.clear()
|
||||
self.file_progress_list.start_cpu_tracking() # Start tracking CPU during installation
|
||||
self._premium_notice_shown = False
|
||||
self._stalled_download_start_time = None # Reset stall detection
|
||||
self._stalled_download_notified = False
|
||||
self._token_error_notified = False # Reset token error notification
|
||||
self._premium_failure_active = False
|
||||
self._post_install_active = False
|
||||
self._post_install_current_step = 0
|
||||
@@ -2203,6 +2208,10 @@ class InstallModlistScreen(QWidget):
|
||||
env_vars = {'NEXUS_API_KEY': self.api_key}
|
||||
if self.oauth_info:
|
||||
env_vars['NEXUS_OAUTH_INFO'] = self.oauth_info
|
||||
# CRITICAL: Set client_id so engine can refresh tokens with correct client_id
|
||||
# Engine's RefreshToken method reads this to use our "jackify" client_id instead of hardcoded "wabbajack"
|
||||
from jackify.backend.services.nexus_oauth_service import NexusOAuthService
|
||||
env_vars['NEXUS_OAUTH_CLIENT_ID'] = NexusOAuthService.CLIENT_ID
|
||||
env = get_clean_subprocess_env(env_vars)
|
||||
self.process_manager = ProcessManager(cmd, env=env, text=False)
|
||||
ansi_escape = re.compile(rb'\x1b\[[0-9;?]*[ -/]*[@-~]')
|
||||
@@ -2479,8 +2488,54 @@ class InstallModlistScreen(QWidget):
|
||||
self._write_to_log_file(message)
|
||||
return
|
||||
|
||||
# Detect known engine bugs and provide helpful guidance
|
||||
# CRITICAL: Detect token/auth errors and ALWAYS show them (even when not in debug mode)
|
||||
msg_lower = message.lower()
|
||||
token_error_keywords = [
|
||||
'token has expired',
|
||||
'token expired',
|
||||
'oauth token',
|
||||
'authentication failed',
|
||||
'unauthorized',
|
||||
'401',
|
||||
'403',
|
||||
'refresh token',
|
||||
'authorization failed',
|
||||
'nexus.*premium.*required',
|
||||
'premium.*required',
|
||||
]
|
||||
|
||||
is_token_error = any(keyword in msg_lower for keyword in token_error_keywords)
|
||||
if is_token_error:
|
||||
# CRITICAL ERROR - always show, even if console is hidden
|
||||
if not hasattr(self, '_token_error_notified'):
|
||||
self._token_error_notified = True
|
||||
# Show error dialog immediately
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
MessageService.error(
|
||||
self,
|
||||
"Authentication Error",
|
||||
(
|
||||
"Nexus Mods authentication has failed. This may be due to:\n\n"
|
||||
"• OAuth token expired and refresh failed\n"
|
||||
"• Nexus Premium required for this modlist\n"
|
||||
"• Network connectivity issues\n\n"
|
||||
"Please check the console output (Show Details) for more information.\n"
|
||||
"You may need to re-authorize in Settings."
|
||||
),
|
||||
safety_level="high"
|
||||
)
|
||||
# Also show in console
|
||||
guidance = (
|
||||
"\n[Jackify] ⚠️ CRITICAL: Authentication/Token Error Detected!\n"
|
||||
"[Jackify] This may cause downloads to stop. Check the error message above.\n"
|
||||
"[Jackify] If OAuth token expired, go to Settings and re-authorize.\n"
|
||||
)
|
||||
self._safe_append_text(guidance)
|
||||
# Force console to be visible so user can see the error
|
||||
if not self.show_details_checkbox.isChecked():
|
||||
self.show_details_checkbox.setChecked(True)
|
||||
|
||||
# Detect known engine bugs and provide helpful guidance
|
||||
if 'destination array was not long enough' in msg_lower or \
|
||||
('argumentexception' in msg_lower and 'downloadmachineurl' in msg_lower):
|
||||
# This is a known bug in jackify-engine 0.4.0 during .wabbajack download
|
||||
@@ -2544,6 +2599,62 @@ class InstallModlistScreen(QWidget):
|
||||
bsa_percent = (progress_state.bsa_building_current / progress_state.bsa_building_total) * 100.0
|
||||
progress_state.overall_percent = min(99.0, bsa_percent) # Cap at 99% until fully complete
|
||||
|
||||
# CRITICAL: Detect stalled downloads (0.0MB/s for extended period)
|
||||
# This catches cases where token refresh fails silently or network issues occur
|
||||
# IMPORTANT: Only check during DOWNLOAD phase, not during VALIDATE phase
|
||||
# Validation checks existing files and shows 0.0MB/s, which is expected behavior
|
||||
import time
|
||||
if progress_state.phase == InstallationPhase.DOWNLOAD:
|
||||
speed_display = progress_state.get_overall_speed_display()
|
||||
# Check if speed is 0 or very low (< 0.1MB/s) for more than 2 minutes
|
||||
# Only trigger if we're actually in download phase (not validation)
|
||||
is_stalled = not speed_display or speed_display == "0.0B/s" or \
|
||||
(speed_display and any(x in speed_display.lower() for x in ['0.0mb/s', '0.0kb/s', '0b/s']))
|
||||
|
||||
# Additional check: Only consider it stalled if we have active download files
|
||||
# If no files are being downloaded, it might just be between downloads
|
||||
has_active_downloads = any(
|
||||
f.operation == OperationType.DOWNLOAD and not f.is_complete
|
||||
for f in progress_state.active_files
|
||||
)
|
||||
|
||||
if is_stalled and has_active_downloads:
|
||||
if self._stalled_download_start_time is None:
|
||||
self._stalled_download_start_time = time.time()
|
||||
else:
|
||||
stalled_duration = time.time() - self._stalled_download_start_time
|
||||
# Warn after 2 minutes of stalled downloads
|
||||
if stalled_duration > 120 and not self._stalled_download_notified:
|
||||
self._stalled_download_notified = True
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
MessageService.warning(
|
||||
self,
|
||||
"Download Stalled",
|
||||
(
|
||||
"Downloads have been stalled (0.0MB/s) for over 2 minutes.\n\n"
|
||||
"Possible causes:\n"
|
||||
"• OAuth token expired and refresh failed\n"
|
||||
"• Network connectivity issues\n"
|
||||
"• Nexus Mods server issues\n\n"
|
||||
"Please check the console output (Show Details) for error messages.\n"
|
||||
"If authentication failed, you may need to re-authorize in Settings."
|
||||
),
|
||||
safety_level="low"
|
||||
)
|
||||
# Force console to be visible
|
||||
if not self.show_details_checkbox.isChecked():
|
||||
self.show_details_checkbox.setChecked(True)
|
||||
# Add warning to console
|
||||
self._safe_append_text(
|
||||
"\n[Jackify] ⚠️ WARNING: Downloads have stalled (0.0MB/s for 2+ minutes)\n"
|
||||
"[Jackify] This may indicate an authentication or network issue.\n"
|
||||
"[Jackify] Check the console above for error messages.\n"
|
||||
)
|
||||
else:
|
||||
# Downloads are active - reset stall timer
|
||||
self._stalled_download_start_time = None
|
||||
self._stalled_download_notified = False
|
||||
|
||||
# Update progress indicator widget
|
||||
self.progress_indicator.update_progress(progress_state)
|
||||
|
||||
@@ -3748,7 +3859,7 @@ class InstallModlistScreen(QWidget):
|
||||
self.steam_restart_progress = None
|
||||
# Controls are managed by the proper control management system
|
||||
|
||||
def on_configuration_complete(self, success, message, modlist_name):
|
||||
def on_configuration_complete(self, success, message, modlist_name, enb_detected=False):
|
||||
"""Handle configuration completion on main thread"""
|
||||
try:
|
||||
# Stop CPU tracking now that everything is complete
|
||||
@@ -3819,6 +3930,16 @@ class InstallModlistScreen(QWidget):
|
||||
parent=self
|
||||
)
|
||||
success_dialog.show()
|
||||
|
||||
# Show ENB Proton dialog if ENB was detected (use stored detection result, no re-detection)
|
||||
if enb_detected:
|
||||
try:
|
||||
from ..dialogs.enb_proton_dialog import ENBProtonDialog
|
||||
enb_dialog = ENBProtonDialog(modlist_name=modlist_name, parent=self)
|
||||
enb_dialog.exec() # Modal dialog - blocks until user clicks OK
|
||||
except Exception as e:
|
||||
# Non-blocking: if dialog fails, just log and continue
|
||||
logger.warning(f"Failed to show ENB dialog: {e}")
|
||||
elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3:
|
||||
# Max retries reached - show failure message
|
||||
MessageService.critical(self, "Manual Steps Failed",
|
||||
@@ -4176,7 +4297,7 @@ class InstallModlistScreen(QWidget):
|
||||
# Create new config thread with updated context
|
||||
class ConfigThread(QThread):
|
||||
progress_update = Signal(str)
|
||||
configuration_complete = Signal(bool, str, str)
|
||||
configuration_complete = Signal(bool, str, str, bool)
|
||||
error_occurred = Signal(str)
|
||||
|
||||
def __init__(self, context, is_steamdeck):
|
||||
@@ -4216,8 +4337,8 @@ class InstallModlistScreen(QWidget):
|
||||
def progress_callback(message):
|
||||
self.progress_update.emit(message)
|
||||
|
||||
def completion_callback(success, message, modlist_name):
|
||||
self.configuration_complete.emit(success, message, modlist_name)
|
||||
def completion_callback(success, message, modlist_name, enb_detected=False):
|
||||
self.configuration_complete.emit(success, message, modlist_name, enb_detected)
|
||||
|
||||
def manual_steps_callback(modlist_name, retry_count):
|
||||
# This shouldn't happen since automated prefix creation is complete
|
||||
|
||||
@@ -2502,7 +2502,7 @@ class InstallTTWScreen(QWidget):
|
||||
self.steam_restart_progress = None
|
||||
# Controls are managed by the proper control management system
|
||||
|
||||
def on_configuration_complete(self, success, message, modlist_name):
|
||||
def on_configuration_complete(self, success, message, modlist_name, enb_detected=False):
|
||||
"""Handle configuration completion on main thread"""
|
||||
try:
|
||||
# Re-enable controls now that installation/configuration is complete
|
||||
@@ -2539,6 +2539,8 @@ class InstallTTWScreen(QWidget):
|
||||
parent=self
|
||||
)
|
||||
success_dialog.show()
|
||||
|
||||
# Note: TTW workflow does NOT need ENB detection/dialog
|
||||
elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3:
|
||||
# Max retries reached - show failure message
|
||||
MessageService.critical(self, "Manual Steps Failed",
|
||||
@@ -2935,8 +2937,8 @@ class InstallTTWScreen(QWidget):
|
||||
def progress_callback(message):
|
||||
self.progress_update.emit(message)
|
||||
|
||||
def completion_callback(success, message, modlist_name):
|
||||
self.configuration_complete.emit(success, message, modlist_name)
|
||||
def completion_callback(success, message, modlist_name, enb_detected=False):
|
||||
self.configuration_complete.emit(success, message, modlist_name, enb_detected)
|
||||
|
||||
def manual_steps_callback(modlist_name, retry_count):
|
||||
# This shouldn't happen since automated prefix creation is complete
|
||||
|
||||
714
jackify/frontends/gui/screens/wabbajack_installer.py
Normal file
714
jackify/frontends/gui/screens/wabbajack_installer.py
Normal file
@@ -0,0 +1,714 @@
|
||||
"""
|
||||
Wabbajack Installer Screen
|
||||
|
||||
Automated Wabbajack.exe installation via Proton with progress tracking.
|
||||
Follows standard Jackify screen layout.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QFileDialog, QLineEdit, QGridLayout, QTextEdit, QTabWidget, QSizePolicy, QCheckBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal, QSize
|
||||
from PySide6.QtGui import QTextCursor
|
||||
|
||||
from jackify.backend.models.configuration import SystemInfo
|
||||
from jackify.backend.handlers.wabbajack_installer_handler import WabbajackInstallerHandler
|
||||
from ..services.message_service import MessageService
|
||||
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
|
||||
from ..utils import set_responsive_minimum
|
||||
from ..widgets.file_progress_list import FileProgressList
|
||||
from ..widgets.progress_indicator import OverallProgressIndicator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WabbajackInstallerWorker(QThread):
|
||||
"""Background worker for Wabbajack installation"""
|
||||
|
||||
progress_update = Signal(str, int) # Status message, percentage
|
||||
activity_update = Signal(str, int, int) # Activity label, current, total
|
||||
log_output = Signal(str) # Console log output
|
||||
installation_complete = Signal(bool, str, str, str, str) # Success, message, launch_options, app_id, time_taken
|
||||
|
||||
def __init__(self, install_folder: Path, shortcut_name: str = "Wabbajack", enable_gog: bool = True):
|
||||
super().__init__()
|
||||
self.install_folder = install_folder
|
||||
self.shortcut_name = shortcut_name
|
||||
self.enable_gog = enable_gog
|
||||
self.handler = WabbajackInstallerHandler()
|
||||
self.launch_options = "" # Store launch options for success message
|
||||
self.start_time = None # Track installation start time
|
||||
|
||||
def _log(self, message: str):
|
||||
"""Emit log message"""
|
||||
self.log_output.emit(message)
|
||||
logger.info(message)
|
||||
|
||||
def run(self):
|
||||
"""Run the installation workflow"""
|
||||
import time
|
||||
self.start_time = time.time()
|
||||
try:
|
||||
total_steps = 12
|
||||
|
||||
# Step 1: Check requirements
|
||||
self.progress_update.emit("Checking requirements...", 5)
|
||||
self.activity_update.emit("Checking requirements", 1, total_steps)
|
||||
self._log("Checking system requirements...")
|
||||
|
||||
proton_path = self.handler.find_proton_experimental()
|
||||
if not proton_path:
|
||||
self.installation_complete.emit(
|
||||
False,
|
||||
"Proton Experimental not found.\nPlease install it from Steam."
|
||||
)
|
||||
return
|
||||
self._log(f"Found Proton Experimental: {proton_path}")
|
||||
|
||||
userdata = self.handler.find_steam_userdata_path()
|
||||
if not userdata:
|
||||
self.installation_complete.emit(
|
||||
False,
|
||||
"Steam userdata not found.\nPlease ensure Steam is installed and you're logged in."
|
||||
)
|
||||
return
|
||||
self._log(f"Found Steam userdata: {userdata}")
|
||||
|
||||
# Step 2: Download Wabbajack
|
||||
self.progress_update.emit("Downloading Wabbajack.exe...", 15)
|
||||
self.activity_update.emit("Downloading Wabbajack.exe", 2, total_steps)
|
||||
self._log("Downloading Wabbajack.exe from GitHub...")
|
||||
wabbajack_exe = self.handler.download_wabbajack(self.install_folder)
|
||||
self._log(f"Downloaded to: {wabbajack_exe}")
|
||||
|
||||
# Step 3: Create dotnet cache
|
||||
self.progress_update.emit("Creating .NET cache directory...", 20)
|
||||
self.activity_update.emit("Creating .NET cache", 3, total_steps)
|
||||
self._log("Creating .NET bundle extract cache...")
|
||||
self.handler.create_dotnet_cache(self.install_folder)
|
||||
|
||||
# Step 4: Stop Steam before modifying shortcuts.vdf
|
||||
self.progress_update.emit("Stopping Steam...", 25)
|
||||
self.activity_update.emit("Stopping Steam", 4, total_steps)
|
||||
self._log("Stopping Steam (required to safely modify shortcuts.vdf)...")
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# Kill Steam using pkill (simple approach like AuCu)
|
||||
try:
|
||||
subprocess.run(['steam', '-shutdown'], timeout=5, capture_output=True)
|
||||
time.sleep(2)
|
||||
subprocess.run(['pkill', '-9', 'steam'], timeout=5, capture_output=True)
|
||||
time.sleep(2)
|
||||
self._log("Steam stopped successfully")
|
||||
except Exception as e:
|
||||
self._log(f"Warning: Steam shutdown had issues: {e}. Proceeding anyway...")
|
||||
|
||||
# Step 5: Add to Steam shortcuts (NO Proton - like AuCu, but with STEAM_COMPAT_MOUNTS for libraries)
|
||||
self.progress_update.emit("Adding to Steam shortcuts...", 30)
|
||||
self.activity_update.emit("Adding to Steam", 5, total_steps)
|
||||
self._log("Adding Wabbajack to Steam shortcuts...")
|
||||
from jackify.backend.services.native_steam_service import NativeSteamService
|
||||
steam_service = NativeSteamService()
|
||||
|
||||
# Generate launch options with STEAM_COMPAT_MOUNTS for additional Steam libraries (like modlist installs)
|
||||
# Default to empty string (like AuCu) - only add options if we have additional libraries
|
||||
# Note: Users may need to manually add other paths (e.g., download directories on different drives) to launch options
|
||||
launch_options = ""
|
||||
try:
|
||||
from jackify.backend.handlers.path_handler import PathHandler
|
||||
path_handler = PathHandler()
|
||||
|
||||
all_libs = path_handler.get_all_steam_library_paths()
|
||||
main_steam_lib_path_obj = path_handler.find_steam_library()
|
||||
if main_steam_lib_path_obj and main_steam_lib_path_obj.name == "common":
|
||||
main_steam_lib_path = main_steam_lib_path_obj.parent.parent
|
||||
|
||||
filtered_libs = [lib for lib in all_libs if str(lib) != str(main_steam_lib_path)]
|
||||
if filtered_libs:
|
||||
mount_paths = ":".join(str(lib) for lib in filtered_libs)
|
||||
launch_options = f'STEAM_COMPAT_MOUNTS="{mount_paths}" %command%'
|
||||
self._log(f"Added STEAM_COMPAT_MOUNTS for additional Steam libraries: {mount_paths}")
|
||||
else:
|
||||
self._log("No additional Steam libraries found - using empty launch options (like AuCu)")
|
||||
except Exception as e:
|
||||
self._log(f"Could not generate STEAM_COMPAT_MOUNTS (non-critical): {e}")
|
||||
# Keep empty string like AuCu
|
||||
|
||||
# Store launch options for success message
|
||||
self.launch_options = launch_options
|
||||
|
||||
# Create shortcut WITHOUT Proton (AuCu does this separately later)
|
||||
success, app_id = steam_service.create_shortcut(
|
||||
app_name=self.shortcut_name,
|
||||
exe_path=str(wabbajack_exe),
|
||||
start_dir=str(wabbajack_exe.parent),
|
||||
launch_options=launch_options, # Empty or with STEAM_COMPAT_MOUNTS
|
||||
tags=["Jackify"]
|
||||
)
|
||||
if not success or app_id is None:
|
||||
raise RuntimeError("Failed to create Steam shortcut")
|
||||
self._log(f"Created Steam shortcut with AppID: {app_id}")
|
||||
|
||||
# Step 6: Initialize Wine prefix
|
||||
self.progress_update.emit("Initializing Wine prefix...", 45)
|
||||
self.activity_update.emit("Initializing Wine prefix", 6, total_steps)
|
||||
self._log("Initializing Wine prefix with Proton...")
|
||||
prefix_path = self.handler.init_wine_prefix(app_id)
|
||||
self._log(f"Wine prefix created: {prefix_path}")
|
||||
|
||||
# Step 7: Install WebView2
|
||||
self.progress_update.emit("Installing WebView2 runtime...", 60)
|
||||
self.activity_update.emit("Installing WebView2", 7, total_steps)
|
||||
self._log("Downloading and installing WebView2...")
|
||||
try:
|
||||
self.handler.install_webview2(app_id, self.install_folder)
|
||||
self._log("WebView2 installed successfully")
|
||||
except Exception as e:
|
||||
self._log(f"WARNING: WebView2 installation may have failed: {e}")
|
||||
self._log("This may prevent Nexus login in Wabbajack. You can manually install WebView2 later.")
|
||||
# Continue installation - WebView2 is not critical for basic functionality
|
||||
|
||||
# Step 8: Apply Win7 registry
|
||||
self.progress_update.emit("Applying Windows 7 registry settings...", 75)
|
||||
self.activity_update.emit("Applying registry settings", 8, total_steps)
|
||||
self._log("Applying Windows 7 compatibility settings...")
|
||||
self.handler.apply_win7_registry(app_id)
|
||||
self._log("Registry settings applied")
|
||||
|
||||
# Step 9: GOG game detection (optional)
|
||||
gog_count = 0
|
||||
if self.enable_gog:
|
||||
self.progress_update.emit("Detecting GOG games from Heroic...", 80)
|
||||
self.activity_update.emit("Detecting GOG games", 9, total_steps)
|
||||
self._log("Searching for GOG games in Heroic...")
|
||||
try:
|
||||
gog_count = self.handler.inject_gog_registry(app_id)
|
||||
if gog_count > 0:
|
||||
self._log(f"Detected and injected {gog_count} GOG games")
|
||||
else:
|
||||
self._log("No GOG games found in Heroic")
|
||||
except Exception as e:
|
||||
self._log(f"GOG injection failed (non-critical): {e}")
|
||||
|
||||
# Step 10: Create Steam library symlinks
|
||||
self.progress_update.emit("Creating Steam library symlinks...", 85)
|
||||
self.activity_update.emit("Creating library symlinks", 10, total_steps)
|
||||
self._log("Creating Steam library symlinks for game detection...")
|
||||
steam_service.create_steam_library_symlinks(app_id)
|
||||
self._log("Steam library symlinks created")
|
||||
|
||||
# Step 11: Set Proton Experimental (separate step like AuCu)
|
||||
self.progress_update.emit("Setting Proton compatibility...", 90)
|
||||
self.activity_update.emit("Setting Proton compatibility", 11, total_steps)
|
||||
self._log("Setting Proton Experimental as compatibility tool...")
|
||||
try:
|
||||
steam_service.set_proton_version(app_id, "proton_experimental")
|
||||
self._log("Proton Experimental set successfully")
|
||||
except Exception as e:
|
||||
self._log(f"Warning: Failed to set Proton version (non-critical): {e}")
|
||||
self._log("You can set it manually in Steam: Properties → Compatibility → Proton Experimental")
|
||||
|
||||
# Step 12: Start Steam at the end
|
||||
self.progress_update.emit("Starting Steam...", 95)
|
||||
self.activity_update.emit("Starting Steam", 12, total_steps)
|
||||
self._log("Starting Steam...")
|
||||
from jackify.backend.services.steam_restart_service import start_steam
|
||||
start_steam()
|
||||
time.sleep(3) # Give Steam time to start
|
||||
self._log("Steam started successfully")
|
||||
|
||||
# Done!
|
||||
self.progress_update.emit("Installation complete!", 100)
|
||||
self.activity_update.emit("Installation complete", 12, total_steps)
|
||||
self._log("\n=== Installation Complete ===")
|
||||
self._log(f"Wabbajack installed to: {self.install_folder}")
|
||||
self._log(f"Steam AppID: {app_id}")
|
||||
if gog_count > 0:
|
||||
self._log(f"GOG games detected: {gog_count}")
|
||||
self._log("You can now launch Wabbajack from Steam")
|
||||
|
||||
# Calculate time taken
|
||||
import time
|
||||
time_taken = int(time.time() - self.start_time)
|
||||
mins, secs = divmod(time_taken, 60)
|
||||
time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds"
|
||||
|
||||
# Store data for success dialog (app_id as string to avoid overflow)
|
||||
self.installation_complete.emit(True, "", self.launch_options, str(app_id), time_str)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Installation failed: {str(e)}"
|
||||
self._log(f"\nERROR: {error_msg}")
|
||||
logger.error(f"Wabbajack installation failed: {e}", exc_info=True)
|
||||
self.installation_complete.emit(False, error_msg, "", "", "")
|
||||
|
||||
|
||||
class WabbajackInstallerScreen(QWidget):
|
||||
"""Wabbajack installer GUI screen following standard Jackify layout"""
|
||||
|
||||
resize_request = Signal(str)
|
||||
|
||||
def __init__(self, stacked_widget=None, additional_tasks_index=3, system_info: Optional[SystemInfo] = None):
|
||||
super().__init__()
|
||||
self.stacked_widget = stacked_widget
|
||||
self.additional_tasks_index = additional_tasks_index
|
||||
self.system_info = system_info or SystemInfo(is_steamdeck=False)
|
||||
self.debug = DEBUG_BORDERS
|
||||
|
||||
self.install_folder = None
|
||||
self.shortcut_name = "Wabbajack"
|
||||
self.worker = None
|
||||
|
||||
# Get config handler for default paths
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
self.config_handler = ConfigHandler()
|
||||
|
||||
# Scroll tracking for professional auto-scroll behavior
|
||||
self._user_manually_scrolled = False
|
||||
self._was_at_bottom = True
|
||||
|
||||
# Initialize progress reporting
|
||||
self.progress_indicator = OverallProgressIndicator(show_progress_bar=True)
|
||||
self.progress_indicator.set_status("Ready", 0)
|
||||
self.file_progress_list = FileProgressList()
|
||||
|
||||
self._setup_ui()
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up UI following standard Jackify pattern"""
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
|
||||
main_layout.setContentsMargins(50, 50, 50, 0)
|
||||
main_layout.setSpacing(12)
|
||||
if self.debug:
|
||||
self.setStyleSheet("border: 2px solid magenta;")
|
||||
|
||||
# Header
|
||||
self._setup_header(main_layout)
|
||||
|
||||
# Upper section: Form (left) + Activity/Process Monitor (right)
|
||||
self._setup_upper_section(main_layout)
|
||||
|
||||
# Status banner with "Show details" toggle
|
||||
self._setup_status_banner(main_layout)
|
||||
|
||||
# Console output (hidden by default)
|
||||
self._setup_console(main_layout)
|
||||
|
||||
# Buttons
|
||||
self._setup_buttons(main_layout)
|
||||
|
||||
def _setup_header(self, layout):
|
||||
"""Set up header section"""
|
||||
header_layout = QVBoxLayout()
|
||||
header_layout.setSpacing(1)
|
||||
|
||||
title = QLabel("<b>Install Wabbajack via Proton</b>")
|
||||
title.setStyleSheet(f"font-size: 20px; color: {JACKIFY_COLOR_BLUE}; margin: 0px; padding: 0px;")
|
||||
title.setAlignment(Qt.AlignHCenter)
|
||||
title.setMaximumHeight(30)
|
||||
header_layout.addWidget(title)
|
||||
|
||||
desc = QLabel(
|
||||
"Automated Wabbajack.exe Installation and configuration for running via Proton"
|
||||
)
|
||||
desc.setWordWrap(True)
|
||||
desc.setStyleSheet("color: #ccc; margin: 0px; padding: 0px; line-height: 1.2;")
|
||||
desc.setAlignment(Qt.AlignHCenter)
|
||||
desc.setMaximumHeight(40)
|
||||
header_layout.addWidget(desc)
|
||||
|
||||
header_widget = QWidget()
|
||||
header_widget.setLayout(header_layout)
|
||||
header_widget.setMaximumHeight(75)
|
||||
layout.addWidget(header_widget)
|
||||
|
||||
def _setup_upper_section(self, layout):
|
||||
"""Set up upper section: Form (left) + Activity/Process Monitor (right)"""
|
||||
upper_hbox = QHBoxLayout()
|
||||
upper_hbox.setContentsMargins(0, 0, 0, 0)
|
||||
upper_hbox.setSpacing(16)
|
||||
|
||||
# LEFT: Form and controls
|
||||
left_vbox = QVBoxLayout()
|
||||
left_vbox.setAlignment(Qt.AlignTop)
|
||||
|
||||
# [Options] header
|
||||
options_header = QLabel("<b>[Options]</b>")
|
||||
options_header.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px; font-weight: bold;")
|
||||
options_header.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
left_vbox.addWidget(options_header)
|
||||
|
||||
# Form grid
|
||||
form_grid = QGridLayout()
|
||||
form_grid.setHorizontalSpacing(12)
|
||||
form_grid.setVerticalSpacing(6)
|
||||
form_grid.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# Shortcut Name
|
||||
shortcut_name_label = QLabel("Shortcut Name:")
|
||||
self.shortcut_name_edit = QLineEdit("Wabbajack")
|
||||
self.shortcut_name_edit.setMaximumHeight(25)
|
||||
self.shortcut_name_edit.setToolTip("Name for the Steam shortcut (useful if installing multiple Wabbajack instances)")
|
||||
form_grid.addWidget(shortcut_name_label, 0, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
|
||||
form_grid.addWidget(self.shortcut_name_edit, 0, 1)
|
||||
|
||||
# Installation Directory
|
||||
install_dir_label = QLabel("Installation Directory:")
|
||||
# Set default to $Install_Base_Dir/Wabbajack with actual text (not placeholder)
|
||||
default_install_dir = Path(self.config_handler.get_modlist_install_base_dir()) / "Wabbajack"
|
||||
self.install_dir_edit = QLineEdit(str(default_install_dir))
|
||||
self.install_dir_edit.setMaximumHeight(25)
|
||||
|
||||
browse_btn = QPushButton("Browse")
|
||||
browse_btn.setFixedSize(80, 25)
|
||||
browse_btn.clicked.connect(self._browse_folder)
|
||||
|
||||
install_dir_hbox = QHBoxLayout()
|
||||
install_dir_hbox.addWidget(self.install_dir_edit)
|
||||
install_dir_hbox.addWidget(browse_btn)
|
||||
|
||||
form_grid.addWidget(install_dir_label, 1, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
|
||||
form_grid.addLayout(install_dir_hbox, 1, 1)
|
||||
|
||||
form_section_widget = QWidget()
|
||||
form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
form_section_widget.setLayout(form_grid)
|
||||
form_section_widget.setMinimumHeight(80)
|
||||
form_section_widget.setMaximumHeight(120)
|
||||
left_vbox.addWidget(form_section_widget)
|
||||
|
||||
# Info text
|
||||
info_label = QLabel(
|
||||
"Enter your preferred name for the Steam shortcut for Wabbajack, then select where Wabbajack should be installed.\n\n"
|
||||
"Jackify will then download Wabbajack.exe, add it as a new non-Steam game and configure the Proton prefix. "
|
||||
"The WebView2 installation and prefix configuration will then take place.\n\n"
|
||||
"While there is initial support for GOG versions, please note that it relies on the game being installed via Heroic Game Launcher. "
|
||||
"The modlist itself must also support the GOG version of the game."
|
||||
)
|
||||
info_label.setStyleSheet("color: #999; font-size: 11px;")
|
||||
info_label.setWordWrap(True)
|
||||
left_vbox.addWidget(info_label)
|
||||
|
||||
left_widget = QWidget()
|
||||
left_widget.setLayout(left_vbox)
|
||||
|
||||
# RIGHT: Activity/Process Monitor tabs
|
||||
# No Process Monitor tab - we're not tracking processes
|
||||
# Just show Activity directly
|
||||
|
||||
# Activity heading
|
||||
activity_heading = QLabel("<b>[Activity]</b>")
|
||||
activity_heading.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px;")
|
||||
|
||||
activity_vbox = QVBoxLayout()
|
||||
activity_vbox.setContentsMargins(0, 0, 0, 0)
|
||||
activity_vbox.setSpacing(2)
|
||||
activity_vbox.addWidget(activity_heading)
|
||||
activity_vbox.addWidget(self.file_progress_list)
|
||||
|
||||
activity_widget = QWidget()
|
||||
activity_widget.setLayout(activity_vbox)
|
||||
|
||||
upper_hbox.addWidget(left_widget, stretch=11)
|
||||
upper_hbox.addWidget(activity_widget, stretch=9)
|
||||
|
||||
upper_section_widget = QWidget()
|
||||
upper_section_widget.setLayout(upper_hbox)
|
||||
upper_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
upper_section_widget.setMaximumHeight(280)
|
||||
layout.addWidget(upper_section_widget)
|
||||
|
||||
def _setup_status_banner(self, layout):
|
||||
"""Set up status banner with Show details checkbox"""
|
||||
banner_row = QHBoxLayout()
|
||||
banner_row.setContentsMargins(0, 0, 0, 0)
|
||||
banner_row.setSpacing(8)
|
||||
banner_row.addWidget(self.progress_indicator, 1)
|
||||
banner_row.addStretch()
|
||||
|
||||
self.show_details_checkbox = QCheckBox("Show details")
|
||||
self.show_details_checkbox.setChecked(False)
|
||||
self.show_details_checkbox.setToolTip("Toggle detailed console output")
|
||||
self.show_details_checkbox.toggled.connect(self._on_show_details_toggled)
|
||||
banner_row.addWidget(self.show_details_checkbox)
|
||||
|
||||
banner_row_widget = QWidget()
|
||||
banner_row_widget.setLayout(banner_row)
|
||||
banner_row_widget.setMaximumHeight(45)
|
||||
banner_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
layout.addWidget(banner_row_widget)
|
||||
|
||||
def _setup_console(self, layout):
|
||||
"""Set up console output area (hidden by default)"""
|
||||
self.console = QTextEdit()
|
||||
self.console.setReadOnly(True)
|
||||
self.console.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
|
||||
self.console.setMinimumHeight(50)
|
||||
self.console.setMaximumHeight(1000)
|
||||
self.console.setFontFamily('monospace')
|
||||
self.console.setVisible(False)
|
||||
if self.debug:
|
||||
self.console.setStyleSheet("border: 2px solid yellow;")
|
||||
|
||||
# Set up scroll tracking for professional auto-scroll behavior
|
||||
self._setup_scroll_tracking()
|
||||
|
||||
layout.addWidget(self.console, stretch=1)
|
||||
|
||||
def _setup_scroll_tracking(self):
|
||||
"""Set up scroll tracking for professional auto-scroll behavior"""
|
||||
scrollbar = self.console.verticalScrollBar()
|
||||
scrollbar.sliderPressed.connect(self._on_scrollbar_pressed)
|
||||
scrollbar.sliderReleased.connect(self._on_scrollbar_released)
|
||||
scrollbar.valueChanged.connect(self._on_scrollbar_value_changed)
|
||||
|
||||
def _on_scrollbar_pressed(self):
|
||||
"""User started manually scrolling"""
|
||||
self._user_manually_scrolled = True
|
||||
|
||||
def _on_scrollbar_released(self):
|
||||
"""User finished manually scrolling"""
|
||||
self._user_manually_scrolled = False
|
||||
|
||||
def _on_scrollbar_value_changed(self):
|
||||
"""Track if user is at bottom of scroll area"""
|
||||
scrollbar = self.console.verticalScrollBar()
|
||||
self._was_at_bottom = scrollbar.value() >= scrollbar.maximum() - 1
|
||||
|
||||
def _setup_buttons(self, layout):
|
||||
"""Set up action buttons"""
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setAlignment(Qt.AlignHCenter)
|
||||
|
||||
self.start_btn = QPushButton("Start Installation")
|
||||
self.start_btn.setFixedHeight(35)
|
||||
# Enable by default since we have a default directory
|
||||
self.start_btn.setEnabled(True)
|
||||
self.start_btn.clicked.connect(self._start_installation)
|
||||
btn_row.addWidget(self.start_btn)
|
||||
|
||||
self.cancel_btn = QPushButton("Cancel")
|
||||
self.cancel_btn.setFixedHeight(35)
|
||||
self.cancel_btn.clicked.connect(self._go_back)
|
||||
btn_row.addWidget(self.cancel_btn)
|
||||
|
||||
btn_row_widget = QWidget()
|
||||
btn_row_widget.setLayout(btn_row)
|
||||
btn_row_widget.setMaximumHeight(50)
|
||||
layout.addWidget(btn_row_widget)
|
||||
|
||||
def _on_show_details_toggled(self, checked):
|
||||
"""Handle Show details checkbox toggle"""
|
||||
self.console.setVisible(checked)
|
||||
if checked:
|
||||
self.resize_request.emit("expand")
|
||||
else:
|
||||
self.resize_request.emit("compact")
|
||||
|
||||
def _browse_folder(self):
|
||||
"""Browse for installation folder"""
|
||||
folder = QFileDialog.getExistingDirectory(
|
||||
self,
|
||||
"Select Wabbajack Installation Folder",
|
||||
str(Path.home()),
|
||||
QFileDialog.ShowDirsOnly
|
||||
)
|
||||
|
||||
if folder:
|
||||
self.install_folder = Path(folder)
|
||||
self.install_dir_edit.setText(str(self.install_folder))
|
||||
self.start_btn.setEnabled(True)
|
||||
|
||||
# Update shortcut name from field
|
||||
self.shortcut_name = self.shortcut_name_edit.text().strip() or "Wabbajack"
|
||||
|
||||
def _start_installation(self):
|
||||
"""Start the installation process"""
|
||||
# Get install folder from text field (may be default or user-selected)
|
||||
install_dir_text = self.install_dir_edit.text().strip()
|
||||
if not install_dir_text:
|
||||
MessageService.warning(self, "No Folder Selected", "Please select an installation folder first.")
|
||||
return
|
||||
|
||||
self.install_folder = Path(install_dir_text)
|
||||
|
||||
# Get shortcut name
|
||||
self.shortcut_name = self.shortcut_name_edit.text().strip() or "Wabbajack"
|
||||
|
||||
# Confirm with user
|
||||
confirm = MessageService.question(
|
||||
self,
|
||||
"Confirm Installation",
|
||||
f"Install Wabbajack to:\n{self.install_folder}\n\n"
|
||||
"This will download Wabbajack, add to Steam, install WebView2,\n"
|
||||
"and configure the Wine prefix automatically.\n\n"
|
||||
"Steam will be restarted during installation.\n\n"
|
||||
"Continue?"
|
||||
)
|
||||
|
||||
if not confirm:
|
||||
return
|
||||
|
||||
# Clear displays
|
||||
self.console.clear()
|
||||
self.file_progress_list.clear()
|
||||
|
||||
# Update UI state
|
||||
self.start_btn.setEnabled(False)
|
||||
self.cancel_btn.setEnabled(False)
|
||||
self.progress_indicator.set_status("Starting installation...", 0)
|
||||
|
||||
# Start worker thread
|
||||
self.worker = WabbajackInstallerWorker(self.install_folder, shortcut_name=self.shortcut_name, enable_gog=True)
|
||||
self.worker.progress_update.connect(self._on_progress_update)
|
||||
self.worker.activity_update.connect(self._on_activity_update)
|
||||
self.worker.log_output.connect(self._on_log_output)
|
||||
self.worker.installation_complete.connect(self._on_installation_complete)
|
||||
self.worker.start()
|
||||
|
||||
def _on_progress_update(self, message: str, percentage: int):
|
||||
"""Handle progress updates"""
|
||||
self.progress_indicator.set_status(message, percentage)
|
||||
|
||||
def _on_activity_update(self, label: str, current: int, total: int):
|
||||
"""Handle activity tab updates"""
|
||||
self.file_progress_list.update_files(
|
||||
[],
|
||||
current_phase=label, # Use the actual step label (e.g., "Checking requirements", "Downloading Wabbajack.exe", etc.)
|
||||
summary_info={"current_step": current, "max_steps": total}
|
||||
)
|
||||
|
||||
def _on_log_output(self, message: str):
|
||||
"""Handle log output with professional auto-scroll"""
|
||||
scrollbar = self.console.verticalScrollBar()
|
||||
was_at_bottom = (scrollbar.value() >= scrollbar.maximum() - 1)
|
||||
|
||||
self.console.append(message)
|
||||
|
||||
# Auto-scroll if user was at bottom and hasn't manually scrolled
|
||||
if (was_at_bottom and not self._user_manually_scrolled) or \
|
||||
(not self._user_manually_scrolled and scrollbar.value() >= scrollbar.maximum() - 2):
|
||||
scrollbar.setValue(scrollbar.maximum())
|
||||
if scrollbar.value() == scrollbar.maximum():
|
||||
self._was_at_bottom = True
|
||||
|
||||
def _on_installation_complete(self, success: bool, message: str, launch_options: str = "", app_id: str = "", time_taken: str = ""):
|
||||
"""Handle installation completion"""
|
||||
if success:
|
||||
self.progress_indicator.set_status("Installation complete!", 100)
|
||||
|
||||
# Use SuccessDialog like other screens
|
||||
from ..dialogs.success_dialog import SuccessDialog
|
||||
from PySide6.QtWidgets import QLabel, QFrame
|
||||
|
||||
success_dialog = SuccessDialog(
|
||||
modlist_name="Wabbajack",
|
||||
workflow_type="install",
|
||||
time_taken=time_taken,
|
||||
game_name=None,
|
||||
parent=self
|
||||
)
|
||||
|
||||
# Increase dialog size to accommodate note section (Steam Deck: 1280x800)
|
||||
# Use wider dialog to reduce vertical space needed (more horizontal space available)
|
||||
success_dialog.setFixedSize(650, 550) # Wider for Steam Deck (1280px width)
|
||||
|
||||
# Add compat mounts note in a separate bordered section
|
||||
note_text = ""
|
||||
if launch_options and "STEAM_COMPAT_MOUNTS" in launch_options:
|
||||
note_text = "<b>Note:</b> To access other drives, add paths to launch options (Steam → Properties). "
|
||||
note_text += "Append with colons: <code>STEAM_COMPAT_MOUNTS=\"/existing:/new/path\" %command%</code>"
|
||||
elif not launch_options:
|
||||
note_text = "<b>Note:</b> To access other drives, add to launch options (Steam → Properties): "
|
||||
note_text += "<code>STEAM_COMPAT_MOUNTS=\"/path/to/directory\" %command%</code>"
|
||||
|
||||
if note_text:
|
||||
# Find the card widget and add a note section after the next steps
|
||||
card = success_dialog.findChild(QFrame, "successCard")
|
||||
if card:
|
||||
# Remove fixed height constraint and increase minimum (Steam Deck optimized)
|
||||
card.setFixedWidth(590) # Wider card to match wider dialog
|
||||
card.setMinimumHeight(380) # Reduced height due to wider text wrapping
|
||||
card.setMaximumHeight(16777215) # Remove max height constraint
|
||||
|
||||
card_layout = card.layout()
|
||||
if card_layout:
|
||||
# Create a bordered note frame with proper sizing
|
||||
note_frame = QFrame()
|
||||
note_frame.setFrameShape(QFrame.StyledPanel)
|
||||
note_frame.setStyleSheet(
|
||||
"QFrame { "
|
||||
" background: #2a2f36; "
|
||||
" border: 1px solid #3fb7d6; "
|
||||
" border-radius: 6px; "
|
||||
" padding: 10px; "
|
||||
" margin-top: 6px; "
|
||||
"}"
|
||||
)
|
||||
# Make note frame size naturally based on content (Steam Deck optimized)
|
||||
note_frame.setMinimumHeight(80)
|
||||
note_frame.setMaximumHeight(16777215) # No max constraint
|
||||
note_frame.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
|
||||
|
||||
note_layout = QVBoxLayout(note_frame)
|
||||
note_layout.setContentsMargins(10, 10, 10, 10) # Reduced padding
|
||||
note_layout.setSpacing(0)
|
||||
|
||||
note_label = QLabel(note_text)
|
||||
note_label.setWordWrap(True)
|
||||
note_label.setTextFormat(Qt.RichText)
|
||||
note_label.setAlignment(Qt.AlignLeft | Qt.AlignTop)
|
||||
note_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
|
||||
# No minimum height - let it size naturally based on content
|
||||
note_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 11px; "
|
||||
" color: #b0b0b0; "
|
||||
" line-height: 1.3; "
|
||||
"}"
|
||||
)
|
||||
note_layout.addWidget(note_label)
|
||||
|
||||
# Insert before the Ko-Fi link (which should be near the end)
|
||||
# Find the index of the Ko-Fi label or add at the end
|
||||
insert_index = card_layout.count() - 2 # Before buttons, after next steps
|
||||
card_layout.insertWidget(insert_index, note_frame)
|
||||
|
||||
success_dialog.show()
|
||||
# Reset UI
|
||||
self.install_folder = None
|
||||
# Reset to default directory
|
||||
default_install_dir = Path(self.config_handler.get_modlist_install_base_dir()) / "Wabbajack"
|
||||
self.install_dir_edit.setText(str(default_install_dir))
|
||||
self.shortcut_name_edit.setText("Wabbajack")
|
||||
self.start_btn.setEnabled(True) # Re-enable since we have default directory
|
||||
self.cancel_btn.setEnabled(True)
|
||||
else:
|
||||
self.progress_indicator.set_status("Installation failed", 0)
|
||||
MessageService.critical(self, "Installation Failed", message)
|
||||
self.start_btn.setEnabled(True)
|
||||
self.cancel_btn.setEnabled(True)
|
||||
|
||||
def _go_back(self):
|
||||
"""Return to Additional Tasks menu"""
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(self.additional_tasks_index)
|
||||
|
||||
def showEvent(self, event):
|
||||
"""Called when widget becomes visible"""
|
||||
super().showEvent(event)
|
||||
try:
|
||||
main_window = self.window()
|
||||
if main_window:
|
||||
from PySide6.QtCore import QSize
|
||||
main_window.setMaximumSize(QSize(16777215, 16777215))
|
||||
set_responsive_minimum(main_window, min_width=960, min_height=420)
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user