mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2511c9334c | ||
|
|
5869a896a8 | ||
|
|
99fb369d5e | ||
|
|
a813236e51 | ||
|
|
a7ed4b2a1e |
73
CHANGELOG.md
73
CHANGELOG.md
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
@@ -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."""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user