Release v0.7.1 - Remote Manifest System, Stability Fixes

This commit is contained in:
Omni
2026-07-02 21:04:19 +01:00
parent 6def3b480a
commit e5d329a2dc
96 changed files with 1950 additions and 1382 deletions
@@ -339,8 +339,9 @@ class ManualDownloadDialog(QDialog):
colour = _STATUS_COLOURS.get(item.status, '#808080')
status_cell = QTableWidgetItem(_STATUS_LABELS.get(item.status, item.status))
status_cell.setForeground(QColor(colour))
if item.error_message:
status_cell.setToolTip(item.error_message)
tooltip_parts = [p for p in (item.download_reason, item.error_message) if p]
if tooltip_parts:
status_cell.setToolTip("\n".join(tooltip_parts))
self._table.setItem(row, _COL_STATUS, status_cell)
def _update_row(self, row: int, item: DownloadItem) -> None:
@@ -349,7 +350,8 @@ class ManualDownloadDialog(QDialog):
if status_cell:
status_cell.setText(_STATUS_LABELS.get(item.status, item.status))
status_cell.setForeground(QColor(_STATUS_COLOURS.get(item.status, '#808080')))
status_cell.setToolTip(item.error_message or "")
tooltip_parts = [p for p in (item.download_reason, item.error_message) if p]
status_cell.setToolTip("\n".join(tooltip_parts))
def _rebuild_row_map(self) -> None:
self._row_map.clear()
@@ -0,0 +1,138 @@
"""Guided dialog for installing a Nexus-only tool via manual browser download."""
import logging
from pathlib import Path
from typing import Optional
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QLineEdit, QFileDialog, QFrame,
)
from jackify.frontends.gui.services.message_service import open_url
logger = logging.getLogger(__name__)
class NexusManualInstallDialog(QDialog):
"""
Guides the user through manually downloading a Nexus-only tool and handing
the archive to Jackify for extraction and installation.
"""
def __init__(self, tool_id: str, display_name: str, nexus_url: str, parent=None):
super().__init__(parent)
self._tool_id = tool_id
self._nexus_url = nexus_url
self._archive_path: Optional[Path] = None
self.setWindowTitle(f"Install {display_name}")
self.setModal(True)
self.setMinimumWidth(480)
self.setStyleSheet("QDialog { background: #181818; color: #fff; }")
self._build_ui(display_name)
self.adjustSize()
def _build_ui(self, display_name: str) -> None:
main_layout = QVBoxLayout(self)
main_layout.setSpacing(0)
main_layout.setContentsMargins(20, 20, 20, 20)
card = QFrame(self)
card.setObjectName("dialogCard")
card.setFrameShape(QFrame.StyledPanel)
card.setFrameShadow(QFrame.Raised)
card.setStyleSheet(
"QFrame#dialogCard { "
" background: #2d2d2d; "
" border-radius: 12px; "
" border: 1px solid #555; "
"}"
)
card_layout = QVBoxLayout(card)
card_layout.setSpacing(16)
card_layout.setContentsMargins(28, 28, 28, 28)
title_label = QLabel(f"Manual download required: {display_name}")
title_label.setStyleSheet("color: #3fd0ea; font-size: 14px; font-weight: 600;")
title_label.setWordWrap(True)
card_layout.addWidget(title_label)
body_label = QLabel(
f"{display_name} is only available on Nexus Mods. As you do not have Nexus "
"Premium, please perform the following steps manually:\n\n"
f"1. Click 'Open Nexus Page' below and click Manual Download on the Nexus page "
f"to download {display_name}\n"
"2. Once the download is complete, click 'Browse...' below and select the "
"downloaded archive\n"
"3. Click Install to complete the installation."
)
body_label.setWordWrap(True)
card_layout.addWidget(body_label)
nexus_btn = QPushButton("Open Nexus Page")
nexus_btn.clicked.connect(self._open_nexus)
card_layout.addWidget(nexus_btn)
file_row = QHBoxLayout()
self._file_edit = QLineEdit()
self._file_edit.setPlaceholderText("No file selected...")
self._file_edit.setReadOnly(True)
self._file_edit.setStyleSheet(
"QLineEdit { "
" background: #1a1a1a; "
" color: #fff; "
" border: 1px solid #555; "
" border-radius: 4px; "
" padding: 8px; "
"}"
)
file_row.addWidget(self._file_edit)
browse_btn = QPushButton("Browse...")
browse_btn.setMinimumWidth(90)
browse_btn.clicked.connect(self._browse)
file_row.addWidget(browse_btn)
card_layout.addLayout(file_row)
btn_row = QHBoxLayout()
btn_row.addStretch()
cancel_btn = QPushButton("Cancel")
cancel_btn.setMinimumWidth(100)
cancel_btn.clicked.connect(self.reject)
btn_row.addWidget(cancel_btn)
self._install_btn = QPushButton("Install")
self._install_btn.setDefault(True)
self._install_btn.setMinimumWidth(100)
self._install_btn.setEnabled(False)
self._install_btn.clicked.connect(self.accept)
btn_row.addWidget(self._install_btn)
card_layout.addLayout(btn_row)
main_layout.addWidget(card)
def _open_nexus(self) -> None:
if self._nexus_url:
open_url(self._nexus_url)
def _browse(self) -> None:
path, _ = QFileDialog.getOpenFileName(
self,
"Select downloaded archive",
str(Path.home() / "Downloads"),
"Archives (*.zip *.tar.gz *.tar.xz *.7z);;All files (*)",
)
if path:
self._archive_path = Path(path)
self._file_edit.setText(path)
self._install_btn.setEnabled(True)
@property
def selected_archive(self) -> Optional[Path]:
return self._archive_path
@property
def tool_id(self) -> str:
return self._tool_id
@@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
from jackify.backend.services.nxm_url import NxmUrl
from jackify.frontends.gui.shared_theme import JACKIFY_COLOR_BLUE
from jackify.frontends.gui.mixins.thread_lifecycle_mixin import ThreadLifecycleMixin
import jackify.backend.services.nxm_session as nxm_session
logger = logging.getLogger(__name__)
@@ -117,7 +118,7 @@ class _DownloadThread(QThread):
self.progress.emit(downloaded, total)
class NxmDownloadDialog(QDialog):
class NxmDownloadDialog(ThreadLifecycleMixin, QDialog):
"""Modlist picker and download runner for incoming nxm:// links.
When auto_start_modlist is provided the picker is hidden and the download
@@ -72,9 +72,7 @@ class SettingsDialog(SettingsDialogTabsMixin, SettingsDialogProtonMixin, QDialog
main_layout.addLayout(btn_layout)
except Exception as e:
print(f"[ERROR] Exception in SettingsDialog.__init__: {e}")
import traceback
traceback.print_exc()
logger.error(f"Exception in SettingsDialog.__init__: {e}", exc_info=True)
def _toggle_api_key_visibility(self, checked):
eye_icon = QIcon.fromTheme("view-visible")
@@ -402,7 +400,7 @@ class SettingsDialog(SettingsDialogTabsMixin, SettingsDialogProtonMixin, QDialog
screen.refresh_paths()
except Exception as e:
print(f"Warning: Could not refresh GUI paths: {e}")
logger.warning(f"Could not refresh GUI paths: {e}")
def _bold_label(self, text):
label = QLabel(text)
@@ -16,6 +16,8 @@ from PySide6.QtWidgets import (
from PySide6.QtCore import Qt, QTimer
from PySide6.QtGui import QPixmap, QIcon, QFont
from jackify.frontends.gui.services.message_service import open_url
logger = logging.getLogger(__name__)
@@ -38,6 +40,8 @@ class SuccessDialog(QDialog):
time_taken: str,
game_name: str = None,
verification_results=None,
disabled_problem_mods=None,
readme_url: str = None,
parent=None,
):
super().__init__(parent)
@@ -46,6 +50,8 @@ class SuccessDialog(QDialog):
self.time_taken = time_taken
self.game_name = game_name
self.verification_results = verification_results
self.disabled_problem_mods = disabled_problem_mods or []
self.readme_url = readme_url
self.setWindowTitle("Complete" if (verification_results and verification_results.failures) else "Success!")
self.setWindowModality(Qt.NonModal)
self.setAttribute(Qt.WA_ShowWithoutActivating, True)
@@ -180,6 +186,25 @@ class SuccessDialog(QDialog):
if self.verification_results is not None:
self._add_verification_section(card_layout)
# Problem mods that were auto-disabled
if self.disabled_problem_mods:
self._add_problem_mods_section(card_layout)
# Readme link (install workflow only)
if self.readme_url:
readme_label = QLabel(
f'<a href="{self.readme_url}" style="color:#3fd0ea; text-decoration:none;">'
"Open modlist readme"
"</a>"
)
readme_label.setAlignment(Qt.AlignCenter)
readme_label.setStyleSheet(
"QLabel { color: #3fd0ea; font-size: 11px; margin-top: 4px; padding: 4px; background-color: transparent; }"
)
readme_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
readme_label.linkActivated.connect(open_url)
card_layout.addWidget(readme_label)
# Subtle Ko-Fi support link
kofi_label = QLabel('<a href="https://ko-fi.com/omni1" style="color:#3fd0ea; text-decoration:none;">Enjoying Jackify? Support development ♥</a>')
kofi_label.setAlignment(Qt.AlignCenter)
@@ -193,7 +218,7 @@ class SuccessDialog(QDialog):
"}"
)
kofi_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
kofi_label.setOpenExternalLinks(True)
kofi_label.linkActivated.connect(open_url)
card_layout.addWidget(kofi_label)
layout.addStretch()
@@ -390,6 +415,29 @@ class SuccessDialog(QDialog):
except Exception as exc:
logger.error("Could not open verification dialog: %s", exc)
def _add_problem_mods_section(self, card_layout):
"""Add an auto-disabled problem mods section to the card layout."""
from PySide6.QtWidgets import QFrame
sep = QFrame()
sep.setFrameShape(QFrame.HLine)
sep.setStyleSheet("color: #444;")
card_layout.addWidget(sep)
header = QLabel("Compatibility Notice")
header.setStyleSheet("font-size: 12px; font-weight: bold; color: #c8a050; margin-top: 4px;")
card_layout.addWidget(header)
msg_text = (
"Due to known compatibility issues with Proton, the following mods were "
"automatically disabled:\n\n"
+ "\n".join(f" - {name}" for name in self.disabled_problem_mods)
)
msg_label = QLabel(msg_text)
msg_label.setWordWrap(True)
msg_label.setStyleSheet("font-size: 11px; color: #bbb; margin-bottom: 4px;")
card_layout.addWidget(msg_label)
def _update_countdown(self):
if self._countdown > 0:
self.return_btn.setText(f"{self._orig_return_text} ({self._countdown}s)")