mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 02:23:45 +02:00
Release v0.7 - Tools Hub, Engine Choice, NXM Link Handling, Native Component Install, NSF/CSF Support
This commit is contained in:
@@ -32,6 +32,15 @@ class MainWindowBackendMixin:
|
||||
from jackify.backend.services.update_service import UpdateService
|
||||
from jackify import __version__
|
||||
self.update_service = UpdateService(__version__)
|
||||
|
||||
from jackify.backend.services.nxm_ipc import NxmIpcServer
|
||||
self._nxm_ipc_server = NxmIpcServer(self)
|
||||
self._nxm_ipc_server.url_received.connect(self._on_nxm_url_received)
|
||||
self._nxm_ipc_server.start()
|
||||
|
||||
from jackify.backend.services.nxm_protocol import ensure_nxm_registered
|
||||
ensure_nxm_registered()
|
||||
|
||||
logger.debug(f"GUI Backend initialized - Steam Deck: {self.system_info.is_steamdeck}")
|
||||
|
||||
def _is_steamdeck(self):
|
||||
|
||||
@@ -126,8 +126,39 @@ class MainWindowDialogsMixin:
|
||||
start_new_session=True
|
||||
)
|
||||
|
||||
def _on_nxm_url_received(self, url: str) -> None:
|
||||
"""Handle an incoming nxm:// URL from the IPC server or initial launch."""
|
||||
try:
|
||||
from jackify.backend.services.nxm_url import parse_nxm_url
|
||||
from jackify.backend.services import nxm_session
|
||||
from jackify.tools.verify_install import discover_installed_modlists
|
||||
from jackify.frontends.gui.dialogs.nxm_download_dialog import NxmDownloadDialog
|
||||
|
||||
nxm = parse_nxm_url(url)
|
||||
modlists = discover_installed_modlists()
|
||||
if not modlists:
|
||||
from jackify.frontends.gui.dialogs.nxm_download_dialog import show_no_modlists_error
|
||||
show_no_modlists_error()
|
||||
return
|
||||
|
||||
# Prefer the actively running MO2 instance; fall back to session memory.
|
||||
auto_start = nxm_session.detect_active_mo2_modlist(modlists)
|
||||
if auto_start is None:
|
||||
remembered = nxm_session.get_remembered_modlist()
|
||||
if remembered:
|
||||
auto_start = next((m for m in modlists if m["name"] == remembered), None)
|
||||
|
||||
dlg = NxmDownloadDialog(nxm, modlists, parent=self, auto_start_modlist=auto_start)
|
||||
dlg.show()
|
||||
dlg.raise_()
|
||||
dlg.activateWindow()
|
||||
except Exception as e:
|
||||
logger.warning("Failed to handle NXM URL %r: %s", url, e)
|
||||
|
||||
def cleanup_processes(self):
|
||||
try:
|
||||
if hasattr(self, '_nxm_ipc_server') and self._nxm_ipc_server is not None:
|
||||
self._nxm_ipc_server.stop()
|
||||
if hasattr(self, '_update_thread') and self._update_thread is not None:
|
||||
self._update_thread = self._stop_qthread(self._update_thread, "_update_thread")
|
||||
if hasattr(self, '_gallery_cache_preload_thread') and self._gallery_cache_preload_thread is not None:
|
||||
|
||||
@@ -135,10 +135,10 @@ class MainWindowGeometryMixin:
|
||||
self.showMaximized()
|
||||
|
||||
def _on_child_resize_request(self, mode: str):
|
||||
logger.debug(f"DEBUG: _on_child_resize_request called with mode='{mode}', current_size={self.size()}")
|
||||
logger.debug(f"_on_child_resize_request called with mode='{mode}', current_size={self.size()}")
|
||||
try:
|
||||
if self.system_info and self.system_info.is_steamdeck:
|
||||
logger.debug("DEBUG: Steam Deck detected, ignoring resize request")
|
||||
logger.debug("Steam Deck detected, ignoring resize request")
|
||||
try:
|
||||
if hasattr(self, 'install_ttw_screen') and self.install_ttw_screen.show_details_checkbox:
|
||||
self.install_ttw_screen.show_details_checkbox.setVisible(False)
|
||||
@@ -183,7 +183,7 @@ class MainWindowGeometryMixin:
|
||||
before = self.size()
|
||||
self._programmatic_resize = True
|
||||
self.resize(self.size().width(), target_height)
|
||||
logger.debug(f"DEBUG: Animated fallback resize from {before} to {self.size()}")
|
||||
logger.debug(f"Animated fallback resize from {before} to {self.size()}")
|
||||
QTimer.singleShot(100, lambda: setattr(self, '_programmatic_resize', False))
|
||||
return
|
||||
start_rect = self.geometry()
|
||||
|
||||
@@ -65,6 +65,47 @@ class MainWindowStartupMixin:
|
||||
except Exception as e:
|
||||
print(f"Error checking protontricks: {e}")
|
||||
|
||||
def _check_tool_updates_on_startup(self):
|
||||
class _ToolUpdateCheckThread(QThread):
|
||||
updates_found = Signal(bool)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
from jackify.backend.services.tool_registry import ToolRegistry, get_effective_definitions
|
||||
registry = ToolRegistry()
|
||||
for defn in get_effective_definitions():
|
||||
if defn.pinned_version is not None:
|
||||
continue
|
||||
status = registry.get_status(defn.tool_id)
|
||||
if not status or not status.installed:
|
||||
continue
|
||||
logger.debug(
|
||||
"Startup tool update check: %s installed=%s version=%s",
|
||||
defn.tool_id, status.installed, status.installed_version,
|
||||
)
|
||||
tag = registry.check_latest_version(defn.tool_id)
|
||||
logger.debug("Startup tool update check: %s latest=%s", defn.tool_id, tag)
|
||||
if tag and status.installed_version and tag.lstrip("v") != status.installed_version.lstrip("v"):
|
||||
logger.debug("Startup tool update check: update available for %s", defn.tool_id)
|
||||
self.updates_found.emit(True)
|
||||
return
|
||||
self.updates_found.emit(False)
|
||||
except Exception as e:
|
||||
logger.warning("Tool update check failed: %s", e, exc_info=True)
|
||||
self.updates_found.emit(False)
|
||||
|
||||
def on_result(has_updates: bool):
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(0, lambda: (
|
||||
self.main_menu.notify_tool_updates(has_updates)
|
||||
if hasattr(self, 'main_menu') and hasattr(self.main_menu, 'notify_tool_updates')
|
||||
else None
|
||||
))
|
||||
|
||||
self._tool_update_check_thread = _ToolUpdateCheckThread()
|
||||
self._tool_update_check_thread.updates_found.connect(on_result)
|
||||
self._tool_update_check_thread.start()
|
||||
|
||||
def _check_for_updates_on_startup(self):
|
||||
try:
|
||||
logger.debug("Checking for updates on startup...")
|
||||
|
||||
@@ -76,13 +76,13 @@ class MainWindowUIMixin:
|
||||
version_label.setStyleSheet("color: #bbb; font-size: 13px;")
|
||||
bottom_bar_layout.addWidget(version_label, alignment=Qt.AlignLeft)
|
||||
bottom_bar_layout.addStretch(1)
|
||||
kofi_link = QLabel('<a href="#" style="color:#72A5F2; text-decoration:none;">Support on Ko-fi</a>')
|
||||
kofi_link.setStyleSheet("color: #72A5F2; font-size: 13px;")
|
||||
kofi_link = QLabel('<a href="#" style="color:#3fd0ea; text-decoration:none;">Support on Ko-fi</a>')
|
||||
kofi_link.setStyleSheet("color: #3fd0ea; font-size: 13px;")
|
||||
kofi_link.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
kofi_link.setOpenExternalLinks(False)
|
||||
kofi_link.linkActivated.connect(lambda: self._open_url("https://ko-fi.com/omni1"))
|
||||
kofi_link.setToolTip("Support Jackify development")
|
||||
bottom_bar_layout.addWidget(kofi_link, alignment=Qt.AlignCenter)
|
||||
bottom_bar_layout.addWidget(kofi_link)
|
||||
bottom_bar_layout.addStretch(1)
|
||||
settings_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">Settings</a>')
|
||||
settings_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
|
||||
@@ -229,9 +229,9 @@ class MainWindowUIMixin:
|
||||
return screen
|
||||
|
||||
def _make_third_party_tools_screen(self):
|
||||
from jackify.frontends.gui.screens.third_party_tools import ThirdPartyToolsScreen
|
||||
screen = ThirdPartyToolsScreen(
|
||||
stacked_widget=self.stacked_widget, main_menu_index=0,
|
||||
from jackify.frontends.gui.screens.tools_hub import ToolsHubScreen
|
||||
screen = ToolsHubScreen(
|
||||
stacked_widget=self.stacked_widget, main_menu_index=0, ttw_screen_index=5,
|
||||
)
|
||||
self.third_party_tools_screen = screen
|
||||
return screen
|
||||
|
||||
@@ -19,6 +19,7 @@ Usage:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -46,10 +47,12 @@ class ThreadLifecycleMixin:
|
||||
return None
|
||||
|
||||
for name in (signal_names or []):
|
||||
try:
|
||||
getattr(thread, name).disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore", RuntimeWarning)
|
||||
try:
|
||||
getattr(thread, name).disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Register in the module-level set so the reference survives screen destruction.
|
||||
# Remove from registry when the thread finishes so it can be GC'd cleanly.
|
||||
@@ -68,6 +71,36 @@ class ThreadLifecycleMixin:
|
||||
pass
|
||||
self._park_all_threads()
|
||||
|
||||
def _kill_prefix_wine_processes(self, appid: str = '') -> None:
|
||||
"""Kill wine/winetricks subprocesses on user-initiated cancel.
|
||||
|
||||
Called before parking threads so the blocked subprocess.run() calls inside
|
||||
the thread actually return rather than running until completion.
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
for pattern in ('winetricks', 'protontricks'):
|
||||
subprocess.run(['pkill', '-9', '-f', pattern], capture_output=True)
|
||||
if not appid:
|
||||
return
|
||||
try:
|
||||
from jackify.backend.handlers.path_handler import PathHandler
|
||||
from jackify.backend.handlers.winetricks_handler import WinetricksHandler
|
||||
compat = PathHandler.find_compat_data(str(appid))
|
||||
if not compat:
|
||||
return
|
||||
pfx = str(compat / 'pfx')
|
||||
wine_bin = WinetricksHandler()._get_wine_binary_for_prefix(pfx)
|
||||
if not wine_bin:
|
||||
return
|
||||
wineserver = os.path.join(os.path.dirname(wine_bin), 'wineserver')
|
||||
if os.path.isfile(wineserver):
|
||||
subprocess.run([wineserver, '-k'],
|
||||
env={**os.environ, 'WINEPREFIX': pfx},
|
||||
timeout=5, capture_output=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _park_all_threads(self):
|
||||
"""Park every running QThread attribute found on this instance.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user