Files
Jackify/jackify/frontends/gui/screens/install_modlist_nexus.py
2026-02-07 18:26:54 +00:00

261 lines
10 KiB
Python

"""Nexus authentication methods for InstallModlistScreen (Mixin)."""
from PySide6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QMessageBox, QProgressDialog, QApplication
from PySide6.QtCore import Qt, QTimer, QThread, Signal
from PySide6.QtGui import QDesktopServices, QGuiApplication
import logging
import webbrowser
logger = logging.getLogger(__name__)
class NexusAuthMixin:
"""Mixin providing Nexus authentication methods for InstallModlistScreen."""
def _update_nexus_status(self):
"""Update the Nexus login status display"""
authenticated, method, username = self.auth_service.get_auth_status()
if authenticated and method == 'oauth':
# OAuth authorised
status_text = "Authorised"
if username:
status_text += f" ({username})"
self.nexus_status.setText(status_text)
self.nexus_status.setStyleSheet("color: #3fd0ea;")
self.nexus_login_btn.setText("Revoke")
self.nexus_login_btn.setVisible(True)
elif authenticated and method == 'api_key':
# API Key in use (fallback - configured in Settings)
self.nexus_status.setText("API Key")
self.nexus_status.setStyleSheet("color: #FFA726;")
self.nexus_login_btn.setText("Authorise")
self.nexus_login_btn.setVisible(True)
else:
# Not authorised
self.nexus_status.setText("Not Authorised")
self.nexus_status.setStyleSheet("color: #f44336;")
self.nexus_login_btn.setText("Authorise")
self.nexus_login_btn.setVisible(True)
def _show_copyable_url_dialog(self, url: str):
"""Show a dialog with a copyable URL"""
dialog = QDialog(self)
dialog.setWindowTitle("Manual Browser Open Required")
dialog.setModal(True)
dialog.setMinimumWidth(600)
layout = QVBoxLayout()
layout.setSpacing(15)
# Explanation label
info_label = QLabel(
"Could not open browser automatically.\n\n"
"Please copy the URL below and paste it into your browser:"
)
info_label.setWordWrap(True)
info_label.setStyleSheet("color: #ccc; font-size: 12px;")
layout.addWidget(info_label)
# URL input (read-only but selectable)
url_input = QLineEdit()
url_input.setText(url)
url_input.setReadOnly(True)
url_input.selectAll() # Pre-select text for easy copying
url_input.setStyleSheet("""
QLineEdit {
background-color: #1a1a1a;
color: #3fd0ea;
border: 1px solid #444;
border-radius: 4px;
padding: 8px;
font-family: monospace;
font-size: 11px;
}
""")
layout.addWidget(url_input)
# Button row
button_layout = QHBoxLayout()
button_layout.addStretch()
# Copy button
copy_btn = QPushButton("Copy URL")
copy_btn.setStyleSheet("""
QPushButton {
background-color: #3fd0ea;
color: #000;
border: none;
border-radius: 4px;
padding: 8px 20px;
font-weight: bold;
}
QPushButton:hover {
background-color: #5fdfff;
}
""")
def copy_to_clipboard():
clipboard = QApplication.clipboard()
clipboard.setText(url)
copy_btn.setText("Copied!")
copy_btn.setEnabled(False)
copy_btn.clicked.connect(copy_to_clipboard)
button_layout.addWidget(copy_btn)
# Close button
close_btn = QPushButton("Close")
close_btn.setStyleSheet("""
QPushButton {
background-color: #444;
color: #ccc;
border: none;
border-radius: 4px;
padding: 8px 20px;
}
QPushButton:hover {
background-color: #555;
}
""")
close_btn.clicked.connect(dialog.accept)
button_layout.addWidget(close_btn)
layout.addLayout(button_layout)
dialog.setLayout(layout)
dialog.exec()
def _handle_nexus_login_click(self):
"""Handle Nexus login button click"""
from jackify.frontends.gui.services.message_service import MessageService
authenticated, method, _ = self.auth_service.get_auth_status()
if authenticated and method == 'oauth':
# OAuth is active - offer to revoke
reply = MessageService.question(self, "Revoke", "Revoke OAuth authorisation?", safety_level="low")
if reply == QMessageBox.Yes:
self.auth_service.revoke_oauth()
self._update_nexus_status()
else:
# Not authorised or using API key - offer to authorise with OAuth
reply = MessageService.question(self, "Authorise with Nexus",
"Your browser will open for Nexus authorisation.\n\n"
"Note: Your browser may ask permission to open 'xdg-open'\n"
"or Jackify's protocol handler - please click 'Open' or 'Allow'.\n\n"
"Please log in and authorise Jackify when prompted.\n\n"
"Continue?", safety_level="low")
if reply != QMessageBox.Yes:
return
# Create progress dialog
progress = QProgressDialog(
"Waiting for authorisation...\n\nPlease check your browser.",
"Cancel",
0, 0,
self
)
progress.setWindowTitle("Nexus OAuth")
progress.setWindowModality(Qt.WindowModal)
progress.setMinimumDuration(0)
progress.setMinimumWidth(400)
# Track cancellation
oauth_cancelled = [False]
def on_cancel():
oauth_cancelled[0] = True
progress.canceled.connect(on_cancel)
progress.show()
QApplication.processEvents()
# Create OAuth thread to prevent GUI freeze
class OAuthThread(QThread):
finished_signal = Signal(bool)
message_signal = Signal(str)
manual_url_signal = Signal(str) # Signal when browser fails to open
def __init__(self, auth_service, parent=None):
super().__init__(parent)
self.auth_service = auth_service
def run(self):
def show_message(msg):
# Check if this is a "browser failed" message with URL
if "Could not open browser" in msg and "Please open this URL manually:" in msg:
# Extract URL from message
url_start = msg.find("Please open this URL manually:") + len("Please open this URL manually:")
url = msg[url_start:].strip()
self.manual_url_signal.emit(url)
else:
self.message_signal.emit(msg)
success = self.auth_service.authorize_oauth(show_browser_message_callback=show_message)
self.finished_signal.emit(success)
oauth_thread = OAuthThread(self.auth_service, self)
# Connect message signal to update progress dialog
def update_progress_message(msg):
if not oauth_cancelled[0]:
progress.setLabelText(f"Waiting for authorisation...\n\n{msg}")
QApplication.processEvents()
# Connect manual URL signal to show copyable dialog
def show_manual_url_dialog(url):
if not oauth_cancelled[0]:
progress.hide() # Hide progress dialog temporarily
self._show_copyable_url_dialog(url)
progress.show()
oauth_thread.message_signal.connect(update_progress_message)
oauth_thread.manual_url_signal.connect(show_manual_url_dialog)
# Wait for thread completion
oauth_success = [False]
def on_oauth_finished(success):
oauth_success[0] = success
oauth_thread.finished_signal.connect(on_oauth_finished)
oauth_thread.start()
# Wait for thread to finish (non-blocking event loop)
while oauth_thread.isRunning():
QApplication.processEvents()
oauth_thread.wait(100) # Check every 100ms
if oauth_cancelled[0]:
# User cancelled - thread will still complete but we ignore result
oauth_thread.wait(2000)
if oauth_thread.isRunning():
oauth_thread.terminate()
break
progress.close()
QApplication.processEvents()
self._update_nexus_status()
self._enable_controls_after_operation()
# Check success first - if OAuth succeeded, ignore cancellation flag
# (progress dialog close can trigger cancel handler even on success)
if oauth_success[0]:
_, _, username = self.auth_service.get_auth_status()
if username:
msg = f"OAuth authorisation successful!<br><br>Authorised as: {username}"
else:
msg = "OAuth authorisation successful!"
MessageService.information(self, "Success", msg, safety_level="low")
elif oauth_cancelled[0]:
MessageService.information(self, "Cancelled", "OAuth authorisation cancelled.", safety_level="low")
else:
MessageService.warning(
self,
"Authorisation Failed",
"OAuth authorisation failed.\n\n"
"If your browser showed a blank page (e.g. Firefox on Steam Deck),\n"
"try again and use 'Paste callback URL' to paste the URL from the address bar.\n\n"
"If you see 'redirect URI mismatch', the OAuth redirect URI must be configured by Nexus.\n\n"
"You can configure an API key in Settings as a fallback.",
safety_level="medium"
)