5 Commits

Author SHA1 Message Date
Omni
2511c9334c Sync from development - prepare for v0.2.0.8 2025-12-29 19:55:38 +00:00
Omni
5869a896a8 Sync from development - prepare for v0.2.0.7 2025-12-28 22:17:44 +00:00
Omni
99fb369d5e Sync from development - prepare for v0.2.0.6 2025-12-28 18:52:07 +00:00
Omni
a813236e51 Sync from development - prepare for v0.2.0.5 2025-12-24 21:53:12 +00:00
Omni
a7ed4b2a1e Sync from development - prepare for v0.2.0.4 2025-12-23 21:49:18 +00:00
69 changed files with 1207 additions and 588 deletions

View File

@@ -1,5 +1,78 @@
# Jackify Changelog
## v0.2.0.8 - Bug Fixes and Improvements
**Release Date:** 2025-12-29
### Bug Fixes
- Fixed Configure New/Existing/TTW screens missing Activity tab and progress updates
- Fixed cancel/back buttons crashing in Configure workflows
### Improvements
- Install directory now auto-appends modlist name when selected from gallery
### Known Issues
- Mod filter temporarily disabled in gallery due to technical issue (tag and game filters still work)
---
## v0.2.0.7 - Critical Auth Fix
**Release Date:** 2025-12-28
### Critical Bug Fixes
- **OAuth Token Loss**: Fixed version comparison bug that was deleting OAuth tokens every time settings were saved (affects users on v0.2.0.4+)
- Fixed internal import paths for improved stability
---
## v0.2.0.6 - Premium Detection and Engine Update
**Release Date:** 2025-12-28
**IMPORTANT:** If you are on v0.2.0.5, automatic updates will not work. You must manually download and install v0.2.0.6.
### Engine Updates
- **jackify-engine 0.4.4**: Latest engine version with improvements
### Critical Bug Fixes
- **Auto-Update System**: Fixed broken update dialog import that prevented automatic updates
- **Premium Detection**: Fixed false Premium errors caused by overly-broad detection pattern triggering on jackify-engine 0.4.3's userinfo JSON output
- **Custom Data Directory**: Fixed AppImage always creating ~/Jackify on startup, even when user configured a custom jackify_data_dir
- **Proton Auto-Selection**: Fixed auto-selection writing invalid "auto" string to config on detection failure
### Quality Improvements
- Added pre-build import validator to prevent broken imports from reaching production
---
## v0.2.0.5 - Emergency OAuth Fix
**Release Date:** 2025-12-24
### Critical Bug Fixes
- **OAuth Authentication**: Fixed regression in v0.2.0.4 that prevented OAuth token encryption/decryption, breaking Nexus authentication for users
---
## v0.2.0.4 - Bugfixes & Improvements
**Release Date:** 2025-12-23
### Engine Updates
- **jackify-engine 0.4.3**: Fixed case sensitivity issues, archive extraction crashes, and improved error messages
### Bug Fixes
- Fixed modlist gallery metadata showing outdated versions (now always fetches fresh data)
- Fixed hardcoded ~/Jackify paths preventing custom data directory settings
- Fixed update check blocking GUI startup
- Improved Steam restart reliability (3-minute timeout, better error handling)
- Fixed Protontricks Flatpak installation on Steam Deck
### Backend Changes
- GPU texture conversion now always enabled (config setting deprecated)
### UI Improvements
- Redesigned modlist detail view to show more of hero image
- Improved gallery loading with animated feedback and faster initial load
---
## v0.2.0.3 - Engine Bugfix & Settings Cleanup
**Release Date:** 2025-12-21

View File

@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
Wabbajack modlists natively on Linux systems.
"""
__version__ = "0.2.0.3"
__version__ = "0.2.0.8"

View File

@@ -680,7 +680,8 @@ class ModlistInstallCLI:
start_time = time.time()
# --- BEGIN: TEE LOGGING SETUP & LOG ROTATION ---
log_dir = Path.home() / "Jackify" / "logs"
from jackify.shared.paths import get_jackify_logs_dir
log_dir = get_jackify_logs_dir()
log_dir.mkdir(parents=True, exist_ok=True)
workflow_log_path = log_dir / "Modlist_Install_workflow.log"
# Log rotation: keep last 3 logs, 1MB each (adjust as needed)
@@ -775,12 +776,6 @@ class ModlistInstallCLI:
cmd.append('--debug')
self.logger.info("Adding --debug flag to jackify-engine")
# Check GPU setting and add --no-gpu flag if disabled
gpu_enabled = config_handler.get('enable_gpu_texture_conversion', True)
if not gpu_enabled:
cmd.append('--no-gpu')
self.logger.info("GPU texture conversion disabled - adding --no-gpu flag to jackify-engine")
# Store original environment values to restore later
original_env_values = {
'NEXUS_API_KEY': os.environ.get('NEXUS_API_KEY'),

View File

@@ -157,7 +157,8 @@ class ConfigHandler:
# Migration: v0.0.x -> v0.2.0
# Encryption changed from cryptography (Fernet) to pycryptodome (AES-GCM)
# Old encrypted API keys cannot be decrypted, must be re-entered
if current_version < "0.2.0":
from packaging import version
if version.parse(current_version) < version.parse("0.2.0"):
# Clear old encrypted credentials
if self.settings.get("nexus_api_key"):
logger.warning("Clearing saved API key due to encryption format change")
@@ -389,6 +390,14 @@ class ConfigHandler:
"""
try:
from Crypto.Cipher import AES
# Check if MODE_GCM is available (pycryptodome has it, old pycrypto doesn't)
if not hasattr(AES, 'MODE_GCM'):
# Fallback to base64 decode if old pycrypto is installed
try:
return base64.b64decode(encrypted_key.encode('utf-8')).decode('utf-8')
except:
return None
# Derive 32-byte AES key
key = base64.urlsafe_b64decode(self._get_encryption_key())
@@ -411,6 +420,12 @@ class ConfigHandler:
return base64.b64decode(encrypted_key.encode('utf-8')).decode('utf-8')
except:
return None
except AttributeError:
# Old pycrypto doesn't have MODE_GCM, fallback to base64
try:
return base64.b64decode(encrypted_key.encode('utf-8')).decode('utf-8')
except:
return None
except Exception as e:
# Might be old base64-only format, try decoding
try:

View File

@@ -1196,7 +1196,8 @@ class InstallWabbajackHandler:
"""Displays the final success message and next steps."""
# Basic log file path (assuming standard location)
# TODO: Get log file path more reliably if needed
log_path = Path.home() / "Jackify" / "logs" / "jackify-cli.log"
from jackify.shared.paths import get_jackify_logs_dir
log_path = get_jackify_logs_dir() / "jackify-cli.log"
print("\n───────────────────────────────────────────────────────────────────")
print(f"{COLOR_INFO}Wabbajack Installation Completed Successfully!{COLOR_RESET}")

View File

@@ -21,7 +21,8 @@ class LoggingHandler:
logger = LoggingHandler().setup_logger('install_wabbajack', 'jackify-install-wabbajack.log')
"""
def __init__(self):
self.log_dir = Path.home() / "Jackify" / "logs"
from jackify.shared.paths import get_jackify_logs_dir
self.log_dir = get_jackify_logs_dir()
self.ensure_log_directory()
def ensure_log_directory(self) -> None:

View File

@@ -558,7 +558,8 @@ class ModlistInstallCLI:
start_time = time.time()
# --- BEGIN: TEE LOGGING SETUP & LOG ROTATION ---
log_dir = Path.home() / "Jackify" / "logs"
from jackify.shared.paths import get_jackify_logs_dir
log_dir = get_jackify_logs_dir()
log_dir.mkdir(parents=True, exist_ok=True)
workflow_log_path = log_dir / "Modlist_Install_workflow.log"
# Log rotation: keep last 3 logs, 1MB each (adjust as needed)
@@ -644,12 +645,6 @@ class ModlistInstallCLI:
cmd.append('--debug')
self.logger.info("Debug mode enabled in config - passing --debug flag to jackify-engine")
# Check GPU setting and add --no-gpu flag if disabled
gpu_enabled = config_handler.get('enable_gpu_texture_conversion', True)
if not gpu_enabled:
cmd.append('--no-gpu')
self.logger.info("GPU texture conversion disabled - passing --no-gpu flag to jackify-engine")
# Determine if this is a local .wabbajack file or an online modlist
modlist_value = self.context.get('modlist_value')
machineid = self.context.get('machineid')

View File

@@ -142,6 +142,11 @@ class OAuthTokenHandler:
"""
try:
from Crypto.Cipher import AES
# Check if MODE_GCM is available (pycryptodome has it, old pycrypto doesn't)
if not hasattr(AES, 'MODE_GCM'):
logger.error("pycryptodome required for token decryption (pycrypto doesn't support MODE_GCM)")
return None
# Derive 32-byte AES key from encryption_key
key = base64.urlsafe_b64decode(self._encryption_key)
@@ -163,6 +168,9 @@ class OAuthTokenHandler:
except ImportError:
logger.error("pycryptodome package not available for token decryption")
return None
except AttributeError:
logger.error("pycryptodome required for token decryption (pycrypto doesn't support MODE_GCM)")
return None
except Exception as e:
logger.error(f"Failed to decrypt data: {e}")
return None

View File

@@ -205,8 +205,8 @@ class ShortcutHandler:
time.sleep(1) # Give some time for the install to complete
# Now import it
import steam_vdf
import vdf as steam_vdf
with open(shortcuts_file, 'rb') as f:
shortcuts_data = steam_vdf.load(f)

View File

@@ -48,6 +48,9 @@ def get_clean_subprocess_env(extra_env=None):
env = os.environ.copy()
# Save APPDIR before removing it (we need it to find bundled tools)
appdir = env.get('APPDIR')
# Remove AppImage-specific variables that can confuse subprocess calls
# These variables cause subprocesses to be interpreted as new AppImage launches
for key in ['APPIMAGE', 'APPDIR', 'ARGV0', 'OWD']:
@@ -57,10 +60,10 @@ def get_clean_subprocess_env(extra_env=None):
for k in list(env):
if k.startswith('_MEIPASS'):
del env[k]
# Get current PATH - ensure we preserve system paths
current_path = env.get('PATH', '')
# Ensure common system directories are in PATH if not already present
# This is critical for tools like lz4 that might be in /usr/bin, /usr/local/bin, etc.
system_paths = ['/usr/bin', '/usr/local/bin', '/bin', '/sbin', '/usr/sbin']
@@ -68,10 +71,10 @@ def get_clean_subprocess_env(extra_env=None):
for sys_path in system_paths:
if sys_path not in path_parts and os.path.isdir(sys_path):
path_parts.append(sys_path)
# Add bundled tools directory to PATH if running as AppImage
# This ensures lz4, unzip, xz, etc. are available to subprocesses
appdir = env.get('APPDIR')
# Note: appdir was saved before env cleanup above
tools_dir = None
if appdir:

View File

@@ -23,7 +23,8 @@ from .subprocess_utils import get_clean_subprocess_env
logger = logging.getLogger(__name__)
# Define default TTW_Linux_Installer paths
JACKIFY_BASE_DIR = Path.home() / "Jackify"
from jackify.shared.paths import get_jackify_data_dir
JACKIFY_BASE_DIR = get_jackify_data_dir()
DEFAULT_TTW_INSTALLER_DIR = JACKIFY_BASE_DIR / "TTW_Linux_Installer"
TTW_INSTALLER_EXECUTABLE_NAME = "ttw_linux_gui" # Same executable, runs in CLI mode with args

View File

@@ -1196,7 +1196,8 @@ class InstallWabbajackHandler:
"""Displays the final success message and next steps."""
# Basic log file path (assuming standard location)
# TODO: Get log file path more reliably if needed
log_path = Path.home() / "Jackify" / "logs" / "jackify-cli.log"
from jackify.shared.paths import get_jackify_logs_dir
log_path = get_jackify_logs_dir() / "jackify-cli.log"
print("\n───────────────────────────────────────────────────────────────────")
print(f"{COLOR_INFO}Wabbajack Installation Completed Successfully!{COLOR_RESET}")

View File

@@ -30,7 +30,8 @@ class AutomatedPrefixService:
"""
def __init__(self, system_info=None):
self.scripts_dir = Path.home() / "Jackify/scripts"
from jackify.shared.paths import get_jackify_data_dir
self.scripts_dir = get_jackify_data_dir() / "scripts"
self.scripts_dir.mkdir(parents=True, exist_ok=True)
self.system_info = system_info
# Use shared timing for consistency across services
@@ -749,7 +750,8 @@ echo Creating Proton prefix...
timeout /t 3 /nobreak >nul
echo Prefix creation complete.
"""
batch_path = Path.home() / "Jackify/temp_prefix_creation.bat"
from jackify.shared.paths import get_jackify_data_dir
batch_path = get_jackify_data_dir() / "temp_prefix_creation.bat"
batch_path.parent.mkdir(parents=True, exist_ok=True)
with open(batch_path, 'w') as f:

View File

@@ -25,7 +25,8 @@ from jackify.shared.paths import get_jackify_data_dir
class ModlistGalleryService:
"""Service for fetching and caching modlist metadata from jackify-engine"""
CACHE_VALIDITY_DAYS = 7 # Refresh cache after 7 days
# REMOVED: CACHE_VALIDITY_DAYS - metadata is now always fetched fresh from engine
# Images are still cached indefinitely (managed separately)
# CRITICAL: Thread lock to prevent concurrent engine calls that could cause recursive spawning
_engine_call_lock = threading.Lock()
@@ -59,31 +60,20 @@ class ModlistGalleryService:
"""
Fetch modlist metadata from jackify-engine.
NOTE: Metadata is ALWAYS fetched fresh from the engine to ensure up-to-date
version numbers and sizes for frequently-updated modlists. Only images are cached.
Args:
include_validation: Include validation status (slower)
include_search_index: Include mod search index (slower)
sort_by: Sort order (title, size, date)
force_refresh: Force refresh even if cache is valid
force_refresh: Deprecated parameter (kept for API compatibility)
Returns:
ModlistMetadataResponse or None if fetch fails
"""
# Check cache first unless force refresh
# If include_search_index is True, check if cache has mods before using it
if not force_refresh:
cached = self._load_from_cache()
if cached and self._is_cache_valid():
# If we need search index, check if cached data has mods
if include_search_index:
# Check if at least one modlist has mods (indicates cache was built with search index)
has_mods = any(hasattr(m, 'mods') and m.mods for m in cached.modlists)
if has_mods:
return cached # Cache has mods, use it
# Cache doesn't have mods, need to fetch fresh
else:
return cached # Don't need search index, use cache
# Fetch fresh data from jackify-engine
# Always fetch fresh data from jackify-engine
# The engine itself is fast (~1-2 seconds) and always gets latest metadata
try:
metadata = self._fetch_from_engine(
include_validation=include_validation,
@@ -91,6 +81,7 @@ class ModlistGalleryService:
sort_by=sort_by
)
# Still save to cache as a fallback for offline scenarios
if metadata:
self._save_to_cache(metadata)
@@ -98,7 +89,8 @@ class ModlistGalleryService:
except Exception as e:
print(f"Error fetching modlist metadata: {e}")
# Fall back to cache if available
print("Falling back to cached metadata (may be outdated)")
# Fall back to cache if network/engine fails
return self._load_from_cache()
def _fetch_from_engine(
@@ -253,17 +245,6 @@ class ModlistGalleryService:
return result
def _is_cache_valid(self) -> bool:
"""Check if cache is still valid based on age"""
if not self.METADATA_CACHE_FILE.exists():
return False
# Check file modification time
mtime = datetime.fromtimestamp(self.METADATA_CACHE_FILE.stat().st_mtime)
age = datetime.now() - mtime
return age < timedelta(days=self.CACHE_VALIDITY_DAYS)
def download_images(
self,
game_filter: Optional[str] = None,

View File

@@ -288,15 +288,6 @@ class ModlistService:
# Build command (copied from working code)
cmd = [engine_path, 'install', '--show-file-progress']
# Check GPU setting
from jackify.backend.handlers.config_handler import ConfigHandler
config_handler = ConfigHandler()
gpu_enabled = config_handler.get('enable_gpu_texture_conversion', True)
logger.info(f"GPU texture conversion setting: {gpu_enabled}")
if not gpu_enabled:
cmd.append('--no-gpu')
logger.info("Added --no-gpu flag to jackify-engine command")
modlist_value = context.get('modlist_value')
if modlist_value and modlist_value.endswith('.wabbajack') and os.path.isfile(modlist_value):
cmd += ['-w', modlist_value]

View File

@@ -177,7 +177,7 @@ class NativeSteamOperationsService:
# Also check additional Steam libraries via libraryfolders.vdf
try:
from jackify.shared.paths import PathHandler
from jackify.backend.handlers.path_handler import PathHandler
all_steam_libs = PathHandler.get_all_steam_library_paths()
for lib_path in all_steam_libs:

View File

@@ -132,14 +132,31 @@ class ProtontricksDetectionService:
logger.error(error_msg)
return False, error_msg
# Install command
install_cmd = ["flatpak", "install", "-u", "-y", "--noninteractive", "flathub", "com.github.Matoking.protontricks"]
# Install command - use --user flag for user-level installation (works on Steam Deck)
# This avoids requiring system-wide installation permissions
install_cmd = ["flatpak", "install", "--user", "-y", "--noninteractive", "flathub", "com.github.Matoking.protontricks"]
# Use clean environment
env = handler._get_clean_subprocess_env()
# Run installation
process = subprocess.run(install_cmd, check=True, text=True, env=env, capture_output=True)
# Log the command for debugging
logger.debug(f"Running flatpak install command: {' '.join(install_cmd)}")
# Run installation with timeout (5 minutes should be plenty)
process = subprocess.run(
install_cmd,
check=True,
text=True,
env=env,
capture_output=True,
timeout=300 # 5 minute timeout
)
# Log stdout/stderr for debugging (even on success, might contain useful info)
if process.stdout:
logger.debug(f"Flatpak install stdout: {process.stdout}")
if process.stderr:
logger.debug(f"Flatpak install stderr: {process.stderr}")
# Clear cache to force re-detection
self._cached_detection_valid = False
@@ -152,13 +169,41 @@ class ProtontricksDetectionService:
error_msg = "Flatpak command not found. Please install Flatpak first."
logger.error(error_msg)
return False, error_msg
except subprocess.CalledProcessError as e:
error_msg = f"Flatpak installation failed: {e}"
except subprocess.TimeoutExpired:
error_msg = "Flatpak installation timed out after 5 minutes. Please check your network connection and try again."
logger.error(error_msg)
return False, error_msg
except subprocess.CalledProcessError as e:
# Include stderr in error message for better debugging
stderr_msg = e.stderr.strip() if e.stderr else "No error details available"
stdout_msg = e.stdout.strip() if e.stdout else ""
# Try to extract meaningful error from stderr
if stderr_msg:
# Common errors: permission denied, network issues, etc.
if "permission" in stderr_msg.lower() or "denied" in stderr_msg.lower():
error_msg = f"Permission denied. Try running: flatpak install --user flathub com.github.Matoking.protontricks\n\nDetails: {stderr_msg}"
elif "network" in stderr_msg.lower() or "connection" in stderr_msg.lower():
error_msg = f"Network error during installation. Check your internet connection.\n\nDetails: {stderr_msg}"
elif "already installed" in stderr_msg.lower():
# This might actually be success - clear cache and re-detect
logger.info("Protontricks appears to already be installed (according to flatpak output)")
self._cached_detection_valid = False
return True, "Protontricks is already installed."
else:
error_msg = f"Flatpak installation failed:\n\n{stderr_msg}"
if stdout_msg:
error_msg += f"\n\nOutput: {stdout_msg}"
else:
error_msg = f"Flatpak installation failed with return code {e.returncode}."
if stdout_msg:
error_msg += f"\n\nOutput: {stdout_msg}"
logger.error(f"Flatpak installation error: {error_msg}")
return False, error_msg
except Exception as e:
error_msg = f"Unexpected error during Flatpak installation: {e}"
logger.error(error_msg)
logger.error(error_msg, exc_info=True)
return False, error_msg
def get_installation_guidance(self) -> str:

View File

@@ -402,14 +402,15 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
if final_check.returncode != 0:
logger.info("Steam processes successfully force terminated.")
else:
report("Failed to terminate Steam processes.")
return False
# Steam might still be running, but proceed anyway - wait phase will verify
logger.warning("Steam processes may still be running after termination attempts. Proceeding to start phase...")
report("Steam shutdown incomplete, but proceeding...")
else:
logger.info("Steam processes successfully terminated.")
except Exception as e:
logger.error(f"Error during Steam shutdown: {e}")
report("Failed to shut down Steam.")
return False
# Don't fail completely on shutdown errors - proceed to start phase
logger.warning(f"Error during Steam shutdown: {e}. Proceeding to start phase anyway...")
report("Steam shutdown had issues, but proceeding...")
report("Steam closed successfully.")
@@ -427,42 +428,56 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
return False
else:
# All other distros: Use start_steam() which now uses -foreground to ensure GUI opens
if not start_steam(
steam_started = start_steam(
is_steamdeck_flag=_is_steam_deck,
is_flatpak_flag=_is_flatpak,
env_override=start_env,
strategy=strategy,
):
report("Failed to start Steam.")
return False
)
# Even if start_steam() returns False, Steam might still be starting
# Give it a chance by proceeding to wait phase
if not steam_started:
logger.warning("start_steam() returned False, but proceeding to wait phase in case Steam is starting anyway")
report("Steam start command issued, waiting for process...")
# Wait for Steam to fully initialize
# CRITICAL: Use steamwebhelper (actual Steam process), not "steam" (matches steam-powerbuttond, etc.)
report("Waiting for Steam to fully start")
logger.info("Waiting up to 2 minutes for Steam to fully initialize...")
max_startup_wait = 120
logger.info("Waiting up to 3 minutes (180 seconds) for Steam to fully initialize...")
max_startup_wait = 180 # Increased from 120 to 180 seconds (3 minutes) for slower systems
elapsed_wait = 0
initial_wait_done = False
last_status_log = 0 # Track when we last logged status
while elapsed_wait < max_startup_wait:
try:
# Log status every 30 seconds so user knows we're still waiting
if elapsed_wait - last_status_log >= 30:
remaining = max_startup_wait - elapsed_wait
logger.info(f"Still waiting for Steam... ({elapsed_wait}s elapsed, {remaining}s remaining)")
if progress_callback:
progress_callback(f"Waiting for Steam... ({elapsed_wait}s / {max_startup_wait}s)")
last_status_log = elapsed_wait
# Use steamwebhelper for detection (matches shutdown logic)
result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=start_env)
if result.returncode == 0:
if not initial_wait_done:
logger.info("Steam process detected. Waiting additional time for full initialization...")
logger.info(f"Steam process detected at {elapsed_wait}s. Waiting additional time for full initialization...")
initial_wait_done = True
time.sleep(5)
elapsed_wait += 5
if initial_wait_done and elapsed_wait >= 15:
# Require at least 20 seconds of stable detection (increased from 15)
if initial_wait_done and elapsed_wait >= 20:
final_check = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=start_env)
if final_check.returncode == 0:
report("Steam started successfully.")
logger.info("Steam confirmed running after wait.")
logger.info(f"Steam confirmed running after {elapsed_wait}s wait.")
return True
else:
logger.warning("Steam process disappeared during final initialization wait.")
break
logger.warning("Steam process disappeared during final initialization wait, continuing to wait...")
# Don't break - continue waiting in case Steam is still starting
initial_wait_done = False # Reset to allow re-detection
else:
logger.debug(f"Steam process not yet detected. Waiting... ({elapsed_wait + 5}s)")
time.sleep(5)
@@ -472,6 +487,7 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
time.sleep(5)
elapsed_wait += 5
report("Steam did not start within timeout.")
logger.error("Steam failed to start/initialize within the allowed time.")
# Only reach here if we've waited the full duration
report(f"Steam did not start within {max_startup_wait}s timeout.")
logger.error(f"Steam failed to start/initialize within the allowed time ({elapsed_wait}s elapsed).")
return False

View File

@@ -271,9 +271,9 @@ class UpdateService:
total_size = int(response.headers.get('content-length', 0))
downloaded_size = 0
# Create update directory in user's home directory
home_dir = Path.home()
update_dir = home_dir / "Jackify" / "updates"
# Create update directory in user's data directory
from jackify.shared.paths import get_jackify_data_dir
update_dir = get_jackify_data_dir() / "updates"
update_dir.mkdir(parents=True, exist_ok=True)
temp_file = update_dir / f"Jackify-{update_info.version}.AppImage"
@@ -345,9 +345,9 @@ class UpdateService:
Path to helper script, or None if creation failed
"""
try:
# Create update directory in user's home directory
home_dir = Path.home()
update_dir = home_dir / "Jackify" / "updates"
# Create update directory in user's data directory
from jackify.shared.paths import get_jackify_data_dir
update_dir = get_jackify_data_dir() / "updates"
update_dir.mkdir(parents=True, exist_ok=True)
helper_script = update_dir / "update_helper.sh"

View File

@@ -34,9 +34,6 @@ def is_non_premium_indicator(line: str) -> bool:
if phrase in normalized:
return True
if "nexus" in normalized and "premium" in normalized:
return True
# Manual download + Nexus URL implies premium requirement in current workflows.
if "manual download" in normalized and ("nexusmods.com" in normalized or "nexus mods" in normalized):
return True

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,7 @@
"targets": {
".NETCoreApp,Version=v8.0": {},
".NETCoreApp,Version=v8.0/linux-x64": {
"jackify-engine/0.4.2": {
"jackify-engine/0.4.4": {
"dependencies": {
"Markdig": "0.40.0",
"Microsoft.Extensions.Configuration.Json": "9.0.1",
@@ -22,16 +22,16 @@
"SixLabors.ImageSharp": "3.1.6",
"System.CommandLine": "2.0.0-beta4.22272.1",
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
"Wabbajack.CLI.Builder": "0.4.2",
"Wabbajack.Downloaders.Bethesda": "0.4.2",
"Wabbajack.Downloaders.Dispatcher": "0.4.2",
"Wabbajack.Hashing.xxHash64": "0.4.2",
"Wabbajack.Networking.Discord": "0.4.2",
"Wabbajack.Networking.GitHub": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2",
"Wabbajack.Server.Lib": "0.4.2",
"Wabbajack.Services.OSIntegrated": "0.4.2",
"Wabbajack.VFS": "0.4.2",
"Wabbajack.CLI.Builder": "0.4.4",
"Wabbajack.Downloaders.Bethesda": "0.4.4",
"Wabbajack.Downloaders.Dispatcher": "0.4.4",
"Wabbajack.Hashing.xxHash64": "0.4.4",
"Wabbajack.Networking.Discord": "0.4.4",
"Wabbajack.Networking.GitHub": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4",
"Wabbajack.Server.Lib": "0.4.4",
"Wabbajack.Services.OSIntegrated": "0.4.4",
"Wabbajack.VFS": "0.4.4",
"MegaApiClient": "1.0.0.0",
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.22"
},
@@ -1781,7 +1781,7 @@
}
}
},
"Wabbajack.CLI.Builder/0.4.2": {
"Wabbajack.CLI.Builder/0.4.4": {
"dependencies": {
"Microsoft.Extensions.Configuration.Json": "9.0.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
@@ -1791,109 +1791,109 @@
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"System.CommandLine": "2.0.0-beta4.22272.1",
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
"Wabbajack.Paths": "0.4.2"
"Wabbajack.Paths": "0.4.4"
},
"runtime": {
"Wabbajack.CLI.Builder.dll": {}
}
},
"Wabbajack.Common/0.4.2": {
"Wabbajack.Common/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"System.Reactive": "6.0.1",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Networking.Http": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2"
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Networking.Http": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4"
},
"runtime": {
"Wabbajack.Common.dll": {}
}
},
"Wabbajack.Compiler/0.4.2": {
"Wabbajack.Compiler/0.4.4": {
"dependencies": {
"F23.StringSimilarity": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Dispatcher": "0.4.2",
"Wabbajack.Installer": "0.4.2",
"Wabbajack.VFS": "0.4.2",
"Wabbajack.Downloaders.Dispatcher": "0.4.4",
"Wabbajack.Installer": "0.4.4",
"Wabbajack.VFS": "0.4.4",
"ini-parser-netstandard": "2.5.2"
},
"runtime": {
"Wabbajack.Compiler.dll": {}
}
},
"Wabbajack.Compression.BSA/0.4.2": {
"Wabbajack.Compression.BSA/0.4.4": {
"dependencies": {
"K4os.Compression.LZ4.Streams": "1.3.8",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SharpZipLib": "1.4.2",
"Wabbajack.Common": "0.4.2",
"Wabbajack.DTOs": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.DTOs": "0.4.4"
},
"runtime": {
"Wabbajack.Compression.BSA.dll": {}
}
},
"Wabbajack.Compression.Zip/0.4.2": {
"Wabbajack.Compression.Zip/0.4.4": {
"dependencies": {
"Wabbajack.IO.Async": "0.4.2"
"Wabbajack.IO.Async": "0.4.4"
},
"runtime": {
"Wabbajack.Compression.Zip.dll": {}
}
},
"Wabbajack.Configuration/0.4.2": {
"Wabbajack.Configuration/0.4.4": {
"runtime": {
"Wabbajack.Configuration.dll": {}
}
},
"Wabbajack.Downloaders.Bethesda/0.4.2": {
"Wabbajack.Downloaders.Bethesda/0.4.4": {
"dependencies": {
"LibAES-CTR": "1.1.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SharpZipLib": "1.4.2",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Networking.BethesdaNet": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Networking.BethesdaNet": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.Bethesda.dll": {}
}
},
"Wabbajack.Downloaders.Dispatcher/0.4.2": {
"Wabbajack.Downloaders.Dispatcher/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Bethesda": "0.4.2",
"Wabbajack.Downloaders.GameFile": "0.4.2",
"Wabbajack.Downloaders.GoogleDrive": "0.4.2",
"Wabbajack.Downloaders.Http": "0.4.2",
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Downloaders.Manual": "0.4.2",
"Wabbajack.Downloaders.MediaFire": "0.4.2",
"Wabbajack.Downloaders.Mega": "0.4.2",
"Wabbajack.Downloaders.ModDB": "0.4.2",
"Wabbajack.Downloaders.Nexus": "0.4.2",
"Wabbajack.Downloaders.VerificationCache": "0.4.2",
"Wabbajack.Downloaders.WabbajackCDN": "0.4.2",
"Wabbajack.Networking.WabbajackClientApi": "0.4.2"
"Wabbajack.Downloaders.Bethesda": "0.4.4",
"Wabbajack.Downloaders.GameFile": "0.4.4",
"Wabbajack.Downloaders.GoogleDrive": "0.4.4",
"Wabbajack.Downloaders.Http": "0.4.4",
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Downloaders.Manual": "0.4.4",
"Wabbajack.Downloaders.MediaFire": "0.4.4",
"Wabbajack.Downloaders.Mega": "0.4.4",
"Wabbajack.Downloaders.ModDB": "0.4.4",
"Wabbajack.Downloaders.Nexus": "0.4.4",
"Wabbajack.Downloaders.VerificationCache": "0.4.4",
"Wabbajack.Downloaders.WabbajackCDN": "0.4.4",
"Wabbajack.Networking.WabbajackClientApi": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.Dispatcher.dll": {}
}
},
"Wabbajack.Downloaders.GameFile/0.4.2": {
"Wabbajack.Downloaders.GameFile/0.4.4": {
"dependencies": {
"GameFinder.StoreHandlers.EADesktop": "4.5.0",
"GameFinder.StoreHandlers.EGS": "4.5.0",
@@ -1903,360 +1903,360 @@
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.VFS": "0.4.2"
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.VFS": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.GameFile.dll": {}
}
},
"Wabbajack.Downloaders.GoogleDrive/0.4.2": {
"Wabbajack.Downloaders.GoogleDrive/0.4.4": {
"dependencies": {
"HtmlAgilityPack": "1.11.72",
"Microsoft.AspNetCore.Http.Extensions": "2.3.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.2",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Networking.Http": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Networking.Http": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.GoogleDrive.dll": {}
}
},
"Wabbajack.Downloaders.Http/0.4.2": {
"Wabbajack.Downloaders.Http/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.2",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Networking.BethesdaNet": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Networking.BethesdaNet": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.Http.dll": {}
}
},
"Wabbajack.Downloaders.Interfaces/0.4.2": {
"Wabbajack.Downloaders.Interfaces/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.Compression.Zip": "0.4.2",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2"
"Wabbajack.Compression.Zip": "0.4.4",
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.Interfaces.dll": {}
}
},
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.2": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.4": {
"dependencies": {
"F23.StringSimilarity": "6.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Networking.Http": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Networking.Http": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
}
},
"Wabbajack.Downloaders.Manual/0.4.2": {
"Wabbajack.Downloaders.Manual/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.Manual.dll": {}
}
},
"Wabbajack.Downloaders.MediaFire/0.4.2": {
"Wabbajack.Downloaders.MediaFire/0.4.4": {
"dependencies": {
"HtmlAgilityPack": "1.11.72",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.MediaFire.dll": {}
}
},
"Wabbajack.Downloaders.Mega/0.4.2": {
"Wabbajack.Downloaders.Mega/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.Mega.dll": {}
}
},
"Wabbajack.Downloaders.ModDB/0.4.2": {
"Wabbajack.Downloaders.ModDB/0.4.4": {
"dependencies": {
"HtmlAgilityPack": "1.11.72",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Networking.Http": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Networking.Http": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.ModDB.dll": {}
}
},
"Wabbajack.Downloaders.Nexus/0.4.2": {
"Wabbajack.Downloaders.Nexus/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Hashing.xxHash64": "0.4.2",
"Wabbajack.Networking.Http": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2",
"Wabbajack.Networking.NexusApi": "0.4.2",
"Wabbajack.Paths": "0.4.2"
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Hashing.xxHash64": "0.4.4",
"Wabbajack.Networking.Http": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4",
"Wabbajack.Networking.NexusApi": "0.4.4",
"Wabbajack.Paths": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.Nexus.dll": {}
}
},
"Wabbajack.Downloaders.VerificationCache/0.4.2": {
"Wabbajack.Downloaders.VerificationCache/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2"
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.VerificationCache.dll": {}
}
},
"Wabbajack.Downloaders.WabbajackCDN/0.4.2": {
"Wabbajack.Downloaders.WabbajackCDN/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Microsoft.Toolkit.HighPerformance": "7.1.2",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Networking.Http": "0.4.2",
"Wabbajack.RateLimiter": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Networking.Http": "0.4.4",
"Wabbajack.RateLimiter": "0.4.4"
},
"runtime": {
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
}
},
"Wabbajack.DTOs/0.4.2": {
"Wabbajack.DTOs/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.Hashing.xxHash64": "0.4.2",
"Wabbajack.Paths": "0.4.2"
"Wabbajack.Hashing.xxHash64": "0.4.4",
"Wabbajack.Paths": "0.4.4"
},
"runtime": {
"Wabbajack.DTOs.dll": {}
}
},
"Wabbajack.FileExtractor/0.4.2": {
"Wabbajack.FileExtractor/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"OMODFramework": "3.0.1",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Compression.BSA": "0.4.2",
"Wabbajack.Hashing.PHash": "0.4.2",
"Wabbajack.Paths": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Compression.BSA": "0.4.4",
"Wabbajack.Hashing.PHash": "0.4.4",
"Wabbajack.Paths": "0.4.4"
},
"runtime": {
"Wabbajack.FileExtractor.dll": {}
}
},
"Wabbajack.Hashing.PHash/0.4.2": {
"Wabbajack.Hashing.PHash/0.4.4": {
"dependencies": {
"BCnEncoder.Net.ImageSharp": "1.1.1",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Shipwreck.Phash": "0.5.0",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Common": "0.4.2",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Paths": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Paths": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4"
},
"runtime": {
"Wabbajack.Hashing.PHash.dll": {}
}
},
"Wabbajack.Hashing.xxHash64/0.4.2": {
"Wabbajack.Hashing.xxHash64/0.4.4": {
"dependencies": {
"Wabbajack.Paths": "0.4.2",
"Wabbajack.RateLimiter": "0.4.2"
"Wabbajack.Paths": "0.4.4",
"Wabbajack.RateLimiter": "0.4.4"
},
"runtime": {
"Wabbajack.Hashing.xxHash64.dll": {}
}
},
"Wabbajack.Installer/0.4.2": {
"Wabbajack.Installer/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"Octopus.Octodiff": "2.0.548",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Downloaders.Dispatcher": "0.4.2",
"Wabbajack.Downloaders.GameFile": "0.4.2",
"Wabbajack.FileExtractor": "0.4.2",
"Wabbajack.Networking.WabbajackClientApi": "0.4.2",
"Wabbajack.Paths": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2",
"Wabbajack.VFS": "0.4.2",
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Downloaders.Dispatcher": "0.4.4",
"Wabbajack.Downloaders.GameFile": "0.4.4",
"Wabbajack.FileExtractor": "0.4.4",
"Wabbajack.Networking.WabbajackClientApi": "0.4.4",
"Wabbajack.Paths": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4",
"Wabbajack.VFS": "0.4.4",
"ini-parser-netstandard": "2.5.2"
},
"runtime": {
"Wabbajack.Installer.dll": {}
}
},
"Wabbajack.IO.Async/0.4.2": {
"Wabbajack.IO.Async/0.4.4": {
"runtime": {
"Wabbajack.IO.Async.dll": {}
}
},
"Wabbajack.Networking.BethesdaNet/0.4.2": {
"Wabbajack.Networking.BethesdaNet/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Networking.Http": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2"
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Networking.Http": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.Networking.BethesdaNet.dll": {}
}
},
"Wabbajack.Networking.Discord/0.4.2": {
"Wabbajack.Networking.Discord/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.Networking.Http.Interfaces": "0.4.2"
"Wabbajack.Networking.Http.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.Networking.Discord.dll": {}
}
},
"Wabbajack.Networking.GitHub/0.4.2": {
"Wabbajack.Networking.GitHub/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Octokit": "14.0.0",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2"
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.Networking.GitHub.dll": {}
}
},
"Wabbajack.Networking.Http/0.4.2": {
"Wabbajack.Networking.Http/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Http": "9.0.1",
"Microsoft.Extensions.Logging": "9.0.1",
"Wabbajack.Configuration": "0.4.2",
"Wabbajack.Downloaders.Interfaces": "0.4.2",
"Wabbajack.Hashing.xxHash64": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2",
"Wabbajack.Paths": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2"
"Wabbajack.Configuration": "0.4.4",
"Wabbajack.Downloaders.Interfaces": "0.4.4",
"Wabbajack.Hashing.xxHash64": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4",
"Wabbajack.Paths": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4"
},
"runtime": {
"Wabbajack.Networking.Http.dll": {}
}
},
"Wabbajack.Networking.Http.Interfaces/0.4.2": {
"Wabbajack.Networking.Http.Interfaces/0.4.4": {
"dependencies": {
"Wabbajack.Hashing.xxHash64": "0.4.2"
"Wabbajack.Hashing.xxHash64": "0.4.4"
},
"runtime": {
"Wabbajack.Networking.Http.Interfaces.dll": {}
}
},
"Wabbajack.Networking.NexusApi/0.4.2": {
"Wabbajack.Networking.NexusApi/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Networking.Http": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2",
"Wabbajack.Networking.WabbajackClientApi": "0.4.2"
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Networking.Http": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4",
"Wabbajack.Networking.WabbajackClientApi": "0.4.4"
},
"runtime": {
"Wabbajack.Networking.NexusApi.dll": {}
}
},
"Wabbajack.Networking.WabbajackClientApi/0.4.2": {
"Wabbajack.Networking.WabbajackClientApi/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"Octokit": "14.0.0",
"Wabbajack.Common": "0.4.2",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2",
"Wabbajack.VFS.Interfaces": "0.4.2",
"Wabbajack.Common": "0.4.4",
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4",
"Wabbajack.VFS.Interfaces": "0.4.4",
"YamlDotNet": "16.3.0"
},
"runtime": {
"Wabbajack.Networking.WabbajackClientApi.dll": {}
}
},
"Wabbajack.Paths/0.4.2": {
"Wabbajack.Paths/0.4.4": {
"runtime": {
"Wabbajack.Paths.dll": {}
}
},
"Wabbajack.Paths.IO/0.4.2": {
"Wabbajack.Paths.IO/0.4.4": {
"dependencies": {
"Wabbajack.Paths": "0.4.2",
"Wabbajack.Paths": "0.4.4",
"shortid": "4.0.0"
},
"runtime": {
"Wabbajack.Paths.IO.dll": {}
}
},
"Wabbajack.RateLimiter/0.4.2": {
"Wabbajack.RateLimiter/0.4.4": {
"runtime": {
"Wabbajack.RateLimiter.dll": {}
}
},
"Wabbajack.Server.Lib/0.4.2": {
"Wabbajack.Server.Lib/0.4.4": {
"dependencies": {
"FluentFTP": "52.0.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
@@ -2264,58 +2264,58 @@
"Nettle": "3.0.0",
"Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Common": "0.4.2",
"Wabbajack.Networking.Http.Interfaces": "0.4.2",
"Wabbajack.Services.OSIntegrated": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.Networking.Http.Interfaces": "0.4.4",
"Wabbajack.Services.OSIntegrated": "0.4.4"
},
"runtime": {
"Wabbajack.Server.Lib.dll": {}
}
},
"Wabbajack.Services.OSIntegrated/0.4.2": {
"Wabbajack.Services.OSIntegrated/0.4.4": {
"dependencies": {
"DeviceId": "6.8.0",
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Newtonsoft.Json": "13.0.3",
"SixLabors.ImageSharp": "3.1.6",
"Wabbajack.Compiler": "0.4.2",
"Wabbajack.Downloaders.Dispatcher": "0.4.2",
"Wabbajack.Installer": "0.4.2",
"Wabbajack.Networking.BethesdaNet": "0.4.2",
"Wabbajack.Networking.Discord": "0.4.2",
"Wabbajack.VFS": "0.4.2"
"Wabbajack.Compiler": "0.4.4",
"Wabbajack.Downloaders.Dispatcher": "0.4.4",
"Wabbajack.Installer": "0.4.4",
"Wabbajack.Networking.BethesdaNet": "0.4.4",
"Wabbajack.Networking.Discord": "0.4.4",
"Wabbajack.VFS": "0.4.4"
},
"runtime": {
"Wabbajack.Services.OSIntegrated.dll": {}
}
},
"Wabbajack.VFS/0.4.2": {
"Wabbajack.VFS/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
"SixLabors.ImageSharp": "3.1.6",
"System.Data.SQLite.Core": "1.0.119",
"Wabbajack.Common": "0.4.2",
"Wabbajack.FileExtractor": "0.4.2",
"Wabbajack.Hashing.PHash": "0.4.2",
"Wabbajack.Hashing.xxHash64": "0.4.2",
"Wabbajack.Paths": "0.4.2",
"Wabbajack.Paths.IO": "0.4.2",
"Wabbajack.VFS.Interfaces": "0.4.2"
"Wabbajack.Common": "0.4.4",
"Wabbajack.FileExtractor": "0.4.4",
"Wabbajack.Hashing.PHash": "0.4.4",
"Wabbajack.Hashing.xxHash64": "0.4.4",
"Wabbajack.Paths": "0.4.4",
"Wabbajack.Paths.IO": "0.4.4",
"Wabbajack.VFS.Interfaces": "0.4.4"
},
"runtime": {
"Wabbajack.VFS.dll": {}
}
},
"Wabbajack.VFS.Interfaces/0.4.2": {
"Wabbajack.VFS.Interfaces/0.4.4": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.1",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
"Wabbajack.DTOs": "0.4.2",
"Wabbajack.Hashing.xxHash64": "0.4.2",
"Wabbajack.Paths": "0.4.2"
"Wabbajack.DTOs": "0.4.4",
"Wabbajack.Hashing.xxHash64": "0.4.4",
"Wabbajack.Paths": "0.4.4"
},
"runtime": {
"Wabbajack.VFS.Interfaces.dll": {}
@@ -2332,7 +2332,7 @@
}
},
"libraries": {
"jackify-engine/0.4.2": {
"jackify-engine/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
@@ -3021,202 +3021,202 @@
"path": "yamldotnet/16.3.0",
"hashPath": "yamldotnet.16.3.0.nupkg.sha512"
},
"Wabbajack.CLI.Builder/0.4.2": {
"Wabbajack.CLI.Builder/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Common/0.4.2": {
"Wabbajack.Common/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compiler/0.4.2": {
"Wabbajack.Compiler/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compression.BSA/0.4.2": {
"Wabbajack.Compression.BSA/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compression.Zip/0.4.2": {
"Wabbajack.Compression.Zip/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Configuration/0.4.2": {
"Wabbajack.Configuration/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Bethesda/0.4.2": {
"Wabbajack.Downloaders.Bethesda/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Dispatcher/0.4.2": {
"Wabbajack.Downloaders.Dispatcher/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.GameFile/0.4.2": {
"Wabbajack.Downloaders.GameFile/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.GoogleDrive/0.4.2": {
"Wabbajack.Downloaders.GoogleDrive/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Http/0.4.2": {
"Wabbajack.Downloaders.Http/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Interfaces/0.4.2": {
"Wabbajack.Downloaders.Interfaces/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.2": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Manual/0.4.2": {
"Wabbajack.Downloaders.Manual/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.MediaFire/0.4.2": {
"Wabbajack.Downloaders.MediaFire/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Mega/0.4.2": {
"Wabbajack.Downloaders.Mega/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.ModDB/0.4.2": {
"Wabbajack.Downloaders.ModDB/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Nexus/0.4.2": {
"Wabbajack.Downloaders.Nexus/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.VerificationCache/0.4.2": {
"Wabbajack.Downloaders.VerificationCache/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.WabbajackCDN/0.4.2": {
"Wabbajack.Downloaders.WabbajackCDN/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.DTOs/0.4.2": {
"Wabbajack.DTOs/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.FileExtractor/0.4.2": {
"Wabbajack.FileExtractor/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Hashing.PHash/0.4.2": {
"Wabbajack.Hashing.PHash/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Hashing.xxHash64/0.4.2": {
"Wabbajack.Hashing.xxHash64/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Installer/0.4.2": {
"Wabbajack.Installer/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.IO.Async/0.4.2": {
"Wabbajack.IO.Async/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.BethesdaNet/0.4.2": {
"Wabbajack.Networking.BethesdaNet/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Discord/0.4.2": {
"Wabbajack.Networking.Discord/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.GitHub/0.4.2": {
"Wabbajack.Networking.GitHub/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Http/0.4.2": {
"Wabbajack.Networking.Http/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Http.Interfaces/0.4.2": {
"Wabbajack.Networking.Http.Interfaces/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.NexusApi/0.4.2": {
"Wabbajack.Networking.NexusApi/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.WabbajackClientApi/0.4.2": {
"Wabbajack.Networking.WabbajackClientApi/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Paths/0.4.2": {
"Wabbajack.Paths/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Paths.IO/0.4.2": {
"Wabbajack.Paths.IO/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.RateLimiter/0.4.2": {
"Wabbajack.RateLimiter/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Server.Lib/0.4.2": {
"Wabbajack.Server.Lib/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Services.OSIntegrated/0.4.2": {
"Wabbajack.Services.OSIntegrated/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.VFS/0.4.2": {
"Wabbajack.VFS/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.VFS.Interfaces/0.4.2": {
"Wabbajack.VFS.Interfaces/0.4.4": {
"type": "project",
"serviceable": false,
"sha512": ""

Binary file not shown.

View File

@@ -105,7 +105,7 @@ from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox, QTabWidget, QRadioButton, QButtonGroup
)
from PySide6.QtCore import Qt, QEvent, QTimer
from PySide6.QtCore import Qt, QEvent, QTimer, QThread, Signal
from PySide6.QtGui import QIcon
import json
@@ -904,20 +904,22 @@ class SettingsDialog(QDialog):
if best_proton:
resolved_install_path = str(best_proton['path'])
resolved_install_version = best_proton['name']
self.config_handler.set("proton_path", resolved_install_path)
self.config_handler.set("proton_version", resolved_install_version)
else:
resolved_install_path = "auto"
resolved_install_version = "auto"
except:
resolved_install_path = "auto"
resolved_install_version = "auto"
# No Proton found - don't write anything, let engine auto-detect
logger.warning("Auto Proton selection failed: No Proton versions found")
# Don't modify existing config values
except Exception as e:
# Exception during detection - log it and don't write anything
logger.error(f"Auto Proton selection failed with exception: {e}", exc_info=True)
# Don't modify existing config values
else:
# User selected specific Proton version
resolved_install_path = selected_install_proton_path
# Extract version from dropdown text
resolved_install_version = self.install_proton_dropdown.currentText()
self.config_handler.set("proton_path", resolved_install_path)
self.config_handler.set("proton_version", resolved_install_version)
self.config_handler.set("proton_path", resolved_install_path)
self.config_handler.set("proton_version", resolved_install_version)
# Save Game Proton selection
selected_game_proton_path = self.game_proton_dropdown.currentData()
@@ -1038,6 +1040,10 @@ class JackifyMainWindow(QMainWindow):
self._details_extra_height = 360
self._initial_show_adjusted = False
# Track open dialogs to prevent duplicates
self._settings_dialog = None
self._about_dialog = None
# Ensure GNOME/Ubuntu exposes full set of window controls (avoid hidden buttons)
self._apply_standard_window_flags()
try:
@@ -1331,6 +1337,15 @@ class JackifyMainWindow(QMainWindow):
self.install_modlist_screen.resize_request.connect(self._on_child_resize_request)
except Exception:
pass
# Let Configure screens request window resize for expand/collapse
try:
self.configure_new_modlist_screen.resize_request.connect(self._on_child_resize_request)
except Exception:
pass
try:
self.configure_existing_modlist_screen.resize_request.connect(self._on_child_resize_request)
except Exception:
pass
# Add screens to stacked widget
self.stacked_widget.addWidget(self.main_menu) # Index 0: Main Menu
@@ -1533,58 +1548,42 @@ class JackifyMainWindow(QMainWindow):
# Continue anyway - don't block startup on detection errors
def _check_for_updates_on_startup(self):
"""Check for updates on startup - SIMPLE VERSION"""
"""Check for updates on startup - non-blocking background check"""
try:
debug_print("Checking for updates on startup...")
# Do it synchronously and simply
update_info = self.update_service.check_for_updates()
if update_info:
# Run update check in background thread to avoid blocking GUI startup
class UpdateCheckThread(QThread):
update_available = Signal(object) # Signal to pass update_info to main thread
def __init__(self, update_service):
super().__init__()
self.update_service = update_service
def run(self):
update_info = self.update_service.check_for_updates()
if update_info:
self.update_available.emit(update_info)
def on_update_available(update_info):
"""Handle update check result in main thread"""
debug_print(f"Update available: v{update_info.version}")
# Simple QMessageBox - no complex dialogs
from PySide6.QtWidgets import QMessageBox
from PySide6.QtCore import QTimer
# Show update dialog after a short delay to ensure GUI is fully loaded
def show_update_dialog():
try:
debug_print("Creating UpdateDialog...")
from jackify.frontends.gui.dialogs.update_dialog import UpdateDialog
dialog = UpdateDialog(update_info, self.update_service, self)
debug_print("UpdateDialog created, showing...")
dialog.show() # Non-blocking
debug_print("UpdateDialog shown successfully")
except Exception as e:
debug_print(f"UpdateDialog failed: {e}, falling back to simple dialog")
# Fallback to simple dialog
reply = QMessageBox.question(
self,
"Update Available",
f"Jackify v{update_info.version} is available.\n\nDownload and install now?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
# Simple download and replace
try:
new_appimage = self.update_service.download_update(update_info)
if new_appimage:
if self.update_service.apply_update(new_appimage):
debug_print("Update applied successfully")
else:
QMessageBox.warning(self, "Update Failed", "Failed to apply update.")
else:
QMessageBox.warning(self, "Update Failed", "Failed to download update.")
except Exception as e:
QMessageBox.warning(self, "Update Failed", f"Update failed: {e}")
from .dialogs.update_dialog import UpdateDialog
dialog = UpdateDialog(update_info, self.update_service, self)
dialog.exec()
# Use QTimer to show dialog after GUI is fully loaded
QTimer.singleShot(1000, show_update_dialog)
else:
debug_print("No updates available")
# Start background thread
self._update_thread = UpdateCheckThread(self.update_service)
self._update_thread.update_available.connect(on_update_available)
self._update_thread.start()
except Exception as e:
debug_print(f"Error checking for updates on startup: {e}")
debug_print(f"Error setting up update check: {e}")
# Continue anyway - don't block startup on update check errors
def cleanup_processes(self):
@@ -1622,23 +1621,74 @@ class JackifyMainWindow(QMainWindow):
event.accept()
def open_settings_dialog(self):
"""Open settings dialog, preventing duplicate instances"""
try:
# Check if dialog already exists and is visible
if self._settings_dialog is not None:
try:
if self._settings_dialog.isVisible():
# Dialog is already open - raise it to front
self._settings_dialog.raise_()
self._settings_dialog.activateWindow()
return
else:
# Dialog exists but is closed - clean up reference
self._settings_dialog = None
except RuntimeError:
# Dialog was deleted - clean up reference
self._settings_dialog = None
# Create new dialog
dlg = SettingsDialog(self)
self._settings_dialog = dlg
# Clean up reference when dialog is closed
def on_dialog_finished():
self._settings_dialog = None
dlg.finished.connect(on_dialog_finished)
dlg.exec()
except Exception as e:
print(f"[ERROR] Exception in open_settings_dialog: {e}")
import traceback
traceback.print_exc()
self._settings_dialog = None
def open_about_dialog(self):
"""Open about dialog, preventing duplicate instances"""
try:
from jackify.frontends.gui.dialogs.about_dialog import AboutDialog
# Check if dialog already exists and is visible
if self._about_dialog is not None:
try:
if self._about_dialog.isVisible():
# Dialog is already open - raise it to front
self._about_dialog.raise_()
self._about_dialog.activateWindow()
return
else:
# Dialog exists but is closed - clean up reference
self._about_dialog = None
except RuntimeError:
# Dialog was deleted - clean up reference
self._about_dialog = None
# Create new dialog
dlg = AboutDialog(self.system_info, self)
self._about_dialog = dlg
# Clean up reference when dialog is closed
def on_dialog_finished():
self._about_dialog = None
dlg.finished.connect(on_dialog_finished)
dlg.exec()
except Exception as e:
print(f"[ERROR] Exception in open_about_dialog: {e}")
import traceback
traceback.print_exc()
self._about_dialog = None
def _open_url(self, url: str):
"""Open URL with clean environment to avoid AppImage library conflicts."""

View File

@@ -32,6 +32,7 @@ def debug_print(message):
class ConfigureExistingModlistScreen(QWidget):
steam_restart_finished = Signal(bool, str)
resize_request = Signal(str)
def __init__(self, stacked_widget=None, main_menu_index=0):
super().__init__()
debug_print("DEBUG: ConfigureExistingModlistScreen __init__ called")
@@ -220,27 +221,49 @@ class ConfigureExistingModlistScreen(QWidget):
if self.debug:
user_config_widget.setStyleSheet("border: 2px solid orange;")
user_config_widget.setToolTip("USER_CONFIG_WIDGET")
# Right: Activity window (FileProgressList widget)
# Fixed size policy to prevent shrinking when window expands
# Right: Tabbed interface with Activity and Process Monitor
# Both tabs are always available, user can switch between them
self.process_monitor = QTextEdit()
self.process_monitor.setReadOnly(True)
self.process_monitor.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
self.process_monitor.setMinimumSize(QSize(300, 20))
self.process_monitor.setStyleSheet(f"background: #222; color: {JACKIFY_COLOR_BLUE}; font-family: monospace; font-size: 11px; border: 1px solid #444;")
self.process_monitor_heading = QLabel("<b>[Process Monitor]</b>")
self.process_monitor_heading.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px; margin-bottom: 2px;")
self.process_monitor_heading.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
process_vbox = QVBoxLayout()
process_vbox.setContentsMargins(0, 0, 0, 0)
process_vbox.setSpacing(2)
process_vbox.addWidget(self.process_monitor_heading)
process_vbox.addWidget(self.process_monitor)
process_monitor_widget = QWidget()
process_monitor_widget.setLayout(process_vbox)
process_monitor_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
if self.debug:
process_monitor_widget.setStyleSheet("border: 2px solid purple;")
process_monitor_widget.setToolTip("PROCESS_MONITOR")
self.process_monitor_widget = process_monitor_widget
# Set up File Progress List (Activity tab)
self.file_progress_list.setMinimumSize(QSize(300, 20))
self.file_progress_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
activity_widget = QWidget()
activity_layout = QVBoxLayout()
activity_layout.setContentsMargins(0, 0, 0, 0)
activity_layout.setSpacing(0)
activity_layout.addWidget(self.file_progress_list)
activity_widget.setLayout(activity_layout)
# Create tab widget to hold both Activity and Process Monitor
self.activity_tabs = QTabWidget()
self.activity_tabs.setStyleSheet("QTabWidget::pane { background: #222; border: 1px solid #444; } QTabBar::tab { background: #222; color: #ccc; padding: 6px 16px; } QTabBar::tab:selected { background: #333; color: #3fd0ea; } QTabWidget { margin: 0px; padding: 0px; } QTabBar { margin: 0px; padding: 0px; }")
self.activity_tabs.setContentsMargins(0, 0, 0, 0)
self.activity_tabs.setDocumentMode(False)
self.activity_tabs.setTabPosition(QTabWidget.North)
if self.debug:
activity_widget.setStyleSheet("border: 2px solid purple;")
activity_widget.setToolTip("ACTIVITY_WINDOW")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(activity_widget, stretch=9)
self.activity_tabs.setStyleSheet("border: 2px solid cyan;")
self.activity_tabs.setToolTip("ACTIVITY_TABS")
# Keep legacy process monitor hidden (for compatibility with existing code)
self.process_monitor = QTextEdit()
self.process_monitor.setReadOnly(True)
self.process_monitor.setVisible(False) # Hidden in compact mode
# Add both widgets as tabs
self.activity_tabs.addTab(self.file_progress_list, "Activity")
self.activity_tabs.addTab(process_monitor_widget, "Process Monitor")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(self.activity_tabs, stretch=9)
upper_hbox.setAlignment(Qt.AlignTop)
upper_section_widget = QWidget()
upper_section_widget.setLayout(upper_hbox)
@@ -490,6 +513,42 @@ class ConfigureExistingModlistScreen(QWidget):
except Exception:
pass
def _handle_progress_update(self, text):
"""Handle progress updates - update console, activity window, and progress indicator"""
# Always append to console
self._safe_append_text(text)
# Parse the message to update UI widgets
message_lower = text.lower()
# Update progress indicator based on key status messages
if "creating steam shortcut" in message_lower:
self.progress_indicator.set_status("Creating Steam shortcut...", 10)
elif "restarting steam" in message_lower or "restart steam" in message_lower:
self.progress_indicator.set_status("Restarting Steam...", 20)
elif "steam restart" in message_lower and "success" in message_lower:
self.progress_indicator.set_status("Steam restarted successfully", 30)
elif "creating proton prefix" in message_lower or "prefix creation" in message_lower:
self.progress_indicator.set_status("Creating Proton prefix...", 50)
elif "prefix created" in message_lower or "prefix creation" in message_lower and "success" in message_lower:
self.progress_indicator.set_status("Proton prefix created", 70)
elif "verifying" in message_lower:
self.progress_indicator.set_status("Verifying setup...", 80)
elif "steam integration complete" in message_lower or "configuration complete" in message_lower:
self.progress_indicator.set_status("Configuration complete", 95)
elif "complete" in message_lower and not "prefix" in message_lower:
self.progress_indicator.set_status("Finishing up...", 90)
# Update activity window with generic configuration status
# Only update if message contains meaningful progress (not blank lines or separators)
if text.strip() and not text.strip().startswith('='):
# Show generic "Configuring modlist..." in activity window
self.file_progress_list.update_files(
[],
current_phase="Configuring",
summary_info={"current": 1, "total": 1, "label": "Setting up modlist"}
)
def _safe_append_text(self, text):
"""Append text with professional auto-scroll behavior"""
# Write all messages to log file
@@ -660,7 +719,7 @@ class ConfigureExistingModlistScreen(QWidget):
# Create and start the configuration thread
self.config_thread = ConfigurationThread(modlist_name, install_dir, resolution)
self.config_thread.progress_update.connect(self._safe_append_text)
self.config_thread.progress_update.connect(self._handle_progress_update)
self.config_thread.configuration_complete.connect(self.on_configuration_complete)
self.config_thread.error_occurred.connect(self.on_configuration_error)
self.config_thread.start()

View File

@@ -98,6 +98,7 @@ class SelectionDialog(QDialog):
class ConfigureNewModlistScreen(QWidget):
steam_restart_finished = Signal(bool, str)
resize_request = Signal(str)
def __init__(self, stacked_widget=None, main_menu_index=0):
super().__init__()
debug_print("DEBUG: ConfigureNewModlistScreen __init__ called")
@@ -300,27 +301,49 @@ class ConfigureNewModlistScreen(QWidget):
if self.debug:
user_config_widget.setStyleSheet("border: 2px solid orange;")
user_config_widget.setToolTip("USER_CONFIG_WIDGET")
# Right: Activity window (FileProgressList widget)
# Fixed size policy to prevent shrinking when window expands
# Right: Tabbed interface with Activity and Process Monitor
# Both tabs are always available, user can switch between them
self.process_monitor = QTextEdit()
self.process_monitor.setReadOnly(True)
self.process_monitor.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
self.process_monitor.setMinimumSize(QSize(300, 20))
self.process_monitor.setStyleSheet(f"background: #222; color: {JACKIFY_COLOR_BLUE}; font-family: monospace; font-size: 11px; border: 1px solid #444;")
self.process_monitor_heading = QLabel("<b>[Process Monitor]</b>")
self.process_monitor_heading.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px; margin-bottom: 2px;")
self.process_monitor_heading.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
process_vbox = QVBoxLayout()
process_vbox.setContentsMargins(0, 0, 0, 0)
process_vbox.setSpacing(2)
process_vbox.addWidget(self.process_monitor_heading)
process_vbox.addWidget(self.process_monitor)
process_monitor_widget = QWidget()
process_monitor_widget.setLayout(process_vbox)
process_monitor_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
if self.debug:
process_monitor_widget.setStyleSheet("border: 2px solid purple;")
process_monitor_widget.setToolTip("PROCESS_MONITOR")
self.process_monitor_widget = process_monitor_widget
# Set up File Progress List (Activity tab)
self.file_progress_list.setMinimumSize(QSize(300, 20))
self.file_progress_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
activity_widget = QWidget()
activity_layout = QVBoxLayout()
activity_layout.setContentsMargins(0, 0, 0, 0)
activity_layout.setSpacing(0)
activity_layout.addWidget(self.file_progress_list)
activity_widget.setLayout(activity_layout)
# Create tab widget to hold both Activity and Process Monitor
self.activity_tabs = QTabWidget()
self.activity_tabs.setStyleSheet("QTabWidget::pane { background: #222; border: 1px solid #444; } QTabBar::tab { background: #222; color: #ccc; padding: 6px 16px; } QTabBar::tab:selected { background: #333; color: #3fd0ea; } QTabWidget { margin: 0px; padding: 0px; } QTabBar { margin: 0px; padding: 0px; }")
self.activity_tabs.setContentsMargins(0, 0, 0, 0)
self.activity_tabs.setDocumentMode(False)
self.activity_tabs.setTabPosition(QTabWidget.North)
if self.debug:
activity_widget.setStyleSheet("border: 2px solid purple;")
activity_widget.setToolTip("ACTIVITY_WINDOW")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(activity_widget, stretch=9)
self.activity_tabs.setStyleSheet("border: 2px solid cyan;")
self.activity_tabs.setToolTip("ACTIVITY_TABS")
# Keep legacy process monitor hidden (for compatibility with existing code)
self.process_monitor = QTextEdit()
self.process_monitor.setReadOnly(True)
self.process_monitor.setVisible(False) # Hidden in compact mode
# Add both widgets as tabs
self.activity_tabs.addTab(self.file_progress_list, "Activity")
self.activity_tabs.addTab(process_monitor_widget, "Process Monitor")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(self.activity_tabs, stretch=9)
upper_hbox.setAlignment(Qt.AlignTop)
upper_section_widget = QWidget()
upper_section_widget.setLayout(upper_hbox)
@@ -570,18 +593,54 @@ class ConfigureNewModlistScreen(QWidget):
except Exception:
pass
def _handle_progress_update(self, text):
"""Handle progress updates - update console, activity window, and progress indicator"""
# Always append to console
self._safe_append_text(text)
# Parse the message to update UI widgets
message_lower = text.lower()
# Update progress indicator based on key status messages
if "creating steam shortcut" in message_lower:
self.progress_indicator.set_status("Creating Steam shortcut...", 10)
elif "restarting steam" in message_lower or "restart steam" in message_lower:
self.progress_indicator.set_status("Restarting Steam...", 20)
elif "steam restart" in message_lower and "success" in message_lower:
self.progress_indicator.set_status("Steam restarted successfully", 30)
elif "creating proton prefix" in message_lower or "prefix creation" in message_lower:
self.progress_indicator.set_status("Creating Proton prefix...", 50)
elif "prefix created" in message_lower or "prefix creation" in message_lower and "success" in message_lower:
self.progress_indicator.set_status("Proton prefix created", 70)
elif "verifying" in message_lower:
self.progress_indicator.set_status("Verifying setup...", 80)
elif "steam integration complete" in message_lower or "configuration complete" in message_lower:
self.progress_indicator.set_status("Configuration complete", 95)
elif "complete" in message_lower and not "prefix" in message_lower:
self.progress_indicator.set_status("Finishing up...", 90)
# Update activity window with generic configuration status
# Only update if message contains meaningful progress (not blank lines or separators)
if text.strip() and not text.strip().startswith('='):
# Show generic "Configuring modlist..." in activity window
self.file_progress_list.update_files(
[],
current_phase="Configuring",
summary_info={"current": 1, "total": 1, "label": "Setting up modlist"}
)
def _safe_append_text(self, text):
"""Append text with professional auto-scroll behavior"""
# Write all messages to log file
self._write_to_log_file(text)
scrollbar = self.console.verticalScrollBar()
# Check if user was at bottom BEFORE adding text
was_at_bottom = (scrollbar.value() >= scrollbar.maximum() - 1) # Allow 1px tolerance
# Add the text
self.console.append(text)
# Auto-scroll if user was at bottom and hasn't manually scrolled
# Re-check bottom state after text addition for better reliability
if (was_at_bottom and not self._user_manually_scrolled) or \
@@ -635,6 +694,11 @@ class ConfigureNewModlistScreen(QWidget):
if hasattr(self, 'file_progress_list'):
self.file_progress_list.stop_cpu_tracking()
# Clean up automated prefix thread if running
if hasattr(self, 'automated_prefix_thread') and self.automated_prefix_thread.isRunning():
self.automated_prefix_thread.terminate()
self.automated_prefix_thread.wait(1000)
# Clean up configuration thread if running
if hasattr(self, 'config_thread') and self.config_thread.isRunning():
self.config_thread.terminate()
@@ -929,7 +993,7 @@ class ConfigureNewModlistScreen(QWidget):
# Create and start the thread
self.automated_prefix_thread = AutomatedPrefixThread(modlist_name, install_dir, mo2_exe_path, _is_steamdeck)
self.automated_prefix_thread.progress_update.connect(self._safe_append_text)
self.automated_prefix_thread.progress_update.connect(self._handle_progress_update)
self.automated_prefix_thread.workflow_complete.connect(self._on_automated_prefix_complete)
self.automated_prefix_thread.error_occurred.connect(self._on_automated_prefix_error)
self.automated_prefix_thread.start()
@@ -1323,7 +1387,7 @@ class ConfigureNewModlistScreen(QWidget):
# Start configuration thread
self.config_thread = ConfigThread(updated_context)
self.config_thread.progress_update.connect(self._safe_append_text)
self.config_thread.progress_update.connect(self._handle_progress_update)
self.config_thread.configuration_complete.connect(self.on_configuration_complete)
self.config_thread.error_occurred.connect(self.on_configuration_error)
self.config_thread.start()
@@ -1424,7 +1488,7 @@ class ConfigureNewModlistScreen(QWidget):
# Create and start the configuration thread
self.config_thread = ConfigThread(updated_context)
self.config_thread.progress_update.connect(self._safe_append_text)
self.config_thread.progress_update.connect(self._handle_progress_update)
self.config_thread.configuration_complete.connect(self.on_configuration_complete)
self.config_thread.error_occurred.connect(self.on_configuration_error)
self.config_thread.start()

View File

@@ -1517,6 +1517,16 @@ class InstallModlistScreen(QWidget):
'force_down': metadata.forceDown
}
self.modlist_name_edit.setText(metadata.title)
# Auto-append modlist name to install directory
base_install_dir = self.config_handler.get_modlist_install_base_dir()
if base_install_dir:
# Sanitize modlist title for filesystem use
import re
safe_title = re.sub(r'[<>:"/\\|?*]', '', metadata.title)
safe_title = safe_title.strip()
modlist_install_path = os.path.join(base_install_dir, safe_title)
self.install_dir_edit.setText(modlist_install_path)
finally:
if cursor_overridden:
QApplication.restoreOverrideCursor()
@@ -2157,12 +2167,6 @@ class InstallModlistScreen(QWidget):
cmd.append('--debug')
debug_print("DEBUG: Added --debug flag to jackify-engine command")
# Check GPU setting and add --no-gpu flag if disabled
gpu_enabled = config_handler.get('enable_gpu_texture_conversion', True)
if not gpu_enabled:
cmd.append('--no-gpu')
debug_print("DEBUG: Added --no-gpu flag (GPU texture conversion disabled)")
# CRITICAL: Print the FULL command so we can see exactly what's being passed
debug_print(f"DEBUG: FULL Engine command: {' '.join(cmd)}")
debug_print(f"DEBUG: modlist value being passed: '{self.modlist}'")
@@ -3959,11 +3963,11 @@ class InstallModlistScreen(QWidget):
self.retry_automated_workflow_with_new_name(new_name)
elif new_name == modlist_name:
# Same name - show warning
from jackify.backend.services.message_service import MessageService
from jackify.frontends.gui.services.message_service import MessageService
MessageService.warning(self, "Same Name", "Please enter a different name to resolve the conflict.")
else:
# Empty name
from jackify.backend.services.message_service import MessageService
from jackify.frontends.gui.services.message_service import MessageService
MessageService.warning(self, "Invalid Name", "Please enter a valid shortcut name.")
def on_cancel():
@@ -4364,7 +4368,7 @@ class InstallModlistScreen(QWidget):
def _show_somnium_post_install_guidance(self):
"""Show guidance popup for Somnium post-installation steps"""
from ..widgets.message_service import MessageService
from ..services.message_service import MessageService
guidance_text = f"""<b>Somnium Post-Installation Required</b><br><br>
Due to Somnium's non-standard folder structure, you need to manually update the binary paths in ModOrganizer:<br><br>

View File

@@ -306,29 +306,49 @@ class InstallTTWScreen(QWidget):
user_config_widget.setStyleSheet("border: 2px solid orange;")
user_config_widget.setToolTip("USER_CONFIG_WIDGET")
# Right: Activity window (FileProgressList widget)
# Fixed size policy to prevent shrinking when window expands
# Right: Tabbed interface with Activity and Process Monitor
# Both tabs are always available, user can switch between them
self.file_progress_list = FileProgressList()
self.file_progress_list.setMinimumSize(QSize(300, 20))
self.file_progress_list.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
activity_widget = QWidget()
activity_layout = QVBoxLayout()
activity_layout.setContentsMargins(0, 0, 0, 0)
activity_layout.setSpacing(0)
activity_layout.addWidget(self.file_progress_list)
activity_widget.setLayout(activity_layout)
if self.debug:
activity_widget.setStyleSheet("border: 2px solid purple;")
activity_widget.setToolTip("ACTIVITY_WINDOW")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(activity_widget, stretch=9)
# Keep legacy process monitor hidden (for compatibility with existing code)
self.process_monitor = QTextEdit()
self.process_monitor.setReadOnly(True)
self.process_monitor.setVisible(False)
self.process_monitor.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
self.process_monitor.setMinimumSize(QSize(300, 20))
self.process_monitor.setStyleSheet(f"background: #222; color: {JACKIFY_COLOR_BLUE}; font-family: monospace; font-size: 11px; border: 1px solid #444;")
self.process_monitor_heading = QLabel("<b>[Process Monitor]</b>")
self.process_monitor_heading.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 13px; margin-bottom: 2px;")
self.process_monitor_heading.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
process_vbox = QVBoxLayout()
process_vbox.setContentsMargins(0, 0, 0, 0)
process_vbox.setSpacing(2)
process_vbox.addWidget(self.process_monitor_heading)
process_vbox.addWidget(self.process_monitor)
process_monitor_widget = QWidget()
process_monitor_widget.setLayout(process_vbox)
process_monitor_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
if self.debug:
process_monitor_widget.setStyleSheet("border: 2px solid purple;")
process_monitor_widget.setToolTip("PROCESS_MONITOR")
self.process_monitor_widget = process_monitor_widget
# Create tab widget to hold both Activity and Process Monitor
self.activity_tabs = QTabWidget()
self.activity_tabs.setStyleSheet("QTabWidget::pane { background: #222; border: 1px solid #444; } QTabBar::tab { background: #222; color: #ccc; padding: 6px 16px; } QTabBar::tab:selected { background: #333; color: #3fd0ea; } QTabWidget { margin: 0px; padding: 0px; } QTabBar { margin: 0px; padding: 0px; }")
self.activity_tabs.setContentsMargins(0, 0, 0, 0)
self.activity_tabs.setDocumentMode(False)
self.activity_tabs.setTabPosition(QTabWidget.North)
if self.debug:
self.activity_tabs.setStyleSheet("border: 2px solid cyan;")
self.activity_tabs.setToolTip("ACTIVITY_TABS")
# Add both widgets as tabs
self.activity_tabs.addTab(self.file_progress_list, "Activity")
self.activity_tabs.addTab(process_monitor_widget, "Process Monitor")
upper_hbox.addWidget(user_config_widget, stretch=11)
upper_hbox.addWidget(self.activity_tabs, stretch=9)
upper_hbox.setAlignment(Qt.AlignTop)
self.upper_section_widget = QWidget()
self.upper_section_widget.setLayout(upper_hbox)
@@ -2815,11 +2835,11 @@ class InstallTTWScreen(QWidget):
self.retry_automated_workflow_with_new_name(new_name)
elif new_name == modlist_name:
# Same name - show warning
from jackify.backend.services.message_service import MessageService
from jackify.frontends.gui.services.message_service import MessageService
MessageService.warning(self, "Same Name", "Please enter a different name to resolve the conflict.")
else:
# Empty name
from jackify.backend.services.message_service import MessageService
from jackify.frontends.gui.services.message_service import MessageService
MessageService.warning(self, "Invalid Name", "Please enter a valid shortcut name.")
def on_cancel():
@@ -3494,7 +3514,7 @@ class InstallTTWScreen(QWidget):
def _show_somnium_post_install_guidance(self):
"""Show guidance popup for Somnium post-installation steps"""
from ..widgets.message_service import MessageService
from ..services.message_service import MessageService
guidance_text = f"""<b>Somnium Post-Installation Required</b><br><br>
Due to Somnium's non-standard folder structure, you need to manually update the binary paths in ModOrganizer:<br><br>

View File

@@ -388,6 +388,15 @@ class ModlistDetailDialog(QDialog):
main_layout = QVBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# --- Banner area with full-width text overlay ---
# Container so we can place a semi-opaque text panel over the banner image
banner_container = QFrame()
banner_container.setFrameShape(QFrame.NoFrame)
banner_container.setStyleSheet("background: #000; border: none;")
banner_layout = QVBoxLayout()
banner_layout.setContentsMargins(0, 0, 0, 0)
banner_layout.setSpacing(0)
banner_container.setLayout(banner_layout)
# Banner image at top with 16:9 aspect ratio (like Wabbajack)
self.banner_label = QLabel()
@@ -396,40 +405,67 @@ class ModlistDetailDialog(QDialog):
self.banner_label.setStyleSheet("background: #1a1a1a; border: none;")
self.banner_label.setAlignment(Qt.AlignCenter)
self.banner_label.setText("Loading image...")
main_layout.addWidget(self.banner_label)
banner_layout.addWidget(self.banner_label)
# Content area with padding
# Full-width transparent container with opaque card inside (only as wide as text)
overlay_container = QWidget()
overlay_container.setStyleSheet("background: transparent;")
overlay_layout = QHBoxLayout()
overlay_layout.setContentsMargins(24, 0, 24, 24)
overlay_layout.setSpacing(0)
overlay_container.setLayout(overlay_layout)
# Opaque text card - only as wide as content needs (where red lines are)
self.banner_text_panel = QFrame()
self.banner_text_panel.setFrameShape(QFrame.StyledPanel)
# Opaque background, rounded corners, sized to content only
self.banner_text_panel.setStyleSheet("""
QFrame {
background-color: rgba(0, 0, 0, 180);
border: 1px solid rgba(255, 255, 255, 30);
border-radius: 8px;
}
""")
self.banner_text_panel.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
banner_text_layout = QVBoxLayout()
banner_text_layout.setContentsMargins(20, 12, 20, 14)
banner_text_layout.setSpacing(6)
self.banner_text_panel.setLayout(banner_text_layout)
# Add card to container (left-aligned, rest stays transparent)
overlay_layout.addWidget(self.banner_text_panel, alignment=Qt.AlignBottom | Qt.AlignLeft)
overlay_layout.addStretch() # Push card left, rest transparent
# Title only (badges moved to tags section below)
title = QLabel(self.metadata.title)
title.setFont(QFont("Sans", 24, QFont.Bold))
title.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE};")
title.setWordWrap(True)
banner_text_layout.addWidget(title)
# Only sizes in overlay (minimal info on image)
if self.metadata.sizes:
sizes_text = (
f"<span style='color: #aaa;'>Download:</span> {self.metadata.sizes.downloadSizeFormatted}"
f"<span style='color: #aaa;'>Install:</span> {self.metadata.sizes.installSizeFormatted}"
f"<span style='color: #aaa;'>Total:</span> {self.metadata.sizes.totalSizeFormatted}"
)
sizes_label = QLabel(sizes_text)
sizes_label.setStyleSheet("color: #fff; font-size: 13px;")
banner_text_layout.addWidget(sizes_label)
# Add full-width transparent container at bottom of banner
banner_layout.addWidget(overlay_container, alignment=Qt.AlignBottom)
main_layout.addWidget(banner_container)
# Content area with padding (tags + description + bottom bar)
content_widget = QWidget()
content_layout = QVBoxLayout()
content_layout.setContentsMargins(24, 20, 24, 20)
content_layout.setSpacing(16)
content_widget.setLayout(content_layout)
# Title row with status badges (UNAVAILABLE, Unofficial - Official and NSFW shown in tags)
title_row = QHBoxLayout()
title_row.setSpacing(12)
title = QLabel(self.metadata.title)
title.setFont(QFont("Sans", 24, QFont.Bold))
title.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE};")
title.setWordWrap(True)
title_row.addWidget(title, stretch=1)
# Status badges in title row
if not self.metadata.is_available():
unavailable_badge = QLabel("UNAVAILABLE")
unavailable_badge.setStyleSheet("background: #666; color: white; padding: 4px 10px; font-size: 10px; font-weight: bold; border-radius: 4px;")
title_row.addWidget(unavailable_badge)
# Show "Unofficial" badge if not official (Official is shown in tags)
if not self.metadata.official:
unofficial_badge = QLabel("Unofficial")
unofficial_badge.setStyleSheet("background: #666; color: white; padding: 4px 10px; font-size: 10px; font-weight: bold; border-radius: 4px;")
title_row.addWidget(unofficial_badge)
content_layout.addLayout(title_row)
# Metadata line (version, author, game) - inline like Wabbajack
# Metadata line (version, author, game) - moved below image
metadata_line_parts = []
if self.metadata.version:
metadata_line_parts.append(f"<span style='color: #aaa;'>version</span> {self.metadata.version}")
@@ -446,13 +482,25 @@ class ModlistDetailDialog(QDialog):
metadata_line.setWordWrap(True)
content_layout.addWidget(metadata_line)
# Tags row (like Wabbajack)
# Tags row (includes status badges moved from overlay)
tags_layout = QHBoxLayout()
tags_layout.setSpacing(6)
tags_layout.setContentsMargins(0, 0, 0, 0)
# Add status badges first (UNAVAILABLE, Unofficial)
if not self.metadata.is_available():
unavailable_badge = QLabel("UNAVAILABLE")
unavailable_badge.setStyleSheet("background: #666; color: white; padding: 6px 12px; font-size: 11px; border-radius: 4px;")
tags_layout.addWidget(unavailable_badge)
if not self.metadata.official:
unofficial_badge = QLabel("Unofficial")
unofficial_badge.setStyleSheet("background: #666; color: white; padding: 6px 12px; font-size: 11px; border-radius: 4px;")
tags_layout.addWidget(unofficial_badge)
# Add regular tags
tags_to_render = getattr(self.metadata, 'normalized_tags_display', self.metadata.tags or [])
if tags_to_render:
tags_layout = QHBoxLayout()
tags_layout.setSpacing(6)
tags_layout.setContentsMargins(0, 0, 0, 0)
for tag in tags_to_render:
tag_badge = QLabel(tag)
# Match Wabbajack tag styling
@@ -463,20 +511,9 @@ class ModlistDetailDialog(QDialog):
else:
tag_badge.setStyleSheet("background: #3a3a3a; color: #ccc; padding: 6px 12px; font-size: 11px; border-radius: 4px;")
tags_layout.addWidget(tag_badge)
tags_layout.addStretch()
content_layout.addLayout(tags_layout)
# Sizes (if available)
if self.metadata.sizes:
sizes_text = (
f"<span style='color: #aaa;'>Download:</span> {self.metadata.sizes.downloadSizeFormatted}"
f"<span style='color: #aaa;'>Install:</span> {self.metadata.sizes.installSizeFormatted}"
f"<span style='color: #aaa;'>Total:</span> {self.metadata.sizes.totalSizeFormatted}"
)
sizes_label = QLabel(sizes_text)
sizes_label.setStyleSheet("color: #fff; font-size: 13px;")
content_layout.addWidget(sizes_label)
tags_layout.addStretch()
content_layout.addLayout(tags_layout)
# Description section
desc_label = QLabel("<b style='color: #aaa; font-size: 14px;'>Description:</b>")
@@ -486,7 +523,8 @@ class ModlistDetailDialog(QDialog):
self.desc_text = QTextEdit()
self.desc_text.setReadOnly(True)
self.desc_text.setPlainText(self.metadata.description or "No description provided.")
self.desc_text.setFixedHeight(300)
# Compact description area; scroll when content is long
self.desc_text.setFixedHeight(120)
self.desc_text.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.desc_text.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.desc_text.setLineWrapMode(QTextEdit.WidgetWidth)
@@ -795,6 +833,8 @@ class ModlistGalleryDialog(QDialog):
self._validation_update_timer = None # Timer for background validation updates
self._setup_ui()
# Disable filter controls during initial load to prevent race conditions
self._set_filter_controls_enabled(False)
# Lazy load - fetch modlists when dialog is shown
def _apply_initial_size(self):
@@ -832,8 +872,8 @@ class ModlistGalleryDialog(QDialog):
main_layout.addWidget(filter_panel)
# Right content area (modlist grid)
content_area = self._create_content_area()
main_layout.addWidget(content_area, stretch=1)
self.content_area = self._create_content_area()
main_layout.addWidget(self.content_area, stretch=1)
self.setLayout(main_layout)
@@ -893,27 +933,28 @@ class ModlistGalleryDialog(QDialog):
# Add spacing between Tags and Mods sections
layout.addSpacing(8)
# Mod filter
mods_label = QLabel("Mods:")
layout.addWidget(mods_label)
self.mod_search = QLineEdit()
self.mod_search.setPlaceholderText("Search mods...")
self.mod_search.setStyleSheet("QLineEdit { background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 4px; }")
self.mod_search.textChanged.connect(self._filter_mods_list)
# Prevent Enter from triggering default button (which would close dialog)
self.mod_search.returnPressed.connect(lambda: self.mod_search.clearFocus())
layout.addWidget(self.mod_search)
self.mods_list = QListWidget()
self.mods_list.setSelectionMode(QListWidget.MultiSelection)
self.mods_list.setMaximumHeight(150)
self.mods_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Remove horizontal scrollbar
self.mods_list.setStyleSheet("QListWidget { background: #2a2a2a; color: #fff; border: 1px solid #555; }")
self.mods_list.itemSelectionChanged.connect(self._apply_filters)
layout.addWidget(self.mods_list)
self.all_mods_list = [] # Store all mods for filtering
# Mod filter - TEMPORARILY DISABLED (not working correctly in v0.2.0.8)
# TODO: Re-enable once mod search index issue is resolved
# mods_label = QLabel("Mods:")
# layout.addWidget(mods_label)
#
# self.mod_search = QLineEdit()
# self.mod_search.setPlaceholderText("Search mods...")
# self.mod_search.setStyleSheet("QLineEdit { background: #2a2a2a; color: #fff; border: 1px solid #555; padding: 4px; }")
# self.mod_search.textChanged.connect(self._filter_mods_list)
# # Prevent Enter from triggering default button (which would close dialog)
# self.mod_search.returnPressed.connect(lambda: self.mod_search.clearFocus())
# layout.addWidget(self.mod_search)
#
# self.mods_list = QListWidget()
# self.mods_list.setSelectionMode(QListWidget.MultiSelection)
# self.mods_list.setMaximumHeight(150)
# self.mods_list.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # Remove horizontal scrollbar
# self.mods_list.setStyleSheet("QListWidget { background: #2a2a2a; color: #fff; border: 1px solid #555; }")
# self.mods_list.itemSelectionChanged.connect(self._apply_filters)
# layout.addWidget(self.mods_list)
#
# self.all_mods_list = [] # Store all mods for filtering
layout.addStretch()
@@ -934,8 +975,8 @@ class ModlistGalleryDialog(QDialog):
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(12)
# Status label (subtle, top-right)
self.status_label = QLabel("Loading modlists...")
# Status label (subtle, top-right) - hidden during initial loading (popup shows instead)
self.status_label = QLabel("")
self.status_label.setStyleSheet("color: #888; font-size: 10px;")
self.status_label.setAlignment(Qt.AlignRight | Qt.AlignTop)
layout.addWidget(self.status_label)
@@ -965,13 +1006,55 @@ class ModlistGalleryDialog(QDialog):
from PySide6.QtCore import QThread, Signal
from PySide6.QtGui import QFont
# Make status label more prominent during loading
self.status_label.setText("Loading modlists...")
# Hide status label during loading (popup dialog will show instead)
self.status_label.setVisible(False)
# Show loading overlay directly in content area (simpler than separate dialog)
self._loading_overlay = QWidget(self.content_area)
self._loading_overlay.setStyleSheet("""
QWidget {
background-color: rgba(35, 35, 35, 240);
border-radius: 8px;
}
""")
overlay_layout = QVBoxLayout()
overlay_layout.setContentsMargins(30, 20, 30, 20)
overlay_layout.setSpacing(12)
self._loading_label = QLabel("Loading modlists")
self._loading_label.setAlignment(Qt.AlignCenter)
# Set fixed width to prevent text shifting when dots animate
# Width accommodates "Loading modlists..." (longest version)
self._loading_label.setFixedWidth(220)
font = QFont()
font.setPointSize(14)
font.setBold(True)
self.status_label.setFont(font)
self.status_label.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 14px; font-weight: bold;")
self._loading_label.setFont(font)
self._loading_label.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE}; font-size: 14px; font-weight: bold;")
overlay_layout.addWidget(self._loading_label)
self._loading_overlay.setLayout(overlay_layout)
self._loading_overlay.setFixedSize(300, 120)
# Animate dots in loading message
self._loading_dot_count = 0
self._loading_dot_timer = QTimer()
self._loading_dot_timer.timeout.connect(self._animate_loading_dots)
self._loading_dot_timer.start(500) # Update every 500ms
# Position overlay in center of content area
def position_overlay():
if hasattr(self, 'content_area') and self.content_area.isVisible():
content_width = self.content_area.width()
content_height = self.content_area.height()
x = (content_width - 300) // 2
y = (content_height - 120) // 2
self._loading_overlay.move(x, y)
self._loading_overlay.show()
self._loading_overlay.raise_()
# Delay slightly to ensure content_area is laid out
QTimer.singleShot(50, position_overlay)
class ModlistLoaderThread(QThread):
"""Background thread to load modlist metadata"""
@@ -987,9 +1070,10 @@ class ModlistGalleryDialog(QDialog):
start_time = time.time()
# Fetch metadata (CPU-intensive work happens here in background)
# Skip search index initially for faster loading - can be loaded later if user searches
metadata_response = self.gallery_service.fetch_modlist_metadata(
include_validation=False,
include_search_index=True,
include_search_index=False, # Skip for faster initial load
sort_by="title"
)
@@ -1010,17 +1094,31 @@ class ModlistGalleryDialog(QDialog):
self._loader_thread.finished.connect(self._on_modlists_loaded)
self._loader_thread.start()
def _animate_loading_dots(self):
"""Animate dots in loading message"""
if hasattr(self, '_loading_label') and self._loading_label:
self._loading_dot_count = (self._loading_dot_count + 1) % 4
dots = "." * self._loading_dot_count
# Pad with spaces to keep text width constant (prevents shifting)
padding = " " * (3 - self._loading_dot_count)
self._loading_label.setText(f"Loading modlists{dots}{padding}")
def _on_modlists_loaded(self, metadata_response, error_message):
"""Handle modlist metadata loaded in background thread (runs in GUI thread)"""
import random
from PySide6.QtCore import QTimer
from PySide6.QtGui import QFont
# Restore normal status label styling
font = QFont()
font.setPointSize(10)
self.status_label.setFont(font)
self.status_label.setStyleSheet("color: #888; font-size: 10px;")
# Stop animation timer and close loading overlay
if hasattr(self, '_loading_dot_timer') and self._loading_dot_timer:
self._loading_dot_timer.stop()
self._loading_dot_timer = None
if hasattr(self, '_loading_overlay') and self._loading_overlay:
self._loading_overlay.hide()
self._loading_overlay.deleteLater()
self._loading_overlay = None
self.status_label.setVisible(True)
if error_message:
self.status_label.setText(f"Error loading modlists: {error_message}")
@@ -1059,9 +1157,9 @@ class ModlistGalleryDialog(QDialog):
if index >= 0:
self.game_combo.setCurrentIndex(index)
# Populate tag and mod filters
# Populate tag filter (mod filter temporarily disabled)
self._populate_tag_filter()
self._populate_mod_filter()
# self._populate_mod_filter() # TEMPORARILY DISABLED
# Create cards immediately (will show placeholders for images not in cache)
self._create_all_cards()
@@ -1073,6 +1171,9 @@ class ModlistGalleryDialog(QDialog):
# Reconnect filter handler
self.game_combo.currentIndexChanged.connect(self._apply_filters)
# Enable filter controls now that data is loaded
self._set_filter_controls_enabled(True)
# Apply filters (will show all modlists for selected game initially)
self._apply_filters()
@@ -1128,9 +1229,9 @@ class ModlistGalleryDialog(QDialog):
if index >= 0:
self.game_combo.setCurrentIndex(index)
# Populate tag and mod filters
# Populate tag filter (mod filter temporarily disabled)
self._populate_tag_filter()
self._populate_mod_filter()
# self._populate_mod_filter() # TEMPORARILY DISABLED
# Create cards immediately (will show placeholders for images not in cache)
self._create_all_cards()
@@ -1240,62 +1341,83 @@ class ModlistGalleryDialog(QDialog):
def _populate_mod_filter(self):
"""Populate mod filter with all available mods from search index"""
all_mods = set()
# Track which mods come from NSFW modlists only
mods_from_nsfw_only = set()
mods_from_sfw = set()
modlists_with_mods = 0
for modlist in self.all_modlists:
if hasattr(modlist, 'mods') and modlist.mods:
modlists_with_mods += 1
for mod in modlist.mods:
all_mods.add(mod)
if modlist.nsfw:
mods_from_nsfw_only.add(mod)
else:
mods_from_sfw.add(mod)
# Mods that are ONLY in NSFW modlists (not in any SFW modlists)
self.nsfw_only_mods = mods_from_nsfw_only - mods_from_sfw
self.all_mods_list = sorted(all_mods)
self._filter_mods_list("") # Populate with all mods initially
# TEMPORARILY DISABLED - mod filter feature removed in v0.2.0.8
return
# all_mods = set()
# # Track which mods come from NSFW modlists only
# mods_from_nsfw_only = set()
# mods_from_sfw = set()
# modlists_with_mods = 0
#
# for modlist in self.all_modlists:
# if hasattr(modlist, 'mods') and modlist.mods:
# modlists_with_mods += 1
# for mod in modlist.mods:
# all_mods.add(mod)
# if modlist.nsfw:
# mods_from_nsfw_only.add(mod)
# else:
# mods_from_sfw.add(mod)
#
# # Mods that are ONLY in NSFW modlists (not in any SFW modlists)
# self.nsfw_only_mods = mods_from_nsfw_only - mods_from_sfw
#
# self.all_mods_list = sorted(all_mods)
#
# self._filter_mods_list("") # Populate with all mods initially
def _filter_mods_list(self, search_text: str = ""):
"""Filter the mods list based on search text and NSFW checkbox"""
# TEMPORARILY DISABLED - mod filter feature removed in v0.2.0.8
return
# Get search text from the widget if not provided
if not search_text and hasattr(self, 'mod_search'):
search_text = self.mod_search.text()
self.mods_list.clear()
search_lower = search_text.lower().strip()
# Start with all mods or filtered by search
if search_lower:
filtered_mods = [m for m in self.all_mods_list if search_lower in m.lower()]
else:
filtered_mods = self.all_mods_list
# Filter out NSFW-only mods if NSFW checkbox is not checked
if not self.show_nsfw.isChecked():
filtered_mods = [m for m in filtered_mods if m not in getattr(self, 'nsfw_only_mods', set())]
# Limit to first 500 results for performance
for mod in filtered_mods[:500]:
self.mods_list.addItem(mod)
if len(filtered_mods) > 500:
self.mods_list.addItem(f"... and {len(filtered_mods) - 500} more (refine search)")
# if not search_text and hasattr(self, 'mod_search'):
# search_text = self.mod_search.text()
#
# self.mods_list.clear()
# search_lower = search_text.lower().strip()
#
# # Start with all mods or filtered by search
# if search_lower:
# filtered_mods = [m for m in self.all_mods_list if search_lower in m.lower()]
# else:
# filtered_mods = self.all_mods_list
#
# # Filter out NSFW-only mods if NSFW checkbox is not checked
# if not self.show_nsfw.isChecked():
# filtered_mods = [m for m in filtered_mods if m not in getattr(self, 'nsfw_only_mods', set())]
#
# # Limit to first 500 results for performance
# for mod in filtered_mods[:500]:
# self.mods_list.addItem(mod)
#
# if len(filtered_mods) > 500:
# self.mods_list.addItem(f"... and {len(filtered_mods) - 500} more (refine search)")
def _on_nsfw_toggled(self, checked: bool):
"""Handle NSFW checkbox toggle - refresh mod list and apply filters"""
self._filter_mods_list() # Refresh mod list based on NSFW state
# self._filter_mods_list() # TEMPORARILY DISABLED - Refresh mod list based on NSFW state
self._apply_filters() # Apply all filters
def _set_filter_controls_enabled(self, enabled: bool):
"""Enable or disable all filter controls"""
self.search_box.setEnabled(enabled)
self.game_combo.setEnabled(enabled)
self.show_official_only.setEnabled(enabled)
self.show_nsfw.setEnabled(enabled)
self.hide_unavailable.setEnabled(enabled)
self.tags_list.setEnabled(enabled)
# self.mod_search.setEnabled(enabled) # TEMPORARILY DISABLED
# self.mods_list.setEnabled(enabled) # TEMPORARILY DISABLED
def _apply_filters(self):
"""Apply current filters to modlist display"""
# CRITICAL: Guard against race condition - don't filter if modlists aren't loaded yet
if not self.all_modlists:
return
filtered = self.all_modlists
# Search filter
@@ -1344,10 +1466,10 @@ class ModlistGalleryDialog(QDialog):
)
]
# Mod filter - modlist must contain ALL selected mods
selected_mods = [item.text() for item in self.mods_list.selectedItems()]
if selected_mods:
filtered = [m for m in filtered if m.mods and all(mod in m.mods for mod in selected_mods)]
# Mod filter - TEMPORARILY DISABLED (not working correctly in v0.2.0.8)
# selected_mods = [item.text() for item in self.mods_list.selectedItems()]
# if selected_mods:
# filtered = [m for m in filtered if m.mods and all(mod in m.mods for mod in selected_mods)]
self.filtered_modlists = filtered
self._update_grid()
@@ -1385,16 +1507,27 @@ class ModlistGalleryDialog(QDialog):
def _update_grid(self):
"""Update grid by removing all cards and re-adding only visible ones"""
# CRITICAL: Guard against race condition - don't update if cards aren't ready yet
if not self.all_cards:
return
# Disable updates during grid update
self.grid_widget.setUpdatesEnabled(False)
try:
# Remove all cards from layout
while self.grid_layout.count():
item = self.grid_layout.takeAt(0)
if item.widget():
item.widget().setParent(None)
# CRITICAL FIX: Properly remove all widgets to prevent overlapping
# Iterate backwards to avoid index shifting issues
for i in range(self.grid_layout.count() - 1, -1, -1):
item = self.grid_layout.takeAt(i)
widget = item.widget() if item else None
if widget:
# Hide widget during removal to prevent visual artifacts
widget.hide()
del item
# Force layout update to ensure all widgets are removed
self.grid_layout.update()
# Calculate number of columns based on available width
# Get the scroll area width (accounting for filter panel ~280px + margins)
@@ -1433,6 +1566,20 @@ class ModlistGalleryDialog(QDialog):
card = self.all_cards.get(modlist.machineURL)
if card:
# Safety check: ensure widget is not already in the layout
# (shouldn't happen after proper removal above, but defensive programming)
already_in_layout = False
for i in range(self.grid_layout.count()):
item = self.grid_layout.itemAt(i)
if item and item.widget() == card:
# Widget is already in layout - this shouldn't happen, but handle it
already_in_layout = True
self.grid_layout.removeWidget(card)
break
# Ensure widget is visible and add to grid
if not already_in_layout or card.isHidden():
card.show()
self.grid_layout.addWidget(card, row, col)
# Set column stretch - don't stretch card columns, but add a spacer column

View File

@@ -20,6 +20,13 @@ from PySide6.QtGui import QFont
from jackify.shared.progress_models import FileProgress, OperationType
from ..shared_theme import JACKIFY_COLOR_BLUE
def _debug_log(message):
"""Log message only if debug mode is enabled"""
from jackify.backend.handlers.config_handler import ConfigHandler
config_handler = ConfigHandler()
if config_handler.get('debug_mode', False):
print(message)
class SummaryProgressWidget(QWidget):
"""Widget showing summary progress for phases like Installing."""
@@ -484,7 +491,28 @@ class FileProgressList(QWidget):
return
# Widget doesn't exist - create it (only clear when creating new widget)
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
_debug_log(f"[WIDGET_FIX] About to clear list_widget for summary widget - count={self.list_widget.count()}")
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item:
widget = self.list_widget.itemWidget(item)
if widget:
_debug_log(f"[WIDGET_FIX] Removing widget before clear (summary) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
self.list_widget.removeItemWidget(item)
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
import traceback
traceback.print_stack()
self.list_widget.clear()
# Check widgets in _file_items dict after clear
for key, widget in list(self._file_items.items()):
if widget:
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
import traceback
traceback.print_stack()
self._file_items.clear()
# Create new summary widget
@@ -510,7 +538,22 @@ class FileProgressList(QWidget):
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item and item.data(Qt.UserRole) == "__summary__":
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
widget = self.list_widget.itemWidget(item)
if widget:
_debug_log(f"[WIDGET_FIX] Removing summary widget - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
self.list_widget.removeItemWidget(item)
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Summary widget became top-level window after removeItemWidget()!")
import traceback
traceback.print_stack()
self.list_widget.takeItem(i)
if widget:
_debug_log(f"[WIDGET_FIX] After takeItem (summary) - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Summary widget is still a top-level window after takeItem()!")
import traceback
traceback.print_stack()
break
self._summary_widget = None
else:
@@ -522,7 +565,22 @@ class FileProgressList(QWidget):
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item and item.data(Qt.UserRole) == "__transition__":
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
widget = self.list_widget.itemWidget(item)
if widget:
_debug_log(f"[WIDGET_FIX] Removing transition label - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
self.list_widget.removeItemWidget(item)
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Transition label became top-level window after removeItemWidget()!")
import traceback
traceback.print_stack()
self.list_widget.takeItem(i)
if widget:
_debug_log(f"[WIDGET_FIX] After takeItem (transition) - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Transition label is still a top-level window after takeItem()!")
import traceback
traceback.print_stack()
break
self._transition_label = None
@@ -533,7 +591,28 @@ class FileProgressList(QWidget):
self._show_transition_message(current_phase)
else:
# Show empty state but keep header stable
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
_debug_log(f"[WIDGET_FIX] About to clear list_widget (empty state) - count={self.list_widget.count()}")
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item:
widget = self.list_widget.itemWidget(item)
if widget:
_debug_log(f"[WIDGET_FIX] Removing widget before clear (empty) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
self.list_widget.removeItemWidget(item)
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
import traceback
traceback.print_stack()
self.list_widget.clear()
# Check widgets in _file_items dict after clear
for key, widget in list(self._file_items.items()):
if widget:
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear (empty) - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
import traceback
traceback.print_stack()
self._file_items.clear()
# Update last phase tracker
@@ -579,7 +658,24 @@ class FileProgressList(QWidget):
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item and item.data(Qt.UserRole) == item_key:
# CRITICAL FIX: Call removeItemWidget() before takeItem() to prevent orphaned widgets
widget = self.list_widget.itemWidget(item)
if widget:
_debug_log(f"[WIDGET_FIX] Removing widget for item_key={item_key} - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
self.list_widget.removeItemWidget(item)
# Check if widget became orphaned after removal
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget()! widget={widget}")
import traceback
traceback.print_stack()
self.list_widget.takeItem(i)
# Final check after takeItem
if widget:
_debug_log(f"[WIDGET_FIX] After takeItem - widget.parent()={widget.parent()}, isWindow()={widget.isWindow()}")
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget is still a top-level window after takeItem()! This is the bug!")
import traceback
traceback.print_stack()
break
del self._file_items[item_key]
@@ -638,7 +734,28 @@ class FileProgressList(QWidget):
def _show_transition_message(self, new_phase: str):
"""Show a brief 'Preparing...' message during phase transitions."""
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
_debug_log(f"[WIDGET_FIX] About to clear list_widget (transition) - count={self.list_widget.count()}")
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item:
widget = self.list_widget.itemWidget(item)
if widget:
_debug_log(f"[WIDGET_FIX] Removing widget before clear (transition) - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
self.list_widget.removeItemWidget(item)
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
import traceback
traceback.print_stack()
self.list_widget.clear()
# Check widgets in _file_items dict after clear
for key, widget in list(self._file_items.items()):
if widget:
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear (transition) - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
import traceback
traceback.print_stack()
self._file_items.clear()
# Header removed - tab label provides context
@@ -663,9 +780,42 @@ class FileProgressList(QWidget):
def clear(self):
"""Clear all file items."""
# CRITICAL FIX: Remove all item widgets before clear() to prevent orphaned widgets
_debug_log(f"[WIDGET_FIX] clear() called - count={self.list_widget.count()}")
for i in range(self.list_widget.count()):
item = self.list_widget.item(i)
if item:
widget = self.list_widget.itemWidget(item)
if widget:
_debug_log(f"[WIDGET_FIX] Removing widget before clear() - widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
self.list_widget.removeItemWidget(item)
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget became top-level window after removeItemWidget() before clear()!")
import traceback
traceback.print_stack()
self.list_widget.clear()
# Check widgets in _file_items dict after clear
for key, widget in list(self._file_items.items()):
if widget:
_debug_log(f"[WIDGET_FIX] Widget in _file_items after clear() - key={key}, widget={widget}, parent={widget.parent()}, isWindow()={widget.isWindow()}")
if widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Widget in _file_items is a top-level window after clear()! This is the bug!")
import traceback
traceback.print_stack()
self._file_items.clear()
if self._summary_widget:
_debug_log(f"[WIDGET_FIX] Clearing summary_widget - widget={self._summary_widget}, parent={self._summary_widget.parent()}, isWindow()={self._summary_widget.isWindow()}")
if self._summary_widget.isWindow():
print(f"[WIDGET_FIX] ERROR: Summary widget is a top-level window in clear()!")
import traceback
traceback.print_stack()
self._summary_widget = None
if self._transition_label:
_debug_log(f"[WIDGET_FIX] Clearing transition_label - widget={self._transition_label}, parent={self._transition_label.parent()}, isWindow()={self._transition_label.isWindow()}")
if self._transition_label.isWindow():
print(f"[WIDGET_FIX] ERROR: Transition label is a top-level window in clear()!")
import traceback
traceback.print_stack()
self._transition_label = None
self._last_phase = None
# Header removed - tab label provides context