mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-17 17:37:46 +02:00
Sync from development - prepare for v0.3.0
This commit is contained in:
260
jackify/frontends/gui/screens/install_modlist_nexus.py
Normal file
260
jackify/frontends/gui/screens/install_modlist_nexus.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user