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:
185
jackify/frontends/gui/dialogs/enb_proton_dialog.py
Normal file
185
jackify/frontends/gui/dialogs/enb_proton_dialog.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
ENB Proton Compatibility Dialog
|
||||
|
||||
Shown when ENB is detected in a modlist installation to warn users
|
||||
about Proton version requirements for ENB compatibility.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QWidget,
|
||||
QSpacerItem, QSizePolicy, QFrame, QApplication
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QIcon, QFont
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ENBProtonDialog(QDialog):
|
||||
"""
|
||||
Dialog shown when ENB is detected, warning users about Proton version requirements.
|
||||
|
||||
Features:
|
||||
- Clear warning about ENB compatibility
|
||||
- Ordered list of recommended Proton versions
|
||||
- Prominent display to ensure users see it
|
||||
"""
|
||||
|
||||
def __init__(self, modlist_name: str, parent=None):
|
||||
super().__init__(parent)
|
||||
self.modlist_name = modlist_name
|
||||
self.setWindowTitle("ENB Detected - Proton Version Required")
|
||||
self.setWindowModality(Qt.ApplicationModal) # Modal to ensure user sees it
|
||||
self.setFixedSize(600, 550) # Increased height to show full Proton version list and button spacing
|
||||
self.setStyleSheet("QDialog { background: #181818; color: #fff; border-radius: 12px; }")
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(30, 30, 30, 30)
|
||||
|
||||
# --- Card background for content ---
|
||||
card = QFrame(self)
|
||||
card.setObjectName("enbCard")
|
||||
card.setFrameShape(QFrame.StyledPanel)
|
||||
card.setFrameShadow(QFrame.Raised)
|
||||
card.setFixedWidth(540)
|
||||
card.setMinimumHeight(400) # Increased to accommodate full Proton version list
|
||||
card.setMaximumHeight(16777215) # Remove max height constraint to allow expansion
|
||||
card_layout = QVBoxLayout(card)
|
||||
card_layout.setSpacing(16)
|
||||
card_layout.setContentsMargins(28, 28, 28, 28)
|
||||
card.setStyleSheet(
|
||||
"QFrame#enbCard { "
|
||||
" background: #23272e; "
|
||||
" border-radius: 12px; "
|
||||
" border: 2px solid #e67e22; " # Orange border for warning
|
||||
"}"
|
||||
)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
|
||||
|
||||
# Warning title (orange/warning color)
|
||||
title_label = QLabel("ENB Detected")
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 24px; "
|
||||
" font-weight: 700; "
|
||||
" color: #e67e22; " # Orange warning color
|
||||
" margin-bottom: 4px; "
|
||||
"}"
|
||||
)
|
||||
card_layout.addWidget(title_label)
|
||||
|
||||
# Main warning message
|
||||
warning_text = (
|
||||
f"If you plan on using ENB as part of <span style='color:#3fb7d6; font-weight:600;'>{self.modlist_name}</span>, "
|
||||
f"you will need to use one of the following Proton versions, otherwise you will have issues running the modlist:"
|
||||
)
|
||||
warning_label = QLabel(warning_text)
|
||||
warning_label.setAlignment(Qt.AlignCenter)
|
||||
warning_label.setWordWrap(True)
|
||||
warning_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 14px; "
|
||||
" color: #e0e0e0; "
|
||||
" line-height: 1.5; "
|
||||
" margin-bottom: 12px; "
|
||||
" padding: 8px; "
|
||||
"}"
|
||||
)
|
||||
warning_label.setTextFormat(Qt.RichText)
|
||||
card_layout.addWidget(warning_label)
|
||||
|
||||
# Proton version list (in order of recommendation)
|
||||
versions_text = (
|
||||
"<div style='text-align: left; padding: 12px; background: #1a1d23; border-radius: 8px; margin: 8px 0;'>"
|
||||
"<div style='font-size: 13px; color: #b0b0b0; margin-bottom: 8px;'><b style='color: #fff;'>(In order of recommendation)</b></div>"
|
||||
"<div style='font-size: 14px; color: #fff; line-height: 1.8;'>"
|
||||
"• <b style='color: #2ecc71;'>Proton-CachyOS</b><br/>"
|
||||
"• <b style='color: #3498db;'>GE-Proton 10-14</b> or <b style='color: #3498db;'>lower</b><br/>"
|
||||
"• <b style='color: #f39c12;'>Proton 9</b> from Valve"
|
||||
"</div>"
|
||||
"</div>"
|
||||
)
|
||||
versions_label = QLabel(versions_text)
|
||||
versions_label.setAlignment(Qt.AlignLeft)
|
||||
versions_label.setWordWrap(True)
|
||||
versions_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 14px; "
|
||||
" color: #e0e0e0; "
|
||||
" line-height: 1.6; "
|
||||
" margin: 8px 0; "
|
||||
"}"
|
||||
)
|
||||
versions_label.setTextFormat(Qt.RichText)
|
||||
card_layout.addWidget(versions_label)
|
||||
|
||||
# Additional note
|
||||
note_text = (
|
||||
"<div style='font-size: 12px; color: #95a5a6; font-style: italic; margin-top: 8px;'>"
|
||||
"Note: Valve's Proton 10 has known ENB compatibility issues."
|
||||
"</div>"
|
||||
)
|
||||
note_label = QLabel(note_text)
|
||||
note_label.setAlignment(Qt.AlignCenter)
|
||||
note_label.setWordWrap(True)
|
||||
note_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 12px; "
|
||||
" color: #95a5a6; "
|
||||
" font-style: italic; "
|
||||
" margin-top: 8px; "
|
||||
"}"
|
||||
)
|
||||
note_label.setTextFormat(Qt.RichText)
|
||||
card_layout.addWidget(note_label)
|
||||
|
||||
layout.addStretch()
|
||||
layout.addWidget(card, alignment=Qt.AlignCenter)
|
||||
layout.addSpacing(20) # Add spacing between card and button
|
||||
|
||||
# OK button
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch()
|
||||
self.ok_btn = QPushButton("I Understand")
|
||||
self.ok_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background: #3fb7d6; "
|
||||
" color: #fff; "
|
||||
" border: none; "
|
||||
" border-radius: 6px; "
|
||||
" padding: 10px 24px; "
|
||||
" font-size: 14px; "
|
||||
" font-weight: 600; "
|
||||
"}"
|
||||
"QPushButton:hover { "
|
||||
" background: #35a5c2; "
|
||||
"}"
|
||||
"QPushButton:pressed { "
|
||||
" background: #2d8fa8; "
|
||||
"}"
|
||||
)
|
||||
self.ok_btn.clicked.connect(self.accept)
|
||||
btn_row.addWidget(self.ok_btn)
|
||||
btn_row.addStretch()
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
# Set the Wabbajack icon if available
|
||||
self._set_dialog_icon()
|
||||
|
||||
logger.info(f"ENBProtonDialog created for modlist: {modlist_name}")
|
||||
|
||||
def _set_dialog_icon(self):
|
||||
"""Set the dialog icon to Wabbajack icon if available"""
|
||||
try:
|
||||
icon_path = Path(__file__).parent.parent.parent.parent.parent / "Files" / "wabbajack-icon.png"
|
||||
if icon_path.exists():
|
||||
icon = QIcon(str(icon_path))
|
||||
self.setWindowIcon(icon)
|
||||
except Exception as e:
|
||||
logger.debug(f"Could not set dialog icon: {e}")
|
||||
|
||||
@@ -54,6 +54,7 @@ class SuccessDialog(QDialog):
|
||||
card.setFrameShadow(QFrame.Raised)
|
||||
card.setFixedWidth(440)
|
||||
card.setMinimumHeight(380)
|
||||
card.setMaximumHeight(16777215) # Remove max height constraint to allow expansion
|
||||
card_layout = QVBoxLayout(card)
|
||||
card_layout.setSpacing(12)
|
||||
card_layout.setContentsMargins(28, 28, 28, 28)
|
||||
@@ -64,7 +65,7 @@ class SuccessDialog(QDialog):
|
||||
" border: 1px solid #353a40; "
|
||||
"}"
|
||||
)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
|
||||
|
||||
# Success title (less saturated green)
|
||||
title_label = QLabel("Success!")
|
||||
@@ -87,21 +88,22 @@ class SuccessDialog(QDialog):
|
||||
else:
|
||||
message_html = message_text
|
||||
message_label = QLabel(message_html)
|
||||
# Center the success message within the wider card for all screen sizes
|
||||
message_label.setAlignment(Qt.AlignCenter)
|
||||
message_label.setWordWrap(True)
|
||||
message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
|
||||
message_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 15px; "
|
||||
" color: #e0e0e0; "
|
||||
" line-height: 1.3; "
|
||||
" margin-bottom: 6px; "
|
||||
" max-width: 400px; "
|
||||
" min-width: 200px; "
|
||||
" word-wrap: break-word; "
|
||||
"}"
|
||||
)
|
||||
message_label.setTextFormat(Qt.RichText)
|
||||
card_layout.addWidget(message_label)
|
||||
# Ensure the label itself is centered in the card layout and uses full width
|
||||
card_layout.addWidget(message_label, alignment=Qt.AlignCenter)
|
||||
|
||||
# Time taken
|
||||
time_label = QLabel(f"Completed in {self.time_taken}")
|
||||
@@ -226,13 +228,13 @@ class SuccessDialog(QDialog):
|
||||
base_message = ""
|
||||
if self.workflow_type == "tuxborn":
|
||||
base_message = f"You can now launch Tuxborn from Steam and enjoy your modded {game_display} experience!"
|
||||
elif self.workflow_type == "install" and self.modlist_name == "Wabbajack":
|
||||
base_message = "You can now launch Wabbajack from Steam and install modlists. Once the modlist install is complete, you can run \"Configure New Modlist\" in Jackify to complete the configuration for running the modlist on Linux."
|
||||
else:
|
||||
base_message = f"You can now launch {self.modlist_name} from Steam and enjoy your modded {game_display} experience!"
|
||||
|
||||
# Add GE-Proton recommendation
|
||||
proton_note = "\n\nNOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of Valve's Proton 10 (known ENB compatibility issues)."
|
||||
|
||||
return base_message + proton_note
|
||||
# Note: ENB-specific Proton warning is now shown in a separate dialog when ENB is detected
|
||||
return base_message
|
||||
|
||||
def _update_countdown(self):
|
||||
if self._countdown > 0:
|
||||
|
||||
@@ -22,19 +22,19 @@ if '--env-diagnostic' in sys.argv:
|
||||
print("Bundled Environment Diagnostic")
|
||||
print("=" * 50)
|
||||
|
||||
# Check if we're running from a frozen bundle
|
||||
is_frozen = getattr(sys, 'frozen', False)
|
||||
meipass = getattr(sys, '_MEIPASS', None)
|
||||
# Check if we're running as AppImage
|
||||
is_appimage = 'APPIMAGE' in os.environ or 'APPDIR' in os.environ
|
||||
appdir = os.environ.get('APPDIR')
|
||||
|
||||
print(f"Frozen: {is_frozen}")
|
||||
print(f"_MEIPASS: {meipass}")
|
||||
print(f"AppImage: {is_appimage}")
|
||||
print(f"APPDIR: {appdir}")
|
||||
|
||||
# Capture environment data
|
||||
env_data = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'context': 'appimage_runtime',
|
||||
'frozen': is_frozen,
|
||||
'meipass': meipass,
|
||||
'appimage': is_appimage,
|
||||
'appdir': appdir,
|
||||
'python_executable': sys.executable,
|
||||
'working_directory': os.getcwd(),
|
||||
'sys_path': sys.path,
|
||||
@@ -737,8 +737,14 @@ class SettingsDialog(QDialog):
|
||||
# Get all available Proton versions
|
||||
available_protons = WineUtils.scan_all_proton_versions()
|
||||
|
||||
# Add "Auto" option first
|
||||
self.install_proton_dropdown.addItem("Auto (Recommended)", "auto")
|
||||
# Check if any Proton versions were found
|
||||
has_proton = len(available_protons) > 0
|
||||
|
||||
# Add "Auto" or "No Proton" option first based on detection
|
||||
if has_proton:
|
||||
self.install_proton_dropdown.addItem("Auto (Recommended)", "auto")
|
||||
else:
|
||||
self.install_proton_dropdown.addItem("No Proton Versions Detected", "none")
|
||||
|
||||
# Filter for fast Proton versions only
|
||||
fast_protons = []
|
||||
@@ -893,9 +899,29 @@ class SettingsDialog(QDialog):
|
||||
jackify_data_dir = self.jackify_data_dir_edit.text().strip()
|
||||
self.config_handler.set("jackify_data_dir", jackify_data_dir)
|
||||
|
||||
# Initialize with existing config values as fallback (prevents UnboundLocalError if auto-detection fails)
|
||||
resolved_install_path = self.config_handler.get("proton_path", "")
|
||||
resolved_install_version = self.config_handler.get("proton_version", "")
|
||||
|
||||
# Save Install Proton selection - resolve "auto" to actual path
|
||||
selected_install_proton_path = self.install_proton_dropdown.currentData()
|
||||
if selected_install_proton_path == "auto":
|
||||
if selected_install_proton_path == "none":
|
||||
# No Proton detected - warn user but allow saving other settings
|
||||
MessageService.warning(
|
||||
self,
|
||||
"No Compatible Proton Installed",
|
||||
"Jackify requires Proton 9.0+, Proton Experimental, or GE-Proton 10+ to install modlists.\n\n"
|
||||
"To install Proton:\n"
|
||||
"1. Install any Windows game in Steam (Proton downloads automatically), OR\n"
|
||||
"2. Install GE-Proton using ProtonPlus or ProtonUp-Qt, OR\n"
|
||||
"3. Download GE-Proton manually from:\n"
|
||||
" https://github.com/GloriousEggroll/proton-ge-custom/releases\n\n"
|
||||
"Your other settings will be saved, but modlist installation may not work without Proton.",
|
||||
safety_level="medium"
|
||||
)
|
||||
logger.warning("No Proton detected - user warned, allowing save to proceed for other settings")
|
||||
# Don't modify Proton config, but continue to save other settings
|
||||
elif selected_install_proton_path == "auto":
|
||||
# Resolve "auto" to actual best Proton path using unified detection
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
@@ -1295,6 +1321,7 @@ class JackifyMainWindow(QMainWindow):
|
||||
InstallModlistScreen, ConfigureNewModlistScreen, ConfigureExistingModlistScreen
|
||||
)
|
||||
from jackify.frontends.gui.screens.install_ttw import InstallTTWScreen
|
||||
from jackify.frontends.gui.screens.wabbajack_installer import WabbajackInstallerScreen
|
||||
|
||||
self.main_menu = MainMenu(stacked_widget=self.stacked_widget, dev_mode=dev_mode)
|
||||
self.feature_placeholder = FeaturePlaceholder(stacked_widget=self.stacked_widget)
|
||||
@@ -1326,6 +1353,11 @@ class JackifyMainWindow(QMainWindow):
|
||||
main_menu_index=0,
|
||||
system_info=self.system_info
|
||||
)
|
||||
self.wabbajack_installer_screen = WabbajackInstallerScreen(
|
||||
stacked_widget=self.stacked_widget,
|
||||
additional_tasks_index=3,
|
||||
system_info=self.system_info
|
||||
)
|
||||
|
||||
# Let TTW screen request window resize for expand/collapse
|
||||
try:
|
||||
@@ -1346,6 +1378,11 @@ class JackifyMainWindow(QMainWindow):
|
||||
self.configure_existing_modlist_screen.resize_request.connect(self._on_child_resize_request)
|
||||
except Exception:
|
||||
pass
|
||||
# Let Wabbajack Installer screen request window resize for expand/collapse
|
||||
try:
|
||||
self.wabbajack_installer_screen.resize_request.connect(self._on_child_resize_request)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Add screens to stacked widget
|
||||
self.stacked_widget.addWidget(self.main_menu) # Index 0: Main Menu
|
||||
@@ -1355,7 +1392,8 @@ class JackifyMainWindow(QMainWindow):
|
||||
self.stacked_widget.addWidget(self.install_modlist_screen) # Index 4: Install Modlist
|
||||
self.stacked_widget.addWidget(self.install_ttw_screen) # Index 5: Install TTW
|
||||
self.stacked_widget.addWidget(self.configure_new_modlist_screen) # Index 6: Configure New
|
||||
self.stacked_widget.addWidget(self.configure_existing_modlist_screen) # Index 7: Configure Existing
|
||||
self.stacked_widget.addWidget(self.wabbajack_installer_screen) # Index 7: Wabbajack Installer
|
||||
self.stacked_widget.addWidget(self.configure_existing_modlist_screen) # Index 8: Configure Existing
|
||||
|
||||
# Add debug tracking for screen changes
|
||||
self.stacked_widget.currentChanged.connect(self._debug_screen_change)
|
||||
@@ -1828,10 +1866,6 @@ class JackifyMainWindow(QMainWindow):
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""Get path to resource file, handling both AppImage and dev modes."""
|
||||
# PyInstaller frozen mode
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
return os.path.join(sys._MEIPASS, relative_path)
|
||||
|
||||
# AppImage mode - use APPDIR if available
|
||||
appdir = os.environ.get('APPDIR')
|
||||
if appdir:
|
||||
|
||||
@@ -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
|
||||
@@ -149,14 +149,6 @@ class FileProgressItem(QWidget):
|
||||
layout.addWidget(percent_label)
|
||||
self.percent_label = percent_label
|
||||
|
||||
# Speed display (if available)
|
||||
speed_label = QLabel()
|
||||
speed_label.setFixedWidth(60)
|
||||
speed_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
|
||||
speed_label.setStyleSheet("color: #888; font-size: 10px;")
|
||||
layout.addWidget(speed_label)
|
||||
self.speed_label = speed_label
|
||||
|
||||
# Progress indicator: either progress bar (with %) or animated spinner (no %)
|
||||
progress_bar = QProgressBar()
|
||||
progress_bar.setFixedHeight(12)
|
||||
@@ -223,7 +215,6 @@ class FileProgressItem(QWidget):
|
||||
if no_progress_bar:
|
||||
self._animation_timer.stop() # Stop animation for items without progress bars
|
||||
self.percent_label.setText("") # No percentage
|
||||
self.speed_label.setText("") # No speed
|
||||
self.progress_bar.setVisible(False) # Hide progress bar
|
||||
return
|
||||
|
||||
@@ -244,14 +235,12 @@ class FileProgressItem(QWidget):
|
||||
if not self._animation_timer.isActive():
|
||||
self._animation_timer.start()
|
||||
|
||||
self.speed_label.setText("") # No speed for summary
|
||||
self.progress_bar.setRange(0, 100)
|
||||
# Progress bar value will be updated by animation timer
|
||||
else:
|
||||
# No max for summary - use custom animated spinner
|
||||
self._is_indeterminate = True
|
||||
self.percent_label.setText("")
|
||||
self.speed_label.setText("")
|
||||
self.progress_bar.setRange(0, 100) # Use determinate range for custom animation
|
||||
if not self._animation_timer.isActive():
|
||||
self._animation_timer.start()
|
||||
@@ -271,7 +260,6 @@ class FileProgressItem(QWidget):
|
||||
self._is_indeterminate = False
|
||||
self._animation_timer.stop()
|
||||
self.percent_label.setText("Queued")
|
||||
self.speed_label.setText("")
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
return
|
||||
@@ -295,15 +283,12 @@ class FileProgressItem(QWidget):
|
||||
if not self._animation_timer.isActive():
|
||||
self._animation_timer.start()
|
||||
|
||||
# Update speed label immediately (doesn't need animation)
|
||||
self.speed_label.setText(self.file_progress.speed_display)
|
||||
self.progress_bar.setRange(0, 100)
|
||||
# Progress bar value will be updated by animation timer
|
||||
else:
|
||||
# No progress data (e.g., texture conversions, BSA building) - use custom animated spinner
|
||||
self._is_indeterminate = True
|
||||
self.percent_label.setText("") # Clear percent label
|
||||
self.speed_label.setText("") # No speed
|
||||
self.progress_bar.setRange(0, 100) # Use determinate range for custom animation
|
||||
# Start animation timer for custom spinner
|
||||
if not self._animation_timer.isActive():
|
||||
|
||||
Reference in New Issue
Block a user