Files
Jackify/jackify/frontends/gui/dialogs/about_dialog.py

427 lines
15 KiB
Python

"""
About dialog for Jackify.
This dialog displays system information, version details, and provides
access to update checking and external links.
"""
import logging
import os
import platform
import subprocess
from pathlib import Path
from typing import Optional
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QTextEdit, QApplication
)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QFont, QClipboard
from ....backend.services.update_service import UpdateService
from ....backend.models.configuration import SystemInfo
from .... import __version__
logger = logging.getLogger(__name__)
class UpdateCheckThread(QThread):
"""Background thread for checking updates."""
update_check_finished = Signal(object) # UpdateInfo or None
def __init__(self, update_service: UpdateService):
super().__init__()
self.update_service = update_service
def run(self):
"""Check for updates in background."""
try:
update_info = self.update_service.check_for_updates()
self.update_check_finished.emit(update_info)
except Exception as e:
logger.error(f"Error checking for updates: {e}")
self.update_check_finished.emit(None)
class AboutDialog(QDialog):
"""About dialog showing system info and app details."""
def __init__(self, system_info: SystemInfo, parent=None):
super().__init__(parent)
self.system_info = system_info
self.update_service = UpdateService(__version__)
self.update_check_thread = None
self.setup_ui()
self.setup_connections()
def setup_ui(self):
"""Set up the dialog UI."""
self.setWindowTitle("About Jackify")
self.setModal(True)
self.setFixedSize(520, 520)
layout = QVBoxLayout(self)
# Header
header_layout = QVBoxLayout()
# App icon/name
title_label = QLabel("Jackify")
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #3fd0ea; margin: 10px;")
header_layout.addWidget(title_label)
subtitle_label = QLabel(f"v{__version__}")
subtitle_font = QFont()
subtitle_font.setPointSize(12)
subtitle_label.setFont(subtitle_font)
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("color: #666; margin-bottom: 10px;")
header_layout.addWidget(subtitle_label)
tagline_label = QLabel("Simplifying Wabbajack modlist installation and configuration on Linux")
tagline_label.setAlignment(Qt.AlignCenter)
tagline_label.setStyleSheet("color: #888; margin-bottom: 20px;")
header_layout.addWidget(tagline_label)
layout.addLayout(header_layout)
# System Information Group
system_group = QGroupBox("System Information")
system_layout = QVBoxLayout(system_group)
system_info_text = self._get_system_info_text()
system_info_label = QLabel(system_info_text)
system_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
system_info_label.setWordWrap(True)
system_layout.addWidget(system_info_label)
layout.addWidget(system_group)
# Jackify Information Group
jackify_group = QGroupBox("Jackify Information")
jackify_layout = QVBoxLayout(jackify_group)
jackify_info_text = self._get_jackify_info_text()
jackify_info_label = QLabel(jackify_info_text)
jackify_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
jackify_layout.addWidget(jackify_info_label)
layout.addWidget(jackify_group)
# Update status
self.update_status_label = QLabel("")
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
self.update_status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.update_status_label)
# Buttons
button_layout = QHBoxLayout()
# Update check button
self.update_button = QPushButton("Check for Updates")
self.update_button.clicked.connect(self.check_for_updates)
self.update_button.setStyleSheet("""
QPushButton {
background-color: #23272e;
color: #3fd0ea;
font-weight: bold;
padding: 8px 16px;
border-radius: 4px;
border: 2px solid #3fd0ea;
}
QPushButton:hover {
background-color: #3fd0ea;
color: #23272e;
}
QPushButton:pressed {
background-color: #2bb8d6;
color: #23272e;
}
QPushButton:disabled {
background-color: #444;
color: #666;
border-color: #666;
}
""")
button_layout.addWidget(self.update_button)
button_layout.addStretch()
# Copy Info button
copy_button = QPushButton("Copy Info")
copy_button.clicked.connect(self.copy_system_info)
button_layout.addWidget(copy_button)
# External links
github_button = QPushButton("GitHub")
github_button.clicked.connect(self.open_github)
button_layout.addWidget(github_button)
nexus_button = QPushButton("Nexus")
nexus_button.clicked.connect(self.open_nexus)
button_layout.addWidget(nexus_button)
layout.addLayout(button_layout)
# Close button
close_layout = QHBoxLayout()
close_layout.addStretch()
close_button = QPushButton("Close")
close_button.setDefault(True)
close_button.clicked.connect(self.accept)
close_layout.addWidget(close_button)
layout.addLayout(close_layout)
def setup_connections(self):
"""Set up signal connections."""
pass
def _get_system_info_text(self) -> str:
"""Get formatted system information."""
try:
# OS info
os_info = self._get_os_info()
kernel = platform.release()
# Desktop environment
desktop = self._get_desktop_environment()
# Display server
display_server = self._get_display_server()
return f"• OS: {os_info}\n• Kernel: {kernel}\n• Desktop: {desktop}\n• Display: {display_server}"
except Exception as e:
logger.error(f"Error getting system info: {e}")
return "• System info unavailable"
def _get_jackify_info_text(self) -> str:
"""Get formatted Jackify information."""
try:
# Engine version
engine_version = self._get_engine_version()
# Python version
python_version = platform.python_version()
return f"• Engine: {engine_version}\n• Python: {python_version}"
except Exception as e:
logger.error(f"Error getting Jackify info: {e}")
return "• Jackify info unavailable"
def _get_os_info(self) -> str:
"""Get OS distribution name and version."""
try:
if os.path.exists("/etc/os-release"):
with open("/etc/os-release", "r") as f:
lines = f.readlines()
pretty_name = None
name = None
version = None
for line in lines:
line = line.strip()
if line.startswith("PRETTY_NAME="):
pretty_name = line.split("=", 1)[1].strip('"')
elif line.startswith("NAME="):
name = line.split("=", 1)[1].strip('"')
elif line.startswith("VERSION="):
version = line.split("=", 1)[1].strip('"')
# Prefer PRETTY_NAME, fallback to NAME + VERSION
if pretty_name:
return pretty_name
elif name and version:
return f"{name} {version}"
elif name:
return name
# Fallback to platform info
return f"{platform.system()} {platform.release()}"
except Exception as e:
logger.error(f"Error getting OS info: {e}")
return "Unknown Linux"
def _get_desktop_environment(self) -> str:
"""Get desktop environment."""
try:
# Try XDG_CURRENT_DESKTOP first
desktop = os.environ.get("XDG_CURRENT_DESKTOP")
if desktop:
return desktop
# Fallback to DESKTOP_SESSION
desktop = os.environ.get("DESKTOP_SESSION")
if desktop:
return desktop
# Try detecting common DEs
if os.environ.get("KDE_FULL_SESSION"):
return "KDE"
elif os.environ.get("GNOME_DESKTOP_SESSION_ID"):
return "GNOME"
elif os.environ.get("XFCE4_SESSION"):
return "XFCE"
return "Unknown"
except Exception as e:
logger.error(f"Error getting desktop environment: {e}")
return "Unknown"
def _get_display_server(self) -> str:
"""Get display server type (Wayland or X11)."""
try:
# Check XDG_SESSION_TYPE first
session_type = os.environ.get("XDG_SESSION_TYPE")
if session_type:
return session_type.capitalize()
# Check for Wayland display
if os.environ.get("WAYLAND_DISPLAY"):
return "Wayland"
# Check for X11 display
if os.environ.get("DISPLAY"):
return "X11"
return "Unknown"
except Exception as e:
logger.error(f"Error getting display server: {e}")
return "Unknown"
def _get_engine_version(self) -> str:
"""Get jackify-engine version."""
try:
# Try to execute jackify-engine --version
engine_path = Path(__file__).parent.parent.parent.parent / "engine" / "jackify-engine"
if engine_path.exists():
from jackify.backend.handlers.subprocess_utils import get_clean_subprocess_env
result = subprocess.run([str(engine_path), "--version"],
capture_output=True, text=True, timeout=5, env=get_clean_subprocess_env())
if result.returncode == 0:
version = result.stdout.strip()
# Extract just the version number (before the +commit hash)
if '+' in version:
version = version.split('+')[0]
return f"v{version}"
return "Unknown"
except Exception as e:
logger.error(f"Error getting engine version: {e}")
return "Unknown"
def check_for_updates(self):
"""Check for updates in background."""
if self.update_check_thread and self.update_check_thread.isRunning():
return
self.update_button.setEnabled(False)
self.update_button.setText("Checking...")
self.update_status_label.setText("Checking for updates...")
self.update_check_thread = UpdateCheckThread(self.update_service)
self.update_check_thread.update_check_finished.connect(self.update_check_finished)
self.update_check_thread.start()
def update_check_finished(self, update_info):
"""Handle update check completion."""
self.update_button.setEnabled(True)
self.update_button.setText("Check for Updates")
if update_info:
self.update_status_label.setText(f"Update available: v{update_info.version}")
self.update_status_label.setStyleSheet("color: #3fd0ea; font-size: 10pt; margin: 5px;")
# Show update dialog
from .update_dialog import UpdateDialog
update_dialog = UpdateDialog(update_info, self.update_service, self)
update_dialog.exec()
else:
self.update_status_label.setText("You're running the latest version")
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
def copy_system_info(self):
"""Copy system information to clipboard."""
try:
info_text = f"""Jackify v{__version__} (Engine {self._get_engine_version()})
OS: {self._get_os_info()} ({platform.release()})
Desktop: {self._get_desktop_environment()} ({self._get_display_server()})
Python: {platform.python_version()}"""
clipboard = QApplication.clipboard()
clipboard.setText(info_text)
# Briefly update button text
sender = self.sender()
original_text = sender.text()
sender.setText("Copied!")
# Reset button text after delay
from PySide6.QtCore import QTimer
QTimer.singleShot(1000, lambda: sender.setText(original_text))
except Exception as e:
logger.error(f"Error copying system info: {e}")
def open_github(self):
"""Open GitHub repository."""
try:
self._open_url("https://github.com/Omni-guides/Jackify")
except Exception as e:
logger.error(f"Error opening GitHub: {e}")
def open_nexus(self):
"""Open Nexus Mods page."""
try:
self._open_url("https://www.nexusmods.com/site/mods/1427")
except Exception as e:
logger.error(f"Error opening Nexus: {e}")
def _open_url(self, url: str):
"""Open URL with clean environment to avoid AppImage library conflicts."""
import os
env = os.environ.copy()
# Remove AppImage-specific environment variables
appimage_vars = [
'LD_LIBRARY_PATH',
'PYTHONPATH',
'PYTHONHOME',
'QT_PLUGIN_PATH',
'QML2_IMPORT_PATH',
]
if 'APPIMAGE' in env or 'APPDIR' in env:
for var in appimage_vars:
if var in env:
del env[var]
subprocess.Popen(
['xdg-open', url],
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
def closeEvent(self, event):
"""Handle dialog close event."""
if self.update_check_thread and self.update_check_thread.isRunning():
self.update_check_thread.terminate()
self.update_check_thread.wait()
event.accept()