mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 01:23:46 +02:00
389 lines
14 KiB
Python
389 lines
14 KiB
Python
"""
|
|
Additional Tasks & Tools Screen
|
|
|
|
Additional tools and automation. Follows the same pattern as ModlistTasksScreen.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from PySide6.QtWidgets import (
|
|
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
|
QGridLayout
|
|
)
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtGui import QFont
|
|
|
|
from jackify.backend.models.configuration import SystemInfo
|
|
from ..shared_theme import JACKIFY_COLOR_BLUE
|
|
from ..utils import set_responsive_minimum
|
|
from ..mixins.thread_lifecycle_mixin import ThreadLifecycleMixin
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AdditionalTasksScreen(ThreadLifecycleMixin, QWidget):
|
|
"""Additional Tasks screen for automation and standalone tools."""
|
|
|
|
def __init__(self, stacked_widget=None, main_menu_index=0, system_info: Optional[SystemInfo] = None,
|
|
install_mo2_screen_index: int = 9):
|
|
super().__init__()
|
|
self.stacked_widget = stacked_widget
|
|
self.main_menu_index = main_menu_index
|
|
self.system_info = system_info or SystemInfo(is_steamdeck=False)
|
|
self.install_mo2_screen_index = install_mo2_screen_index
|
|
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
"""Set up the user interface following ModlistTasksScreen pattern"""
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(30, 30, 30, 30)
|
|
layout.setSpacing(12) # Match main menu spacing
|
|
|
|
# Header section
|
|
self._setup_header(layout)
|
|
|
|
# Menu buttons section
|
|
self._setup_menu_buttons(layout)
|
|
|
|
# Bottom spacer
|
|
layout.addStretch()
|
|
self.setLayout(layout)
|
|
|
|
def _setup_header(self, layout):
|
|
"""Set up the header section"""
|
|
header_widget = QWidget()
|
|
header_layout = QVBoxLayout()
|
|
header_layout.setContentsMargins(0, 0, 0, 0)
|
|
header_layout.setSpacing(2)
|
|
|
|
# Title
|
|
title = QLabel("<b>Additional Tasks & Tools</b>")
|
|
title.setStyleSheet(f"font-size: 20px; color: {JACKIFY_COLOR_BLUE};")
|
|
title.setAlignment(Qt.AlignHCenter)
|
|
header_layout.addWidget(title)
|
|
|
|
header_layout.addSpacing(10)
|
|
|
|
# Description area with fixed height
|
|
desc = QLabel("Wabbajack installer, MO2 setup, and additional tools.")
|
|
desc.setWordWrap(True)
|
|
desc.setStyleSheet("color: #ccc; font-size: 13px;")
|
|
desc.setAlignment(Qt.AlignHCenter)
|
|
desc.setMaximumHeight(50) # Fixed height for description zone
|
|
header_layout.addWidget(desc)
|
|
|
|
header_layout.addSpacing(12)
|
|
|
|
# Separator
|
|
sep = QLabel()
|
|
sep.setFixedHeight(2)
|
|
sep.setFixedWidth(400) # Match button width
|
|
sep.setStyleSheet("background: #fff;")
|
|
header_layout.addWidget(sep, alignment=Qt.AlignHCenter)
|
|
|
|
header_layout.addSpacing(10)
|
|
|
|
header_widget.setLayout(header_layout)
|
|
header_widget.setFixedHeight(120) # Fixed total header height
|
|
layout.addWidget(header_widget)
|
|
|
|
def _setup_menu_buttons(self, layout):
|
|
"""Set up the menu buttons section"""
|
|
# Menu options
|
|
MENU_ITEMS = [
|
|
("Run Install Verifier", "run_verifier", "Check an installed modlist for common configuration problems"),
|
|
("Configure Tool Compatibility", "tool_config", "Apply xEdit, Pandora and DLL fixes to an existing modlist prefix"),
|
|
("Setup Mod Organizer 2", "setup_mo2", "Download and configure a standalone MO2 instance"),
|
|
("Install Wabbajack", "wabbajack_install", "Install Wabbajack.exe via Proton (automated setup)"),
|
|
("Create Diagnostic Bundle", "diagnostic_bundle", "Package logs and system info for support reporting"),
|
|
("Return to Main Menu", "return_main_menu", "Go back to the main menu"),
|
|
]
|
|
|
|
# Create grid layout for buttons (mirror ModlistTasksScreen pattern)
|
|
button_grid = QGridLayout()
|
|
button_grid.setSpacing(12)
|
|
button_grid.setAlignment(Qt.AlignHCenter)
|
|
|
|
button_width = 400
|
|
button_height = 40
|
|
|
|
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: 6px;
|
|
font-size: 13px;
|
|
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._handle_button_click(a))
|
|
|
|
# Description label
|
|
desc_label = QLabel(description)
|
|
desc_label.setAlignment(Qt.AlignHCenter)
|
|
desc_label.setStyleSheet("color: #999; font-size: 11px;")
|
|
desc_label.setWordWrap(True)
|
|
desc_label.setFixedWidth(button_width)
|
|
|
|
# Add to grid (button row, then description row)
|
|
button_grid.addWidget(btn, i * 2, 0, Qt.AlignHCenter)
|
|
button_grid.addWidget(desc_label, i * 2 + 1, 0, Qt.AlignHCenter)
|
|
|
|
layout.addLayout(button_grid)
|
|
|
|
# Removed _create_menu_button; using same pattern as ModlistTasksScreen
|
|
|
|
def _handle_button_click(self, action_id):
|
|
"""Handle button clicks"""
|
|
if action_id == "run_verifier":
|
|
self._run_install_verifier()
|
|
elif action_id == "wabbajack_install":
|
|
self._show_wabbajack_installer()
|
|
elif action_id == "setup_mo2":
|
|
self._show_mo2_setup()
|
|
elif action_id == "tool_config":
|
|
self._show_tool_config()
|
|
elif action_id == "diagnostic_bundle":
|
|
self._run_diagnostic_bundle()
|
|
elif action_id == "return_main_menu":
|
|
self._return_to_main_menu()
|
|
|
|
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_mo2_setup(self):
|
|
"""Navigate to standalone MO2 setup screen"""
|
|
if self.stacked_widget:
|
|
self.stacked_widget.setCurrentIndex(self.install_mo2_screen_index)
|
|
|
|
def _show_tool_config(self):
|
|
if self.stacked_widget:
|
|
self.stacked_widget.setCurrentIndex(11)
|
|
|
|
def _run_install_verifier(self):
|
|
"""Prompt user to pick a modlist, run the verifier, and show results."""
|
|
from ..services.message_service import MessageService
|
|
try:
|
|
from jackify.backend.services.install_verifier_service import _load_verifier
|
|
verifier_mod = _load_verifier()
|
|
modlists = verifier_mod.discover_installed_modlists()
|
|
except Exception as e:
|
|
MessageService.critical(
|
|
self,
|
|
"Verifier Error",
|
|
f"Could not load install verifier: {e}",
|
|
)
|
|
return
|
|
|
|
if not modlists:
|
|
MessageService.information(
|
|
self,
|
|
"No Modlists Found",
|
|
"No installed modlists were found in Steam shortcuts.\n\n"
|
|
"Ensure ModOrganizer.exe shortcuts exist in Steam for your modlists.",
|
|
)
|
|
return
|
|
|
|
from PySide6.QtWidgets import QDialog, QVBoxLayout, QListWidget, QListWidgetItem, QPushButton, QLabel, QHBoxLayout
|
|
picker = QDialog(self)
|
|
picker.setWindowTitle("Select Modlist to Verify")
|
|
picker.setMinimumWidth(480)
|
|
picker.setMinimumHeight(260)
|
|
picker_layout = QVBoxLayout(picker)
|
|
picker_layout.addWidget(QLabel("Select a modlist to verify:"))
|
|
|
|
lw = QListWidget()
|
|
for m in modlists:
|
|
pfx_ok = m["pfx"] and m["pfx"].is_dir()
|
|
suffix = "" if pfx_ok else " (prefix not found)"
|
|
item = QListWidgetItem(f"{m['name']}{suffix}")
|
|
item.setData(1000, m)
|
|
lw.addItem(item)
|
|
lw.setCurrentRow(0)
|
|
picker_layout.addWidget(lw)
|
|
|
|
btn_row = QHBoxLayout()
|
|
ok_btn = QPushButton("Run Verifier")
|
|
cancel_btn = QPushButton("Cancel")
|
|
btn_row.addStretch()
|
|
btn_row.addWidget(ok_btn)
|
|
btn_row.addWidget(cancel_btn)
|
|
picker_layout.addLayout(btn_row)
|
|
|
|
ok_btn.clicked.connect(picker.accept)
|
|
cancel_btn.clicked.connect(picker.reject)
|
|
lw.itemDoubleClicked.connect(lambda _: picker.accept())
|
|
|
|
if picker.exec() != QDialog.Accepted:
|
|
return
|
|
|
|
selected_item = lw.currentItem()
|
|
if not selected_item:
|
|
return
|
|
selected = selected_item.data(1000)
|
|
|
|
pfx = selected.get("pfx")
|
|
if not pfx or not pfx.is_dir():
|
|
MessageService.warning(
|
|
self,
|
|
"Prefix Not Found",
|
|
f"The Proton prefix for '{selected['name']}' was not found.\n\n"
|
|
"Launch the modlist from Steam at least once to create the prefix.",
|
|
)
|
|
return
|
|
|
|
from PySide6.QtCore import QThread, Signal as _Signal
|
|
|
|
class _VerifierThread(QThread):
|
|
done = _Signal(object)
|
|
|
|
def __init__(self, verifier_module, entry, parent=None):
|
|
super().__init__(parent)
|
|
self._verifier = verifier_module
|
|
self._entry = entry
|
|
|
|
def run(self):
|
|
try:
|
|
r = self._verifier.run_verification(
|
|
pfx=self._entry["pfx"],
|
|
modlist_dir=self._entry["modlist_dir"],
|
|
game_type=self._entry["game_type"],
|
|
appid=self._entry["appid"],
|
|
modlist_name=self._entry.get("name", ""),
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("On-demand verifier error: %s", exc)
|
|
r = None
|
|
self.done.emit(r)
|
|
|
|
from jackify.frontends.gui.services.message_service import MessageService as _MS
|
|
progress_dlg = QDialog(self)
|
|
progress_dlg.setWindowTitle("Verifying...")
|
|
progress_dlg.setModal(True)
|
|
prog_layout = QVBoxLayout(progress_dlg)
|
|
prog_layout.addWidget(QLabel(f"Running verifier for '{selected['name']}'...\nThis may take a moment."))
|
|
progress_dlg.setFixedSize(340, 100)
|
|
progress_dlg.show()
|
|
|
|
self._verifier_ondemand_thread = _VerifierThread(verifier_mod, selected, parent=self)
|
|
|
|
def _on_done(results):
|
|
progress_dlg.accept()
|
|
self._verifier_ondemand_thread = None
|
|
if results is None:
|
|
MessageService.critical(
|
|
self,
|
|
"Verifier Error",
|
|
"The verifier encountered an error and could not complete.",
|
|
)
|
|
return
|
|
from jackify.frontends.gui.dialogs.verification_results_dialog import VerificationResultsDialog
|
|
dlg = VerificationResultsDialog(results, parent=self)
|
|
dlg.exec()
|
|
|
|
self._verifier_ondemand_thread.done.connect(_on_done)
|
|
self._verifier_ondemand_thread.start()
|
|
|
|
def _run_diagnostic_bundle(self):
|
|
"""Open the diagnostic bundle dialog; bundle is only created when the user confirms."""
|
|
from PySide6.QtWidgets import (
|
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit
|
|
)
|
|
from PySide6.QtCore import QThread, Signal as _Signal
|
|
|
|
class _BundleThread(QThread):
|
|
done = _Signal(object, str) # (bundle_path or None, error_msg)
|
|
|
|
def run(self):
|
|
try:
|
|
from jackify.backend.services.diagnostic_service import build_bundle
|
|
path = build_bundle()
|
|
self.done.emit(path, "")
|
|
except Exception as exc:
|
|
self.done.emit(None, str(exc))
|
|
|
|
dlg = QDialog(self)
|
|
dlg.setWindowTitle("Diagnostic Bundle")
|
|
dlg.setMinimumWidth(600)
|
|
dlg.setMinimumHeight(220)
|
|
dlg.setStyleSheet("QDialog { background: #181818; color: #fff; }")
|
|
|
|
layout = QVBoxLayout(dlg)
|
|
layout.setContentsMargins(20, 16, 20, 16)
|
|
layout.setSpacing(10)
|
|
|
|
status_label = QLabel("Package logs and system info into a file for support reporting.")
|
|
layout.addWidget(status_label)
|
|
|
|
path_box = QTextEdit()
|
|
path_box.setReadOnly(True)
|
|
path_box.setMinimumHeight(60)
|
|
path_box.setVisible(False)
|
|
layout.addWidget(path_box)
|
|
|
|
btn_row = QHBoxLayout()
|
|
create_btn = QPushButton("Create Bundle")
|
|
cancel_btn = QPushButton("Cancel")
|
|
btn_row.addWidget(create_btn)
|
|
btn_row.addWidget(cancel_btn)
|
|
layout.addLayout(btn_row)
|
|
|
|
cancel_btn.clicked.connect(dlg.reject)
|
|
|
|
def _on_create():
|
|
create_btn.setEnabled(False)
|
|
cancel_btn.setEnabled(False)
|
|
status_label.setText("Collecting logs and system info...")
|
|
self._diag_thread = _BundleThread(parent=self)
|
|
self._diag_thread.done.connect(_on_done)
|
|
self._diag_thread.start()
|
|
|
|
def _on_done(bundle_path, error):
|
|
self._diag_thread = None
|
|
cancel_btn.setEnabled(True)
|
|
cancel_btn.setText("Close")
|
|
if not bundle_path:
|
|
status_label.setText(f"Failed: {error}")
|
|
return
|
|
status_label.setText("Bundle created:")
|
|
path_box.setPlainText(str(bundle_path))
|
|
path_box.setVisible(True)
|
|
|
|
create_btn.clicked.connect(_on_create)
|
|
dlg.exec()
|
|
|
|
def _return_to_main_menu(self):
|
|
"""Return to main menu"""
|
|
if self.stacked_widget:
|
|
self.stacked_widget.setCurrentIndex(self.main_menu_index)
|
|
|
|
def showEvent(self, event):
|
|
"""Called when the widget becomes visible - resize to compact size"""
|
|
super().showEvent(event)
|
|
try:
|
|
main_window = self.window()
|
|
if main_window:
|
|
from PySide6.QtCore import QSize
|
|
# Only set minimum size - DO NOT RESIZE
|
|
main_window.setMaximumSize(QSize(16777215, 16777215))
|
|
set_responsive_minimum(main_window, min_width=960, min_height=420)
|
|
# DO NOT resize - let window stay at current size
|
|
except Exception:
|
|
pass |