Release v0.7 - Tools Hub, Engine Choice, NXM Link Handling, Native Component Install, NSF/CSF Support

This commit is contained in:
Omni
2026-06-21 21:47:48 +01:00
parent 33b3fbaed2
commit 7fff107389
483 changed files with 11150 additions and 6050 deletions
+203 -27
View File
@@ -1,8 +1,7 @@
"""
Additional Tasks & Tools Screen
Simple screen for TTW automation only.
Follows the same pattern as ModlistTasksScreen.
Additional tools and automation. Follows the same pattern as ModlistTasksScreen.
"""
import logging
@@ -23,7 +22,7 @@ logger = logging.getLogger(__name__)
class AdditionalTasksScreen(QWidget):
"""Simple Additional Tasks screen for TTW only"""
"""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):
@@ -67,7 +66,7 @@ class AdditionalTasksScreen(QWidget):
header_layout.addSpacing(10)
# Description area with fixed height
desc = QLabel("TTW automation, Wabbajack installer, and additional tools.")
desc = QLabel("Wabbajack installer, MO2 setup, and additional tools.")
desc.setWordWrap(True)
desc.setStyleSheet("color: #ccc; font-size: 13px;")
desc.setAlignment(Qt.AlignHCenter)
@@ -93,10 +92,11 @@ class AdditionalTasksScreen(QWidget):
"""Set up the menu buttons section"""
# Menu options
MENU_ITEMS = [
("Install TTW", "ttw_install", "Install Tale of Two Wastelands using TTW_Linux_Installer"),
("Install Wabbajack", "wabbajack_install", "Install Wabbajack.exe via Proton (automated setup)"),
("Setup Mod Organizer 2", "setup_mo2", "Download and configure a standalone MO2 instance"),
("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"),
]
@@ -148,25 +148,19 @@ class AdditionalTasksScreen(QWidget):
def _handle_button_click(self, action_id):
"""Handle button clicks"""
if action_id == "ttw_install":
self._show_ttw_info()
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 == "coming_soon":
self._show_coming_soon_info()
elif action_id == "diagnostic_bundle":
self._run_diagnostic_bundle()
elif action_id == "return_main_menu":
self._return_to_main_menu()
def _show_ttw_info(self):
"""Navigate to TTW installation screen"""
if self.stacked_widget:
# 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:
@@ -178,20 +172,202 @@ class AdditionalTasksScreen(QWidget):
if self.stacked_widget:
self.stacked_widget.setCurrentIndex(self.install_mo2_screen_index)
def _show_coming_soon_info(self):
"""Show coming soon info"""
from ..services.message_service import MessageService
MessageService.information(
self,
"Coming Soon",
"Additional tools and features will be added in future updates.\n\n"
"Check back later for more functionality!"
)
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: