mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 03:13:44 +02:00
Release v0.7 - Tools Hub, Engine Choice, NXM Link Handling, Native Component Install, NSF/CSF Support
This commit is contained in:
@@ -6,5 +6,6 @@ Custom dialogs for the Jackify GUI application.
|
||||
|
||||
from .completion_dialog import NextStepsDialog
|
||||
from .success_dialog import SuccessDialog
|
||||
from .verification_results_dialog import VerificationResultsDialog
|
||||
|
||||
__all__ = ['NextStepsDialog', 'SuccessDialog']
|
||||
__all__ = ['NextStepsDialog', 'SuccessDialog', 'VerificationResultsDialog']
|
||||
@@ -112,8 +112,8 @@ class NextStepsDialog(QDialog):
|
||||
content_text.setReadOnly(True)
|
||||
content_text.setStyleSheet(
|
||||
"QTextEdit { "
|
||||
" background-color: #f8f9fa; "
|
||||
" border: 1px solid #dee2e6; "
|
||||
" background-color: #2a2a2a; "
|
||||
" border: 1px solid #3a3a3a; "
|
||||
" border-radius: 6px; "
|
||||
" padding: 12px; "
|
||||
" font-family: 'Segoe UI', Arial, sans-serif; "
|
||||
@@ -139,7 +139,7 @@ class NextStepsDialog(QDialog):
|
||||
return_btn.clicked.connect(self.accept) # This will close dialog and return to menu
|
||||
return_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #3498db; "
|
||||
" background-color: #1a5fa8; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
@@ -147,10 +147,10 @@ class NextStepsDialog(QDialog):
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #2980b9; "
|
||||
" background-color: #2470b0; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #21618c; "
|
||||
" background-color: #3fd0ea; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(return_btn)
|
||||
@@ -163,7 +163,7 @@ class NextStepsDialog(QDialog):
|
||||
exit_btn.clicked.connect(self.reject) # This will close dialog and potentially exit app
|
||||
exit_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #95a5a6; "
|
||||
" background-color: #4a5568; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
@@ -171,10 +171,10 @@ class NextStepsDialog(QDialog):
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #7f8c8d; "
|
||||
" background-color: #5a6578; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #6c7b7d; "
|
||||
" background-color: #3fd0ea; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(exit_btn)
|
||||
@@ -194,15 +194,12 @@ class NextStepsDialog(QDialog):
|
||||
completion_title = "Modlist Configuration complete!" if is_existing else "Modlist Install and Configuration complete!"
|
||||
completion_log = "Configure_Existing_Modlist_workflow.log" if is_existing else "Configure_New_Modlist_workflow.log"
|
||||
|
||||
completion_text = f"""✓ Configuration completed successfully!
|
||||
completion_text = f"""Configuration completed successfully!
|
||||
|
||||
{completion_title}
|
||||
|
||||
• You should now be able to Launch '{self.modlist_name}' through Steam.
|
||||
• Congratulations and enjoy the game!
|
||||
|
||||
NOTE: If you experience ENB issues, consider using GE-Proton 10-14 instead of
|
||||
Valve's Proton 10 (known ENB compatibility issues in Valve's Proton 10).
|
||||
- You should now be able to Launch '{self.modlist_name}' through Steam.
|
||||
- Congratulations and enjoy the game!
|
||||
|
||||
Detailed log available at: {get_jackify_logs_dir()}/{completion_log}"""
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class ENBProtonDialog(QDialog):
|
||||
"QFrame#enbCard { "
|
||||
" background: #23272e; "
|
||||
" border-radius: 12px; "
|
||||
" border: 2px solid #e67e22; " # Orange border for warning
|
||||
" border: 2px solid #f0c040;" # Orange border for warning
|
||||
"}"
|
||||
)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
|
||||
@@ -67,17 +67,26 @@ class ENBProtonDialog(QDialog):
|
||||
"QLabel { "
|
||||
" font-size: 24px; "
|
||||
" font-weight: 700; "
|
||||
" color: #e67e22; " # Orange warning color
|
||||
" color: #f0c040;" # 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:"
|
||||
)
|
||||
from jackify.backend.data.modlist_proton_requirements import get_proton_requirement
|
||||
_proton_req = get_proton_requirement(self.modlist_name)
|
||||
|
||||
if _proton_req:
|
||||
warning_text = (
|
||||
f"<span style='color:#3fb7d6; font-weight:600;'>{self.modlist_name}</span> "
|
||||
f"requires a specific Proton version for ENB compatibility:"
|
||||
)
|
||||
else:
|
||||
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)
|
||||
@@ -93,17 +102,28 @@ class ENBProtonDialog(QDialog):
|
||||
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>"
|
||||
)
|
||||
if _proton_req:
|
||||
versions_text = (
|
||||
"<div style='text-align: left; padding: 12px; background: #1a1d23; border-radius: 8px; margin: 8px 0;'>"
|
||||
f"<div style='font-size: 16px; color: #3fd0ea; font-weight: 700; margin-bottom: 8px;'>{_proton_req['required']}</div>"
|
||||
f"<div style='font-size: 13px; color: #b0b0b0;'>{_proton_req['note']}</div>"
|
||||
"</div>"
|
||||
)
|
||||
else:
|
||||
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: #3fd0ea;'>Proton-CachyOS</b><br/>"
|
||||
"- <b style='color: #aaa;'>GE-Proton</b><br/>"
|
||||
"- <b style='color: #777;'>Proton 9</b> from Valve"
|
||||
"</div>"
|
||||
"<div style='font-size: 12px; color: #777; font-style: italic; margin-top: 10px;'>"
|
||||
"Valve Proton 10 has known ENB compatibility issues."
|
||||
"</div>"
|
||||
"</div>"
|
||||
)
|
||||
|
||||
versions_label = QLabel(versions_text)
|
||||
versions_label.setAlignment(Qt.AlignLeft)
|
||||
versions_label.setWordWrap(True)
|
||||
@@ -118,26 +138,6 @@ class ENBProtonDialog(QDialog):
|
||||
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
|
||||
|
||||
@@ -36,12 +36,12 @@ _STATUS_LABELS = {
|
||||
|
||||
_STATUS_COLOURS = {
|
||||
'pending': '#808080',
|
||||
'browser_opened': '#3498db',
|
||||
'validating': '#f39c12',
|
||||
'complete': '#27ae60',
|
||||
'deferred': '#e67e22',
|
||||
'skipped': '#e67e22',
|
||||
'error': '#e74c3c',
|
||||
'browser_opened': '#3fd0ea',
|
||||
'validating': '#f0c040',
|
||||
'complete': '#3fd0ea',
|
||||
'deferred': '#f0c040',
|
||||
'skipped': '#888',
|
||||
'error': '#e05050',
|
||||
}
|
||||
|
||||
# Column indices
|
||||
@@ -61,6 +61,15 @@ def _fmt_size(n: int) -> str:
|
||||
return f"{n:.1f} TB"
|
||||
|
||||
|
||||
class _SizeTableItem(QTableWidgetItem):
|
||||
"""QTableWidgetItem that sorts by raw byte count stored in UserRole."""
|
||||
def __lt__(self, other: 'QTableWidgetItem') -> bool:
|
||||
try:
|
||||
return int(self.data(Qt.UserRole) or 0) < int(other.data(Qt.UserRole) or 0)
|
||||
except (TypeError, ValueError):
|
||||
return super().__lt__(other)
|
||||
|
||||
|
||||
class _Bridge(QObject):
|
||||
"""Tiny bridge so worker-thread callbacks can update the Qt table safely."""
|
||||
item_updated = Signal(object) # DownloadItem
|
||||
@@ -152,15 +161,17 @@ class ManualDownloadDialog(QDialog):
|
||||
|
||||
# Batch-insert new rows with viewport updates suspended to avoid O(n²) repaints
|
||||
if new_items:
|
||||
self._table.setSortingEnabled(False)
|
||||
self._table.setUpdatesEnabled(False)
|
||||
try:
|
||||
start_row = self._table.rowCount()
|
||||
self._table.setRowCount(start_row + len(new_items))
|
||||
for i, item in enumerate(new_items):
|
||||
self._fill_row(start_row + i, item)
|
||||
self._row_map[item.file_name] = start_row + i
|
||||
finally:
|
||||
self._table.setUpdatesEnabled(True)
|
||||
self._table.setSortingEnabled(True)
|
||||
self._rebuild_row_map()
|
||||
self._table.viewport().update()
|
||||
|
||||
self._refresh_header()
|
||||
@@ -222,7 +233,9 @@ class ManualDownloadDialog(QDialog):
|
||||
self._table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self._table.setEditTriggers(QTableWidget.NoEditTriggers)
|
||||
self._table.setAlternatingRowColors(True)
|
||||
self._table.setSortingEnabled(True)
|
||||
self._table.verticalHeader().setVisible(False)
|
||||
self._table.horizontalHeader().sectionClicked.connect(self._on_header_clicked)
|
||||
self._table.cellDoubleClicked.connect(self._on_row_double_clicked)
|
||||
self._table.setStyleSheet(
|
||||
"QTableWidget { background: #1a1d23; alternate-background-color: #1e2228; "
|
||||
@@ -284,6 +297,14 @@ class ManualDownloadDialog(QDialog):
|
||||
self._open_selected_btn.clicked.connect(self._on_open_selected)
|
||||
btn_row.addWidget(self._open_selected_btn)
|
||||
|
||||
self._scan_now_btn = QPushButton("Scan Now")
|
||||
self._scan_now_btn.setToolTip(
|
||||
"Re-scan the watch directory for already-downloaded files.\n"
|
||||
"Use this if a file downloaded successfully but was not detected automatically."
|
||||
)
|
||||
self._scan_now_btn.clicked.connect(self._on_scan_now)
|
||||
btn_row.addWidget(self._scan_now_btn)
|
||||
|
||||
btn_row.addStretch()
|
||||
|
||||
self._start_pause_btn = QPushButton("Start")
|
||||
@@ -308,9 +329,13 @@ class ManualDownloadDialog(QDialog):
|
||||
def _fill_row(self, row: int, item: DownloadItem) -> None:
|
||||
"""Populate cells for a pre-allocated row (row must already exist in the table)."""
|
||||
from PySide6.QtGui import QColor
|
||||
self._table.setItem(row, _COL_MOD, QTableWidgetItem(item.mod_name))
|
||||
mod_cell = QTableWidgetItem(item.mod_name)
|
||||
mod_cell.setData(Qt.UserRole, item.file_name) # stable ID for row lookups after sort
|
||||
self._table.setItem(row, _COL_MOD, mod_cell)
|
||||
self._table.setItem(row, _COL_NAME, QTableWidgetItem(item.file_name))
|
||||
self._table.setItem(row, _COL_SIZE, QTableWidgetItem(_fmt_size(item.expected_size)))
|
||||
size_cell = _SizeTableItem(_fmt_size(item.expected_size))
|
||||
size_cell.setData(Qt.UserRole, item.expected_size)
|
||||
self._table.setItem(row, _COL_SIZE, size_cell)
|
||||
colour = _STATUS_COLOURS.get(item.status, '#808080')
|
||||
status_cell = QTableWidgetItem(_STATUS_LABELS.get(item.status, item.status))
|
||||
status_cell.setForeground(QColor(colour))
|
||||
@@ -326,6 +351,26 @@ class ManualDownloadDialog(QDialog):
|
||||
status_cell.setForeground(QColor(_STATUS_COLOURS.get(item.status, '#808080')))
|
||||
status_cell.setToolTip(item.error_message or "")
|
||||
|
||||
def _rebuild_row_map(self) -> None:
|
||||
self._row_map.clear()
|
||||
for row in range(self._table.rowCount()):
|
||||
cell = self._table.item(row, _COL_MOD)
|
||||
if cell:
|
||||
file_name = cell.data(Qt.UserRole)
|
||||
if file_name:
|
||||
self._row_map[file_name] = row
|
||||
|
||||
def _sync_pending_order_to_visual(self) -> None:
|
||||
visual_order: dict[str, int] = {}
|
||||
for row in range(self._table.rowCount()):
|
||||
cell = self._table.item(row, _COL_MOD)
|
||||
if cell:
|
||||
file_name = cell.data(Qt.UserRole)
|
||||
if file_name:
|
||||
visual_order[file_name] = row
|
||||
with self._manager._lock:
|
||||
self._manager._items.sort(key=lambda i: visual_order.get(i.file_name, 999999))
|
||||
|
||||
def _refresh_header(self) -> None:
|
||||
items = self._manager.items
|
||||
total = len(items)
|
||||
@@ -345,6 +390,10 @@ class ManualDownloadDialog(QDialog):
|
||||
# Slots
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _on_header_clicked(self, logical_index: int) -> None:
|
||||
self._rebuild_row_map()
|
||||
self._sync_pending_order_to_visual()
|
||||
|
||||
def _on_item_updated_slot(self, item: DownloadItem) -> None:
|
||||
row = self._row_map.get(item.file_name)
|
||||
if row is not None:
|
||||
@@ -369,7 +418,6 @@ class ManualDownloadDialog(QDialog):
|
||||
self._folder_label.setText(chosen)
|
||||
self._manager._watch_dir = self._watch_dir
|
||||
self._manager._watcher._config.watch_directory = self._watch_dir
|
||||
self._manager._watcher._known = {}
|
||||
try:
|
||||
cfg = ConfigHandler()
|
||||
cfg.set("manual_download_watch_directory", str(self._watch_dir))
|
||||
@@ -430,6 +478,16 @@ class ManualDownloadDialog(QDialog):
|
||||
return
|
||||
self._manager.reopen_item(file_name)
|
||||
|
||||
def _on_scan_now(self) -> None:
|
||||
self._scan_now_btn.setEnabled(False)
|
||||
self._scan_now_btn.setText("Scanning...")
|
||||
self._manager.force_rescan()
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(2000, lambda: (
|
||||
self._scan_now_btn.setEnabled(True),
|
||||
self._scan_now_btn.setText("Scan Now"),
|
||||
))
|
||||
|
||||
def _on_row_double_clicked(self, row: int, _column: int) -> None:
|
||||
file_item = self._table.item(row, _COL_NAME)
|
||||
if file_item is None:
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
"""NXM download dialog: modlist picker and download runner."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
|
||||
from PySide6.QtCore import QThread, Signal, Qt, QTimer
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QLabel,
|
||||
QListWidget,
|
||||
QListWidgetItem,
|
||||
QCheckBox,
|
||||
QPushButton,
|
||||
QProgressBar,
|
||||
QFrame,
|
||||
)
|
||||
|
||||
from jackify.backend.services.nxm_url import NxmUrl
|
||||
from jackify.frontends.gui.shared_theme import JACKIFY_COLOR_BLUE
|
||||
import jackify.backend.services.nxm_session as nxm_session
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_BG_DARK = "#16191d"
|
||||
_BG_CARD = "#1e2228"
|
||||
_BG_LIST = "#1a1d23"
|
||||
_BORDER = "#333"
|
||||
_TEXT = "#d0d0d0"
|
||||
_TEXT_DIM = "#8f98a3"
|
||||
_TEXT_BRIGHT = "#e0e0e0"
|
||||
_ERROR = "#cc4444"
|
||||
|
||||
|
||||
class _ModNameFetchThread(QThread):
|
||||
name_ready = Signal(str)
|
||||
|
||||
def __init__(self, nxm: NxmUrl, parent=None):
|
||||
super().__init__(parent)
|
||||
self.nxm = nxm
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
from jackify.backend.services.nexus_auth_service import NexusAuthService
|
||||
svc = NexusAuthService()
|
||||
token = svc.get_auth_token()
|
||||
method = svc.get_auth_method() or "api_key"
|
||||
except Exception:
|
||||
try:
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
token = APIKeyService().get_saved_api_key()
|
||||
method = "api_key"
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if not token:
|
||||
return
|
||||
|
||||
try:
|
||||
import requests
|
||||
if method == "oauth":
|
||||
headers = {"Authorization": f"Bearer {token}", "User-Agent": "jackify"}
|
||||
else:
|
||||
headers = {"apikey": token, "User-Agent": "jackify"}
|
||||
url = (
|
||||
f"https://api.nexusmods.com/v1/games/{self.nxm.game}"
|
||||
f"/mods/{self.nxm.mod_id}.json"
|
||||
)
|
||||
resp = requests.get(url, headers=headers, timeout=10)
|
||||
resp.raise_for_status()
|
||||
name = resp.json().get("name", "")
|
||||
if name:
|
||||
self.name_ready.emit(name)
|
||||
except Exception as e:
|
||||
logger.debug("Mod name fetch failed: %s", e)
|
||||
|
||||
|
||||
class _DownloadThread(QThread):
|
||||
progress = Signal(int, int)
|
||||
finished = Signal(bool, str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nxm: NxmUrl,
|
||||
download_dir: Path,
|
||||
auth_token: str,
|
||||
auth_method: str,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.nxm = nxm
|
||||
self.download_dir = download_dir
|
||||
self.auth_token = auth_token
|
||||
self.auth_method = auth_method
|
||||
|
||||
def run(self) -> None:
|
||||
from jackify.backend.services.nxm_downloader import (
|
||||
get_nxm_download_url,
|
||||
download_nxm_file,
|
||||
filename_from_cdn_url,
|
||||
)
|
||||
|
||||
cdn_url = get_nxm_download_url(self.nxm, self.auth_token, self.auth_method)
|
||||
if not cdn_url:
|
||||
self.finished.emit(False, "Could not resolve download URL from Nexus API.")
|
||||
return
|
||||
|
||||
filename = filename_from_cdn_url(
|
||||
cdn_url, f"mod_{self.nxm.mod_id}_file_{self.nxm.file_id}.zip"
|
||||
)
|
||||
ok, msg = download_nxm_file(cdn_url, self.download_dir, filename, self._on_progress)
|
||||
self.finished.emit(ok, msg)
|
||||
|
||||
def _on_progress(self, downloaded: int, total: int) -> None:
|
||||
self.progress.emit(downloaded, total)
|
||||
|
||||
|
||||
class NxmDownloadDialog(QDialog):
|
||||
"""Modlist picker and download runner for incoming nxm:// links.
|
||||
|
||||
When auto_start_modlist is provided the picker is hidden and the download
|
||||
begins immediately - used when session memory has a remembered modlist.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nxm: NxmUrl,
|
||||
modlists: List[Dict],
|
||||
parent=None,
|
||||
auto_start_modlist: Optional[Dict] = None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.nxm = nxm
|
||||
self.modlists = modlists
|
||||
self._thread: Optional[_DownloadThread] = None
|
||||
self._name_thread: Optional[_ModNameFetchThread] = None
|
||||
self._auto_start_modlist = auto_start_modlist
|
||||
|
||||
self.setWindowTitle("NXM Download")
|
||||
self.setMinimumWidth(520)
|
||||
self.setModal(False)
|
||||
self.setStyleSheet(f"QDialog {{ background: {_BG_DARK}; color: {_TEXT}; }}")
|
||||
self._build_ui(show_picker=auto_start_modlist is None)
|
||||
if auto_start_modlist is None:
|
||||
self._apply_session_memory()
|
||||
self._fetch_mod_name()
|
||||
|
||||
# --- UI construction ---
|
||||
|
||||
def _build_ui(self, show_picker: bool = True) -> None:
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(10)
|
||||
|
||||
layout.addWidget(self._make_header())
|
||||
|
||||
if show_picker:
|
||||
layout.addWidget(self._make_section_label("Select modlist:"))
|
||||
layout.addWidget(self._make_modlist_list())
|
||||
layout.addWidget(self._make_dest_label())
|
||||
layout.addWidget(self._make_session_row())
|
||||
else:
|
||||
name = self._auto_start_modlist.get("name", "")
|
||||
lbl = QLabel(f"Downloading to: <span style='color:{JACKIFY_COLOR_BLUE}'>{name}</span>")
|
||||
lbl.setTextFormat(Qt.RichText)
|
||||
lbl.setStyleSheet(f"color: {_TEXT};")
|
||||
layout.addWidget(lbl)
|
||||
self._clear_session_btn = QPushButton("Change modlist (clear session)")
|
||||
self._clear_session_btn.setFlat(True)
|
||||
self._clear_session_btn.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE};")
|
||||
self._clear_session_btn.clicked.connect(self._on_clear_and_reopen)
|
||||
layout.addWidget(self._clear_session_btn)
|
||||
|
||||
layout.addWidget(self._make_progress_section())
|
||||
layout.addWidget(self._make_separator())
|
||||
layout.addLayout(self._make_button_row())
|
||||
|
||||
def _make_header(self) -> QFrame:
|
||||
card = QFrame()
|
||||
card.setStyleSheet(
|
||||
f"QFrame {{ background: {_BG_CARD}; border-radius: 6px; border: 1px solid {_BORDER}; }}"
|
||||
)
|
||||
vbox = QVBoxLayout(card)
|
||||
vbox.setContentsMargins(12, 10, 12, 10)
|
||||
vbox.setSpacing(4)
|
||||
|
||||
self._title_label = QLabel(f"<b>NXM Download: Mod {self.nxm.mod_id}</b>")
|
||||
self._title_label.setStyleSheet(f"color: {_TEXT_BRIGHT}; font-size: 13px; border: none;")
|
||||
self._title_label.setTextFormat(Qt.RichText)
|
||||
vbox.addWidget(self._title_label)
|
||||
|
||||
detail = QLabel(
|
||||
f"Game: <span style='color:{JACKIFY_COLOR_BLUE}'>{self.nxm.game}</span>"
|
||||
f" | Mod ID: {self.nxm.mod_id}"
|
||||
f" | File ID: {self.nxm.file_id}"
|
||||
)
|
||||
detail.setTextFormat(Qt.RichText)
|
||||
detail.setStyleSheet(f"color: {_TEXT_DIM}; font-size: 11px; border: none;")
|
||||
vbox.addWidget(detail)
|
||||
return card
|
||||
|
||||
def _make_section_label(self, text: str) -> QLabel:
|
||||
lbl = QLabel(text)
|
||||
lbl.setStyleSheet(f"color: {_TEXT_DIM}; font-size: 11px;")
|
||||
return lbl
|
||||
|
||||
def _make_modlist_list(self) -> QListWidget:
|
||||
self._list = QListWidget()
|
||||
self._list.setMaximumHeight(150)
|
||||
self._list.setStyleSheet(
|
||||
f"QListWidget {{ background: {_BG_LIST}; color: {_TEXT}; "
|
||||
f"border: 1px solid {_BORDER}; border-radius: 4px; }}"
|
||||
f"QListWidget::item:selected {{ background: #2a3a4a; color: {_TEXT_BRIGHT}; }}"
|
||||
f"QListWidget::item:hover {{ background: #222830; }}"
|
||||
)
|
||||
for ml in self.modlists:
|
||||
item = QListWidgetItem(ml["name"])
|
||||
item.setData(Qt.UserRole, ml)
|
||||
self._list.addItem(item)
|
||||
self._list.currentItemChanged.connect(self._on_selection_changed)
|
||||
return self._list
|
||||
|
||||
def _make_dest_label(self) -> QLabel:
|
||||
self._dest_label = QLabel("Download directory: (none)")
|
||||
self._dest_label.setStyleSheet(f"color: {_TEXT_DIM}; font-size: 11px;")
|
||||
self._dest_label.setWordWrap(True)
|
||||
return self._dest_label
|
||||
|
||||
def _make_session_row(self) -> QFrame:
|
||||
container = QFrame()
|
||||
container.setStyleSheet("QFrame { border: none; }")
|
||||
row = QHBoxLayout(container)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self._remember_cb = QCheckBox("Remember for this session")
|
||||
self._remember_cb.setStyleSheet(f"color: {_TEXT};")
|
||||
row.addWidget(self._remember_cb)
|
||||
row.addStretch()
|
||||
|
||||
self._clear_btn = QPushButton("Clear remembered modlist")
|
||||
self._clear_btn.setFlat(True)
|
||||
self._clear_btn.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE};")
|
||||
self._clear_btn.clicked.connect(self._on_clear_session)
|
||||
row.addWidget(self._clear_btn)
|
||||
|
||||
self._update_clear_button()
|
||||
return container
|
||||
|
||||
def _make_progress_section(self) -> QFrame:
|
||||
container = QFrame()
|
||||
container.setStyleSheet("QFrame { border: none; }")
|
||||
vbox = QVBoxLayout(container)
|
||||
vbox.setContentsMargins(0, 0, 0, 0)
|
||||
vbox.setSpacing(4)
|
||||
|
||||
self._status_label = QLabel("")
|
||||
self._status_label.setStyleSheet(f"color: {_TEXT_DIM};")
|
||||
self._status_label.setVisible(False)
|
||||
self._status_label.setWordWrap(True)
|
||||
self._status_label.setTextInteractionFlags(Qt.TextSelectableByMouse)
|
||||
vbox.addWidget(self._status_label)
|
||||
|
||||
self._progress_bar = QProgressBar()
|
||||
self._progress_bar.setStyleSheet(
|
||||
f"QProgressBar {{ border: 1px solid {_BORDER}; border-radius: 4px; "
|
||||
f"background: #2c2c2c; height: 10px; color: transparent; }}"
|
||||
f"QProgressBar::chunk {{ background: {JACKIFY_COLOR_BLUE}; border-radius: 3px; }}"
|
||||
)
|
||||
self._progress_bar.setVisible(False)
|
||||
vbox.addWidget(self._progress_bar)
|
||||
return container
|
||||
|
||||
def _make_button_row(self) -> QHBoxLayout:
|
||||
row = QHBoxLayout()
|
||||
row.addStretch()
|
||||
|
||||
self._cancel_btn = QPushButton("Cancel")
|
||||
self._cancel_btn.setStyleSheet(
|
||||
f"QPushButton {{ background: #3a2020; color: {_TEXT}; border: 1px solid {_BORDER}; "
|
||||
f"border-radius: 4px; padding: 5px 14px; }}"
|
||||
f"QPushButton:hover {{ background: #5a2828; }}"
|
||||
)
|
||||
self._cancel_btn.clicked.connect(self.reject)
|
||||
row.addWidget(self._cancel_btn)
|
||||
|
||||
self._download_btn = QPushButton("Download")
|
||||
self._download_btn.setEnabled(False)
|
||||
self._download_btn.setDefault(True)
|
||||
self._download_btn.setStyleSheet(
|
||||
f"QPushButton {{ background: {JACKIFY_COLOR_BLUE}; color: #000; font-weight: 600; "
|
||||
f"border: none; border-radius: 4px; padding: 5px 18px; }}"
|
||||
f"QPushButton:hover {{ background: #5ae0f5; }}"
|
||||
f"QPushButton:disabled {{ background: #2a4a52; color: #555; }}"
|
||||
)
|
||||
self._download_btn.clicked.connect(self._on_download)
|
||||
row.addWidget(self._download_btn)
|
||||
return row
|
||||
|
||||
@staticmethod
|
||||
def _make_separator() -> QFrame:
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.HLine)
|
||||
sep.setStyleSheet(f"color: {_BORDER};")
|
||||
return sep
|
||||
|
||||
# --- Session memory ---
|
||||
|
||||
def showEvent(self, event) -> None:
|
||||
super().showEvent(event)
|
||||
if self._auto_start_modlist is not None:
|
||||
QTimer.singleShot(0, self._start_auto_download)
|
||||
|
||||
def _start_auto_download(self) -> None:
|
||||
self._start_download_for(self._auto_start_modlist)
|
||||
|
||||
def _on_clear_and_reopen(self) -> None:
|
||||
nxm_session.clear_remembered_modlist()
|
||||
self.reject()
|
||||
dlg = NxmDownloadDialog(self.nxm, self.modlists, parent=self.parent())
|
||||
dlg.show()
|
||||
|
||||
def _fetch_mod_name(self) -> None:
|
||||
self._name_thread = _ModNameFetchThread(self.nxm, self)
|
||||
self._name_thread.name_ready.connect(self._on_mod_name_ready)
|
||||
self._name_thread.start()
|
||||
|
||||
def _on_mod_name_ready(self, name: str) -> None:
|
||||
self._title_label.setText(f"<b>NXM Download: {name}</b>")
|
||||
self._name_thread = None
|
||||
|
||||
def _apply_session_memory(self) -> None:
|
||||
remembered = nxm_session.get_remembered_modlist()
|
||||
if not remembered:
|
||||
return
|
||||
for i in range(self._list.count()):
|
||||
if self._list.item(i).text() == remembered:
|
||||
self._list.setCurrentRow(i)
|
||||
self._remember_cb.setChecked(True)
|
||||
break
|
||||
|
||||
def _on_clear_session(self) -> None:
|
||||
nxm_session.clear_remembered_modlist()
|
||||
self._remember_cb.setChecked(False)
|
||||
self._update_clear_button()
|
||||
|
||||
def _update_clear_button(self) -> None:
|
||||
self._clear_btn.setVisible(bool(nxm_session.get_remembered_modlist()))
|
||||
|
||||
def _on_selection_changed(self) -> None:
|
||||
item = self._list.currentItem()
|
||||
self._download_btn.setEnabled(item is not None)
|
||||
if item:
|
||||
modlist = item.data(Qt.UserRole)
|
||||
self._update_dest_label(modlist)
|
||||
|
||||
def _update_dest_label(self, modlist: Dict) -> None:
|
||||
from jackify.backend.services.nxm_downloader import resolve_mo2_download_dir
|
||||
modlist_dir = Path(modlist.get("modlist_dir", ""))
|
||||
download_dir = resolve_mo2_download_dir(modlist_dir)
|
||||
if download_dir:
|
||||
self._dest_label.setText(f"Download directory: {download_dir}")
|
||||
self._dest_label.setStyleSheet(f"color: {_TEXT_DIM}; font-size: 11px;")
|
||||
else:
|
||||
self._dest_label.setText("Download directory: not configured - run Configure first")
|
||||
self._dest_label.setStyleSheet(f"color: {_ERROR}; font-size: 11px;")
|
||||
|
||||
# --- Download ---
|
||||
|
||||
def _on_download(self) -> None:
|
||||
item = self._list.currentItem()
|
||||
if not item:
|
||||
return
|
||||
modlist = item.data(Qt.UserRole)
|
||||
if self._remember_cb.isChecked():
|
||||
nxm_session.set_remembered_modlist(modlist["name"])
|
||||
self._update_clear_button()
|
||||
self._start_download_for(modlist)
|
||||
|
||||
def _start_download_for(self, modlist: Dict) -> None:
|
||||
modlist_dir = Path(modlist.get("modlist_dir", ""))
|
||||
|
||||
from jackify.backend.services.nxm_downloader import resolve_mo2_download_dir
|
||||
download_dir = resolve_mo2_download_dir(modlist_dir)
|
||||
|
||||
if not download_dir or not download_dir.exists():
|
||||
self._set_status(
|
||||
f"Download directory not found for {modlist['name']}. "
|
||||
"Run Configure for this modlist first.",
|
||||
error=True,
|
||||
)
|
||||
return
|
||||
|
||||
token, method = self._get_auth()
|
||||
if not token:
|
||||
self._set_status(
|
||||
"Not logged in to Nexus. Please log in via Settings > Nexus Authentication.",
|
||||
error=True,
|
||||
)
|
||||
return
|
||||
|
||||
self._set_downloading(True)
|
||||
self._thread = _DownloadThread(self.nxm, download_dir, token, method, self)
|
||||
self._thread.progress.connect(self._on_progress)
|
||||
self._thread.finished.connect(self._on_download_finished)
|
||||
self._thread.start()
|
||||
|
||||
def _get_auth(self) -> Tuple[Optional[str], str]:
|
||||
"""Return (token, auth_method). auth_method is 'oauth' or 'api_key'."""
|
||||
try:
|
||||
from jackify.backend.services.nexus_auth_service import NexusAuthService
|
||||
svc = NexusAuthService()
|
||||
token = svc.get_auth_token()
|
||||
method = svc.get_auth_method() or "api_key"
|
||||
if token:
|
||||
return token, method
|
||||
except Exception as e:
|
||||
logger.warning("NexusAuthService unavailable: %s", e)
|
||||
try:
|
||||
from jackify.backend.services.api_key_service import APIKeyService
|
||||
key = APIKeyService().get_saved_api_key()
|
||||
if key:
|
||||
return key, "api_key"
|
||||
except Exception as e:
|
||||
logger.warning("APIKeyService unavailable: %s", e)
|
||||
return None, "api_key"
|
||||
|
||||
def _on_progress(self, downloaded: int, total: int) -> None:
|
||||
if total > 0:
|
||||
self._progress_bar.setValue(int(downloaded * 100 / total))
|
||||
|
||||
def _on_download_finished(self, success: bool, message: str) -> None:
|
||||
self._set_downloading(False)
|
||||
self._thread = None
|
||||
if success:
|
||||
self._set_status("Download complete. MO2 will pick it up automatically.", error=False)
|
||||
QTimer.singleShot(3000, self.accept)
|
||||
else:
|
||||
self._set_status(f"Download failed: {message}", error=True)
|
||||
|
||||
def _set_downloading(self, active: bool) -> None:
|
||||
self._download_btn.setEnabled(not active)
|
||||
self._cancel_btn.setEnabled(not active)
|
||||
if hasattr(self, "_list"):
|
||||
self._list.setEnabled(not active)
|
||||
self._progress_bar.setVisible(active)
|
||||
self._progress_bar.setValue(0)
|
||||
if active:
|
||||
self._set_status("Downloading...", error=False)
|
||||
|
||||
def _set_status(self, text: str, error: bool = False) -> None:
|
||||
self._status_label.setText(text)
|
||||
self._status_label.setVisible(bool(text))
|
||||
colour = _ERROR if error else JACKIFY_COLOR_BLUE
|
||||
self._status_label.setStyleSheet(f"color: {colour};")
|
||||
|
||||
|
||||
def show_no_modlists_error() -> None:
|
||||
"""Show a standalone error when no modlists are found."""
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
msg = QMessageBox()
|
||||
msg.setWindowTitle("Jackify - NXM Handler")
|
||||
msg.setIcon(QMessageBox.Warning)
|
||||
msg.setText(
|
||||
"No installed modlists found.\n\n"
|
||||
"NXM download handling requires at least one modlist that has been "
|
||||
"installed and configured with Jackify."
|
||||
)
|
||||
msg.exec()
|
||||
@@ -62,7 +62,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
"QFrame#protontricksCard { "
|
||||
" background: #2d2323; "
|
||||
" border-radius: 12px; "
|
||||
" border: 2px solid #e74c3c; "
|
||||
" border: 2px solid #8b2020; "
|
||||
"}"
|
||||
)
|
||||
|
||||
@@ -74,7 +74,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
"QLabel { "
|
||||
" font-size: 36px; "
|
||||
" font-weight: bold; "
|
||||
" color: #e74c3c; "
|
||||
" color: #f0c040; "
|
||||
" margin-bottom: 4px; "
|
||||
"}"
|
||||
)
|
||||
@@ -87,7 +87,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
"QLabel { "
|
||||
" font-size: 20px; "
|
||||
" font-weight: 600; "
|
||||
" color: #e74c3c; "
|
||||
" color: #f0c040; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
)
|
||||
@@ -126,7 +126,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
" text-align: center; "
|
||||
"} "
|
||||
"QProgressBar::chunk { "
|
||||
" background-color: #4fc3f7; "
|
||||
" background-color: #3fd0ea; "
|
||||
" border-radius: 3px; "
|
||||
"}"
|
||||
)
|
||||
@@ -139,7 +139,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
self.status_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 14px; "
|
||||
" color: #4fc3f7; "
|
||||
" color: #3fd0ea; "
|
||||
" margin: 8px 0; "
|
||||
"}"
|
||||
)
|
||||
@@ -155,7 +155,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
self.flatpak_btn.clicked.connect(self._install_flatpak)
|
||||
self.flatpak_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #4fc3f7; "
|
||||
" background-color: #1a5fa8; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 6px; "
|
||||
@@ -164,10 +164,10 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #3498db; "
|
||||
" background-color: #2470b0; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #2980b9; "
|
||||
" background-color: #3fd0ea; "
|
||||
"} "
|
||||
"QPushButton:disabled { "
|
||||
" background-color: #555; "
|
||||
@@ -182,7 +182,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
self.native_btn.clicked.connect(self._show_native_guidance)
|
||||
self.native_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #95a5a6; "
|
||||
" background-color: #4a5568; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 6px; "
|
||||
@@ -191,10 +191,10 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #7f8c8d; "
|
||||
" background-color: #5a6578; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #6c7b7d; "
|
||||
" background-color: #3fd0ea; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(self.native_btn)
|
||||
@@ -211,7 +211,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
self.redetect_btn.clicked.connect(self._redetect)
|
||||
self.redetect_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #27ae60; "
|
||||
" background-color: #4a5568; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
@@ -219,10 +219,10 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #229954; "
|
||||
" background-color: #5a6578; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #1e8449; "
|
||||
" background-color: #3fd0ea; "
|
||||
"}"
|
||||
)
|
||||
bottom_layout.addWidget(self.redetect_btn)
|
||||
@@ -235,7 +235,7 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
exit_btn.clicked.connect(self._exit_app)
|
||||
exit_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #e74c3c; "
|
||||
" background-color: #8b2020; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
@@ -243,10 +243,10 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #c0392b; "
|
||||
" background-color: #a02828; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #a93226; "
|
||||
" background-color: #7a1a1a; "
|
||||
"}"
|
||||
)
|
||||
bottom_layout.addWidget(exit_btn)
|
||||
@@ -287,12 +287,12 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
|
||||
if success:
|
||||
self.status_label.setText("✓ Installation successful!")
|
||||
self.status_label.setStyleSheet("QLabel { color: #27ae60; font-size: 14px; margin: 8px 0; }")
|
||||
self.status_label.setStyleSheet("QLabel { color: #3fd0ea; font-size: 14px; margin: 8px 0; }")
|
||||
# Auto-redetect after successful installation
|
||||
self._redetect()
|
||||
else:
|
||||
self.status_label.setText(f"✗ Installation failed: {message}")
|
||||
self.status_label.setStyleSheet("QLabel { color: #e74c3c; font-size: 14px; margin: 8px 0; }")
|
||||
self.status_label.setStyleSheet("QLabel { color: #e05050; font-size: 14px; margin: 8px 0; }")
|
||||
|
||||
def _show_native_guidance(self):
|
||||
"""Show native installation guidance"""
|
||||
@@ -307,12 +307,12 @@ class ProtontricksErrorDialog(ThreadLifecycleMixin, QDialog):
|
||||
|
||||
if is_installed:
|
||||
self.status_label.setText("✓ Protontricks found!")
|
||||
self.status_label.setStyleSheet("QLabel { color: #27ae60; font-size: 14px; margin: 8px 0; }")
|
||||
self.status_label.setStyleSheet("QLabel { color: #3fd0ea; font-size: 14px; margin: 8px 0; }")
|
||||
self.status_label.setVisible(True)
|
||||
self.accept() # Close dialog successfully
|
||||
else:
|
||||
self.status_label.setText("✗ Protontricks still not found")
|
||||
self.status_label.setStyleSheet("QLabel { color: #e74c3c; font-size: 14px; margin: 8px 0; }")
|
||||
self.status_label.setStyleSheet("QLabel { color: #e05050; font-size: 14px; margin: 8px 0; }")
|
||||
self.status_label.setVisible(True)
|
||||
|
||||
def _exit_app(self):
|
||||
|
||||
@@ -86,7 +86,7 @@ class SettingsDialog(SettingsDialogTabsMixin, SettingsDialogProtonMixin, QDialog
|
||||
self.api_show_btn.setText("\U0001F441")
|
||||
if checked:
|
||||
self.api_key_edit.setEchoMode(QLineEdit.Normal)
|
||||
self.api_show_btn.setStyleSheet("QToolButton { color: #4fc3f7; }")
|
||||
self.api_show_btn.setStyleSheet("QToolButton { color: #3fd0ea; }")
|
||||
else:
|
||||
self.api_key_edit.setEchoMode(QLineEdit.Password)
|
||||
self.api_show_btn.setStyleSheet("")
|
||||
@@ -122,8 +122,8 @@ class SettingsDialog(SettingsDialogTabsMixin, SettingsDialogProtonMixin, QDialog
|
||||
self.config_handler.clear_api_key()
|
||||
MessageService.information(self, "API Key Cleared", "Nexus API Key has been cleared.", safety_level="low")
|
||||
|
||||
def _on_api_key_changed(self, text):
|
||||
api_key = text.strip()
|
||||
def _on_api_key_changed(self):
|
||||
api_key = self.api_key_edit.text().strip()
|
||||
self.config_handler.save_api_key(api_key)
|
||||
|
||||
|
||||
@@ -314,6 +314,9 @@ class SettingsDialog(SettingsDialogTabsMixin, SettingsDialogProtonMixin, QDialog
|
||||
# Save auto tool compat preference
|
||||
self.config_handler.set('auto_tool_compat', self.auto_tool_compat_checkbox.isChecked())
|
||||
self.config_handler.set('force_github_updates', self.force_github_updates_checkbox.isChecked())
|
||||
if getattr(self, 'clf3_default_checkbox', None):
|
||||
from jackify.backend.services.tool_registry import set_active_engine_id
|
||||
set_active_engine_id("clf3" if self.clf3_default_checkbox.isChecked() else "jackify-engine")
|
||||
|
||||
# Save component installation method preference
|
||||
if self.winetricks_radio.isChecked():
|
||||
|
||||
@@ -153,7 +153,7 @@ class SettingsDialogTabsMixin:
|
||||
api_key = self.config_handler.get_api_key()
|
||||
self.api_key_edit.setText(api_key if api_key else "")
|
||||
self.api_key_edit.setToolTip("Your Nexus API Key (legacy authentication method)")
|
||||
self.api_key_edit.textChanged.connect(self._on_api_key_changed)
|
||||
self.api_key_edit.editingFinished.connect(self._on_api_key_changed)
|
||||
self.api_show_btn = QToolButton()
|
||||
self.api_show_btn.setCheckable(True)
|
||||
self.api_show_btn.setIcon(QIcon.fromTheme("view-visible"))
|
||||
@@ -297,6 +297,20 @@ class SettingsDialogTabsMixin:
|
||||
self.force_github_updates_checkbox.setStyleSheet("color: #fff;")
|
||||
component_layout.addWidget(self.force_github_updates_checkbox)
|
||||
|
||||
self.clf3_default_checkbox = QCheckBox("Use CLF3 as default install engine")
|
||||
self.clf3_default_checkbox.setToolTip(
|
||||
"Use CLF3 (SulfurNitride) as the default engine for all installs. "
|
||||
"CLF3 will be downloaded automatically if not already installed. "
|
||||
"You can still override this per-install on the Install screen."
|
||||
)
|
||||
self.clf3_default_checkbox.setStyleSheet("color: #fff;")
|
||||
try:
|
||||
from jackify.backend.services.tool_registry import get_active_engine_id
|
||||
self.clf3_default_checkbox.setChecked(get_active_engine_id() == "clf3")
|
||||
except Exception:
|
||||
pass
|
||||
component_layout.addWidget(self.clf3_default_checkbox)
|
||||
|
||||
advanced_layout.addWidget(component_group)
|
||||
advanced_layout.addStretch()
|
||||
self.tab_widget.addTab(advanced_tab, "Advanced")
|
||||
|
||||
@@ -31,22 +31,32 @@ class SuccessDialog(QDialog):
|
||||
- Return and Exit buttons
|
||||
"""
|
||||
|
||||
def __init__(self, modlist_name: str, workflow_type: str, time_taken: str, game_name: str = None, parent=None):
|
||||
def __init__(
|
||||
self,
|
||||
modlist_name: str,
|
||||
workflow_type: str,
|
||||
time_taken: str,
|
||||
game_name: str = None,
|
||||
verification_results=None,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.modlist_name = modlist_name
|
||||
self.workflow_type = workflow_type
|
||||
self.time_taken = time_taken
|
||||
self.game_name = game_name
|
||||
self.setWindowTitle("Success!")
|
||||
self.verification_results = verification_results
|
||||
self.setWindowTitle("Complete" if (verification_results and verification_results.failures) else "Success!")
|
||||
self.setWindowModality(Qt.NonModal)
|
||||
self.setAttribute(Qt.WA_ShowWithoutActivating, True)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||
self.setFixedSize(500, 500)
|
||||
self.setFixedWidth(500)
|
||||
self.setMinimumHeight(400)
|
||||
self.setWindowFlag(Qt.WindowDoesNotAcceptFocus, True)
|
||||
self.setStyleSheet("QDialog { background: #181818; color: #fff; border-radius: 12px; }" )
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(30, 20, 30, 20) # Reduced top/bottom margins to prevent truncation
|
||||
layout.setContentsMargins(20, 20, 20, 20)
|
||||
|
||||
# --- Card background for content ---
|
||||
card = QFrame(self)
|
||||
@@ -69,22 +79,36 @@ class SuccessDialog(QDialog):
|
||||
)
|
||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.MinimumExpanding)
|
||||
|
||||
# Success title (less saturated green)
|
||||
title_label = QLabel("Success!")
|
||||
has_verify_failures = bool(
|
||||
self.verification_results and self.verification_results.failures
|
||||
)
|
||||
|
||||
title_text = "Complete" if has_verify_failures else "Success!"
|
||||
title_color = "#f0c040" if has_verify_failures else "#3fd0ea"
|
||||
title_label = QLabel(title_text)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 22px; "
|
||||
" font-weight: 600; "
|
||||
" color: #2ecc71; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
f"QLabel {{ "
|
||||
f" font-size: 22px; "
|
||||
f" font-weight: 600; "
|
||||
f" color: {title_color}; "
|
||||
f" margin-bottom: 2px; "
|
||||
f"}}"
|
||||
)
|
||||
card_layout.addWidget(title_label)
|
||||
|
||||
# Personalized success message (modlist name in Jackify Blue, but less bold)
|
||||
# Personalized message (modlist name in Jackify Blue, but less bold)
|
||||
modlist_name_html = f'<span style="color:#3fb7d6; font-size:17px; font-weight:500;">{self.modlist_name}</span>'
|
||||
if self.workflow_type == "install":
|
||||
if has_verify_failures:
|
||||
suffix_map = {
|
||||
"install": "installed with issues - review verification results.",
|
||||
"update": "updated with issues - review verification results.",
|
||||
"configure_new": "configured with issues - review verification results.",
|
||||
"configure_existing": "configuration updated with issues - review verification results.",
|
||||
"tool_config": "tool compatibility configured with issues - review verification results.",
|
||||
}
|
||||
suffix_text = suffix_map.get(self.workflow_type, "completed with issues - review verification results.")
|
||||
elif self.workflow_type == "install":
|
||||
suffix_text = "installed successfully!"
|
||||
elif self.workflow_type == "update":
|
||||
suffix_text = "updated successfully!"
|
||||
@@ -95,7 +119,6 @@ class SuccessDialog(QDialog):
|
||||
elif self.workflow_type == "tool_config":
|
||||
suffix_text = "tool compatibility configured successfully!"
|
||||
else:
|
||||
# Fallback for other workflow types
|
||||
message_text = self._build_success_message()
|
||||
suffix_text = message_text.replace(self.modlist_name, "").strip()
|
||||
|
||||
@@ -153,12 +176,16 @@ class SuccessDialog(QDialog):
|
||||
)
|
||||
card_layout.addWidget(next_steps_label)
|
||||
|
||||
# Verification results summary
|
||||
if self.verification_results is not None:
|
||||
self._add_verification_section(card_layout)
|
||||
|
||||
# Subtle Ko-Fi support link
|
||||
kofi_label = QLabel('<a href="https://ko-fi.com/omni1" style="color:#72A5F2; text-decoration:none;">Enjoying Jackify? Support development ♥</a>')
|
||||
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)
|
||||
kofi_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" color: #72A5F2; "
|
||||
" color: #3fd0ea; "
|
||||
" font-size: 11px; "
|
||||
" margin-top: 8px; "
|
||||
" padding: 4px; "
|
||||
@@ -275,6 +302,94 @@ class SuccessDialog(QDialog):
|
||||
# ENB Proton warning shown in separate dialog
|
||||
return base_message
|
||||
|
||||
def _add_verification_section(self, card_layout):
|
||||
"""Add a verification summary section to the card layout."""
|
||||
from PySide6.QtWidgets import QScrollArea
|
||||
|
||||
r = self.verification_results
|
||||
n_pass = len(r.passes)
|
||||
n_warn = len(r.warnings)
|
||||
n_fail = len(r.failures)
|
||||
|
||||
sep = QFrame()
|
||||
sep.setFrameShape(QFrame.HLine)
|
||||
sep.setStyleSheet("color: #353a40;")
|
||||
card_layout.addWidget(sep)
|
||||
|
||||
if n_fail:
|
||||
summary_text = f"[FAIL] Verification: {n_fail} failure(s), {n_warn} warning(s)"
|
||||
summary_color = "#e05050"
|
||||
elif n_warn:
|
||||
summary_text = f"[WARN] Verification: {n_warn} warning(s) - review before playing"
|
||||
summary_color = "#f0c040"
|
||||
else:
|
||||
summary_text = f"[OK] Verification passed ({n_pass} checks)"
|
||||
summary_color = "#3fd0ea"
|
||||
|
||||
summary_row = QHBoxLayout()
|
||||
summary_row.setContentsMargins(0, 0, 0, 0)
|
||||
summary_row.setSpacing(8)
|
||||
|
||||
summary_lbl = QLabel(summary_text)
|
||||
summary_lbl.setAlignment(Qt.AlignCenter)
|
||||
summary_lbl.setWordWrap(True)
|
||||
summary_lbl.setStyleSheet(
|
||||
f"QLabel {{ font-size: 12px; font-weight: bold; color: {summary_color}; }}"
|
||||
)
|
||||
summary_row.addStretch()
|
||||
summary_row.addWidget(summary_lbl)
|
||||
|
||||
view_btn = QPushButton("View checks")
|
||||
view_btn.setFixedWidth(90)
|
||||
view_btn.setStyleSheet(
|
||||
"QPushButton { font-size: 11px; color: #ccc; background: #3a3a3a; "
|
||||
"border: 1px solid #555; border-radius: 4px; padding: 3px 6px; }"
|
||||
"QPushButton:hover { background: #4a4a4a; color: #fff; }"
|
||||
)
|
||||
view_btn.setCursor(Qt.PointingHandCursor)
|
||||
view_btn.clicked.connect(lambda: self._show_verification_detail())
|
||||
summary_row.addWidget(view_btn)
|
||||
summary_row.addStretch()
|
||||
|
||||
row_widget = QWidget()
|
||||
row_widget.setLayout(summary_row)
|
||||
card_layout.addWidget(row_widget)
|
||||
|
||||
if n_fail or n_warn:
|
||||
detail_widget = QWidget()
|
||||
detail_layout = QVBoxLayout(detail_widget)
|
||||
detail_layout.setContentsMargins(0, 0, 0, 0)
|
||||
detail_layout.setSpacing(2)
|
||||
|
||||
for msg in r.failures:
|
||||
lbl = QLabel(f"[FAIL] {msg}")
|
||||
lbl.setWordWrap(True)
|
||||
lbl.setStyleSheet("color: #e05050; font-size: 11px;")
|
||||
detail_layout.addWidget(lbl)
|
||||
for msg in r.warnings:
|
||||
lbl = QLabel(f"[WARN] {msg}")
|
||||
lbl.setWordWrap(True)
|
||||
lbl.setStyleSheet("color: #f0c040; font-size: 11px;")
|
||||
detail_layout.addWidget(lbl)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidget(detail_widget)
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setMaximumHeight(120)
|
||||
scroll.setStyleSheet(
|
||||
"QScrollArea { border: 1px solid #353a40; border-radius: 4px; background: #1a1d23; }"
|
||||
)
|
||||
card_layout.addWidget(scroll)
|
||||
|
||||
def _show_verification_detail(self):
|
||||
"""Open the full verification results dialog."""
|
||||
try:
|
||||
from jackify.frontends.gui.dialogs.verification_results_dialog import VerificationResultsDialog
|
||||
dlg = VerificationResultsDialog(self.verification_results, parent=self)
|
||||
dlg.show()
|
||||
except Exception as exc:
|
||||
logger.error("Could not open verification dialog: %s", exc)
|
||||
|
||||
def _update_countdown(self):
|
||||
if self._countdown > 0:
|
||||
self.return_btn.setText(f"{self._orig_return_text} ({self._countdown}s)")
|
||||
|
||||
@@ -389,11 +389,13 @@ class UlimitGuidanceDialog(QDialog):
|
||||
"""Apply dialog styling"""
|
||||
self.setStyleSheet("""
|
||||
QDialog {
|
||||
background-color: #f5f5f5;
|
||||
background-color: #232323;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
QGroupBox {
|
||||
font-weight: bold;
|
||||
border: 2px solid #cccccc;
|
||||
color: #e0e0e0;
|
||||
border: 2px solid #3a3a3a;
|
||||
border-radius: 5px;
|
||||
margin-top: 1ex;
|
||||
padding-top: 10px;
|
||||
@@ -404,13 +406,14 @@ class UlimitGuidanceDialog(QDialog):
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
QTextEdit {
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
background-color: #2a2a2a;
|
||||
color: #e0e0e0;
|
||||
border: 1px solid #3a3a3a;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #007acc;
|
||||
background-color: #4a5568;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
@@ -418,14 +421,14 @@ class UlimitGuidanceDialog(QDialog):
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #005a9e;
|
||||
background-color: #5a6578;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #004175;
|
||||
background-color: #3fd0ea;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
background-color: #333;
|
||||
color: #666;
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
"""Verification results dialog shown after install/configure workflows."""
|
||||
|
||||
import logging
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QClipboard, QGuiApplication
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QHBoxLayout, QLabel, QPushButton,
|
||||
QTextEdit, QVBoxLayout,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_COLOR_FAIL = "#e05050"
|
||||
_COLOR_WARN = "#f0c040"
|
||||
_COLOR_OK = "#3fd0ea"
|
||||
_COLOR_DIM = "#888888"
|
||||
|
||||
|
||||
def _html_row(prefix: str, color: str, msg: str) -> str:
|
||||
safe = msg.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
return (
|
||||
f'<span style="color:{color}; font-family:monospace;">'
|
||||
f'<b>{prefix}</b> {safe}'
|
||||
f'</span><br>'
|
||||
)
|
||||
|
||||
|
||||
class VerificationResultsDialog(QDialog):
|
||||
"""Shows the output of verify_install.py after a workflow completes."""
|
||||
|
||||
def __init__(self, results, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Installation Verification")
|
||||
self.setWindowModality(Qt.NonModal)
|
||||
self.setAttribute(Qt.WA_DeleteOnClose, True)
|
||||
self.setMinimumWidth(600)
|
||||
|
||||
self._results = results
|
||||
n_pass = len(results.passes)
|
||||
n_warn = len(results.warnings)
|
||||
n_fail = len(results.failures)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(20, 16, 20, 16)
|
||||
layout.setSpacing(10)
|
||||
|
||||
# --- Headline ---
|
||||
if n_fail:
|
||||
headline = f"[FAIL] {n_fail} failure{'s' if n_fail != 1 else ''}, {n_warn} warning{'s' if n_warn != 1 else ''}, {n_pass} passed"
|
||||
h_color = _COLOR_FAIL
|
||||
elif n_warn:
|
||||
headline = f"[WARN] {n_warn} warning{'s' if n_warn != 1 else ''}, {n_pass} passed"
|
||||
h_color = _COLOR_WARN
|
||||
else:
|
||||
headline = f"[OK] All {n_pass} checks passed"
|
||||
h_color = _COLOR_OK
|
||||
|
||||
headline_label = QLabel(headline)
|
||||
headline_label.setStyleSheet(
|
||||
f"font-size: 14px; font-weight: bold; color: {h_color};"
|
||||
)
|
||||
layout.addWidget(headline_label)
|
||||
|
||||
# --- Issues view (always visible) ---
|
||||
self._issues_view = QTextEdit()
|
||||
self._issues_view.setReadOnly(True)
|
||||
self._issues_view.setStyleSheet(
|
||||
"QTextEdit { background: #1e1e1e; border: 1px solid #444; "
|
||||
"border-radius: 4px; font-size: 12px; padding: 6px; }"
|
||||
)
|
||||
self._issues_view.setHtml(self._build_issues_html(results))
|
||||
self._issues_view.setMinimumHeight(80)
|
||||
self._issues_view.setMaximumHeight(220)
|
||||
layout.addWidget(self._issues_view)
|
||||
|
||||
# --- Full report view (hidden by default) ---
|
||||
self._full_view = QTextEdit()
|
||||
self._full_view.setReadOnly(True)
|
||||
self._full_view.setStyleSheet(
|
||||
"QTextEdit { background: #1a1a1a; border: 1px solid #333; "
|
||||
"border-radius: 4px; font-size: 11px; padding: 6px; }"
|
||||
)
|
||||
self._full_view.setHtml(self._build_full_html(results))
|
||||
self._full_view.setMinimumHeight(160)
|
||||
self._full_view.setMaximumHeight(300)
|
||||
self._full_view.setVisible(False)
|
||||
layout.addWidget(self._full_view)
|
||||
|
||||
# --- Button row ---
|
||||
btn_row = QHBoxLayout()
|
||||
|
||||
self._toggle_btn = QPushButton("Show all checks")
|
||||
self._toggle_btn.setFixedWidth(130)
|
||||
self._toggle_btn.setStyleSheet(
|
||||
"QPushButton { font-size: 11px; color: #ccc; background: #3a3a3a; "
|
||||
"border: 1px solid #555; border-radius: 4px; padding: 4px 8px; }"
|
||||
"QPushButton:hover { background: #4a4a4a; color: #fff; }"
|
||||
)
|
||||
self._toggle_btn.setCursor(Qt.PointingHandCursor)
|
||||
self._toggle_btn.clicked.connect(self._toggle_full_report)
|
||||
btn_row.addWidget(self._toggle_btn)
|
||||
|
||||
btn_row.addStretch()
|
||||
|
||||
copy_btn = QPushButton("Copy Report")
|
||||
copy_btn.setFixedWidth(110)
|
||||
copy_btn.setToolTip("Copy plain-text report to clipboard")
|
||||
copy_btn.clicked.connect(self._copy_report)
|
||||
btn_row.addWidget(copy_btn)
|
||||
|
||||
close_btn = QPushButton("Close")
|
||||
close_btn.setFixedWidth(80)
|
||||
close_btn.clicked.connect(self.accept)
|
||||
btn_row.addWidget(close_btn)
|
||||
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
self.adjustSize()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_issues_html(self, results) -> str:
|
||||
lines = []
|
||||
for msg in results.failures:
|
||||
lines.append(_html_row("[FAIL]", _COLOR_FAIL, msg))
|
||||
for msg in results.warnings:
|
||||
lines.append(_html_row("[WARN]", _COLOR_WARN, msg))
|
||||
if not results.failures and not results.warnings:
|
||||
lines.append(_html_row("[OK]", _COLOR_OK, "No issues found. Your modlist is correctly configured."))
|
||||
return "".join(lines)
|
||||
|
||||
def _build_full_html(self, results) -> str:
|
||||
lines = []
|
||||
if results.failures:
|
||||
lines.append(f'<span style="color:{_COLOR_DIM}; font-size:10px;">FAILURES</span><br>')
|
||||
for msg in results.failures:
|
||||
lines.append(_html_row("[FAIL]", _COLOR_FAIL, msg))
|
||||
lines.append("<br>")
|
||||
if results.warnings:
|
||||
lines.append(f'<span style="color:{_COLOR_DIM}; font-size:10px;">WARNINGS</span><br>')
|
||||
for msg in results.warnings:
|
||||
lines.append(_html_row("[WARN]", _COLOR_WARN, msg))
|
||||
lines.append("<br>")
|
||||
if results.passes:
|
||||
lines.append(f'<span style="color:{_COLOR_DIM}; font-size:10px;">PASSED</span><br>')
|
||||
for msg in results.passes:
|
||||
lines.append(_html_row("[OK] ", _COLOR_OK, msg))
|
||||
components = getattr(results, "installed_components", [])
|
||||
if components:
|
||||
lines.append("<br>")
|
||||
lines.append(f'<span style="color:{_COLOR_DIM}; font-size:10px;">INSTALLED COMPONENTS ({len(components)})</span><br>')
|
||||
for c in components:
|
||||
method = c.get("method", "unknown")
|
||||
method_color = _COLOR_OK if method == "native" else _COLOR_DIM
|
||||
safe_name = c["name"].replace("&", "&").replace("<", "<")
|
||||
lines.append(
|
||||
f'<span style="font-family:monospace; font-size:11px;">'
|
||||
f'{safe_name}'
|
||||
f' <span style="color:{method_color}; font-size:10px;">({method})</span>'
|
||||
f'</span><br>'
|
||||
)
|
||||
return "".join(lines)
|
||||
|
||||
def _build_plain_text(self) -> str:
|
||||
r = self._results
|
||||
n_pass = len(r.passes)
|
||||
n_warn = len(r.warnings)
|
||||
n_fail = len(r.failures)
|
||||
|
||||
lines = ["Jackify - Installation Verification", "=" * 40]
|
||||
if n_fail:
|
||||
lines.append(f"RESULT: FAILED ({n_fail} failures, {n_warn} warnings, {n_pass} passed)")
|
||||
elif n_warn:
|
||||
lines.append(f"RESULT: WARNING ({n_warn} warnings, {n_pass} passed)")
|
||||
else:
|
||||
lines.append(f"RESULT: OK (all {n_pass} checks passed)")
|
||||
lines.append("")
|
||||
|
||||
if r.failures:
|
||||
lines.append("FAILURES:")
|
||||
for msg in r.failures:
|
||||
lines.append(f" [FAIL] {msg}")
|
||||
lines.append("")
|
||||
if r.warnings:
|
||||
lines.append("WARNINGS:")
|
||||
for msg in r.warnings:
|
||||
lines.append(f" [WARN] {msg}")
|
||||
lines.append("")
|
||||
if r.passes:
|
||||
lines.append("PASSED:")
|
||||
for msg in r.passes:
|
||||
lines.append(f" [OK] {msg}")
|
||||
components = getattr(r, "installed_components", [])
|
||||
if components:
|
||||
lines.append("")
|
||||
lines.append("INSTALLED COMPONENTS:")
|
||||
for c in components:
|
||||
lines.append(f" {c['name']} ({c.get('method', 'unknown')})")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _toggle_full_report(self):
|
||||
visible = self._full_view.isVisible()
|
||||
self._full_view.setVisible(not visible)
|
||||
self._toggle_btn.setText("Hide all checks" if not visible else "Show all checks")
|
||||
self.adjustSize()
|
||||
|
||||
def _copy_report(self):
|
||||
QGuiApplication.clipboard().setText(self._build_plain_text())
|
||||
@@ -49,7 +49,7 @@ class WarningDialog(QDialog):
|
||||
"QFrame#warningCard { "
|
||||
" background: #2d2323; "
|
||||
" border-radius: 12px; "
|
||||
" border: 2px solid #e67e22; "
|
||||
" border: 2px solid #f0c040; "
|
||||
"}"
|
||||
)
|
||||
|
||||
@@ -61,7 +61,7 @@ class WarningDialog(QDialog):
|
||||
"QLabel { "
|
||||
" font-size: 36px; "
|
||||
" font-weight: bold; "
|
||||
" color: #e67e22; "
|
||||
" color: #f0c040; "
|
||||
" margin-bottom: 4px; "
|
||||
"}"
|
||||
)
|
||||
@@ -74,7 +74,7 @@ class WarningDialog(QDialog):
|
||||
"QLabel { "
|
||||
" font-size: 20px; "
|
||||
" font-weight: 600; "
|
||||
" color: #e67e22; "
|
||||
" color: #f0c040; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
)
|
||||
@@ -106,7 +106,7 @@ class WarningDialog(QDialog):
|
||||
self.confirm_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 13px; "
|
||||
" color: #e67e22; "
|
||||
" color: #f0c040; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
)
|
||||
@@ -118,11 +118,11 @@ class WarningDialog(QDialog):
|
||||
self._default_lineedit_style = (
|
||||
"QLineEdit { "
|
||||
" font-size: 15px; "
|
||||
" border: 1px solid #e67e22; "
|
||||
" border: 1px solid #f0c040; "
|
||||
" border-radius: 6px; "
|
||||
" padding: 6px; "
|
||||
" background: #23272e; "
|
||||
" color: #e67e22; "
|
||||
" color: #e0e0e0; "
|
||||
"}"
|
||||
)
|
||||
self.confirm_edit.setStyleSheet(self._default_lineedit_style)
|
||||
@@ -140,7 +140,7 @@ class WarningDialog(QDialog):
|
||||
cancel_btn.clicked.connect(self.reject)
|
||||
cancel_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #95a5a6; "
|
||||
" background-color: #4a5568; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
@@ -148,10 +148,10 @@ class WarningDialog(QDialog):
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #7f8c8d; "
|
||||
" background-color: #5a6578; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #6c7b7d; "
|
||||
" background-color: #3fd0ea; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(cancel_btn)
|
||||
@@ -161,7 +161,7 @@ class WarningDialog(QDialog):
|
||||
confirm_btn.clicked.connect(self._on_confirm)
|
||||
confirm_btn.setStyleSheet(
|
||||
"QPushButton { "
|
||||
" background-color: #e67e22; "
|
||||
" background-color: #8b2020; "
|
||||
" color: white; "
|
||||
" border: none; "
|
||||
" border-radius: 4px; "
|
||||
@@ -169,10 +169,10 @@ class WarningDialog(QDialog):
|
||||
" padding: 8px 16px; "
|
||||
"} "
|
||||
"QPushButton:hover { "
|
||||
" background-color: #d35400; "
|
||||
" background-color: #a02828; "
|
||||
"} "
|
||||
"QPushButton:pressed { "
|
||||
" background-color: #b34700; "
|
||||
" background-color: #7a1a1a; "
|
||||
"}"
|
||||
)
|
||||
button_layout.addWidget(confirm_btn)
|
||||
@@ -200,7 +200,7 @@ class WarningDialog(QDialog):
|
||||
self.confirm_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 13px; "
|
||||
" color: #e67e22; "
|
||||
" color: #f0c040; "
|
||||
" margin-bottom: 2px; "
|
||||
"}"
|
||||
)
|
||||
@@ -231,7 +231,7 @@ class WarningDialog(QDialog):
|
||||
self.confirm_label.setStyleSheet(
|
||||
"QLabel { "
|
||||
" font-size: 13px; "
|
||||
" color: #c0392b; " # Red for error
|
||||
" color: #e05050; "
|
||||
" margin-bottom: 2px; "
|
||||
" font-weight: bold; "
|
||||
"}"
|
||||
@@ -242,10 +242,10 @@ class WarningDialog(QDialog):
|
||||
self.confirm_edit.setStyleSheet(
|
||||
"QLineEdit { "
|
||||
" font-size: 15px; "
|
||||
" border: 2px solid #c0392b; " # Red border for error
|
||||
" border: 2px solid #8b2020; "
|
||||
" border-radius: 6px; "
|
||||
" padding: 6px; "
|
||||
" background: #3b2323; " # Darker red background
|
||||
" color: #e67e22; "
|
||||
" background: #3b2323; "
|
||||
" color: #e0e0e0; "
|
||||
"}"
|
||||
)
|
||||
Reference in New Issue
Block a user