mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Initial public release v0.1.0 - Linux Wabbajack Modlist Application
Jackify provides native Linux support for Wabbajack modlist installation and management with automated Steam integration and Proton configuration. Key Features: - Almost Native Linux implementation (texconv.exe run via proton) - Automated Steam shortcut creation and Proton prefix management - Both CLI and GUI interfaces, with Steam Deck optimization Supported Games: - Skyrim Special Edition - Fallout 4 - Fallout New Vegas - Oblivion, Starfield, Enderal, and diverse other games Technical Architecture: - Clean separation between frontend and backend services - Powered by jackify-engine 0.3.x for Wabbajack-matching modlist installation
This commit is contained in:
21
jackify/frontends/gui/screens/__init__.py
Normal file
21
jackify/frontends/gui/screens/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
GUI Screens Module
|
||||
|
||||
Contains all the GUI screen components for Jackify.
|
||||
"""
|
||||
|
||||
from .main_menu import MainMenu
|
||||
from .tuxborn_installer import TuxbornInstallerScreen
|
||||
from .modlist_tasks import ModlistTasksScreen
|
||||
from .install_modlist import InstallModlistScreen
|
||||
from .configure_new_modlist import ConfigureNewModlistScreen
|
||||
from .configure_existing_modlist import ConfigureExistingModlistScreen
|
||||
|
||||
__all__ = [
|
||||
'MainMenu',
|
||||
'TuxbornInstallerScreen',
|
||||
'ModlistTasksScreen',
|
||||
'InstallModlistScreen',
|
||||
'ConfigureNewModlistScreen',
|
||||
'ConfigureExistingModlistScreen'
|
||||
]
|
||||
710
jackify/frontends/gui/screens/configure_existing_modlist.py
Normal file
710
jackify/frontends/gui/screens/configure_existing_modlist.py
Normal file
@@ -0,0 +1,710 @@
|
||||
# Copy of ConfigureNewModlistScreen, adapted for existing modlists
|
||||
from PySide6.QtWidgets import *
|
||||
from PySide6.QtCore import *
|
||||
from PySide6.QtGui import *
|
||||
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
|
||||
from ..utils import ansi_to_html
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from jackify.backend.handlers.shortcut_handler import ShortcutHandler
|
||||
import traceback
|
||||
import signal
|
||||
from jackify.backend.core.modlist_operations import get_jackify_engine_path
|
||||
from jackify.backend.handlers.subprocess_utils import ProcessManager
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
from jackify.backend.services.resolution_service import ResolutionService
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
from ..dialogs import SuccessDialog
|
||||
|
||||
def debug_print(message):
|
||||
"""Print debug message only if debug mode is enabled"""
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
config_handler = ConfigHandler()
|
||||
if config_handler.get('debug_mode', False):
|
||||
print(message)
|
||||
|
||||
class ConfigureExistingModlistScreen(QWidget):
|
||||
steam_restart_finished = Signal(bool, str)
|
||||
def __init__(self, stacked_widget=None, main_menu_index=0):
|
||||
super().__init__()
|
||||
debug_print("DEBUG: ConfigureExistingModlistScreen __init__ called")
|
||||
self.stacked_widget = stacked_widget
|
||||
self.main_menu_index = main_menu_index
|
||||
self.debug = DEBUG_BORDERS
|
||||
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_Existing_Modlist_workflow.log')
|
||||
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
|
||||
|
||||
# --- Detect Steam Deck ---
|
||||
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
|
||||
self.shortcut_handler = ShortcutHandler(steamdeck=steamdeck)
|
||||
|
||||
# Initialize services early
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
from jackify.backend.services.resolution_service import ResolutionService
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
self.api_key_service = APIKeyService()
|
||||
self.resolution_service = ResolutionService()
|
||||
self.config_handler = ConfigHandler()
|
||||
|
||||
# --- Fetch shortcuts for ModOrganizer.exe using existing backend functionality ---
|
||||
# Use existing discover_executable_shortcuts which already filters by protontricks availability
|
||||
from jackify.backend.handlers.modlist_handler import ModlistHandler
|
||||
|
||||
# Initialize modlist handler with empty config dict to use default initialization
|
||||
modlist_handler = ModlistHandler({})
|
||||
discovered_modlists = modlist_handler.discover_executable_shortcuts("ModOrganizer.exe")
|
||||
|
||||
# Convert to shortcut_handler format for UI compatibility
|
||||
self.mo2_shortcuts = []
|
||||
for modlist in discovered_modlists:
|
||||
# Convert discovered modlist format to shortcut format
|
||||
shortcut = {
|
||||
'AppName': modlist.get('name', 'Unknown'),
|
||||
'AppID': modlist.get('appid', ''),
|
||||
'StartDir': modlist.get('path', ''),
|
||||
'Exe': f"{modlist.get('path', '')}/ModOrganizer.exe"
|
||||
}
|
||||
self.mo2_shortcuts.append(shortcut)
|
||||
# --- UI Layout ---
|
||||
main_overall_vbox = QVBoxLayout(self)
|
||||
main_overall_vbox.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
|
||||
main_overall_vbox.setContentsMargins(50, 50, 50, 0) # No bottom margin
|
||||
if self.debug:
|
||||
self.setStyleSheet("border: 2px solid magenta;")
|
||||
# --- Header (title, description) ---
|
||||
header_layout = QVBoxLayout()
|
||||
header_layout.setSpacing(1) # Reduce spacing between title and description
|
||||
title = QLabel("<b>Configure Existing Modlist</b>")
|
||||
title.setStyleSheet(f"font-size: 20px; color: {JACKIFY_COLOR_BLUE}; margin: 0px; padding: 0px;")
|
||||
title.setAlignment(Qt.AlignHCenter)
|
||||
title.setMaximumHeight(30) # Force compact height
|
||||
header_layout.addWidget(title)
|
||||
desc = QLabel(
|
||||
"This screen allows you to configure an existing modlist in Jackify. "
|
||||
"Select your Steam shortcut for ModOrganizer.exe, set your resolution, and complete post-install configuration."
|
||||
)
|
||||
desc.setWordWrap(True)
|
||||
desc.setStyleSheet("color: #ccc; margin: 0px; padding: 0px; line-height: 1.2;")
|
||||
desc.setAlignment(Qt.AlignHCenter)
|
||||
desc.setMaximumHeight(40) # Force compact height for description
|
||||
header_layout.addWidget(desc)
|
||||
header_widget = QWidget()
|
||||
header_widget.setLayout(header_layout)
|
||||
header_widget.setMaximumHeight(75) # Match other screens
|
||||
if self.debug:
|
||||
header_widget.setStyleSheet("border: 2px solid pink;")
|
||||
header_widget.setToolTip("HEADER_SECTION")
|
||||
main_overall_vbox.addWidget(header_widget)
|
||||
# --- Upper section: shortcut selector (left) + process monitor (right) ---
|
||||
upper_hbox = QHBoxLayout()
|
||||
upper_hbox.setContentsMargins(0, 0, 0, 0)
|
||||
upper_hbox.setSpacing(16)
|
||||
user_config_vbox = QVBoxLayout()
|
||||
user_config_vbox.setAlignment(Qt.AlignTop)
|
||||
# --- [Options] header (moved here for alignment) ---
|
||||
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)
|
||||
user_config_vbox.addWidget(options_header)
|
||||
form_grid = QGridLayout()
|
||||
form_grid.setHorizontalSpacing(12)
|
||||
form_grid.setVerticalSpacing(6) # Reduced from 8 to 6 for better readability
|
||||
form_grid.setContentsMargins(0, 0, 0, 0)
|
||||
# --- Shortcut selector ---
|
||||
shortcut_label = QLabel("Select Modlist:")
|
||||
self.shortcut_combo = QComboBox()
|
||||
self.shortcut_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.shortcut_combo.addItem("Please Select...")
|
||||
self.shortcut_map = []
|
||||
for shortcut in self.mo2_shortcuts:
|
||||
display = f"{shortcut.get('AppName', 'Unknown')} ({shortcut.get('StartDir', '')})"
|
||||
self.shortcut_combo.addItem(display)
|
||||
self.shortcut_map.append(shortcut)
|
||||
|
||||
# Add refresh button next to dropdown
|
||||
refresh_btn = QPushButton("↻")
|
||||
refresh_btn.setToolTip("Refresh modlist list")
|
||||
refresh_btn.setFixedSize(30, 30)
|
||||
refresh_btn.clicked.connect(self.refresh_modlist_list)
|
||||
|
||||
# Create horizontal layout for dropdown and refresh button
|
||||
shortcut_hbox = QHBoxLayout()
|
||||
shortcut_hbox.addWidget(self.shortcut_combo)
|
||||
shortcut_hbox.addWidget(refresh_btn)
|
||||
shortcut_hbox.setSpacing(4)
|
||||
shortcut_hbox.setStretch(0, 1) # Make dropdown expand
|
||||
|
||||
form_grid.addWidget(shortcut_label, 0, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
|
||||
form_grid.addLayout(shortcut_hbox, 0, 1)
|
||||
# --- Info message under shortcut selector ---
|
||||
info_label = QLabel("<span style='color:#aaa'>If you don't see your modlist entry in this list, please ensure you have added it to Steam as a non-steam game, set a proton version in properties, and have started the modlist Steam entry at least once. You can also click the refresh button (↻) to update the list.</span>")
|
||||
info_label.setWordWrap(True)
|
||||
form_grid.addWidget(info_label, 1, 0, 1, 2)
|
||||
# --- Resolution selector ---
|
||||
resolution_label = QLabel("Resolution:")
|
||||
self.resolution_combo = QComboBox()
|
||||
self.resolution_combo.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.resolution_combo.addItem("Leave unchanged")
|
||||
self.resolution_combo.addItems([
|
||||
"1280x720",
|
||||
"1280x800 (Steam Deck)",
|
||||
"1366x768",
|
||||
"1440x900",
|
||||
"1600x900",
|
||||
"1600x1200",
|
||||
"1680x1050",
|
||||
"1920x1080",
|
||||
"1920x1200",
|
||||
"2048x1152",
|
||||
"2560x1080",
|
||||
"2560x1440",
|
||||
"2560x1600",
|
||||
"3440x1440",
|
||||
"3840x1600",
|
||||
"3840x2160",
|
||||
"3840x2400",
|
||||
"5120x1440",
|
||||
"5120x2160",
|
||||
"7680x4320"
|
||||
])
|
||||
form_grid.addWidget(resolution_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
|
||||
form_grid.addWidget(self.resolution_combo, 2, 1)
|
||||
|
||||
# Load saved resolution if available
|
||||
saved_resolution = self.resolution_service.get_saved_resolution()
|
||||
is_steam_deck = False
|
||||
try:
|
||||
if os.path.exists('/etc/os-release'):
|
||||
with open('/etc/os-release') as f:
|
||||
if 'steamdeck' in f.read().lower():
|
||||
is_steam_deck = True
|
||||
except Exception:
|
||||
pass
|
||||
if saved_resolution:
|
||||
combo_items = [self.resolution_combo.itemText(i) for i in range(self.resolution_combo.count())]
|
||||
resolution_index = self.resolution_service.get_resolution_index(saved_resolution, combo_items)
|
||||
self.resolution_combo.setCurrentIndex(resolution_index)
|
||||
debug_print(f"DEBUG: Loaded saved resolution: {saved_resolution} (index: {resolution_index})")
|
||||
elif is_steam_deck:
|
||||
# Set default to 1280x800 (Steam Deck)
|
||||
combo_items = [self.resolution_combo.itemText(i) for i in range(self.resolution_combo.count())]
|
||||
if "1280x800 (Steam Deck)" in combo_items:
|
||||
self.resolution_combo.setCurrentIndex(combo_items.index("1280x800 (Steam Deck)"))
|
||||
else:
|
||||
self.resolution_combo.setCurrentIndex(0)
|
||||
# Otherwise, default is 'Leave unchanged' (index 0)
|
||||
form_section_widget = QWidget()
|
||||
form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
form_section_widget.setLayout(form_grid)
|
||||
form_section_widget.setMinimumHeight(160) # Reduced to match compact form
|
||||
form_section_widget.setMaximumHeight(240) # Increased to show resolution dropdown
|
||||
if self.debug:
|
||||
form_section_widget.setStyleSheet("border: 2px solid blue;")
|
||||
form_section_widget.setToolTip("FORM_SECTION")
|
||||
user_config_vbox.addWidget(form_section_widget)
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setAlignment(Qt.AlignHCenter)
|
||||
self.start_btn = QPushButton("Start Configuration")
|
||||
btn_row.addWidget(self.start_btn)
|
||||
cancel_btn = QPushButton("Cancel")
|
||||
cancel_btn.clicked.connect(self.go_back)
|
||||
btn_row.addWidget(cancel_btn)
|
||||
user_config_widget = QWidget()
|
||||
user_config_widget.setLayout(user_config_vbox)
|
||||
if self.debug:
|
||||
user_config_widget.setStyleSheet("border: 2px solid orange;")
|
||||
user_config_widget.setToolTip("USER_CONFIG_WIDGET")
|
||||
self.process_monitor = QTextEdit()
|
||||
self.process_monitor.setReadOnly(True)
|
||||
self.process_monitor.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
|
||||
self.process_monitor.setMinimumSize(QSize(300, 20))
|
||||
self.process_monitor.setStyleSheet(f"background: #222; color: {JACKIFY_COLOR_BLUE}; font-family: monospace; font-size: 11px; border: 1px solid #444;")
|
||||
self.process_monitor_heading = QLabel("<b>[Process Monitor]</b>")
|
||||
self.process_monitor_heading.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px; margin-bottom: 2px;")
|
||||
self.process_monitor_heading.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
process_vbox = QVBoxLayout()
|
||||
process_vbox.setContentsMargins(0, 0, 0, 0)
|
||||
process_vbox.setSpacing(2)
|
||||
process_vbox.addWidget(self.process_monitor_heading)
|
||||
process_vbox.addWidget(self.process_monitor)
|
||||
process_monitor_widget = QWidget()
|
||||
process_monitor_widget.setLayout(process_vbox)
|
||||
if self.debug:
|
||||
process_monitor_widget.setStyleSheet("border: 2px solid purple;")
|
||||
process_monitor_widget.setToolTip("PROCESS_MONITOR")
|
||||
upper_hbox.addWidget(user_config_widget, stretch=11)
|
||||
upper_hbox.addWidget(process_monitor_widget, stretch=9)
|
||||
upper_hbox.setAlignment(Qt.AlignTop)
|
||||
upper_section_widget = QWidget()
|
||||
upper_section_widget.setLayout(upper_hbox)
|
||||
upper_section_widget.setMaximumHeight(280) # Increased to show resolution dropdown
|
||||
if self.debug:
|
||||
upper_section_widget.setStyleSheet("border: 2px solid green;")
|
||||
upper_section_widget.setToolTip("UPPER_SECTION")
|
||||
main_overall_vbox.addWidget(upper_section_widget)
|
||||
# Remove spacing - console should expand to fill available space
|
||||
self.console = QTextEdit()
|
||||
self.console.setReadOnly(True)
|
||||
self.console.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
|
||||
self.console.setMinimumHeight(50) # Very small minimum - can shrink to almost nothing
|
||||
self.console.setMaximumHeight(1000) # Allow growth when space available
|
||||
self.console.setFontFamily('monospace')
|
||||
if self.debug:
|
||||
self.console.setStyleSheet("border: 2px solid yellow;")
|
||||
self.console.setToolTip("CONSOLE")
|
||||
|
||||
# Set up scroll tracking for professional auto-scroll behavior
|
||||
self._setup_scroll_tracking()
|
||||
|
||||
# Wrap button row in widget for debug borders
|
||||
btn_row_widget = QWidget()
|
||||
btn_row_widget.setLayout(btn_row)
|
||||
btn_row_widget.setMaximumHeight(50) # Limit height to make it more compact
|
||||
if self.debug:
|
||||
btn_row_widget.setStyleSheet("border: 2px solid red;")
|
||||
btn_row_widget.setToolTip("BUTTON_ROW")
|
||||
|
||||
# Create a container that holds console + button row with proper spacing
|
||||
console_and_buttons_widget = QWidget()
|
||||
console_and_buttons_layout = QVBoxLayout()
|
||||
console_and_buttons_layout.setContentsMargins(0, 0, 0, 0)
|
||||
console_and_buttons_layout.setSpacing(8) # Small gap between console and buttons
|
||||
|
||||
console_and_buttons_layout.addWidget(self.console, stretch=1) # Console fills most space
|
||||
console_and_buttons_layout.addWidget(btn_row_widget) # Buttons at bottom of this container
|
||||
|
||||
console_and_buttons_widget.setLayout(console_and_buttons_layout)
|
||||
if self.debug:
|
||||
console_and_buttons_widget.setStyleSheet("border: 2px solid lightblue;")
|
||||
console_and_buttons_widget.setToolTip("CONSOLE_AND_BUTTONS_CONTAINER")
|
||||
main_overall_vbox.addWidget(console_and_buttons_widget, stretch=1) # This container fills remaining space
|
||||
self.setLayout(main_overall_vbox)
|
||||
self.process = None
|
||||
self.log_timer = None
|
||||
self.last_log_pos = 0
|
||||
self.top_timer = QTimer(self)
|
||||
self.top_timer.timeout.connect(self.update_top_panel)
|
||||
self.top_timer.start(2000)
|
||||
self.start_btn.clicked.connect(self.validate_and_start_configure)
|
||||
self.steam_restart_finished.connect(self._on_steam_restart_finished)
|
||||
|
||||
# Scroll tracking for professional auto-scroll behavior
|
||||
self._user_manually_scrolled = False
|
||||
self._was_at_bottom = True
|
||||
|
||||
# Time tracking for workflow completion
|
||||
self._workflow_start_time = None
|
||||
|
||||
def resizeEvent(self, event):
|
||||
"""Handle window resize to prioritize form over console"""
|
||||
super().resizeEvent(event)
|
||||
self._adjust_console_for_form_priority()
|
||||
|
||||
def _adjust_console_for_form_priority(self):
|
||||
"""Console now dynamically fills available space with stretch=1, no manual calculation needed"""
|
||||
# The console automatically fills remaining space due to stretch=1 in the layout
|
||||
# Remove any fixed height constraints to allow natural stretching
|
||||
self.console.setMaximumHeight(16777215) # Reset to default maximum
|
||||
self.console.setMinimumHeight(50) # Keep minimum height for usability
|
||||
|
||||
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()
|
||||
# Use tolerance to account for rounding and rapid updates
|
||||
self._was_at_bottom = scrollbar.value() >= scrollbar.maximum() - 1
|
||||
|
||||
# If user manually scrolls to bottom, reset manual scroll flag
|
||||
if self._was_at_bottom and self._user_manually_scrolled:
|
||||
# Small delay to allow user to scroll away if they want
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(100, self._reset_manual_scroll_if_at_bottom)
|
||||
|
||||
def _reset_manual_scroll_if_at_bottom(self):
|
||||
"""Reset manual scroll flag if user is still at bottom after delay"""
|
||||
scrollbar = self.console.verticalScrollBar()
|
||||
if scrollbar.value() >= scrollbar.maximum() - 1:
|
||||
self._user_manually_scrolled = False
|
||||
|
||||
def _safe_append_text(self, text):
|
||||
"""Append text with professional auto-scroll behavior"""
|
||||
# Write all messages to log file
|
||||
self._write_to_log_file(text)
|
||||
|
||||
scrollbar = self.console.verticalScrollBar()
|
||||
# Check if user was at bottom BEFORE adding text
|
||||
was_at_bottom = (scrollbar.value() >= scrollbar.maximum() - 1) # Allow 1px tolerance
|
||||
|
||||
# Add the text
|
||||
self.console.append(text)
|
||||
|
||||
# Auto-scroll if user was at bottom and hasn't manually scrolled
|
||||
# Re-check bottom state after text addition for better reliability
|
||||
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())
|
||||
# Ensure user can still manually scroll up during rapid updates
|
||||
if scrollbar.value() == scrollbar.maximum():
|
||||
self._was_at_bottom = True
|
||||
|
||||
def _write_to_log_file(self, message):
|
||||
"""Write message to workflow log file with timestamp"""
|
||||
try:
|
||||
from datetime import datetime
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
with open(self.modlist_log_path, 'a', encoding='utf-8') as f:
|
||||
f.write(f"[{timestamp}] {message}\n")
|
||||
except Exception:
|
||||
# Logging should never break the workflow
|
||||
pass
|
||||
|
||||
def validate_and_start_configure(self):
|
||||
# Rotate log file at start of each workflow run (keep 5 backups)
|
||||
from jackify.backend.handlers.logging_handler import LoggingHandler
|
||||
from pathlib import Path
|
||||
log_handler = LoggingHandler()
|
||||
log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5)
|
||||
|
||||
# Get selected shortcut
|
||||
idx = self.shortcut_combo.currentIndex() - 1 # Account for 'Please Select...'
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
if idx < 0 or idx >= len(self.shortcut_map):
|
||||
MessageService.critical(self, "No Shortcut Selected", "Please select a ModOrganizer.exe Steam shortcut to configure.", safety_level="medium")
|
||||
return
|
||||
shortcut = self.shortcut_map[idx]
|
||||
modlist_name = shortcut.get('AppName', '')
|
||||
install_dir = shortcut.get('StartDir', '')
|
||||
if not modlist_name or not install_dir:
|
||||
MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium")
|
||||
return
|
||||
resolution = self.resolution_combo.currentText()
|
||||
# Handle resolution saving
|
||||
if resolution and resolution != "Leave unchanged":
|
||||
success = self.resolution_service.save_resolution(resolution)
|
||||
if success:
|
||||
debug_print(f"DEBUG: Resolution saved successfully: {resolution}")
|
||||
else:
|
||||
debug_print("DEBUG: Failed to save resolution")
|
||||
else:
|
||||
# Clear saved resolution if "Leave unchanged" is selected
|
||||
if self.resolution_service.has_saved_resolution():
|
||||
self.resolution_service.clear_saved_resolution()
|
||||
debug_print("DEBUG: Saved resolution cleared")
|
||||
# Start the workflow (no shortcut creation needed)
|
||||
self.start_workflow(modlist_name, install_dir, resolution)
|
||||
|
||||
def start_workflow(self, modlist_name, install_dir, resolution):
|
||||
"""Start the configuration workflow using backend service directly"""
|
||||
try:
|
||||
# Start time tracking
|
||||
self._workflow_start_time = time.time()
|
||||
|
||||
self._safe_append_text("[Jackify] Starting post-install configuration...")
|
||||
|
||||
# Create configuration thread using backend service
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
|
||||
class ConfigurationThread(QThread):
|
||||
progress_update = Signal(str)
|
||||
configuration_complete = Signal(bool, str, str)
|
||||
error_occurred = Signal(str)
|
||||
|
||||
def __init__(self, modlist_name, install_dir, resolution):
|
||||
super().__init__()
|
||||
self.modlist_name = modlist_name
|
||||
self.install_dir = install_dir
|
||||
self.resolution = resolution
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
from jackify.backend.models.configuration import SystemInfo
|
||||
from jackify.backend.services.modlist_service import ModlistService
|
||||
from jackify.backend.models.modlist import ModlistContext
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
# Initialize backend service
|
||||
system_info = SystemInfo(is_steamdeck=False) # TODO: Detect Steam Deck
|
||||
modlist_service = ModlistService(system_info)
|
||||
|
||||
# Create modlist context for existing modlist configuration
|
||||
mo2_exe_path = os.path.join(self.install_dir, "ModOrganizer.exe")
|
||||
modlist_context = ModlistContext(
|
||||
name=self.modlist_name,
|
||||
install_dir=Path(self.install_dir),
|
||||
download_dir=Path(self.install_dir).parent / 'Downloads', # Default
|
||||
game_type='skyrim', # Default for now - TODO: detect from modlist
|
||||
nexus_api_key='', # Not needed for configuration-only
|
||||
modlist_value='', # Not needed for existing modlist
|
||||
modlist_source='existing',
|
||||
skip_confirmation=True
|
||||
)
|
||||
|
||||
# For existing modlists, add resolution if specified
|
||||
if self.resolution != "Leave unchanged":
|
||||
modlist_context.resolution = self.resolution.split()[0]
|
||||
|
||||
# Define callbacks
|
||||
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 manual_steps_callback(modlist_name, retry_count):
|
||||
# Existing modlists shouldn't need manual steps, but handle gracefully
|
||||
self.progress_update.emit(f"Note: Manual steps callback triggered for {modlist_name} (retry {retry_count})")
|
||||
|
||||
# Call the working configuration service method
|
||||
self.progress_update.emit("Starting existing modlist configuration...")
|
||||
|
||||
# For existing modlists, call configure_modlist_post_steam directly
|
||||
# since Steam setup and manual steps should already be done
|
||||
success = modlist_service.configure_modlist_post_steam(
|
||||
context=modlist_context,
|
||||
progress_callback=progress_callback,
|
||||
manual_steps_callback=manual_steps_callback,
|
||||
completion_callback=completion_callback
|
||||
)
|
||||
|
||||
if not success:
|
||||
self.error_occurred.emit("Configuration failed - check logs for details")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_msg = f"Configuration error: {e}\n{traceback.format_exc()}"
|
||||
self.error_occurred.emit(error_msg)
|
||||
|
||||
# Create and start the configuration thread
|
||||
self.config_thread = ConfigurationThread(modlist_name, install_dir, resolution)
|
||||
self.config_thread.progress_update.connect(self._safe_append_text)
|
||||
self.config_thread.configuration_complete.connect(self.on_configuration_complete)
|
||||
self.config_thread.error_occurred.connect(self.on_configuration_error)
|
||||
self.config_thread.start()
|
||||
|
||||
except Exception as e:
|
||||
self._safe_append_text(f"[ERROR] Failed to start configuration: {e}")
|
||||
MessageService.critical(self, "Configuration Error", f"Failed to start configuration: {e}", safety_level="medium")
|
||||
|
||||
def on_configuration_complete(self, success, message, modlist_name):
|
||||
"""Handle configuration completion"""
|
||||
if success:
|
||||
# Calculate time taken
|
||||
time_taken = self._calculate_time_taken()
|
||||
|
||||
# Show success dialog with celebration
|
||||
success_dialog = SuccessDialog(
|
||||
modlist_name=modlist_name,
|
||||
workflow_type="configure_existing",
|
||||
time_taken=time_taken,
|
||||
game_name=getattr(self, '_current_game_name', None),
|
||||
parent=self
|
||||
)
|
||||
success_dialog.show()
|
||||
else:
|
||||
self._safe_append_text(f"Configuration failed: {message}")
|
||||
MessageService.critical(self, "Configuration Failed",
|
||||
f"Configuration failed: {message}", safety_level="medium")
|
||||
|
||||
def on_configuration_error(self, error_message):
|
||||
"""Handle configuration error"""
|
||||
self._safe_append_text(f"Configuration error: {error_message}")
|
||||
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium")
|
||||
|
||||
def show_manual_steps_dialog(self, extra_warning=""):
|
||||
modlist_name = self.shortcut_combo.currentText().split('(')[0].strip() or "your modlist"
|
||||
msg = (
|
||||
f"<b>Manual Proton Setup Required for <span style='color:#3fd0ea'>{modlist_name}</span></b><br>"
|
||||
"After Steam restarts, complete the following steps in Steam:<br>"
|
||||
f"1. Locate the '<b>{modlist_name}</b>' entry in your Steam Library<br>"
|
||||
"2. Right-click and select 'Properties'<br>"
|
||||
"3. Switch to the 'Compatibility' tab<br>"
|
||||
"4. Check the box labeled 'Force the use of a specific Steam Play compatibility tool'<br>"
|
||||
"5. Select 'Proton - Experimental' from the dropdown menu<br>"
|
||||
"6. Close the Properties window<br>"
|
||||
f"7. Launch '<b>{modlist_name}</b>' from your Steam Library<br>"
|
||||
"8. Wait for Wabbajack to download its files and fully load<br>"
|
||||
"9. Once Wabbajack has fully loaded, CLOSE IT completely and return here<br>"
|
||||
"<br>Once you have completed ALL the steps above, click OK to continue."
|
||||
f"{extra_warning}"
|
||||
)
|
||||
reply = MessageService.question(self, "Manual Steps Required", msg, safety_level="medium")
|
||||
if reply == QMessageBox.Yes:
|
||||
if self.config_process and self.config_process.state() == QProcess.Running:
|
||||
self.config_process.write(b'\n')
|
||||
self.config_process.waitForBytesWritten(1000)
|
||||
self._config_prompt_state = None
|
||||
self._manual_steps_buffer = []
|
||||
else:
|
||||
# User clicked Cancel or closed the dialog - cancel the workflow
|
||||
self._safe_append_text("\n🛑 Manual steps cancelled by user. Workflow stopped.")
|
||||
# Terminate the configuration process
|
||||
if self.config_process and self.config_process.state() == QProcess.Running:
|
||||
self.config_process.terminate()
|
||||
self.config_process.waitForFinished(2000)
|
||||
# Reset button states
|
||||
self.start_btn.setEnabled(True)
|
||||
self.cancel_btn.setVisible(True)
|
||||
|
||||
def show_next_steps_dialog(self, message):
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton, QHBoxLayout, QApplication
|
||||
dlg = QDialog(self)
|
||||
dlg.setWindowTitle("Next Steps")
|
||||
dlg.setModal(True)
|
||||
layout = QVBoxLayout(dlg)
|
||||
label = QLabel(message)
|
||||
label.setWordWrap(True)
|
||||
layout.addWidget(label)
|
||||
btn_row = QHBoxLayout()
|
||||
btn_return = QPushButton("Return")
|
||||
btn_exit = QPushButton("Exit")
|
||||
btn_row.addWidget(btn_return)
|
||||
btn_row.addWidget(btn_exit)
|
||||
layout.addLayout(btn_row)
|
||||
def on_return():
|
||||
dlg.accept()
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
def on_exit():
|
||||
QApplication.quit()
|
||||
btn_return.clicked.connect(on_return)
|
||||
btn_exit.clicked.connect(on_exit)
|
||||
dlg.exec()
|
||||
|
||||
def go_back(self):
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(self.main_menu_index)
|
||||
|
||||
def update_top_panel(self):
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"ps", "-eo", "pcpu,pmem,comm,args"
|
||||
], stdout=subprocess.PIPE, text=True, timeout=2)
|
||||
lines = result.stdout.splitlines()
|
||||
header = "CPU%\tMEM%\tCOMMAND"
|
||||
filtered = [header]
|
||||
process_rows = []
|
||||
for line in lines[1:]:
|
||||
line_lower = line.lower()
|
||||
# Include jackify-engine and related heavy processes
|
||||
heavy_processes = (
|
||||
"jackify-engine" in line_lower or "7zz" in line_lower or
|
||||
"compressonator" in line_lower or "wine" in line_lower or
|
||||
"wine64" in line_lower or "protontricks" in line_lower
|
||||
)
|
||||
# Include Python processes running configure-modlist command
|
||||
configure_processes = (
|
||||
"python" in line_lower and "configure-modlist" in line_lower
|
||||
)
|
||||
# Include configuration threads that might be running
|
||||
config_threads = (
|
||||
hasattr(self, 'config_thread') and
|
||||
self.config_thread and
|
||||
self.config_thread.isRunning() and
|
||||
("python" in line_lower or "jackify" in line_lower)
|
||||
)
|
||||
|
||||
if (heavy_processes or configure_processes or config_threads) and "jackify-gui.py" not in line_lower:
|
||||
cols = line.strip().split(None, 3)
|
||||
if len(cols) >= 3:
|
||||
process_rows.append(cols)
|
||||
process_rows.sort(key=lambda x: float(x[0]), reverse=True)
|
||||
for cols in process_rows:
|
||||
filtered.append('\t'.join(cols))
|
||||
if len(filtered) == 1:
|
||||
filtered.append("[No Jackify-related processes found]")
|
||||
self.process_monitor.setPlainText('\n'.join(filtered))
|
||||
except Exception as e:
|
||||
self.process_monitor.setPlainText(f"[process info unavailable: {e}]")
|
||||
|
||||
def _on_steam_restart_finished(self, success, message):
|
||||
pass
|
||||
|
||||
def refresh_modlist_list(self):
|
||||
"""Refresh the modlist dropdown by re-detecting ModOrganizer.exe shortcuts"""
|
||||
try:
|
||||
# Re-detect shortcuts using existing backend functionality
|
||||
from jackify.backend.handlers.modlist_handler import ModlistHandler
|
||||
|
||||
# Initialize modlist handler with empty config dict to use default initialization
|
||||
modlist_handler = ModlistHandler({})
|
||||
discovered_modlists = modlist_handler.discover_executable_shortcuts("ModOrganizer.exe")
|
||||
|
||||
# Convert to shortcut_handler format for UI compatibility
|
||||
self.mo2_shortcuts = []
|
||||
for modlist in discovered_modlists:
|
||||
# Convert discovered modlist format to shortcut format
|
||||
shortcut = {
|
||||
'AppName': modlist.get('name', 'Unknown'),
|
||||
'AppID': modlist.get('appid', ''),
|
||||
'StartDir': modlist.get('path', ''),
|
||||
'Exe': f"{modlist.get('path', '')}/ModOrganizer.exe"
|
||||
}
|
||||
self.mo2_shortcuts.append(shortcut)
|
||||
|
||||
# Clear and repopulate the combo box
|
||||
self.shortcut_combo.clear()
|
||||
self.shortcut_combo.addItem("Please Select...")
|
||||
self.shortcut_map.clear()
|
||||
|
||||
for shortcut in self.mo2_shortcuts:
|
||||
display = f"{shortcut.get('AppName', 'Unknown')} ({shortcut.get('StartDir', '')})"
|
||||
self.shortcut_combo.addItem(display)
|
||||
self.shortcut_map.append(shortcut)
|
||||
|
||||
# Show feedback to user in UI only (don't write to log before workflow starts)
|
||||
# Feedback is shown by the updated dropdown items
|
||||
|
||||
except Exception as e:
|
||||
# Don't write to log file before workflow starts - just show error in UI
|
||||
MessageService.warning(self, "Refresh Error", f"Failed to refresh modlist list: {e}", safety_level="low")
|
||||
|
||||
def _calculate_time_taken(self) -> str:
|
||||
"""Calculate and format the time taken for the workflow"""
|
||||
if self._workflow_start_time is None:
|
||||
return "unknown time"
|
||||
|
||||
elapsed_seconds = time.time() - self._workflow_start_time
|
||||
elapsed_minutes = int(elapsed_seconds // 60)
|
||||
elapsed_seconds_remainder = int(elapsed_seconds % 60)
|
||||
|
||||
if elapsed_minutes > 0:
|
||||
if elapsed_minutes == 1:
|
||||
return f"{elapsed_minutes} minute {elapsed_seconds_remainder} seconds"
|
||||
else:
|
||||
return f"{elapsed_minutes} minutes {elapsed_seconds_remainder} seconds"
|
||||
else:
|
||||
return f"{elapsed_seconds_remainder} seconds"
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up any running threads when the screen is closed"""
|
||||
debug_print("DEBUG: cleanup called - cleaning up ConfigurationThread")
|
||||
|
||||
# Clean up config thread if running
|
||||
if hasattr(self, 'config_thread') and self.config_thread and self.config_thread.isRunning():
|
||||
debug_print("DEBUG: Terminating ConfigurationThread")
|
||||
try:
|
||||
self.config_thread.progress_update.disconnect()
|
||||
self.config_thread.configuration_complete.disconnect()
|
||||
self.config_thread.error_occurred.disconnect()
|
||||
except:
|
||||
pass
|
||||
self.config_thread.terminate()
|
||||
self.config_thread.wait(2000) # Wait up to 2 seconds
|
||||
1223
jackify/frontends/gui/screens/configure_new_modlist.py
Normal file
1223
jackify/frontends/gui/screens/configure_new_modlist.py
Normal file
File diff suppressed because it is too large
Load Diff
2528
jackify/frontends/gui/screens/install_modlist.py
Normal file
2528
jackify/frontends/gui/screens/install_modlist.py
Normal file
File diff suppressed because it is too large
Load Diff
128
jackify/frontends/gui/screens/main_menu.py
Normal file
128
jackify/frontends/gui/screens/main_menu.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""
|
||||
MainMenu screen for Jackify GUI (Refactored)
|
||||
"""
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
|
||||
from PySide6.QtGui import QPixmap, QFont
|
||||
from PySide6.QtCore import Qt
|
||||
import os
|
||||
from ..shared_theme import JACKIFY_COLOR_BLUE, LOGO_PATH, DISCLAIMER_TEXT
|
||||
|
||||
class MainMenu(QWidget):
|
||||
def __init__(self, stacked_widget=None, dev_mode=False):
|
||||
super().__init__()
|
||||
self.stacked_widget = stacked_widget
|
||||
self.dev_mode = dev_mode
|
||||
layout = QVBoxLayout()
|
||||
layout.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
|
||||
layout.setContentsMargins(50, 50, 50, 50)
|
||||
layout.setSpacing(20)
|
||||
|
||||
# Title
|
||||
title = QLabel("<b>Jackify</b>")
|
||||
title.setStyleSheet(f"font-size: 24px; color: {JACKIFY_COLOR_BLUE};")
|
||||
title.setAlignment(Qt.AlignHCenter)
|
||||
layout.addWidget(title)
|
||||
|
||||
# Description
|
||||
desc = QLabel(
|
||||
"Manage your modlists with native Linux tools. "
|
||||
"Choose from the options below to install, "
|
||||
"configure, or manage modlists."
|
||||
)
|
||||
desc.setWordWrap(True)
|
||||
desc.setStyleSheet("color: #ccc;")
|
||||
desc.setAlignment(Qt.AlignHCenter)
|
||||
layout.addWidget(desc)
|
||||
|
||||
# Separator
|
||||
layout.addSpacing(16)
|
||||
sep = QLabel()
|
||||
sep.setFixedHeight(2)
|
||||
sep.setStyleSheet("background: #fff;")
|
||||
layout.addWidget(sep)
|
||||
layout.addSpacing(16)
|
||||
|
||||
# Menu buttons
|
||||
button_width = 400
|
||||
button_height = 60
|
||||
MENU_ITEMS = [
|
||||
("Modlist Tasks", "modlist_tasks", "Manage your modlists with native Linux tools"),
|
||||
("Coming Soon...", "coming_soon", "More features coming soon!"),
|
||||
]
|
||||
if self.dev_mode:
|
||||
MENU_ITEMS.append(("Hoolamike Tasks", "hoolamike_tasks", "Manage Hoolamike modding tools"))
|
||||
MENU_ITEMS.append(("Additional Tasks", "additional_tasks", "Additional utilities and tools"))
|
||||
MENU_ITEMS.append(("Exit Jackify", "exit_jackify", "Close the application"))
|
||||
|
||||
for label, action_id, description in MENU_ITEMS:
|
||||
# Main button
|
||||
btn = QPushButton(label)
|
||||
btn.setFixedSize(button_width, 50)
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: #4a5568;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: #5a6578;
|
||||
}}
|
||||
QPushButton:pressed {{
|
||||
background-color: {JACKIFY_COLOR_BLUE};
|
||||
}}
|
||||
""")
|
||||
btn.clicked.connect(lambda checked, a=action_id: self.menu_action(a))
|
||||
|
||||
# Button container with proper alignment
|
||||
btn_container = QWidget()
|
||||
btn_layout = QVBoxLayout()
|
||||
btn_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btn_layout.setSpacing(4)
|
||||
btn_layout.setAlignment(Qt.AlignHCenter)
|
||||
btn_layout.addWidget(btn)
|
||||
|
||||
# Description label with proper alignment
|
||||
desc_label = QLabel(description)
|
||||
desc_label.setAlignment(Qt.AlignHCenter)
|
||||
desc_label.setStyleSheet("color: #999; font-size: 12px;")
|
||||
desc_label.setWordWrap(True)
|
||||
desc_label.setFixedWidth(button_width) # Match button width for proper alignment
|
||||
btn_layout.addWidget(desc_label)
|
||||
|
||||
btn_container.setLayout(btn_layout)
|
||||
layout.addWidget(btn_container)
|
||||
|
||||
# Disclaimer
|
||||
layout.addSpacing(20)
|
||||
disclaimer = QLabel(DISCLAIMER_TEXT)
|
||||
disclaimer.setWordWrap(True)
|
||||
disclaimer.setAlignment(Qt.AlignCenter)
|
||||
disclaimer.setStyleSheet("color: #666; font-size: 10px;")
|
||||
disclaimer.setFixedWidth(button_width)
|
||||
layout.addWidget(disclaimer, alignment=Qt.AlignHCenter)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def menu_action(self, action_id):
|
||||
if action_id == "exit_jackify":
|
||||
from PySide6.QtWidgets import QApplication
|
||||
QApplication.quit()
|
||||
elif action_id == "coming_soon":
|
||||
# Show a friendly message about upcoming features
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
msg = QMessageBox(self)
|
||||
msg.setWindowTitle("Coming Soon")
|
||||
msg.setText("More features are coming in future releases!\n\nFor now, you can install and configure any modlist using the 'Modlist Tasks' button.")
|
||||
msg.setIcon(QMessageBox.Information)
|
||||
msg.exec()
|
||||
elif action_id == "modlist_tasks" and self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(3)
|
||||
elif action_id == "return_main_menu":
|
||||
# This is the main menu, so do nothing
|
||||
pass
|
||||
elif self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(2) # Placeholder for now
|
||||
214
jackify/frontends/gui/screens/modlist_tasks.py
Normal file
214
jackify/frontends/gui/screens/modlist_tasks.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""
|
||||
Migrated Modlist Tasks Screen
|
||||
|
||||
This is a migrated version of the original modlist tasks menu that uses backend services
|
||||
directly instead of subprocess calls to jackify-cli.py.
|
||||
|
||||
Key changes:
|
||||
- Uses backend services directly instead of subprocess.Popen()
|
||||
- Direct backend service integration
|
||||
- Maintains same UI and workflow
|
||||
- Improved error handling and progress reporting
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QGridLayout, QSizePolicy, QApplication, QFrame, QMessageBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal, QTimer
|
||||
from PySide6.QtGui import QFont, QPalette, QColor, QPixmap
|
||||
|
||||
# Import our GUI services
|
||||
from jackify.backend.models.configuration import SystemInfo
|
||||
from ..shared_theme import JACKIFY_COLOR_BLUE
|
||||
|
||||
# Constants
|
||||
DEBUG_BORDERS = False
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModlistTasksScreen(QWidget):
|
||||
"""
|
||||
Migrated Modlist Tasks screen that uses backend services directly.
|
||||
|
||||
This replaces the original ModlistTasksMenu's subprocess calls with
|
||||
direct navigation to existing automated workflows.
|
||||
"""
|
||||
|
||||
def __init__(self, stacked_widget=None, main_menu_index=0, system_info: Optional[SystemInfo] = None, dev_mode=False):
|
||||
super().__init__()
|
||||
logger.info("ModlistTasksScreen initializing (migrated version)")
|
||||
|
||||
self.stacked_widget = stacked_widget
|
||||
self.main_menu_index = main_menu_index
|
||||
self.debug = DEBUG_BORDERS
|
||||
self.dev_mode = dev_mode
|
||||
|
||||
# Initialize backend services
|
||||
if system_info is None:
|
||||
system_info = SystemInfo(is_steamdeck=self._is_steamdeck())
|
||||
self.system_info = system_info
|
||||
|
||||
# Setup UI
|
||||
self._setup_ui()
|
||||
|
||||
logger.info("ModlistTasksScreen initialized (migrated version)")
|
||||
|
||||
def _is_steamdeck(self) -> bool:
|
||||
"""Check if running on Steam Deck"""
|
||||
try:
|
||||
if os.path.exists("/etc/os-release"):
|
||||
with open("/etc/os-release", "r") as f:
|
||||
content = f.read()
|
||||
if "steamdeck" in content:
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _setup_ui(self):
|
||||
"""Set up the user interface"""
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
|
||||
main_layout.setContentsMargins(50, 50, 50, 50)
|
||||
|
||||
if self.debug:
|
||||
self.setStyleSheet("border: 2px solid green;")
|
||||
|
||||
# Header section
|
||||
self._setup_header(main_layout)
|
||||
|
||||
# Menu buttons section
|
||||
self._setup_menu_buttons(main_layout)
|
||||
|
||||
# Bottom navigation
|
||||
self._setup_navigation(main_layout)
|
||||
|
||||
def _setup_header(self, layout):
|
||||
"""Set up the header section"""
|
||||
header_layout = QVBoxLayout()
|
||||
header_layout.setSpacing(2)
|
||||
|
||||
# Title
|
||||
title = QLabel("<b>Modlist Tasks</b>")
|
||||
title.setStyleSheet(f"font-size: 24px; color: {JACKIFY_COLOR_BLUE};")
|
||||
title.setAlignment(Qt.AlignHCenter)
|
||||
header_layout.addWidget(title)
|
||||
|
||||
# Add a spacer to match main menu vertical spacing
|
||||
header_layout.addSpacing(16)
|
||||
|
||||
# Description
|
||||
desc = QLabel(
|
||||
"Manage your modlists with native Linux tools. Choose "
|
||||
"from the options below to install or configure modlists.<br> "
|
||||
)
|
||||
desc.setWordWrap(True)
|
||||
desc.setStyleSheet("color: #ccc;")
|
||||
desc.setAlignment(Qt.AlignHCenter)
|
||||
header_layout.addWidget(desc)
|
||||
|
||||
header_layout.addSpacing(24)
|
||||
|
||||
# Separator
|
||||
sep = QLabel()
|
||||
sep.setFixedHeight(2)
|
||||
sep.setStyleSheet("background: #fff;")
|
||||
header_layout.addWidget(sep)
|
||||
|
||||
header_layout.addSpacing(16)
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
def _setup_menu_buttons(self, layout):
|
||||
"""Set up the menu buttons section"""
|
||||
# Menu options
|
||||
MENU_ITEMS = [
|
||||
("Install a Modlist (Automated)", "install_modlist", "Download and install modlists automatically"),
|
||||
("Configure New Modlist (Post-Download)", "configure_new_modlist", "Configure a newly downloaded modlist"),
|
||||
("Configure Existing Modlist (In Steam)", "configure_existing_modlist", "Reconfigure an existing Steam modlist"),
|
||||
]
|
||||
if self.dev_mode:
|
||||
MENU_ITEMS.append(("Install Wabbajack Application", "install_wabbajack", "Set up the Wabbajack application"))
|
||||
MENU_ITEMS.append(("Return to Main Menu", "return_main_menu", "Go back to the main menu"))
|
||||
|
||||
# Create grid layout for buttons
|
||||
button_grid = QGridLayout()
|
||||
button_grid.setSpacing(16)
|
||||
button_grid.setAlignment(Qt.AlignHCenter)
|
||||
|
||||
button_width = 400
|
||||
button_height = 50
|
||||
|
||||
for i, (label, action_id, description) in enumerate(MENU_ITEMS):
|
||||
# Create button
|
||||
btn = QPushButton(label)
|
||||
btn.setFixedSize(button_width, button_height)
|
||||
btn.setStyleSheet(f"""
|
||||
QPushButton {{
|
||||
background-color: #4a5568;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: #5a6578;
|
||||
}}
|
||||
QPushButton:pressed {{
|
||||
background-color: {JACKIFY_COLOR_BLUE};
|
||||
}}
|
||||
""")
|
||||
btn.clicked.connect(lambda checked, a=action_id: self.menu_action(a))
|
||||
|
||||
# Create description label
|
||||
desc_label = QLabel(description)
|
||||
desc_label.setAlignment(Qt.AlignHCenter)
|
||||
desc_label.setStyleSheet("color: #999; font-size: 12px;")
|
||||
desc_label.setWordWrap(True)
|
||||
desc_label.setFixedWidth(button_width)
|
||||
|
||||
# Add to grid
|
||||
button_grid.addWidget(btn, i * 2, 0, Qt.AlignHCenter)
|
||||
button_grid.addWidget(desc_label, i * 2 + 1, 0, Qt.AlignHCenter)
|
||||
|
||||
layout.addLayout(button_grid)
|
||||
|
||||
def _setup_navigation(self, layout):
|
||||
"""Set up the navigation section"""
|
||||
# Remove the bottom navigation bar entirely (no gray Back to Main Menu button)
|
||||
pass
|
||||
|
||||
def menu_action(self, action_id):
|
||||
"""Handle menu button clicks"""
|
||||
logger.info(f"Modlist tasks menu action: {action_id}")
|
||||
|
||||
if not self.stacked_widget:
|
||||
return
|
||||
|
||||
# Navigate to different screens based on action
|
||||
if action_id == "return_main_menu":
|
||||
self.stacked_widget.setCurrentIndex(0)
|
||||
elif action_id == "install_modlist":
|
||||
self.stacked_widget.setCurrentIndex(4)
|
||||
elif action_id == "configure_new_modlist":
|
||||
self.stacked_widget.setCurrentIndex(5)
|
||||
elif action_id == "configure_existing_modlist":
|
||||
self.stacked_widget.setCurrentIndex(6)
|
||||
|
||||
def go_back(self):
|
||||
"""Return to main menu"""
|
||||
if self.stacked_widget:
|
||||
self.stacked_widget.setCurrentIndex(self.main_menu_index)
|
||||
|
||||
def cleanup(self):
|
||||
"""Clean up resources when the screen is closed"""
|
||||
pass
|
||||
1829
jackify/frontends/gui/screens/tuxborn_installer.py
Normal file
1829
jackify/frontends/gui/screens/tuxborn_installer.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user