Sync from development - prepare for v0.2.0.4

This commit is contained in:
Omni
2025-12-23 21:49:18 +00:00
parent 523681a254
commit a7ed4b2a1e
62 changed files with 575 additions and 429 deletions

View File

@@ -1,5 +1,27 @@
# Jackify Changelog
## v0.2.0.4 - Bugfixes & Improvements
**Release Date:** 2025-12-23
### Engine Updates
- **jackify-engine 0.4.3**: Fixed case sensitivity issues, archive extraction crashes, and improved error messages
### Bug Fixes
- Fixed modlist gallery metadata showing outdated versions (now always fetches fresh data)
- Fixed hardcoded ~/Jackify paths preventing custom data directory settings
- Fixed update check blocking GUI startup
- Improved Steam restart reliability (3-minute timeout, better error handling)
- Fixed Protontricks Flatpak installation on Steam Deck
### Backend Changes
- GPU texture conversion now always enabled (config setting deprecated)
### UI Improvements
- Redesigned modlist detail view to show more of hero image
- Improved gallery loading with animated feedback and faster initial load
---
## v0.2.0.3 - Engine Bugfix & Settings Cleanup
**Release Date:** 2025-12-21

View File

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

View File

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

View File

@@ -390,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 +419,12 @@ class ConfigHandler:
return base64.b64decode(encrypted_key.encode('utf-8')).decode('utf-8')
except:
return None
except AttributeError:
# Old pycrypto doesn't have MODE_GCM, fallback to base64
try:
return base64.b64decode(encrypted_key.encode('utf-8')).decode('utf-8')
except:
return None
except Exception as e:
# Might be old base64-only format, try decoding
try:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,7 +7,7 @@
"targets": {
".NETCoreApp,Version=v8.0": {},
".NETCoreApp,Version=v8.0/linux-x64": {
"jackify-engine/0.4.2": {
"jackify-engine/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Bethesda": "0.4.3",
"Wabbajack.Downloaders.Dispatcher": "0.4.3",
"Wabbajack.Hashing.xxHash64": "0.4.3",
"Wabbajack.Networking.Discord": "0.4.3",
"Wabbajack.Networking.GitHub": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3",
"Wabbajack.Server.Lib": "0.4.3",
"Wabbajack.Services.OSIntegrated": "0.4.3",
"Wabbajack.VFS": "0.4.3",
"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.3": {
"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.3"
},
"runtime": {
"Wabbajack.CLI.Builder.dll": {}
}
},
"Wabbajack.Common/0.4.2": {
"Wabbajack.Common/0.4.3": {
"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.3",
"Wabbajack.Networking.Http": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3"
},
"runtime": {
"Wabbajack.Common.dll": {}
}
},
"Wabbajack.Compiler/0.4.2": {
"Wabbajack.Compiler/0.4.3": {
"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.3",
"Wabbajack.Installer": "0.4.3",
"Wabbajack.VFS": "0.4.3",
"ini-parser-netstandard": "2.5.2"
},
"runtime": {
"Wabbajack.Compiler.dll": {}
}
},
"Wabbajack.Compression.BSA/0.4.2": {
"Wabbajack.Compression.BSA/0.4.3": {
"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.3",
"Wabbajack.DTOs": "0.4.3"
},
"runtime": {
"Wabbajack.Compression.BSA.dll": {}
}
},
"Wabbajack.Compression.Zip/0.4.2": {
"Wabbajack.Compression.Zip/0.4.3": {
"dependencies": {
"Wabbajack.IO.Async": "0.4.2"
"Wabbajack.IO.Async": "0.4.3"
},
"runtime": {
"Wabbajack.Compression.Zip.dll": {}
}
},
"Wabbajack.Configuration/0.4.2": {
"Wabbajack.Configuration/0.4.3": {
"runtime": {
"Wabbajack.Configuration.dll": {}
}
},
"Wabbajack.Downloaders.Bethesda/0.4.2": {
"Wabbajack.Downloaders.Bethesda/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Networking.BethesdaNet": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.Bethesda.dll": {}
}
},
"Wabbajack.Downloaders.Dispatcher/0.4.2": {
"Wabbajack.Downloaders.Dispatcher/0.4.3": {
"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.3",
"Wabbajack.Downloaders.GameFile": "0.4.3",
"Wabbajack.Downloaders.GoogleDrive": "0.4.3",
"Wabbajack.Downloaders.Http": "0.4.3",
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.4.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Downloaders.Manual": "0.4.3",
"Wabbajack.Downloaders.MediaFire": "0.4.3",
"Wabbajack.Downloaders.Mega": "0.4.3",
"Wabbajack.Downloaders.ModDB": "0.4.3",
"Wabbajack.Downloaders.Nexus": "0.4.3",
"Wabbajack.Downloaders.VerificationCache": "0.4.3",
"Wabbajack.Downloaders.WabbajackCDN": "0.4.3",
"Wabbajack.Networking.WabbajackClientApi": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.Dispatcher.dll": {}
}
},
"Wabbajack.Downloaders.GameFile/0.4.2": {
"Wabbajack.Downloaders.GameFile/0.4.3": {
"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.3",
"Wabbajack.VFS": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.GameFile.dll": {}
}
},
"Wabbajack.Downloaders.GoogleDrive/0.4.2": {
"Wabbajack.Downloaders.GoogleDrive/0.4.3": {
"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.3",
"Wabbajack.DTOs": "0.4.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Networking.Http": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.GoogleDrive.dll": {}
}
},
"Wabbajack.Downloaders.Http/0.4.2": {
"Wabbajack.Downloaders.Http/0.4.3": {
"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.3",
"Wabbajack.DTOs": "0.4.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Networking.BethesdaNet": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.Http.dll": {}
}
},
"Wabbajack.Downloaders.Interfaces/0.4.2": {
"Wabbajack.Downloaders.Interfaces/0.4.3": {
"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.3",
"Wabbajack.DTOs": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.Interfaces.dll": {}
}
},
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.2": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Networking.Http": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
}
},
"Wabbajack.Downloaders.Manual/0.4.2": {
"Wabbajack.Downloaders.Manual/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.Manual.dll": {}
}
},
"Wabbajack.Downloaders.MediaFire/0.4.2": {
"Wabbajack.Downloaders.MediaFire/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.MediaFire.dll": {}
}
},
"Wabbajack.Downloaders.Mega/0.4.2": {
"Wabbajack.Downloaders.Mega/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.Mega.dll": {}
}
},
"Wabbajack.Downloaders.ModDB/0.4.2": {
"Wabbajack.Downloaders.ModDB/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Networking.Http": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.ModDB.dll": {}
}
},
"Wabbajack.Downloaders.Nexus/0.4.2": {
"Wabbajack.Downloaders.Nexus/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Hashing.xxHash64": "0.4.3",
"Wabbajack.Networking.Http": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3",
"Wabbajack.Networking.NexusApi": "0.4.3",
"Wabbajack.Paths": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.Nexus.dll": {}
}
},
"Wabbajack.Downloaders.VerificationCache/0.4.2": {
"Wabbajack.Downloaders.VerificationCache/0.4.3": {
"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.3",
"Wabbajack.Paths.IO": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.VerificationCache.dll": {}
}
},
"Wabbajack.Downloaders.WabbajackCDN/0.4.2": {
"Wabbajack.Downloaders.WabbajackCDN/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Networking.Http": "0.4.3",
"Wabbajack.RateLimiter": "0.4.3"
},
"runtime": {
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
}
},
"Wabbajack.DTOs/0.4.2": {
"Wabbajack.DTOs/0.4.3": {
"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.3",
"Wabbajack.Paths": "0.4.3"
},
"runtime": {
"Wabbajack.DTOs.dll": {}
}
},
"Wabbajack.FileExtractor/0.4.2": {
"Wabbajack.FileExtractor/0.4.3": {
"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.3",
"Wabbajack.Compression.BSA": "0.4.3",
"Wabbajack.Hashing.PHash": "0.4.3",
"Wabbajack.Paths": "0.4.3"
},
"runtime": {
"Wabbajack.FileExtractor.dll": {}
}
},
"Wabbajack.Hashing.PHash/0.4.2": {
"Wabbajack.Hashing.PHash/0.4.3": {
"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.3",
"Wabbajack.DTOs": "0.4.3",
"Wabbajack.Paths": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3"
},
"runtime": {
"Wabbajack.Hashing.PHash.dll": {}
}
},
"Wabbajack.Hashing.xxHash64/0.4.2": {
"Wabbajack.Hashing.xxHash64/0.4.3": {
"dependencies": {
"Wabbajack.Paths": "0.4.2",
"Wabbajack.RateLimiter": "0.4.2"
"Wabbajack.Paths": "0.4.3",
"Wabbajack.RateLimiter": "0.4.3"
},
"runtime": {
"Wabbajack.Hashing.xxHash64.dll": {}
}
},
"Wabbajack.Installer/0.4.2": {
"Wabbajack.Installer/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Dispatcher": "0.4.3",
"Wabbajack.Downloaders.GameFile": "0.4.3",
"Wabbajack.FileExtractor": "0.4.3",
"Wabbajack.Networking.WabbajackClientApi": "0.4.3",
"Wabbajack.Paths": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3",
"Wabbajack.VFS": "0.4.3",
"ini-parser-netstandard": "2.5.2"
},
"runtime": {
"Wabbajack.Installer.dll": {}
}
},
"Wabbajack.IO.Async/0.4.2": {
"Wabbajack.IO.Async/0.4.3": {
"runtime": {
"Wabbajack.IO.Async.dll": {}
}
},
"Wabbajack.Networking.BethesdaNet/0.4.2": {
"Wabbajack.Networking.BethesdaNet/0.4.3": {
"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.3",
"Wabbajack.Networking.Http": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3"
},
"runtime": {
"Wabbajack.Networking.BethesdaNet.dll": {}
}
},
"Wabbajack.Networking.Discord/0.4.2": {
"Wabbajack.Networking.Discord/0.4.3": {
"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.3"
},
"runtime": {
"Wabbajack.Networking.Discord.dll": {}
}
},
"Wabbajack.Networking.GitHub/0.4.2": {
"Wabbajack.Networking.GitHub/0.4.3": {
"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.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3"
},
"runtime": {
"Wabbajack.Networking.GitHub.dll": {}
}
},
"Wabbajack.Networking.Http/0.4.2": {
"Wabbajack.Networking.Http/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Interfaces": "0.4.3",
"Wabbajack.Hashing.xxHash64": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3",
"Wabbajack.Paths": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3"
},
"runtime": {
"Wabbajack.Networking.Http.dll": {}
}
},
"Wabbajack.Networking.Http.Interfaces/0.4.2": {
"Wabbajack.Networking.Http.Interfaces/0.4.3": {
"dependencies": {
"Wabbajack.Hashing.xxHash64": "0.4.2"
"Wabbajack.Hashing.xxHash64": "0.4.3"
},
"runtime": {
"Wabbajack.Networking.Http.Interfaces.dll": {}
}
},
"Wabbajack.Networking.NexusApi/0.4.2": {
"Wabbajack.Networking.NexusApi/0.4.3": {
"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.3",
"Wabbajack.Networking.Http": "0.4.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3",
"Wabbajack.Networking.WabbajackClientApi": "0.4.3"
},
"runtime": {
"Wabbajack.Networking.NexusApi.dll": {}
}
},
"Wabbajack.Networking.WabbajackClientApi/0.4.2": {
"Wabbajack.Networking.WabbajackClientApi/0.4.3": {
"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.3",
"Wabbajack.DTOs": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3",
"Wabbajack.VFS.Interfaces": "0.4.3",
"YamlDotNet": "16.3.0"
},
"runtime": {
"Wabbajack.Networking.WabbajackClientApi.dll": {}
}
},
"Wabbajack.Paths/0.4.2": {
"Wabbajack.Paths/0.4.3": {
"runtime": {
"Wabbajack.Paths.dll": {}
}
},
"Wabbajack.Paths.IO/0.4.2": {
"Wabbajack.Paths.IO/0.4.3": {
"dependencies": {
"Wabbajack.Paths": "0.4.2",
"Wabbajack.Paths": "0.4.3",
"shortid": "4.0.0"
},
"runtime": {
"Wabbajack.Paths.IO.dll": {}
}
},
"Wabbajack.RateLimiter/0.4.2": {
"Wabbajack.RateLimiter/0.4.3": {
"runtime": {
"Wabbajack.RateLimiter.dll": {}
}
},
"Wabbajack.Server.Lib/0.4.2": {
"Wabbajack.Server.Lib/0.4.3": {
"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.3",
"Wabbajack.Networking.Http.Interfaces": "0.4.3",
"Wabbajack.Services.OSIntegrated": "0.4.3"
},
"runtime": {
"Wabbajack.Server.Lib.dll": {}
}
},
"Wabbajack.Services.OSIntegrated/0.4.2": {
"Wabbajack.Services.OSIntegrated/0.4.3": {
"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.3",
"Wabbajack.Downloaders.Dispatcher": "0.4.3",
"Wabbajack.Installer": "0.4.3",
"Wabbajack.Networking.BethesdaNet": "0.4.3",
"Wabbajack.Networking.Discord": "0.4.3",
"Wabbajack.VFS": "0.4.3"
},
"runtime": {
"Wabbajack.Services.OSIntegrated.dll": {}
}
},
"Wabbajack.VFS/0.4.2": {
"Wabbajack.VFS/0.4.3": {
"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.3",
"Wabbajack.FileExtractor": "0.4.3",
"Wabbajack.Hashing.PHash": "0.4.3",
"Wabbajack.Hashing.xxHash64": "0.4.3",
"Wabbajack.Paths": "0.4.3",
"Wabbajack.Paths.IO": "0.4.3",
"Wabbajack.VFS.Interfaces": "0.4.3"
},
"runtime": {
"Wabbajack.VFS.dll": {}
}
},
"Wabbajack.VFS.Interfaces/0.4.2": {
"Wabbajack.VFS.Interfaces/0.4.3": {
"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.3",
"Wabbajack.Hashing.xxHash64": "0.4.3",
"Wabbajack.Paths": "0.4.3"
},
"runtime": {
"Wabbajack.VFS.Interfaces.dll": {}
@@ -2332,7 +2332,7 @@
}
},
"libraries": {
"jackify-engine/0.4.2": {
"jackify-engine/0.4.3": {
"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.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Common/0.4.2": {
"Wabbajack.Common/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compiler/0.4.2": {
"Wabbajack.Compiler/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compression.BSA/0.4.2": {
"Wabbajack.Compression.BSA/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Compression.Zip/0.4.2": {
"Wabbajack.Compression.Zip/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Configuration/0.4.2": {
"Wabbajack.Configuration/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Bethesda/0.4.2": {
"Wabbajack.Downloaders.Bethesda/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Dispatcher/0.4.2": {
"Wabbajack.Downloaders.Dispatcher/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.GameFile/0.4.2": {
"Wabbajack.Downloaders.GameFile/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.GoogleDrive/0.4.2": {
"Wabbajack.Downloaders.GoogleDrive/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Http/0.4.2": {
"Wabbajack.Downloaders.Http/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Interfaces/0.4.2": {
"Wabbajack.Downloaders.Interfaces/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.2": {
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Manual/0.4.2": {
"Wabbajack.Downloaders.Manual/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.MediaFire/0.4.2": {
"Wabbajack.Downloaders.MediaFire/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Mega/0.4.2": {
"Wabbajack.Downloaders.Mega/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.ModDB/0.4.2": {
"Wabbajack.Downloaders.ModDB/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.Nexus/0.4.2": {
"Wabbajack.Downloaders.Nexus/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.VerificationCache/0.4.2": {
"Wabbajack.Downloaders.VerificationCache/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Downloaders.WabbajackCDN/0.4.2": {
"Wabbajack.Downloaders.WabbajackCDN/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.DTOs/0.4.2": {
"Wabbajack.DTOs/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.FileExtractor/0.4.2": {
"Wabbajack.FileExtractor/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Hashing.PHash/0.4.2": {
"Wabbajack.Hashing.PHash/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Hashing.xxHash64/0.4.2": {
"Wabbajack.Hashing.xxHash64/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Installer/0.4.2": {
"Wabbajack.Installer/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.IO.Async/0.4.2": {
"Wabbajack.IO.Async/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.BethesdaNet/0.4.2": {
"Wabbajack.Networking.BethesdaNet/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Discord/0.4.2": {
"Wabbajack.Networking.Discord/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.GitHub/0.4.2": {
"Wabbajack.Networking.GitHub/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Http/0.4.2": {
"Wabbajack.Networking.Http/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.Http.Interfaces/0.4.2": {
"Wabbajack.Networking.Http.Interfaces/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.NexusApi/0.4.2": {
"Wabbajack.Networking.NexusApi/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Networking.WabbajackClientApi/0.4.2": {
"Wabbajack.Networking.WabbajackClientApi/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Paths/0.4.2": {
"Wabbajack.Paths/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Paths.IO/0.4.2": {
"Wabbajack.Paths.IO/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.RateLimiter/0.4.2": {
"Wabbajack.RateLimiter/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Server.Lib/0.4.2": {
"Wabbajack.Server.Lib/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.Services.OSIntegrated/0.4.2": {
"Wabbajack.Services.OSIntegrated/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.VFS/0.4.2": {
"Wabbajack.VFS/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"Wabbajack.VFS.Interfaces/0.4.2": {
"Wabbajack.VFS.Interfaces/0.4.3": {
"type": "project",
"serviceable": false,
"sha512": ""

Binary file not shown.

View File

@@ -105,7 +105,7 @@ from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox, QTabWidget, QRadioButton, QButtonGroup
)
from PySide6.QtCore import Qt, QEvent, QTimer
from PySide6.QtCore import Qt, QEvent, QTimer, QThread, Signal
from PySide6.QtGui import QIcon
import json
@@ -1533,58 +1533,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
# 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
from ..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}")
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):

View File

@@ -2157,12 +2157,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}'")

View File

@@ -388,6 +388,15 @@ class ModlistDetailDialog(QDialog):
main_layout = QVBoxLayout()
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.setSpacing(0)
# --- Banner area with full-width text overlay ---
# Container so we can place a semi-opaque text panel over the banner image
banner_container = QFrame()
banner_container.setFrameShape(QFrame.NoFrame)
banner_container.setStyleSheet("background: #000; border: none;")
banner_layout = QVBoxLayout()
banner_layout.setContentsMargins(0, 0, 0, 0)
banner_layout.setSpacing(0)
banner_container.setLayout(banner_layout)
# Banner image at top with 16:9 aspect ratio (like Wabbajack)
self.banner_label = QLabel()
@@ -396,40 +405,67 @@ class ModlistDetailDialog(QDialog):
self.banner_label.setStyleSheet("background: #1a1a1a; border: none;")
self.banner_label.setAlignment(Qt.AlignCenter)
self.banner_label.setText("Loading image...")
main_layout.addWidget(self.banner_label)
banner_layout.addWidget(self.banner_label)
# Content area with padding
# Full-width transparent container with opaque card inside (only as wide as text)
overlay_container = QWidget()
overlay_container.setStyleSheet("background: transparent;")
overlay_layout = QHBoxLayout()
overlay_layout.setContentsMargins(24, 0, 24, 24)
overlay_layout.setSpacing(0)
overlay_container.setLayout(overlay_layout)
# Opaque text card - only as wide as content needs (where red lines are)
self.banner_text_panel = QFrame()
self.banner_text_panel.setFrameShape(QFrame.StyledPanel)
# Opaque background, rounded corners, sized to content only
self.banner_text_panel.setStyleSheet("""
QFrame {
background-color: rgba(0, 0, 0, 180);
border: 1px solid rgba(255, 255, 255, 30);
border-radius: 8px;
}
""")
self.banner_text_panel.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Maximum)
banner_text_layout = QVBoxLayout()
banner_text_layout.setContentsMargins(20, 12, 20, 14)
banner_text_layout.setSpacing(6)
self.banner_text_panel.setLayout(banner_text_layout)
# Add card to container (left-aligned, rest stays transparent)
overlay_layout.addWidget(self.banner_text_panel, alignment=Qt.AlignBottom | Qt.AlignLeft)
overlay_layout.addStretch() # Push card left, rest transparent
# Title only (badges moved to tags section below)
title = QLabel(self.metadata.title)
title.setFont(QFont("Sans", 24, QFont.Bold))
title.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE};")
title.setWordWrap(True)
banner_text_layout.addWidget(title)
# Only sizes in overlay (minimal info on image)
if self.metadata.sizes:
sizes_text = (
f"<span style='color: #aaa;'>Download:</span> {self.metadata.sizes.downloadSizeFormatted}"
f"<span style='color: #aaa;'>Install:</span> {self.metadata.sizes.installSizeFormatted}"
f"<span style='color: #aaa;'>Total:</span> {self.metadata.sizes.totalSizeFormatted}"
)
sizes_label = QLabel(sizes_text)
sizes_label.setStyleSheet("color: #fff; font-size: 13px;")
banner_text_layout.addWidget(sizes_label)
# Add full-width transparent container at bottom of banner
banner_layout.addWidget(overlay_container, alignment=Qt.AlignBottom)
main_layout.addWidget(banner_container)
# Content area with padding (tags + description + bottom bar)
content_widget = QWidget()
content_layout = QVBoxLayout()
content_layout.setContentsMargins(24, 20, 24, 20)
content_layout.setSpacing(16)
content_widget.setLayout(content_layout)
# Title row with status badges (UNAVAILABLE, Unofficial - Official and NSFW shown in tags)
title_row = QHBoxLayout()
title_row.setSpacing(12)
title = QLabel(self.metadata.title)
title.setFont(QFont("Sans", 24, QFont.Bold))
title.setStyleSheet(f"color: {JACKIFY_COLOR_BLUE};")
title.setWordWrap(True)
title_row.addWidget(title, stretch=1)
# Status badges in title row
if not self.metadata.is_available():
unavailable_badge = QLabel("UNAVAILABLE")
unavailable_badge.setStyleSheet("background: #666; color: white; padding: 4px 10px; font-size: 10px; font-weight: bold; border-radius: 4px;")
title_row.addWidget(unavailable_badge)
# Show "Unofficial" badge if not official (Official is shown in tags)
if not self.metadata.official:
unofficial_badge = QLabel("Unofficial")
unofficial_badge.setStyleSheet("background: #666; color: white; padding: 4px 10px; font-size: 10px; font-weight: bold; border-radius: 4px;")
title_row.addWidget(unofficial_badge)
content_layout.addLayout(title_row)
# Metadata line (version, author, game) - inline like Wabbajack
# Metadata line (version, author, game) - moved below image
metadata_line_parts = []
if self.metadata.version:
metadata_line_parts.append(f"<span style='color: #aaa;'>version</span> {self.metadata.version}")
@@ -446,13 +482,25 @@ class ModlistDetailDialog(QDialog):
metadata_line.setWordWrap(True)
content_layout.addWidget(metadata_line)
# Tags row (like Wabbajack)
tags_to_render = getattr(self.metadata, 'normalized_tags_display', self.metadata.tags or [])
if tags_to_render:
# 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:
for tag in tags_to_render:
tag_badge = QLabel(tag)
# Match Wabbajack tag styling
@@ -467,17 +515,6 @@ class ModlistDetailDialog(QDialog):
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)
# Description section
desc_label = QLabel("<b style='color: #aaa; font-size: 14px;'>Description:</b>")
content_layout.addWidget(desc_label)
@@ -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)
@@ -832,8 +870,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)
@@ -934,8 +972,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 +1003,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 +1067,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 +1091,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}")