mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 11:37:01 +01:00
Sync from development - prepare for v0.2.0.1
This commit is contained in:
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,5 +1,29 @@
|
|||||||
# Jackify Changelog
|
# Jackify Changelog
|
||||||
|
|
||||||
|
## v0.2.0.1 - Critical Bugfix Release
|
||||||
|
**Release Date:** 2025-12-15
|
||||||
|
|
||||||
|
### Critical Bug Fixes
|
||||||
|
- **Directory Safety Validation**: Fixed data loss bug where directories with only a `downloads/` folder were incorrectly identified as valid modlist directories
|
||||||
|
- **Flatpak Steam Restart**: Fixed Steam restart failures on Ubuntu/PopOS by removing incompatible `-foreground` flag and increasing startup wait
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- **External Links**: Fixed Ko-fi, GitHub, and Nexus links not opening on some distros using xdg-open with clean environment
|
||||||
|
- **TTW Console Output**: Filtered standalone "OK"/"DONE" noise messages from TTW installation console
|
||||||
|
- **Activity Window**: Fixed progress display updates in TTW Installer and other workflows
|
||||||
|
- **Wine Component Installation**: Added status feedback during component installation showing component list
|
||||||
|
- **Progress Parser**: Added defensive checks to prevent segfaults from malformed engine output
|
||||||
|
- **Progress Parser Speed Info**: Fixed 'OperationType' object has no attribute 'lower' error by converting enum to string value when extracting speed info from timestamp status patterns
|
||||||
|
|
||||||
|
### Improvements
|
||||||
|
- **Default Wine Components**: Added dxvk to default component list for better graphics compatibility
|
||||||
|
- **TTW Installer UI**: Show version numbers in status displays
|
||||||
|
|
||||||
|
### Engine Updates
|
||||||
|
- **jackify-engine 0.4.1**: Download reliability fixes, BSA case sensitivity handling, external drive I/O limiting, GPU detection caching, and texture processing performance improvements
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.2.0 - Modlist Gallery, OAuth Authentication & Performance Improvements
|
## v0.2.0 - Modlist Gallery, OAuth Authentication & Performance Improvements
|
||||||
**Release Date:** 2025-12-06
|
**Release Date:** 2025-12-06
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing
|
|||||||
Wabbajack modlists natively on Linux systems.
|
Wabbajack modlists natively on Linux systems.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.2.0"
|
__version__ = "0.2.0.1"
|
||||||
|
|||||||
@@ -722,7 +722,7 @@ class ModlistHandler:
|
|||||||
try:
|
try:
|
||||||
self.logger.info("Installing Wine components using user's preferred method...")
|
self.logger.info("Installing Wine components using user's preferred method...")
|
||||||
self.logger.debug(f"Calling winetricks_handler.install_wine_components with wineprefix={wineprefix}, game_var={self.game_var_full}, components={components}")
|
self.logger.debug(f"Calling winetricks_handler.install_wine_components with wineprefix={wineprefix}, game_var={self.game_var_full}, components={components}")
|
||||||
success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components)
|
success = self.winetricks_handler.install_wine_components(wineprefix, self.game_var_full, specific_components=components, status_callback=status_callback)
|
||||||
if success:
|
if success:
|
||||||
self.logger.info("Wine component installation completed successfully")
|
self.logger.info("Wine component installation completed successfully")
|
||||||
if status_callback:
|
if status_callback:
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ class ProgressParser:
|
|||||||
speed_info = self._parse_speed_from_string(speed_str)
|
speed_info = self._parse_speed_from_string(speed_str)
|
||||||
if speed_info:
|
if speed_info:
|
||||||
operation = self._detect_operation_from_line(status_text)
|
operation = self._detect_operation_from_line(status_text)
|
||||||
result.speed_info = (operation, speed_info)
|
result.speed_info = (operation.value, speed_info)
|
||||||
|
|
||||||
# Calculate overall percentage from step progress
|
# Calculate overall percentage from step progress
|
||||||
if max_steps > 0:
|
if max_steps > 0:
|
||||||
@@ -400,6 +400,18 @@ class ProgressParser:
|
|||||||
|
|
||||||
def _extract_file_progress(self, line: str) -> Optional[FileProgress]:
|
def _extract_file_progress(self, line: str) -> Optional[FileProgress]:
|
||||||
"""Extract file-level progress information."""
|
"""Extract file-level progress information."""
|
||||||
|
# CRITICAL: Defensive checks to prevent segfault in regex engine
|
||||||
|
# Segfaults happen in C code before Python exceptions, so we must validate input first
|
||||||
|
if not line or not isinstance(line, str):
|
||||||
|
return None
|
||||||
|
# Limit line length to prevent stack overflow in regex (10KB should be more than enough)
|
||||||
|
if len(line) > 10000:
|
||||||
|
return None
|
||||||
|
# Check for null bytes or other problematic characters that could corrupt regex
|
||||||
|
if '\x00' in line:
|
||||||
|
# Replace null bytes to prevent corruption
|
||||||
|
line = line.replace('\x00', '')
|
||||||
|
|
||||||
# PRIORITY: Check for [FILE_PROGRESS] prefix first (new engine format)
|
# PRIORITY: Check for [FILE_PROGRESS] prefix first (new engine format)
|
||||||
# Format: [FILE_PROGRESS] Downloading: filename.zip (20.0%) [3.7MB/s]
|
# Format: [FILE_PROGRESS] Downloading: filename.zip (20.0%) [3.7MB/s]
|
||||||
# Updated format: [FILE_PROGRESS] (Downloading|Extracting|Installing|Converting|Completed|etc): filename.zip (20.0%) [3.7MB/s] (current/total)
|
# Updated format: [FILE_PROGRESS] (Downloading|Extracting|Installing|Converting|Completed|etc): filename.zip (20.0%) [3.7MB/s] (current/total)
|
||||||
@@ -907,6 +919,13 @@ class ProgressStateManager:
|
|||||||
self._remove_synthetic_wabbajack()
|
self._remove_synthetic_wabbajack()
|
||||||
# Mark that we have a real .wabbajack entry to prevent synthetic ones
|
# Mark that we have a real .wabbajack entry to prevent synthetic ones
|
||||||
self._has_real_wabbajack = True
|
self._has_real_wabbajack = True
|
||||||
|
else:
|
||||||
|
# CRITICAL: If we get a real archive file (not .wabbajack), remove all .wabbajack entries
|
||||||
|
# This ensures .wabbajack entries disappear as soon as archive downloads start
|
||||||
|
from jackify.shared.progress_models import OperationType
|
||||||
|
if parsed.file_progress.operation == OperationType.DOWNLOAD:
|
||||||
|
self._remove_all_wabbajack_entries()
|
||||||
|
self._has_real_wabbajack = True # Prevent re-adding
|
||||||
self._augment_file_metrics(parsed.file_progress)
|
self._augment_file_metrics(parsed.file_progress)
|
||||||
# Don't add files that are already at 100% unless they're being updated
|
# Don't add files that are already at 100% unless they're being updated
|
||||||
# This prevents re-adding completed files
|
# This prevents re-adding completed files
|
||||||
@@ -934,6 +953,21 @@ class ProgressStateManager:
|
|||||||
self.state.add_file(parsed.file_progress)
|
self.state.add_file(parsed.file_progress)
|
||||||
updated = True
|
updated = True
|
||||||
elif parsed.data_info:
|
elif parsed.data_info:
|
||||||
|
# CRITICAL: Remove .wabbajack entries as soon as archive download phase starts
|
||||||
|
# Check if we're in "Downloading Mod Archives" phase or have real archive files downloading
|
||||||
|
phase_name_lower = (parsed.phase_name or "").lower()
|
||||||
|
message_lower = (parsed.message or "").lower()
|
||||||
|
is_archive_phase = (
|
||||||
|
'mod archives' in phase_name_lower or
|
||||||
|
'downloading mod archives' in message_lower or
|
||||||
|
(parsed.phase == InstallationPhase.DOWNLOAD and self._has_real_download_activity())
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_archive_phase:
|
||||||
|
# Archive download phase has started - remove all .wabbajack entries immediately
|
||||||
|
self._remove_all_wabbajack_entries()
|
||||||
|
self._has_real_wabbajack = True # Prevent re-adding
|
||||||
|
|
||||||
# Only create synthetic .wabbajack entry if we don't already have a real one
|
# Only create synthetic .wabbajack entry if we don't already have a real one
|
||||||
if not getattr(self, '_has_real_wabbajack', False):
|
if not getattr(self, '_has_real_wabbajack', False):
|
||||||
if self._maybe_add_wabbajack_progress(parsed):
|
if self._maybe_add_wabbajack_progress(parsed):
|
||||||
@@ -1164,4 +1198,19 @@ class ProgressStateManager:
|
|||||||
remaining.append(fp)
|
remaining.append(fp)
|
||||||
if removed:
|
if removed:
|
||||||
self.state.active_files = remaining
|
self.state.active_files = remaining
|
||||||
|
|
||||||
|
def _remove_all_wabbajack_entries(self):
|
||||||
|
"""Remove ALL .wabbajack entries (synthetic and real) when archive download phase starts."""
|
||||||
|
remaining = []
|
||||||
|
removed = False
|
||||||
|
for fp in self.state.active_files:
|
||||||
|
if fp.filename.lower().endswith('.wabbajack') or 'wabbajack' in fp.filename.lower():
|
||||||
|
removed = True
|
||||||
|
self._file_history.pop(fp.filename, None)
|
||||||
|
continue
|
||||||
|
remaining.append(fp)
|
||||||
|
if removed:
|
||||||
|
self.state.active_files = remaining
|
||||||
|
# Also clear the wabbajack entry name to prevent re-adding
|
||||||
|
self._wabbajack_entry_name = None
|
||||||
|
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ class ValidationHandler:
|
|||||||
def looks_like_modlist_dir(self, path: Path) -> bool:
|
def looks_like_modlist_dir(self, path: Path) -> bool:
|
||||||
"""Return True if the directory contains files/folders typical of a modlist install."""
|
"""Return True if the directory contains files/folders typical of a modlist install."""
|
||||||
expected = [
|
expected = [
|
||||||
'ModOrganizer.exe', 'profiles', 'mods', 'downloads', '.wabbajack', '.jackify_modlist_marker', 'ModOrganizer.ini'
|
'ModOrganizer.exe', 'profiles', 'mods', '.wabbajack', '.jackify_modlist_marker', 'ModOrganizer.ini'
|
||||||
]
|
]
|
||||||
for item in expected:
|
for item in expected:
|
||||||
if (path / item).exists():
|
if (path / item).exists():
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import os
|
|||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, List
|
from typing import Optional, List, Callable
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -110,10 +110,16 @@ class WinetricksHandler:
|
|||||||
self.logger.error(f"Error testing winetricks: {e}")
|
self.logger.error(f"Error testing winetricks: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def install_wine_components(self, wineprefix: str, game_var: str, specific_components: Optional[List[str]] = None) -> bool:
|
def install_wine_components(self, wineprefix: str, game_var: str, specific_components: Optional[List[str]] = None, status_callback: Optional[Callable[[str], None]] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Install the specified Wine components into the given prefix using winetricks.
|
Install the specified Wine components into the given prefix using winetricks.
|
||||||
If specific_components is None, use the default set (fontsmooth=rgb, xact, xact_x64, vcrun2022).
|
If specific_components is None, use the default set (fontsmooth=rgb, xact, xact_x64, vcrun2022).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wineprefix: Path to Wine prefix
|
||||||
|
game_var: Game name for logging
|
||||||
|
specific_components: Optional list of specific components to install
|
||||||
|
status_callback: Optional callback function(status_message: str) for progress updates
|
||||||
"""
|
"""
|
||||||
if not self.is_available():
|
if not self.is_available():
|
||||||
self.logger.error("Winetricks is not available")
|
self.logger.error("Winetricks is not available")
|
||||||
@@ -268,11 +274,18 @@ class WinetricksHandler:
|
|||||||
|
|
||||||
if not all_components:
|
if not all_components:
|
||||||
self.logger.info("No Wine components to install.")
|
self.logger.info("No Wine components to install.")
|
||||||
|
if status_callback:
|
||||||
|
status_callback("No Wine components to install")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Reorder components for proper installation sequence
|
# Reorder components for proper installation sequence
|
||||||
components_to_install = self._reorder_components_for_installation(all_components)
|
components_to_install = self._reorder_components_for_installation(all_components)
|
||||||
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}")
|
self.logger.info(f"WINEPREFIX: {wineprefix}, Game: {game_var}, Ordered Components: {components_to_install}")
|
||||||
|
|
||||||
|
# Show status with component list
|
||||||
|
if status_callback:
|
||||||
|
components_list = ', '.join(components_to_install)
|
||||||
|
status_callback(f"Installing Wine components: {components_list}")
|
||||||
|
|
||||||
# Check user preference for component installation method
|
# Check user preference for component installation method
|
||||||
from ..handlers.config_handler import ConfigHandler
|
from ..handlers.config_handler import ConfigHandler
|
||||||
@@ -290,7 +303,7 @@ class WinetricksHandler:
|
|||||||
# Choose installation method based on user preference
|
# Choose installation method based on user preference
|
||||||
if method == 'system_protontricks':
|
if method == 'system_protontricks':
|
||||||
self.logger.info("Using system protontricks for all components")
|
self.logger.info("Using system protontricks for all components")
|
||||||
return self._install_components_protontricks_only(components_to_install, wineprefix, game_var)
|
return self._install_components_protontricks_only(components_to_install, wineprefix, game_var, status_callback)
|
||||||
# else: method == 'winetricks' (default behavior continues below)
|
# else: method == 'winetricks' (default behavior continues below)
|
||||||
|
|
||||||
# Install all components together with winetricks (faster)
|
# Install all components together with winetricks (faster)
|
||||||
@@ -358,6 +371,9 @@ class WinetricksHandler:
|
|||||||
# Verify components were actually installed
|
# Verify components were actually installed
|
||||||
if self._verify_components_installed(wineprefix, components_to_install, env):
|
if self._verify_components_installed(wineprefix, components_to_install, env):
|
||||||
self.logger.info("Component verification successful - all components installed correctly.")
|
self.logger.info("Component verification successful - all components installed correctly.")
|
||||||
|
components_list = ', '.join(components_to_install)
|
||||||
|
if status_callback:
|
||||||
|
status_callback(f"Wine components installed and verified: {components_list}")
|
||||||
# Set Windows 10 mode after component installation (matches legacy script timing)
|
# Set Windows 10 mode after component installation (matches legacy script timing)
|
||||||
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
self._set_windows_10_mode(wineprefix, env.get('WINE', ''))
|
||||||
return True
|
return True
|
||||||
@@ -461,7 +477,7 @@ class WinetricksHandler:
|
|||||||
self.logger.warning("=" * 80)
|
self.logger.warning("=" * 80)
|
||||||
|
|
||||||
# Attempt fallback to protontricks
|
# Attempt fallback to protontricks
|
||||||
fallback_success = self._install_components_protontricks_only(components_to_install, wineprefix, game_var)
|
fallback_success = self._install_components_protontricks_only(components_to_install, wineprefix, game_var, status_callback)
|
||||||
|
|
||||||
if fallback_success:
|
if fallback_success:
|
||||||
self.logger.info("SUCCESS: Protontricks fallback succeeded where winetricks failed")
|
self.logger.info("SUCCESS: Protontricks fallback succeeded where winetricks failed")
|
||||||
@@ -698,7 +714,7 @@ class WinetricksHandler:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Error setting Windows 10 mode: {e}")
|
self.logger.warning(f"Error setting Windows 10 mode: {e}")
|
||||||
|
|
||||||
def _install_components_protontricks_only(self, components: list, wineprefix: str, game_var: str) -> bool:
|
def _install_components_protontricks_only(self, components: list, wineprefix: str, game_var: str, status_callback: Optional[Callable[[str], None]] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Install all components using protontricks only.
|
Install all components using protontricks only.
|
||||||
This matches the behavior of the original bash script.
|
This matches the behavior of the original bash script.
|
||||||
@@ -732,6 +748,9 @@ class WinetricksHandler:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Install all components using protontricks
|
# Install all components using protontricks
|
||||||
|
components_list = ', '.join(components)
|
||||||
|
if status_callback:
|
||||||
|
status_callback(f"Installing Wine components via protontricks: {components_list}")
|
||||||
success = protontricks_handler.install_wine_components(appid, game_var, components)
|
success = protontricks_handler.install_wine_components(appid, game_var, components)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
|
|||||||
@@ -150,8 +150,14 @@ def is_flatpak_steam() -> bool:
|
|||||||
stderr=subprocess.DEVNULL, # Suppress stderr to avoid error messages
|
stderr=subprocess.DEVNULL, # Suppress stderr to avoid error messages
|
||||||
text=True,
|
text=True,
|
||||||
timeout=5)
|
timeout=5)
|
||||||
if result.returncode == 0 and 'com.valvesoftware.Steam' in result.stdout:
|
if result.returncode == 0:
|
||||||
return True
|
# Check for exact match - "com.valvesoftware.Steam" as a whole word
|
||||||
|
# This prevents matching "com.valvesoftware.SteamLink" or similar
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
if parts and parts[0] == 'com.valvesoftware.Steam':
|
||||||
|
return True
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Error detecting Flatpak Steam: {e}")
|
logger.debug(f"Error detecting Flatpak Steam: {e}")
|
||||||
return False
|
return False
|
||||||
@@ -222,7 +228,8 @@ def _start_steam_nak_style(is_steamdeck_flag=False, is_flatpak_flag=False, env_o
|
|||||||
subprocess.Popen("steam", shell=True, env=env)
|
subprocess.Popen("steam", shell=True, env=env)
|
||||||
|
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
check_result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
# Use steamwebhelper for detection (actual Steam process, not steam-powerbuttond)
|
||||||
|
check_result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=env)
|
||||||
if check_result.returncode == 0:
|
if check_result.returncode == 0:
|
||||||
logger.info("NaK-style restart detected running Steam process.")
|
logger.info("NaK-style restart detected running Steam process.")
|
||||||
return True
|
return True
|
||||||
@@ -282,7 +289,8 @@ def start_steam(is_steamdeck_flag=None, is_flatpak_flag=None, env_override=None,
|
|||||||
subprocess.Popen(["flatpak", "run", "com.valvesoftware.Steam"],
|
subprocess.Popen(["flatpak", "run", "com.valvesoftware.Steam"],
|
||||||
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||||
time.sleep(7) # Give Flatpak more time to start
|
time.sleep(7) # Give Flatpak more time to start
|
||||||
check_result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
# For Flatpak Steam, check for the flatpak process, not steamwebhelper
|
||||||
|
check_result = subprocess.run(['pgrep', '-f', 'com.valvesoftware.Steam'], capture_output=True, timeout=10, env=env)
|
||||||
if check_result.returncode == 0:
|
if check_result.returncode == 0:
|
||||||
logger.info("Flatpak Steam started successfully")
|
logger.info("Flatpak Steam started successfully")
|
||||||
return True
|
return True
|
||||||
@@ -308,7 +316,8 @@ def start_steam(is_steamdeck_flag=None, is_flatpak_flag=None, env_override=None,
|
|||||||
if process is not None:
|
if process is not None:
|
||||||
logger.info(f"Initiated Steam start with {method_name}.")
|
logger.info(f"Initiated Steam start with {method_name}.")
|
||||||
time.sleep(5) # Wait 5 seconds as in existing logic
|
time.sleep(5) # Wait 5 seconds as in existing logic
|
||||||
check_result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=env)
|
# Use steamwebhelper for detection (actual Steam process, not steam-powerbuttond)
|
||||||
|
check_result = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=env)
|
||||||
if check_result.returncode == 0:
|
if check_result.returncode == 0:
|
||||||
logger.info(f"Steam process detected after using {method_name}. Proceeding to wait phase.")
|
logger.info(f"Steam process detected after using {method_name}. Proceeding to wait phase.")
|
||||||
return True
|
return True
|
||||||
@@ -367,7 +376,7 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
|||||||
try:
|
try:
|
||||||
report("Flatpak Steam detected - stopping via flatpak...")
|
report("Flatpak Steam detected - stopping via flatpak...")
|
||||||
subprocess.run(['flatpak', 'kill', 'com.valvesoftware.Steam'],
|
subprocess.run(['flatpak', 'kill', 'com.valvesoftware.Steam'],
|
||||||
timeout=15, check=False, capture_output=True, stderr=subprocess.DEVNULL, env=shutdown_env)
|
timeout=15, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, env=shutdown_env)
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"flatpak kill failed: {e}")
|
logger.debug(f"flatpak kill failed: {e}")
|
||||||
@@ -427,7 +436,8 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
|||||||
report("Failed to start Steam.")
|
report("Failed to start Steam.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Wait for Steam to fully initialize using existing logic
|
# 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")
|
report("Waiting for Steam to fully start")
|
||||||
logger.info("Waiting up to 2 minutes for Steam to fully initialize...")
|
logger.info("Waiting up to 2 minutes for Steam to fully initialize...")
|
||||||
max_startup_wait = 120
|
max_startup_wait = 120
|
||||||
@@ -436,7 +446,8 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
|||||||
|
|
||||||
while elapsed_wait < max_startup_wait:
|
while elapsed_wait < max_startup_wait:
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=start_env)
|
# 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 result.returncode == 0:
|
||||||
if not initial_wait_done:
|
if not initial_wait_done:
|
||||||
logger.info("Steam process detected. Waiting additional time for full initialization...")
|
logger.info("Steam process detected. Waiting additional time for full initialization...")
|
||||||
@@ -444,7 +455,7 @@ def robust_steam_restart(progress_callback: Optional[Callable[[str], None]] = No
|
|||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
elapsed_wait += 5
|
elapsed_wait += 5
|
||||||
if initial_wait_done and elapsed_wait >= 15:
|
if initial_wait_done and elapsed_wait >= 15:
|
||||||
final_check = subprocess.run(['pgrep', '-f', 'steam'], capture_output=True, timeout=10, env=start_env)
|
final_check = subprocess.run(['pgrep', '-f', 'steamwebhelper'], capture_output=True, timeout=10, env=start_env)
|
||||||
if final_check.returncode == 0:
|
if final_check.returncode == 0:
|
||||||
report("Steam started successfully.")
|
report("Steam started successfully.")
|
||||||
logger.info("Steam confirmed running after wait.")
|
logger.info("Steam confirmed running after wait.")
|
||||||
|
|||||||
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": {
|
"targets": {
|
||||||
".NETCoreApp,Version=v8.0": {},
|
".NETCoreApp,Version=v8.0": {},
|
||||||
".NETCoreApp,Version=v8.0/linux-x64": {
|
".NETCoreApp,Version=v8.0/linux-x64": {
|
||||||
"jackify-engine/0.4.0": {
|
"jackify-engine/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Markdig": "0.40.0",
|
"Markdig": "0.40.0",
|
||||||
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
||||||
@@ -22,16 +22,16 @@
|
|||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"System.CommandLine": "2.0.0-beta4.22272.1",
|
"System.CommandLine": "2.0.0-beta4.22272.1",
|
||||||
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
||||||
"Wabbajack.CLI.Builder": "0.4.0",
|
"Wabbajack.CLI.Builder": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Bethesda": "0.4.0",
|
"Wabbajack.Downloaders.Bethesda": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Dispatcher": "0.4.0",
|
"Wabbajack.Downloaders.Dispatcher": "0.4.1",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.4.0",
|
"Wabbajack.Hashing.xxHash64": "0.4.1",
|
||||||
"Wabbajack.Networking.Discord": "0.4.0",
|
"Wabbajack.Networking.Discord": "0.4.1",
|
||||||
"Wabbajack.Networking.GitHub": "0.4.0",
|
"Wabbajack.Networking.GitHub": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0",
|
"Wabbajack.Paths.IO": "0.4.1",
|
||||||
"Wabbajack.Server.Lib": "0.4.0",
|
"Wabbajack.Server.Lib": "0.4.1",
|
||||||
"Wabbajack.Services.OSIntegrated": "0.4.0",
|
"Wabbajack.Services.OSIntegrated": "0.4.1",
|
||||||
"Wabbajack.VFS": "0.4.0",
|
"Wabbajack.VFS": "0.4.1",
|
||||||
"MegaApiClient": "1.0.0.0",
|
"MegaApiClient": "1.0.0.0",
|
||||||
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.21"
|
"runtimepack.Microsoft.NETCore.App.Runtime.linux-x64": "8.0.21"
|
||||||
},
|
},
|
||||||
@@ -1781,7 +1781,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.CLI.Builder/0.4.0": {
|
"Wabbajack.CLI.Builder/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
"Microsoft.Extensions.Configuration.Json": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
@@ -1791,109 +1791,109 @@
|
|||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"System.CommandLine": "2.0.0-beta4.22272.1",
|
"System.CommandLine": "2.0.0-beta4.22272.1",
|
||||||
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
"System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1",
|
||||||
"Wabbajack.Paths": "0.4.0"
|
"Wabbajack.Paths": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.CLI.Builder.dll": {}
|
"Wabbajack.CLI.Builder.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Common/0.4.0": {
|
"Wabbajack.Common/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"System.Reactive": "6.0.1",
|
"System.Reactive": "6.0.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Networking.Http": "0.4.0",
|
"Wabbajack.Networking.Http": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0"
|
"Wabbajack.Paths.IO": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Common.dll": {}
|
"Wabbajack.Common.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Compiler/0.4.0": {
|
"Wabbajack.Compiler/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"F23.StringSimilarity": "6.0.0",
|
"F23.StringSimilarity": "6.0.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Downloaders.Dispatcher": "0.4.0",
|
"Wabbajack.Downloaders.Dispatcher": "0.4.1",
|
||||||
"Wabbajack.Installer": "0.4.0",
|
"Wabbajack.Installer": "0.4.1",
|
||||||
"Wabbajack.VFS": "0.4.0",
|
"Wabbajack.VFS": "0.4.1",
|
||||||
"ini-parser-netstandard": "2.5.2"
|
"ini-parser-netstandard": "2.5.2"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Compiler.dll": {}
|
"Wabbajack.Compiler.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Compression.BSA/0.4.0": {
|
"Wabbajack.Compression.BSA/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"K4os.Compression.LZ4.Streams": "1.3.8",
|
"K4os.Compression.LZ4.Streams": "1.3.8",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"SharpZipLib": "1.4.2",
|
"SharpZipLib": "1.4.2",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.DTOs": "0.4.0"
|
"Wabbajack.DTOs": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Compression.BSA.dll": {}
|
"Wabbajack.Compression.BSA.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Compression.Zip/0.4.0": {
|
"Wabbajack.Compression.Zip/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Wabbajack.IO.Async": "0.4.0"
|
"Wabbajack.IO.Async": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Compression.Zip.dll": {}
|
"Wabbajack.Compression.Zip.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Configuration/0.4.0": {
|
"Wabbajack.Configuration/0.4.1": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Configuration.dll": {}
|
"Wabbajack.Configuration.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Bethesda/0.4.0": {
|
"Wabbajack.Downloaders.Bethesda/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"LibAES-CTR": "1.1.0",
|
"LibAES-CTR": "1.1.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"SharpZipLib": "1.4.2",
|
"SharpZipLib": "1.4.2",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.BethesdaNet": "0.4.0"
|
"Wabbajack.Networking.BethesdaNet": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Bethesda.dll": {}
|
"Wabbajack.Downloaders.Bethesda.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Dispatcher/0.4.0": {
|
"Wabbajack.Downloaders.Dispatcher/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Downloaders.Bethesda": "0.4.0",
|
"Wabbajack.Downloaders.Bethesda": "0.4.1",
|
||||||
"Wabbajack.Downloaders.GameFile": "0.4.0",
|
"Wabbajack.Downloaders.GameFile": "0.4.1",
|
||||||
"Wabbajack.Downloaders.GoogleDrive": "0.4.0",
|
"Wabbajack.Downloaders.GoogleDrive": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Http": "0.4.0",
|
"Wabbajack.Downloaders.Http": "0.4.1",
|
||||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.4.0",
|
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Manual": "0.4.0",
|
"Wabbajack.Downloaders.Manual": "0.4.1",
|
||||||
"Wabbajack.Downloaders.MediaFire": "0.4.0",
|
"Wabbajack.Downloaders.MediaFire": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Mega": "0.4.0",
|
"Wabbajack.Downloaders.Mega": "0.4.1",
|
||||||
"Wabbajack.Downloaders.ModDB": "0.4.0",
|
"Wabbajack.Downloaders.ModDB": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Nexus": "0.4.0",
|
"Wabbajack.Downloaders.Nexus": "0.4.1",
|
||||||
"Wabbajack.Downloaders.VerificationCache": "0.4.0",
|
"Wabbajack.Downloaders.VerificationCache": "0.4.1",
|
||||||
"Wabbajack.Downloaders.WabbajackCDN": "0.4.0",
|
"Wabbajack.Downloaders.WabbajackCDN": "0.4.1",
|
||||||
"Wabbajack.Networking.WabbajackClientApi": "0.4.0"
|
"Wabbajack.Networking.WabbajackClientApi": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Dispatcher.dll": {}
|
"Wabbajack.Downloaders.Dispatcher.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.GameFile/0.4.0": {
|
"Wabbajack.Downloaders.GameFile/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"GameFinder.StoreHandlers.EADesktop": "4.5.0",
|
"GameFinder.StoreHandlers.EADesktop": "4.5.0",
|
||||||
"GameFinder.StoreHandlers.EGS": "4.5.0",
|
"GameFinder.StoreHandlers.EGS": "4.5.0",
|
||||||
@@ -1903,360 +1903,360 @@
|
|||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.VFS": "0.4.0"
|
"Wabbajack.VFS": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.GameFile.dll": {}
|
"Wabbajack.Downloaders.GameFile.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.GoogleDrive/0.4.0": {
|
"Wabbajack.Downloaders.GoogleDrive/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"HtmlAgilityPack": "1.11.72",
|
"HtmlAgilityPack": "1.11.72",
|
||||||
"Microsoft.AspNetCore.Http.Extensions": "2.3.0",
|
"Microsoft.AspNetCore.Http.Extensions": "2.3.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.Http": "0.4.0",
|
"Wabbajack.Networking.Http": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0"
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.GoogleDrive.dll": {}
|
"Wabbajack.Downloaders.GoogleDrive.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Http/0.4.0": {
|
"Wabbajack.Downloaders.Http/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.BethesdaNet": "0.4.0",
|
"Wabbajack.Networking.BethesdaNet": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0",
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0"
|
"Wabbajack.Paths.IO": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Http.dll": {}
|
"Wabbajack.Downloaders.Http.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Interfaces/0.4.0": {
|
"Wabbajack.Downloaders.Interfaces/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Compression.Zip": "0.4.0",
|
"Wabbajack.Compression.Zip": "0.4.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0"
|
"Wabbajack.Paths.IO": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Interfaces.dll": {}
|
"Wabbajack.Downloaders.Interfaces.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.0": {
|
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"F23.StringSimilarity": "6.0.0",
|
"F23.StringSimilarity": "6.0.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.Http": "0.4.0",
|
"Wabbajack.Networking.Http": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0"
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
|
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Manual/0.4.0": {
|
"Wabbajack.Downloaders.Manual/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0"
|
"Wabbajack.Downloaders.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Manual.dll": {}
|
"Wabbajack.Downloaders.Manual.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.MediaFire/0.4.0": {
|
"Wabbajack.Downloaders.MediaFire/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"HtmlAgilityPack": "1.11.72",
|
"HtmlAgilityPack": "1.11.72",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0"
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.MediaFire.dll": {}
|
"Wabbajack.Downloaders.MediaFire.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Mega/0.4.0": {
|
"Wabbajack.Downloaders.Mega/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0"
|
"Wabbajack.Paths.IO": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Mega.dll": {}
|
"Wabbajack.Downloaders.Mega.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.ModDB/0.4.0": {
|
"Wabbajack.Downloaders.ModDB/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"HtmlAgilityPack": "1.11.72",
|
"HtmlAgilityPack": "1.11.72",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.Http": "0.4.0",
|
"Wabbajack.Networking.Http": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0"
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.ModDB.dll": {}
|
"Wabbajack.Downloaders.ModDB.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Nexus/0.4.0": {
|
"Wabbajack.Downloaders.Nexus/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.4.0",
|
"Wabbajack.Hashing.xxHash64": "0.4.1",
|
||||||
"Wabbajack.Networking.Http": "0.4.0",
|
"Wabbajack.Networking.Http": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0",
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.NexusApi": "0.4.0",
|
"Wabbajack.Networking.NexusApi": "0.4.1",
|
||||||
"Wabbajack.Paths": "0.4.0"
|
"Wabbajack.Paths": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.Nexus.dll": {}
|
"Wabbajack.Downloaders.Nexus.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.VerificationCache/0.4.0": {
|
"Wabbajack.Downloaders.VerificationCache/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
|
"Stub.System.Data.SQLite.Core.NetStandard": "1.0.119",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0"
|
"Wabbajack.Paths.IO": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.VerificationCache.dll": {}
|
"Wabbajack.Downloaders.VerificationCache.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.WabbajackCDN/0.4.0": {
|
"Wabbajack.Downloaders.WabbajackCDN/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Microsoft.Toolkit.HighPerformance": "7.1.2",
|
"Microsoft.Toolkit.HighPerformance": "7.1.2",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.Http": "0.4.0",
|
"Wabbajack.Networking.Http": "0.4.1",
|
||||||
"Wabbajack.RateLimiter": "0.4.0"
|
"Wabbajack.RateLimiter": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
|
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.DTOs/0.4.0": {
|
"Wabbajack.DTOs/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.4.0",
|
"Wabbajack.Hashing.xxHash64": "0.4.1",
|
||||||
"Wabbajack.Paths": "0.4.0"
|
"Wabbajack.Paths": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.DTOs.dll": {}
|
"Wabbajack.DTOs.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.FileExtractor/0.4.0": {
|
"Wabbajack.FileExtractor/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"OMODFramework": "3.0.1",
|
"OMODFramework": "3.0.1",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Compression.BSA": "0.4.0",
|
"Wabbajack.Compression.BSA": "0.4.1",
|
||||||
"Wabbajack.Hashing.PHash": "0.4.0",
|
"Wabbajack.Hashing.PHash": "0.4.1",
|
||||||
"Wabbajack.Paths": "0.4.0"
|
"Wabbajack.Paths": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.FileExtractor.dll": {}
|
"Wabbajack.FileExtractor.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Hashing.PHash/0.4.0": {
|
"Wabbajack.Hashing.PHash/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"BCnEncoder.Net.ImageSharp": "1.1.1",
|
"BCnEncoder.Net.ImageSharp": "1.1.1",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Shipwreck.Phash": "0.5.0",
|
"Shipwreck.Phash": "0.5.0",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Paths": "0.4.0",
|
"Wabbajack.Paths": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0"
|
"Wabbajack.Paths.IO": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Hashing.PHash.dll": {}
|
"Wabbajack.Hashing.PHash.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Hashing.xxHash64/0.4.0": {
|
"Wabbajack.Hashing.xxHash64/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Wabbajack.Paths": "0.4.0",
|
"Wabbajack.Paths": "0.4.1",
|
||||||
"Wabbajack.RateLimiter": "0.4.0"
|
"Wabbajack.RateLimiter": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Hashing.xxHash64.dll": {}
|
"Wabbajack.Hashing.xxHash64.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Installer/0.4.0": {
|
"Wabbajack.Installer/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"Octopus.Octodiff": "2.0.548",
|
"Octopus.Octodiff": "2.0.548",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Dispatcher": "0.4.0",
|
"Wabbajack.Downloaders.Dispatcher": "0.4.1",
|
||||||
"Wabbajack.Downloaders.GameFile": "0.4.0",
|
"Wabbajack.Downloaders.GameFile": "0.4.1",
|
||||||
"Wabbajack.FileExtractor": "0.4.0",
|
"Wabbajack.FileExtractor": "0.4.1",
|
||||||
"Wabbajack.Networking.WabbajackClientApi": "0.4.0",
|
"Wabbajack.Networking.WabbajackClientApi": "0.4.1",
|
||||||
"Wabbajack.Paths": "0.4.0",
|
"Wabbajack.Paths": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0",
|
"Wabbajack.Paths.IO": "0.4.1",
|
||||||
"Wabbajack.VFS": "0.4.0",
|
"Wabbajack.VFS": "0.4.1",
|
||||||
"ini-parser-netstandard": "2.5.2"
|
"ini-parser-netstandard": "2.5.2"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Installer.dll": {}
|
"Wabbajack.Installer.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.IO.Async/0.4.0": {
|
"Wabbajack.IO.Async/0.4.1": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.IO.Async.dll": {}
|
"Wabbajack.IO.Async.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.BethesdaNet/0.4.0": {
|
"Wabbajack.Networking.BethesdaNet/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Networking.Http": "0.4.0",
|
"Wabbajack.Networking.Http": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0"
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.BethesdaNet.dll": {}
|
"Wabbajack.Networking.BethesdaNet.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Discord/0.4.0": {
|
"Wabbajack.Networking.Discord/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0"
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.Discord.dll": {}
|
"Wabbajack.Networking.Discord.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.GitHub/0.4.0": {
|
"Wabbajack.Networking.GitHub/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Octokit": "14.0.0",
|
"Octokit": "14.0.0",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0"
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.GitHub.dll": {}
|
"Wabbajack.Networking.GitHub.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Http/0.4.0": {
|
"Wabbajack.Networking.Http/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Http": "9.0.1",
|
"Microsoft.Extensions.Http": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging": "9.0.1",
|
"Microsoft.Extensions.Logging": "9.0.1",
|
||||||
"Wabbajack.Configuration": "0.4.0",
|
"Wabbajack.Configuration": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Interfaces": "0.4.0",
|
"Wabbajack.Downloaders.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.4.0",
|
"Wabbajack.Hashing.xxHash64": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0",
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Paths": "0.4.0",
|
"Wabbajack.Paths": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0"
|
"Wabbajack.Paths.IO": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.Http.dll": {}
|
"Wabbajack.Networking.Http.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Http.Interfaces/0.4.0": {
|
"Wabbajack.Networking.Http.Interfaces/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Wabbajack.Hashing.xxHash64": "0.4.0"
|
"Wabbajack.Hashing.xxHash64": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.Http.Interfaces.dll": {}
|
"Wabbajack.Networking.Http.Interfaces.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.NexusApi/0.4.0": {
|
"Wabbajack.Networking.NexusApi/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Networking.Http": "0.4.0",
|
"Wabbajack.Networking.Http": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0",
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Networking.WabbajackClientApi": "0.4.0"
|
"Wabbajack.Networking.WabbajackClientApi": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.NexusApi.dll": {}
|
"Wabbajack.Networking.NexusApi.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.WabbajackClientApi/0.4.0": {
|
"Wabbajack.Networking.WabbajackClientApi/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"Octokit": "14.0.0",
|
"Octokit": "14.0.0",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0",
|
"Wabbajack.Paths.IO": "0.4.1",
|
||||||
"Wabbajack.VFS.Interfaces": "0.4.0",
|
"Wabbajack.VFS.Interfaces": "0.4.1",
|
||||||
"YamlDotNet": "16.3.0"
|
"YamlDotNet": "16.3.0"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Networking.WabbajackClientApi.dll": {}
|
"Wabbajack.Networking.WabbajackClientApi.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Paths/0.4.0": {
|
"Wabbajack.Paths/0.4.1": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Paths.dll": {}
|
"Wabbajack.Paths.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Paths.IO/0.4.0": {
|
"Wabbajack.Paths.IO/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Wabbajack.Paths": "0.4.0",
|
"Wabbajack.Paths": "0.4.1",
|
||||||
"shortid": "4.0.0"
|
"shortid": "4.0.0"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Paths.IO.dll": {}
|
"Wabbajack.Paths.IO.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.RateLimiter/0.4.0": {
|
"Wabbajack.RateLimiter/0.4.1": {
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.RateLimiter.dll": {}
|
"Wabbajack.RateLimiter.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Server.Lib/0.4.0": {
|
"Wabbajack.Server.Lib/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"FluentFTP": "52.0.0",
|
"FluentFTP": "52.0.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
@@ -2264,58 +2264,58 @@
|
|||||||
"Nettle": "3.0.0",
|
"Nettle": "3.0.0",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.Networking.Http.Interfaces": "0.4.0",
|
"Wabbajack.Networking.Http.Interfaces": "0.4.1",
|
||||||
"Wabbajack.Services.OSIntegrated": "0.4.0"
|
"Wabbajack.Services.OSIntegrated": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Server.Lib.dll": {}
|
"Wabbajack.Server.Lib.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.Services.OSIntegrated/0.4.0": {
|
"Wabbajack.Services.OSIntegrated/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"DeviceId": "6.8.0",
|
"DeviceId": "6.8.0",
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Newtonsoft.Json": "13.0.3",
|
"Newtonsoft.Json": "13.0.3",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"Wabbajack.Compiler": "0.4.0",
|
"Wabbajack.Compiler": "0.4.1",
|
||||||
"Wabbajack.Downloaders.Dispatcher": "0.4.0",
|
"Wabbajack.Downloaders.Dispatcher": "0.4.1",
|
||||||
"Wabbajack.Installer": "0.4.0",
|
"Wabbajack.Installer": "0.4.1",
|
||||||
"Wabbajack.Networking.BethesdaNet": "0.4.0",
|
"Wabbajack.Networking.BethesdaNet": "0.4.1",
|
||||||
"Wabbajack.Networking.Discord": "0.4.0",
|
"Wabbajack.Networking.Discord": "0.4.1",
|
||||||
"Wabbajack.VFS": "0.4.0"
|
"Wabbajack.VFS": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.Services.OSIntegrated.dll": {}
|
"Wabbajack.Services.OSIntegrated.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.VFS/0.4.0": {
|
"Wabbajack.VFS/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
"Microsoft.Extensions.Logging.Abstractions": "9.0.1",
|
||||||
"SixLabors.ImageSharp": "3.1.6",
|
"SixLabors.ImageSharp": "3.1.6",
|
||||||
"System.Data.SQLite.Core": "1.0.119",
|
"System.Data.SQLite.Core": "1.0.119",
|
||||||
"Wabbajack.Common": "0.4.0",
|
"Wabbajack.Common": "0.4.1",
|
||||||
"Wabbajack.FileExtractor": "0.4.0",
|
"Wabbajack.FileExtractor": "0.4.1",
|
||||||
"Wabbajack.Hashing.PHash": "0.4.0",
|
"Wabbajack.Hashing.PHash": "0.4.1",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.4.0",
|
"Wabbajack.Hashing.xxHash64": "0.4.1",
|
||||||
"Wabbajack.Paths": "0.4.0",
|
"Wabbajack.Paths": "0.4.1",
|
||||||
"Wabbajack.Paths.IO": "0.4.0",
|
"Wabbajack.Paths.IO": "0.4.1",
|
||||||
"Wabbajack.VFS.Interfaces": "0.4.0"
|
"Wabbajack.VFS.Interfaces": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.VFS.dll": {}
|
"Wabbajack.VFS.dll": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Wabbajack.VFS.Interfaces/0.4.0": {
|
"Wabbajack.VFS.Interfaces/0.4.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection": "9.0.1",
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
|
||||||
"Wabbajack.DTOs": "0.4.0",
|
"Wabbajack.DTOs": "0.4.1",
|
||||||
"Wabbajack.Hashing.xxHash64": "0.4.0",
|
"Wabbajack.Hashing.xxHash64": "0.4.1",
|
||||||
"Wabbajack.Paths": "0.4.0"
|
"Wabbajack.Paths": "0.4.1"
|
||||||
},
|
},
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"Wabbajack.VFS.Interfaces.dll": {}
|
"Wabbajack.VFS.Interfaces.dll": {}
|
||||||
@@ -2332,7 +2332,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"libraries": {
|
"libraries": {
|
||||||
"jackify-engine/0.4.0": {
|
"jackify-engine/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
@@ -3021,202 +3021,202 @@
|
|||||||
"path": "yamldotnet/16.3.0",
|
"path": "yamldotnet/16.3.0",
|
||||||
"hashPath": "yamldotnet.16.3.0.nupkg.sha512"
|
"hashPath": "yamldotnet.16.3.0.nupkg.sha512"
|
||||||
},
|
},
|
||||||
"Wabbajack.CLI.Builder/0.4.0": {
|
"Wabbajack.CLI.Builder/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Common/0.4.0": {
|
"Wabbajack.Common/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Compiler/0.4.0": {
|
"Wabbajack.Compiler/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Compression.BSA/0.4.0": {
|
"Wabbajack.Compression.BSA/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Compression.Zip/0.4.0": {
|
"Wabbajack.Compression.Zip/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Configuration/0.4.0": {
|
"Wabbajack.Configuration/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Bethesda/0.4.0": {
|
"Wabbajack.Downloaders.Bethesda/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Dispatcher/0.4.0": {
|
"Wabbajack.Downloaders.Dispatcher/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.GameFile/0.4.0": {
|
"Wabbajack.Downloaders.GameFile/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.GoogleDrive/0.4.0": {
|
"Wabbajack.Downloaders.GoogleDrive/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Http/0.4.0": {
|
"Wabbajack.Downloaders.Http/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Interfaces/0.4.0": {
|
"Wabbajack.Downloaders.Interfaces/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.0": {
|
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Manual/0.4.0": {
|
"Wabbajack.Downloaders.Manual/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.MediaFire/0.4.0": {
|
"Wabbajack.Downloaders.MediaFire/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Mega/0.4.0": {
|
"Wabbajack.Downloaders.Mega/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.ModDB/0.4.0": {
|
"Wabbajack.Downloaders.ModDB/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.Nexus/0.4.0": {
|
"Wabbajack.Downloaders.Nexus/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.VerificationCache/0.4.0": {
|
"Wabbajack.Downloaders.VerificationCache/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Downloaders.WabbajackCDN/0.4.0": {
|
"Wabbajack.Downloaders.WabbajackCDN/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.DTOs/0.4.0": {
|
"Wabbajack.DTOs/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.FileExtractor/0.4.0": {
|
"Wabbajack.FileExtractor/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Hashing.PHash/0.4.0": {
|
"Wabbajack.Hashing.PHash/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Hashing.xxHash64/0.4.0": {
|
"Wabbajack.Hashing.xxHash64/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Installer/0.4.0": {
|
"Wabbajack.Installer/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.IO.Async/0.4.0": {
|
"Wabbajack.IO.Async/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.BethesdaNet/0.4.0": {
|
"Wabbajack.Networking.BethesdaNet/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Discord/0.4.0": {
|
"Wabbajack.Networking.Discord/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.GitHub/0.4.0": {
|
"Wabbajack.Networking.GitHub/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Http/0.4.0": {
|
"Wabbajack.Networking.Http/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.Http.Interfaces/0.4.0": {
|
"Wabbajack.Networking.Http.Interfaces/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.NexusApi/0.4.0": {
|
"Wabbajack.Networking.NexusApi/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Networking.WabbajackClientApi/0.4.0": {
|
"Wabbajack.Networking.WabbajackClientApi/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Paths/0.4.0": {
|
"Wabbajack.Paths/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Paths.IO/0.4.0": {
|
"Wabbajack.Paths.IO/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.RateLimiter/0.4.0": {
|
"Wabbajack.RateLimiter/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Server.Lib/0.4.0": {
|
"Wabbajack.Server.Lib/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.Services.OSIntegrated/0.4.0": {
|
"Wabbajack.Services.OSIntegrated/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.VFS/0.4.0": {
|
"Wabbajack.VFS/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
},
|
},
|
||||||
"Wabbajack.VFS.Interfaces/0.4.0": {
|
"Wabbajack.VFS.Interfaces/0.4.1": {
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"serviceable": false,
|
"serviceable": false,
|
||||||
"sha512": ""
|
"sha512": ""
|
||||||
@@ -3264,7 +3264,7 @@
|
|||||||
"any",
|
"any",
|
||||||
"base"
|
"base"
|
||||||
],
|
],
|
||||||
"fedora.42-x64": [
|
"fedora.43-x64": [
|
||||||
"linux-x64",
|
"linux-x64",
|
||||||
"linux",
|
"linux",
|
||||||
"unix-x64",
|
"unix-x64",
|
||||||
|
|||||||
Binary file not shown.
@@ -379,18 +379,44 @@ Python: {platform.python_version()}"""
|
|||||||
def open_github(self):
|
def open_github(self):
|
||||||
"""Open GitHub repository."""
|
"""Open GitHub repository."""
|
||||||
try:
|
try:
|
||||||
import webbrowser
|
self._open_url("https://github.com/Omni-guides/Jackify")
|
||||||
webbrowser.open("https://github.com/Omni-guides/Jackify")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error opening GitHub: {e}")
|
logger.error(f"Error opening GitHub: {e}")
|
||||||
|
|
||||||
def open_nexus(self):
|
def open_nexus(self):
|
||||||
"""Open Nexus Mods page."""
|
"""Open Nexus Mods page."""
|
||||||
try:
|
try:
|
||||||
import webbrowser
|
self._open_url("https://www.nexusmods.com/site/mods/1427")
|
||||||
webbrowser.open("https://www.nexusmods.com/site/mods/1427")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error opening Nexus: {e}")
|
logger.error(f"Error opening Nexus: {e}")
|
||||||
|
|
||||||
|
def _open_url(self, url: str):
|
||||||
|
"""Open URL with clean environment to avoid AppImage library conflicts."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
# Remove AppImage-specific environment variables
|
||||||
|
appimage_vars = [
|
||||||
|
'LD_LIBRARY_PATH',
|
||||||
|
'PYTHONPATH',
|
||||||
|
'PYTHONHOME',
|
||||||
|
'QT_PLUGIN_PATH',
|
||||||
|
'QML2_IMPORT_PATH',
|
||||||
|
]
|
||||||
|
|
||||||
|
if 'APPIMAGE' in env or 'APPDIR' in env:
|
||||||
|
for var in appimage_vars:
|
||||||
|
if var in env:
|
||||||
|
del env[var]
|
||||||
|
|
||||||
|
subprocess.Popen(
|
||||||
|
['xdg-open', url],
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True
|
||||||
|
)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Handle dialog close event."""
|
"""Handle dialog close event."""
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ src_dir = Path(__file__).parent.parent.parent.parent
|
|||||||
sys.path.insert(0, str(src_dir))
|
sys.path.insert(0, str(src_dir))
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QSizePolicy,
|
QSizePolicy, QScrollArea,
|
||||||
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
|
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
|
||||||
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox, QTabWidget, QRadioButton, QButtonGroup
|
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox, QTabWidget, QRadioButton, QButtonGroup
|
||||||
)
|
)
|
||||||
@@ -171,8 +171,8 @@ class SettingsDialog(QDialog):
|
|||||||
self._original_debug_mode = self.config_handler.get('debug_mode', False)
|
self._original_debug_mode = self.config_handler.get('debug_mode', False)
|
||||||
self.setWindowTitle("Settings")
|
self.setWindowTitle("Settings")
|
||||||
self.setModal(True)
|
self.setModal(True)
|
||||||
self.setMinimumWidth(650) # Reduced width for Steam Deck compatibility
|
self.setMinimumWidth(650)
|
||||||
self.setMaximumWidth(800) # Maximum width to prevent excessive stretching
|
self.setMaximumWidth(800)
|
||||||
self.setStyleSheet("QDialog { background-color: #232323; color: #eee; } QPushButton:hover { background-color: #333; }")
|
self.setStyleSheet("QDialog { background-color: #232323; color: #eee; } QPushButton:hover { background-color: #333; }")
|
||||||
|
|
||||||
main_layout = QVBoxLayout()
|
main_layout = QVBoxLayout()
|
||||||
@@ -420,69 +420,119 @@ class SettingsDialog(QDialog):
|
|||||||
|
|
||||||
resource_group = QGroupBox("Resource Limits")
|
resource_group = QGroupBox("Resource Limits")
|
||||||
resource_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
|
resource_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
|
||||||
resource_layout = QGridLayout()
|
resource_outer_layout = QVBoxLayout()
|
||||||
resource_group.setLayout(resource_layout)
|
resource_group.setLayout(resource_outer_layout)
|
||||||
resource_layout.setVerticalSpacing(4)
|
|
||||||
resource_layout.setHorizontalSpacing(8)
|
|
||||||
resource_layout.addWidget(self._bold_label("Resource"), 0, 0, 1, 1, Qt.AlignLeft)
|
|
||||||
resource_layout.addWidget(self._bold_label("Max Tasks"), 0, 1, 1, 1, Qt.AlignLeft)
|
|
||||||
self.resource_settings_path = os.path.expanduser("~/.config/jackify/resource_settings.json")
|
self.resource_settings_path = os.path.expanduser("~/.config/jackify/resource_settings.json")
|
||||||
self.resource_settings = self._load_json(self.resource_settings_path)
|
self.resource_settings = self._load_json(self.resource_settings_path)
|
||||||
self.resource_edits = {}
|
self.resource_edits = {}
|
||||||
resource_row_index = 0
|
|
||||||
for resource_row_index, (k, v) in enumerate(self.resource_settings.items(), start=1):
|
|
||||||
try:
|
|
||||||
# Create resource label
|
|
||||||
resource_layout.addWidget(QLabel(f"{k}:", parent=self), resource_row_index, 0, 1, 1, Qt.AlignLeft)
|
|
||||||
|
|
||||||
max_tasks_spin = QSpinBox()
|
|
||||||
max_tasks_spin.setMinimum(1)
|
|
||||||
max_tasks_spin.setMaximum(128)
|
|
||||||
max_tasks_spin.setValue(v.get('MaxTasks', 16))
|
|
||||||
max_tasks_spin.setToolTip("Maximum number of concurrent tasks for this resource.")
|
|
||||||
max_tasks_spin.setFixedWidth(160)
|
|
||||||
resource_layout.addWidget(max_tasks_spin, resource_row_index, 1)
|
|
||||||
|
|
||||||
# Store the widgets
|
|
||||||
self.resource_edits[k] = (None, max_tasks_spin)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[ERROR] Failed to create widgets for resource '{k}': {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If no resources exist, show helpful message
|
# If no resources exist, show helpful message
|
||||||
if not self.resource_edits:
|
if not self.resource_settings:
|
||||||
info_label = QLabel("Resource Limit settings will be generated once a modlist install action is performed")
|
info_label = QLabel("Resource Limit settings will be generated once a modlist install action is performed")
|
||||||
info_label.setStyleSheet("color: #aaa; font-style: italic; padding: 20px; font-size: 11pt;")
|
info_label.setStyleSheet("color: #aaa; font-style: italic; padding: 20px; font-size: 11pt;")
|
||||||
info_label.setWordWrap(True)
|
info_label.setWordWrap(True)
|
||||||
info_label.setAlignment(Qt.AlignCenter)
|
info_label.setAlignment(Qt.AlignCenter)
|
||||||
info_label.setMinimumHeight(60) # Ensure enough height to prevent cutoff
|
info_label.setMinimumHeight(60)
|
||||||
resource_layout.addWidget(info_label, 1, 0, 3, 2) # Span more rows for better space
|
resource_outer_layout.addWidget(info_label)
|
||||||
|
|
||||||
# Bandwidth limiter row (only show if Downloads resource exists)
|
|
||||||
if "Downloads" in self.resource_settings:
|
|
||||||
downloads_throughput_bytes = self.resource_settings["Downloads"].get("MaxThroughput", 0)
|
|
||||||
# Convert bytes/s to KB/s for display
|
|
||||||
downloads_throughput_kb = downloads_throughput_bytes // 1024 if downloads_throughput_bytes > 0 else 0
|
|
||||||
|
|
||||||
self.bandwidth_spin = QSpinBox()
|
|
||||||
self.bandwidth_spin.setMinimum(0)
|
|
||||||
self.bandwidth_spin.setMaximum(1000000)
|
|
||||||
self.bandwidth_spin.setValue(downloads_throughput_kb)
|
|
||||||
self.bandwidth_spin.setSuffix(" KB/s")
|
|
||||||
self.bandwidth_spin.setFixedWidth(160)
|
|
||||||
self.bandwidth_spin.setToolTip("Set the maximum download speed for modlist downloads. 0 = unlimited.")
|
|
||||||
bandwidth_note = QLabel("(0 = unlimited)")
|
|
||||||
bandwidth_note.setStyleSheet("color: #aaa; font-size: 10pt;")
|
|
||||||
# Create horizontal layout for bandwidth row
|
|
||||||
bandwidth_row = QHBoxLayout()
|
|
||||||
bandwidth_row.addWidget(self.bandwidth_spin)
|
|
||||||
bandwidth_row.addWidget(bandwidth_note)
|
|
||||||
bandwidth_row.addStretch() # Push to the left
|
|
||||||
|
|
||||||
resource_layout.addWidget(QLabel("Bandwidth Limit:", parent=self), resource_row_index+1, 0, 1, 1, Qt.AlignLeft)
|
|
||||||
resource_layout.addLayout(bandwidth_row, resource_row_index+1, 1)
|
|
||||||
else:
|
else:
|
||||||
self.bandwidth_spin = None # No bandwidth UI if Downloads resource doesn't exist
|
# Two-column layout for better space usage
|
||||||
|
# Use a single grid with proper column spacing
|
||||||
|
resource_grid = QGridLayout()
|
||||||
|
resource_grid.setVerticalSpacing(4)
|
||||||
|
resource_grid.setHorizontalSpacing(8)
|
||||||
|
resource_grid.setColumnMinimumWidth(2, 40) # Spacing between columns
|
||||||
|
|
||||||
|
# Headers for left column (columns 0-1)
|
||||||
|
resource_grid.addWidget(self._bold_label("Resource"), 0, 0, 1, 1, Qt.AlignLeft)
|
||||||
|
resource_grid.addWidget(self._bold_label("Max Tasks"), 0, 1, 1, 1, Qt.AlignLeft)
|
||||||
|
|
||||||
|
# Headers for right column (columns 3-4, skip column 2 for spacing)
|
||||||
|
resource_grid.addWidget(self._bold_label("Resource"), 0, 3, 1, 1, Qt.AlignLeft)
|
||||||
|
resource_grid.addWidget(self._bold_label("Max Tasks"), 0, 4, 1, 1, Qt.AlignLeft)
|
||||||
|
|
||||||
|
# Split resources between left and right columns (4 + 4)
|
||||||
|
resource_items = list(self.resource_settings.items())
|
||||||
|
|
||||||
|
# Find Bandwidth info from Downloads resource if it exists
|
||||||
|
bandwidth_kb = 0
|
||||||
|
if "Downloads" in self.resource_settings:
|
||||||
|
downloads_throughput_bytes = self.resource_settings["Downloads"].get("MaxThroughput", 0)
|
||||||
|
bandwidth_kb = downloads_throughput_bytes // 1024 if downloads_throughput_bytes > 0 else 0
|
||||||
|
|
||||||
|
# Left column gets first 4 resources (columns 0-1)
|
||||||
|
left_row = 1
|
||||||
|
for k, v in resource_items[:4]:
|
||||||
|
try:
|
||||||
|
resource_grid.addWidget(QLabel(f"{k}:", parent=self), left_row, 0, 1, 1, Qt.AlignLeft)
|
||||||
|
|
||||||
|
max_tasks_spin = QSpinBox()
|
||||||
|
max_tasks_spin.setMinimum(1)
|
||||||
|
max_tasks_spin.setMaximum(128)
|
||||||
|
max_tasks_spin.setValue(v.get('MaxTasks', 16))
|
||||||
|
max_tasks_spin.setToolTip("Maximum number of concurrent tasks for this resource.")
|
||||||
|
max_tasks_spin.setFixedWidth(100)
|
||||||
|
resource_grid.addWidget(max_tasks_spin, left_row, 1)
|
||||||
|
|
||||||
|
self.resource_edits[k] = (None, max_tasks_spin)
|
||||||
|
left_row += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to create widgets for resource '{k}': {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Right column gets next 4 resources (columns 3-4, skip column 2 for spacing)
|
||||||
|
right_row = 1
|
||||||
|
for k, v in resource_items[4:]:
|
||||||
|
try:
|
||||||
|
resource_grid.addWidget(QLabel(f"{k}:", parent=self), right_row, 3, 1, 1, Qt.AlignLeft)
|
||||||
|
|
||||||
|
max_tasks_spin = QSpinBox()
|
||||||
|
max_tasks_spin.setMinimum(1)
|
||||||
|
max_tasks_spin.setMaximum(128)
|
||||||
|
max_tasks_spin.setValue(v.get('MaxTasks', 16))
|
||||||
|
max_tasks_spin.setToolTip("Maximum number of concurrent tasks for this resource.")
|
||||||
|
max_tasks_spin.setFixedWidth(100)
|
||||||
|
resource_grid.addWidget(max_tasks_spin, right_row, 4)
|
||||||
|
|
||||||
|
self.resource_edits[k] = (None, max_tasks_spin)
|
||||||
|
right_row += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] Failed to create widgets for resource '{k}': {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add Bandwidth Limit at the bottom of right column
|
||||||
|
if "Downloads" in self.resource_settings:
|
||||||
|
resource_grid.addWidget(QLabel("Bandwidth Limit:", parent=self), right_row, 3, 1, 1, Qt.AlignLeft)
|
||||||
|
|
||||||
|
self.bandwidth_spin = QSpinBox()
|
||||||
|
self.bandwidth_spin.setMinimum(0)
|
||||||
|
self.bandwidth_spin.setMaximum(1000000)
|
||||||
|
self.bandwidth_spin.setValue(bandwidth_kb)
|
||||||
|
self.bandwidth_spin.setSuffix(" KB/s")
|
||||||
|
self.bandwidth_spin.setFixedWidth(100)
|
||||||
|
self.bandwidth_spin.setToolTip("Set the maximum download speed for modlist downloads. 0 = unlimited.")
|
||||||
|
|
||||||
|
# Create a layout for the spinbox and note
|
||||||
|
bandwidth_widget_layout = QHBoxLayout()
|
||||||
|
bandwidth_widget_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
bandwidth_widget_layout.addWidget(self.bandwidth_spin)
|
||||||
|
|
||||||
|
bandwidth_note = QLabel("(0 = unlimited)")
|
||||||
|
bandwidth_note.setStyleSheet("color: #aaa; font-size: 9pt;")
|
||||||
|
bandwidth_widget_layout.addWidget(bandwidth_note)
|
||||||
|
bandwidth_widget_layout.addStretch()
|
||||||
|
|
||||||
|
# Create container widget for the layout
|
||||||
|
bandwidth_container = QWidget()
|
||||||
|
bandwidth_container.setLayout(bandwidth_widget_layout)
|
||||||
|
resource_grid.addWidget(bandwidth_container, right_row, 4, 1, 1, Qt.AlignLeft)
|
||||||
|
else:
|
||||||
|
self.bandwidth_spin = None
|
||||||
|
|
||||||
|
# Add stretch column at the end to push content left
|
||||||
|
resource_grid.setColumnStretch(5, 1)
|
||||||
|
|
||||||
|
resource_outer_layout.addLayout(resource_grid)
|
||||||
|
|
||||||
advanced_layout.addWidget(resource_group)
|
advanced_layout.addWidget(resource_group)
|
||||||
|
|
||||||
@@ -1358,10 +1408,11 @@ class JackifyMainWindow(QMainWindow):
|
|||||||
bottom_bar_layout.addStretch(1)
|
bottom_bar_layout.addStretch(1)
|
||||||
|
|
||||||
# Ko-Fi support link (center)
|
# Ko-Fi support link (center)
|
||||||
kofi_link = QLabel('<a href="https://ko-fi.com/omni1" style="color:#72A5F2; text-decoration:none;">♥ Support on Ko-fi</a>')
|
kofi_link = QLabel('<a href="#" style="color:#72A5F2; text-decoration:none;">♥ Support on Ko-fi</a>')
|
||||||
kofi_link.setStyleSheet("color: #72A5F2; font-size: 13px;")
|
kofi_link.setStyleSheet("color: #72A5F2; font-size: 13px;")
|
||||||
kofi_link.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
kofi_link.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||||
kofi_link.setOpenExternalLinks(True)
|
kofi_link.setOpenExternalLinks(False)
|
||||||
|
kofi_link.linkActivated.connect(lambda: self._open_url("https://ko-fi.com/omni1"))
|
||||||
kofi_link.setToolTip("Support Jackify development")
|
kofi_link.setToolTip("Support Jackify development")
|
||||||
bottom_bar_layout.addWidget(kofi_link, alignment=Qt.AlignCenter)
|
bottom_bar_layout.addWidget(kofi_link, alignment=Qt.AlignCenter)
|
||||||
|
|
||||||
@@ -1629,6 +1680,35 @@ class JackifyMainWindow(QMainWindow):
|
|||||||
import traceback
|
import traceback
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def _open_url(self, url: str):
|
||||||
|
"""Open URL with clean environment to avoid AppImage library conflicts."""
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
# Remove AppImage-specific environment variables
|
||||||
|
appimage_vars = [
|
||||||
|
'LD_LIBRARY_PATH',
|
||||||
|
'PYTHONPATH',
|
||||||
|
'PYTHONHOME',
|
||||||
|
'QT_PLUGIN_PATH',
|
||||||
|
'QML2_IMPORT_PATH',
|
||||||
|
]
|
||||||
|
|
||||||
|
if 'APPIMAGE' in env or 'APPDIR' in env:
|
||||||
|
for var in appimage_vars:
|
||||||
|
if var in env:
|
||||||
|
del env[var]
|
||||||
|
|
||||||
|
subprocess.Popen(
|
||||||
|
['xdg-open', url],
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True
|
||||||
|
)
|
||||||
|
|
||||||
def _on_child_resize_request(self, mode: str):
|
def _on_child_resize_request(self, mode: str):
|
||||||
"""
|
"""
|
||||||
Handle child screen resize requests (expand/collapse console).
|
Handle child screen resize requests (expand/collapse console).
|
||||||
@@ -1761,6 +1841,20 @@ def resource_path(relative_path):
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point for the GUI application"""
|
"""Main entry point for the GUI application"""
|
||||||
|
# CRITICAL: Enable faulthandler for segfault debugging
|
||||||
|
# This will print Python stack traces on segfault
|
||||||
|
import faulthandler
|
||||||
|
import signal
|
||||||
|
# Enable faulthandler to both stderr and file
|
||||||
|
try:
|
||||||
|
log_dir = Path.home() / '.local' / 'share' / 'jackify' / 'logs'
|
||||||
|
log_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
trace_file = open(log_dir / 'segfault_trace.txt', 'w')
|
||||||
|
faulthandler.enable(file=trace_file, all_threads=True)
|
||||||
|
except Exception:
|
||||||
|
# Fallback to stderr only if file can't be opened
|
||||||
|
faulthandler.enable(all_threads=True)
|
||||||
|
|
||||||
# Check for CLI mode argument
|
# Check for CLI mode argument
|
||||||
if len(sys.argv) > 1 and '--cli' in sys.argv:
|
if len(sys.argv) > 1 and '--cli' in sys.argv:
|
||||||
# Launch CLI frontend instead of GUI
|
# Launch CLI frontend instead of GUI
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
InstallModlistScreen for Jackify GUI
|
InstallModlistScreen for Jackify GUI
|
||||||
"""
|
"""
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QApplication, QCheckBox, QStyledItemDelegate, QStyle, QTableWidget, QTableWidgetItem, QHeaderView
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QApplication, QCheckBox, QStyledItemDelegate, QStyle, QTableWidget, QTableWidgetItem, QHeaderView, QMainWindow
|
||||||
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QProcess, QMetaObject, QUrl
|
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QProcess, QMetaObject, QUrl
|
||||||
from PySide6.QtGui import QPixmap, QTextCursor, QColor, QPainter, QFont
|
from PySide6.QtGui import QPixmap, QTextCursor, QColor, QPainter, QFont
|
||||||
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
|
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
|
||||||
@@ -2533,10 +2533,34 @@ class InstallModlistScreen(QWidget):
|
|||||||
debug_print(f"DEBUG: - {fp.filename}: {fp.percent:.1f}% ({fp.operation.value})")
|
debug_print(f"DEBUG: - {fp.filename}: {fp.percent:.1f}% ({fp.operation.value})")
|
||||||
# Pass phase label to update header (e.g., "[Activity - Downloading]")
|
# Pass phase label to update header (e.g., "[Activity - Downloading]")
|
||||||
# Explicitly clear summary_info when showing file list
|
# Explicitly clear summary_info when showing file list
|
||||||
self.file_progress_list.update_files(progress_state.active_files, current_phase=phase_label, summary_info=None)
|
try:
|
||||||
|
self.file_progress_list.update_files(progress_state.active_files, current_phase=phase_label, summary_info=None)
|
||||||
|
except RuntimeError as e:
|
||||||
|
# Widget was deleted - ignore to prevent coredump
|
||||||
|
if "already deleted" in str(e):
|
||||||
|
if self.debug:
|
||||||
|
debug_print(f"DEBUG: Ignoring widget deletion error: {e}")
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
# Catch any other exceptions to prevent coredump
|
||||||
|
if self.debug:
|
||||||
|
debug_print(f"DEBUG: Error updating file progress list: {e}")
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).error(f"Error updating file progress list: {e}", exc_info=True)
|
||||||
else:
|
else:
|
||||||
# Show empty state so widget stays visible even when no files are active
|
# Show empty state so widget stays visible even when no files are active
|
||||||
self.file_progress_list.update_files([], current_phase=phase_label)
|
try:
|
||||||
|
self.file_progress_list.update_files([], current_phase=phase_label)
|
||||||
|
except RuntimeError as e:
|
||||||
|
# Widget was deleted - ignore to prevent coredump
|
||||||
|
if "already deleted" in str(e):
|
||||||
|
return
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
# Catch any other exceptions to prevent coredump
|
||||||
|
import logging
|
||||||
|
logging.getLogger(__name__).error(f"Error updating file progress list: {e}", exc_info=True)
|
||||||
|
|
||||||
def _on_show_details_toggled(self, checked: bool):
|
def _on_show_details_toggled(self, checked: bool):
|
||||||
"""R&D: Toggle console visibility (reuse TTW pattern)"""
|
"""R&D: Toggle console visibility (reuse TTW pattern)"""
|
||||||
@@ -2996,19 +3020,24 @@ class InstallModlistScreen(QWidget):
|
|||||||
normalized = text.lower()
|
normalized = text.lower()
|
||||||
total = max(1, self._post_install_total_steps)
|
total = max(1, self._post_install_total_steps)
|
||||||
matched = False
|
matched = False
|
||||||
|
matched_step = None
|
||||||
for idx, step in enumerate(self._post_install_sequence, start=1):
|
for idx, step in enumerate(self._post_install_sequence, start=1):
|
||||||
if any(keyword in normalized for keyword in step['keywords']):
|
if any(keyword in normalized for keyword in step['keywords']):
|
||||||
matched = True
|
matched = True
|
||||||
|
matched_step = idx
|
||||||
|
# Always update to the highest step we've seen (don't go backwards)
|
||||||
if idx >= self._post_install_current_step:
|
if idx >= self._post_install_current_step:
|
||||||
self._post_install_current_step = idx
|
self._post_install_current_step = idx
|
||||||
self._post_install_last_label = step['label']
|
self._post_install_last_label = step['label']
|
||||||
self._update_post_install_ui(step['label'], idx, total, detail=text)
|
# CRITICAL: Always use the current step (not the matched step) to ensure consistency
|
||||||
else:
|
# This prevents Activity window showing different step than progress banner
|
||||||
self._update_post_install_ui(step['label'], idx, total, detail=text)
|
self._update_post_install_ui(step['label'], self._post_install_current_step, total, detail=text)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# If no match but we have a current step, update with that step (not a new one)
|
||||||
if not matched and self._post_install_current_step > 0:
|
if not matched and self._post_install_current_step > 0:
|
||||||
label = self._post_install_last_label or "Post-installation"
|
label = self._post_install_last_label or "Post-installation"
|
||||||
|
# CRITICAL: Use _post_install_current_step (not a new step) to keep displays in sync
|
||||||
self._update_post_install_ui(label, self._post_install_current_step, total, detail=text)
|
self._update_post_install_ui(label, self._post_install_current_step, total, detail=text)
|
||||||
|
|
||||||
def _strip_timestamp_prefix(self, text: str) -> str:
|
def _strip_timestamp_prefix(self, text: str) -> str:
|
||||||
@@ -3020,11 +3049,14 @@ class InstallModlistScreen(QWidget):
|
|||||||
|
|
||||||
def _update_post_install_ui(self, label: str, step: int, total: int, detail: Optional[str] = None):
|
def _update_post_install_ui(self, label: str, step: int, total: int, detail: Optional[str] = None):
|
||||||
"""Update progress indicator + activity summary for post-install steps."""
|
"""Update progress indicator + activity summary for post-install steps."""
|
||||||
|
# Use the label as the primary display, but include step info in Activity window
|
||||||
display_label = label
|
display_label = label
|
||||||
if detail:
|
if detail:
|
||||||
# Remove timestamp prefix from detail messages
|
# Remove timestamp prefix from detail messages
|
||||||
clean_detail = self._strip_timestamp_prefix(detail.strip())
|
clean_detail = self._strip_timestamp_prefix(detail.strip())
|
||||||
if clean_detail:
|
if clean_detail:
|
||||||
|
# For Activity window, show the detail with step counter
|
||||||
|
# But keep label simple for progress banner
|
||||||
if clean_detail.lower().startswith(label.lower()):
|
if clean_detail.lower().startswith(label.lower()):
|
||||||
display_label = clean_detail
|
display_label = clean_detail
|
||||||
else:
|
else:
|
||||||
@@ -3032,18 +3064,24 @@ class InstallModlistScreen(QWidget):
|
|||||||
total = max(1, total)
|
total = max(1, total)
|
||||||
step_clamped = max(0, min(step, total))
|
step_clamped = max(0, min(step, total))
|
||||||
overall_percent = (step_clamped / total) * 100.0
|
overall_percent = (step_clamped / total) * 100.0
|
||||||
|
|
||||||
|
# CRITICAL: Ensure both displays use the SAME step counter
|
||||||
|
# Progress banner uses phase_step/phase_max_steps from progress_state
|
||||||
progress_state = InstallationProgress(
|
progress_state = InstallationProgress(
|
||||||
phase=InstallationPhase.FINALIZE,
|
phase=InstallationPhase.FINALIZE,
|
||||||
phase_name=display_label,
|
phase_name=display_label, # This will show in progress banner
|
||||||
phase_step=step_clamped,
|
phase_step=step_clamped, # This creates [step/total] in display_text
|
||||||
phase_max_steps=total,
|
phase_max_steps=total,
|
||||||
overall_percent=overall_percent
|
overall_percent=overall_percent
|
||||||
)
|
)
|
||||||
self.progress_indicator.update_progress(progress_state)
|
self.progress_indicator.update_progress(progress_state)
|
||||||
|
|
||||||
|
# Activity window uses summary_info with the SAME step counter
|
||||||
summary_info = {
|
summary_info = {
|
||||||
'current_step': step_clamped,
|
'current_step': step_clamped, # Must match phase_step above
|
||||||
'max_steps': total,
|
'max_steps': total, # Must match phase_max_steps above
|
||||||
}
|
}
|
||||||
|
# Use the same label for consistency
|
||||||
self.file_progress_list.update_files([], current_phase=display_label, summary_info=summary_info)
|
self.file_progress_list.update_files([], current_phase=display_label, summary_info=summary_info)
|
||||||
|
|
||||||
def _end_post_install_feedback(self, success: bool):
|
def _end_post_install_feedback(self, success: bool):
|
||||||
@@ -3263,6 +3301,52 @@ class InstallModlistScreen(QWidget):
|
|||||||
debug_print(f"DEBUG: steam -foreground failed: {e2}")
|
debug_print(f"DEBUG: steam -foreground failed: {e2}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
debug_print(f"DEBUG: Error ensuring Steam GUI visibility: {e}")
|
debug_print(f"DEBUG: Error ensuring Steam GUI visibility: {e}")
|
||||||
|
|
||||||
|
# CRITICAL: Bring Jackify window back to focus after Steam restart
|
||||||
|
# This ensures the user can continue with the installation workflow
|
||||||
|
debug_print("DEBUG: Bringing Jackify window back to focus")
|
||||||
|
try:
|
||||||
|
# Get the main window - use window() to get top-level widget, then find QMainWindow
|
||||||
|
top_level = self.window()
|
||||||
|
main_window = None
|
||||||
|
|
||||||
|
# Try to find QMainWindow in the widget hierarchy
|
||||||
|
if isinstance(top_level, QMainWindow):
|
||||||
|
main_window = top_level
|
||||||
|
else:
|
||||||
|
# Walk up the parent chain
|
||||||
|
current = self
|
||||||
|
while current:
|
||||||
|
if isinstance(current, QMainWindow):
|
||||||
|
main_window = current
|
||||||
|
break
|
||||||
|
current = current.parent()
|
||||||
|
|
||||||
|
# Last resort: use top-level widget
|
||||||
|
if not main_window and top_level:
|
||||||
|
main_window = top_level
|
||||||
|
|
||||||
|
if main_window:
|
||||||
|
# Restore window if minimized
|
||||||
|
if hasattr(main_window, 'isMinimized') and main_window.isMinimized():
|
||||||
|
main_window.showNormal()
|
||||||
|
|
||||||
|
# Bring to front and activate - use multiple methods for reliability
|
||||||
|
main_window.raise_()
|
||||||
|
main_window.activateWindow()
|
||||||
|
main_window.show()
|
||||||
|
|
||||||
|
# Force focus with multiple attempts (some window managers need this)
|
||||||
|
from PySide6.QtCore import QTimer
|
||||||
|
QTimer.singleShot(50, lambda: main_window.activateWindow() if main_window else None)
|
||||||
|
QTimer.singleShot(200, lambda: (main_window.raise_(), main_window.activateWindow()) if main_window else None)
|
||||||
|
QTimer.singleShot(500, lambda: main_window.activateWindow() if main_window else None)
|
||||||
|
|
||||||
|
debug_print(f"DEBUG: Jackify window brought back to focus (type: {type(main_window).__name__})")
|
||||||
|
else:
|
||||||
|
debug_print("DEBUG: Could not find main window to bring to focus")
|
||||||
|
except Exception as e:
|
||||||
|
debug_print(f"DEBUG: Error bringing Jackify to focus: {e}")
|
||||||
|
|
||||||
# Save context for later use in configuration
|
# Save context for later use in configuration
|
||||||
self._manual_steps_retry_count = 0
|
self._manual_steps_retry_count = 0
|
||||||
|
|||||||
@@ -472,13 +472,15 @@ class InstallTTWScreen(QWidget):
|
|||||||
# Check version against latest
|
# Check version against latest
|
||||||
update_available, installed_v, latest_v = ttw_installer_handler.is_ttw_installer_update_available()
|
update_available, installed_v, latest_v = ttw_installer_handler.is_ttw_installer_update_available()
|
||||||
if update_available:
|
if update_available:
|
||||||
self.ttw_installer_status.setText("Out of date")
|
version_text = f"Out of date (v{installed_v} → v{latest_v})" if installed_v and latest_v else "Out of date"
|
||||||
|
self.ttw_installer_status.setText(version_text)
|
||||||
self.ttw_installer_status.setStyleSheet("color: #f44336;")
|
self.ttw_installer_status.setStyleSheet("color: #f44336;")
|
||||||
self.ttw_installer_btn.setText("Update now")
|
self.ttw_installer_btn.setText("Update now")
|
||||||
self.ttw_installer_btn.setEnabled(True)
|
self.ttw_installer_btn.setEnabled(True)
|
||||||
self.ttw_installer_btn.setVisible(True)
|
self.ttw_installer_btn.setVisible(True)
|
||||||
else:
|
else:
|
||||||
self.ttw_installer_status.setText("Ready")
|
version_text = f"Ready (v{installed_v})" if installed_v else "Ready"
|
||||||
|
self.ttw_installer_status.setText(version_text)
|
||||||
self.ttw_installer_status.setStyleSheet("color: #3fd0ea;")
|
self.ttw_installer_status.setStyleSheet("color: #3fd0ea;")
|
||||||
self.ttw_installer_btn.setText("Update now")
|
self.ttw_installer_btn.setText("Update now")
|
||||||
self.ttw_installer_btn.setEnabled(False) # Greyed out when ready
|
self.ttw_installer_btn.setEnabled(False) # Greyed out when ready
|
||||||
@@ -1418,8 +1420,11 @@ class InstallTTWScreen(QWidget):
|
|||||||
is_warning = 'warning:' in lower_cleaned
|
is_warning = 'warning:' in lower_cleaned
|
||||||
is_milestone = any(kw in lower_cleaned for kw in ['===', 'complete', 'finished', 'validation', 'configuration valid'])
|
is_milestone = any(kw in lower_cleaned for kw in ['===', 'complete', 'finished', 'validation', 'configuration valid'])
|
||||||
is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx'])
|
is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx'])
|
||||||
|
|
||||||
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op)
|
# Filter out meaningless standalone messages (just "OK", etc.)
|
||||||
|
is_noise = cleaned.strip().upper() in ['OK', 'OK.', 'OK!', 'DONE', 'DONE.', 'SUCCESS', 'SUCCESS.']
|
||||||
|
|
||||||
|
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op and not is_noise)
|
||||||
|
|
||||||
if should_show:
|
if should_show:
|
||||||
if is_error or is_warning:
|
if is_error or is_warning:
|
||||||
@@ -1550,7 +1555,10 @@ class InstallTTWScreen(QWidget):
|
|||||||
is_milestone = any(kw in lower_cleaned for kw in ['===', 'complete', 'finished', 'validation', 'configuration valid'])
|
is_milestone = any(kw in lower_cleaned for kw in ['===', 'complete', 'finished', 'validation', 'configuration valid'])
|
||||||
is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx'])
|
is_file_op = any(ext in lower_cleaned for ext in ['.ogg', '.mp3', '.bsa', '.dds', '.nif', '.kf', '.hkx'])
|
||||||
|
|
||||||
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op)
|
# Filter out meaningless standalone messages (just "OK", etc.)
|
||||||
|
is_noise = cleaned.strip().upper() in ['OK', 'OK.', 'OK!', 'DONE', 'DONE.', 'SUCCESS', 'SUCCESS.']
|
||||||
|
|
||||||
|
should_show = (is_error or is_warning or is_milestone) or (self.show_details_checkbox.isChecked() and not is_file_op and not is_noise)
|
||||||
|
|
||||||
if should_show:
|
if should_show:
|
||||||
# Direct console append - no recursion, no complex processing
|
# Direct console append - no recursion, no complex processing
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from PySide6.QtWidgets import (
|
|||||||
QFrame, QSizePolicy, QDialog, QTextEdit, QTextBrowser, QMessageBox, QListWidget
|
QFrame, QSizePolicy, QDialog, QTextEdit, QTextBrowser, QMessageBox, QListWidget
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt, Signal, QSize, QThread, QUrl, QTimer, QObject
|
from PySide6.QtCore import Qt, Signal, QSize, QThread, QUrl, QTimer, QObject
|
||||||
from PySide6.QtGui import QPixmap, QFont, QDesktopServices, QPainter, QColor, QTextOption, QPalette
|
from PySide6.QtGui import QPixmap, QFont, QPainter, QColor, QTextOption, QPalette
|
||||||
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional, Dict
|
from typing import List, Optional, Dict
|
||||||
@@ -536,7 +536,7 @@ class ModlistDetailDialog(QDialog):
|
|||||||
background: #3C45A5;
|
background: #3C45A5;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
discord_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(self.metadata.links.discordURL)))
|
discord_btn.clicked.connect(lambda: self._open_url(self.metadata.links.discordURL))
|
||||||
links_layout.addWidget(discord_btn)
|
links_layout.addWidget(discord_btn)
|
||||||
|
|
||||||
if self.metadata.links.websiteURL:
|
if self.metadata.links.websiteURL:
|
||||||
@@ -558,7 +558,7 @@ class ModlistDetailDialog(QDialog):
|
|||||||
background: #2a2a2a;
|
background: #2a2a2a;
|
||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
website_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(self.metadata.links.websiteURL)))
|
website_btn.clicked.connect(lambda: self._open_url(self.metadata.links.websiteURL))
|
||||||
links_layout.addWidget(website_btn)
|
links_layout.addWidget(website_btn)
|
||||||
|
|
||||||
if self.metadata.links.readme:
|
if self.metadata.links.readme:
|
||||||
@@ -581,7 +581,7 @@ class ModlistDetailDialog(QDialog):
|
|||||||
}
|
}
|
||||||
""")
|
""")
|
||||||
readme_url = self._convert_raw_github_url(self.metadata.links.readme)
|
readme_url = self._convert_raw_github_url(self.metadata.links.readme)
|
||||||
readme_btn.clicked.connect(lambda: QDesktopServices.openUrl(QUrl(readme_url)))
|
readme_btn.clicked.connect(lambda: self._open_url(readme_url))
|
||||||
links_layout.addWidget(readme_btn)
|
links_layout.addWidget(readme_btn)
|
||||||
|
|
||||||
bottom_bar.addLayout(links_layout)
|
bottom_bar.addLayout(links_layout)
|
||||||
@@ -732,6 +732,35 @@ class ModlistDetailDialog(QDialog):
|
|||||||
self.install_requested.emit(self.metadata)
|
self.install_requested.emit(self.metadata)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
def _open_url(self, url: str):
|
||||||
|
"""Open URL with clean environment to avoid AppImage library conflicts."""
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
|
||||||
|
# Remove AppImage-specific environment variables
|
||||||
|
appimage_vars = [
|
||||||
|
'LD_LIBRARY_PATH',
|
||||||
|
'PYTHONPATH',
|
||||||
|
'PYTHONHOME',
|
||||||
|
'QT_PLUGIN_PATH',
|
||||||
|
'QML2_IMPORT_PATH',
|
||||||
|
]
|
||||||
|
|
||||||
|
if 'APPIMAGE' in env or 'APPDIR' in env:
|
||||||
|
for var in appimage_vars:
|
||||||
|
if var in env:
|
||||||
|
del env[var]
|
||||||
|
|
||||||
|
subprocess.Popen(
|
||||||
|
['xdg-open', url],
|
||||||
|
env=env,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
start_new_session=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModlistGalleryDialog(QDialog):
|
class ModlistGalleryDialog(QDialog):
|
||||||
"""Enhanced modlist gallery dialog with visual browsing"""
|
"""Enhanced modlist gallery dialog with visual browsing"""
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ class FileProgressItem(QWidget):
|
|||||||
self.file_progress = file_progress
|
self.file_progress = file_progress
|
||||||
self._target_percent = file_progress.percent # Target value for smooth animation
|
self._target_percent = file_progress.percent # Target value for smooth animation
|
||||||
self._current_display_percent = file_progress.percent # Currently displayed value
|
self._current_display_percent = file_progress.percent # Currently displayed value
|
||||||
|
self._spinner_position = 0 # For custom indeterminate spinner animation (0-200 range for smooth wraparound)
|
||||||
|
self._is_indeterminate = False # Track if we're in indeterminate mode
|
||||||
self._animation_timer = QTimer(self)
|
self._animation_timer = QTimer(self)
|
||||||
self._animation_timer.timeout.connect(self._animate_progress)
|
self._animation_timer.timeout.connect(self._animate_progress)
|
||||||
self._animation_timer.setInterval(16) # ~60fps for smooth animation
|
self._animation_timer.setInterval(16) # ~60fps for smooth animation
|
||||||
@@ -239,10 +241,32 @@ class FileProgressItem(QWidget):
|
|||||||
self.progress_bar.setRange(0, 100)
|
self.progress_bar.setRange(0, 100)
|
||||||
# Progress bar value will be updated by animation timer
|
# Progress bar value will be updated by animation timer
|
||||||
else:
|
else:
|
||||||
# Indeterminate if no max - use Qt's built-in smooth animation
|
# No max for summary - use custom animated spinner
|
||||||
|
self._is_indeterminate = True
|
||||||
self.percent_label.setText("")
|
self.percent_label.setText("")
|
||||||
self.speed_label.setText("")
|
self.speed_label.setText("")
|
||||||
self.progress_bar.setRange(0, 0) # Qt handles animation smoothly
|
self.progress_bar.setRange(0, 100) # Use determinate range for custom animation
|
||||||
|
if not self._animation_timer.isActive():
|
||||||
|
self._animation_timer.start()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if this is a queued item (not yet started)
|
||||||
|
# Queued items have total_size > 0 but percent == 0, current_size == 0, speed <= 0
|
||||||
|
is_queued = (
|
||||||
|
self.file_progress.total_size > 0 and
|
||||||
|
self.file_progress.percent == 0 and
|
||||||
|
self.file_progress.current_size == 0 and
|
||||||
|
self.file_progress.speed <= 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if is_queued:
|
||||||
|
# Queued download - show "Queued" text with empty progress bar
|
||||||
|
self._is_indeterminate = False
|
||||||
|
self._animation_timer.stop()
|
||||||
|
self.percent_label.setText("Queued")
|
||||||
|
self.speed_label.setText("")
|
||||||
|
self.progress_bar.setRange(0, 100)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if we have meaningful progress data
|
# Check if we have meaningful progress data
|
||||||
@@ -253,49 +277,69 @@ class FileProgressItem(QWidget):
|
|||||||
(self.file_progress.speed > 0 and self.file_progress.percent >= 0)
|
(self.file_progress.speed > 0 and self.file_progress.percent >= 0)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use determinate mode if we have actual progress data, otherwise use Qt's indeterminate mode
|
# Use determinate mode if we have actual progress data, otherwise use custom animated spinner
|
||||||
if has_meaningful_progress:
|
if has_meaningful_progress:
|
||||||
|
# Normal progress mode
|
||||||
|
self._is_indeterminate = False
|
||||||
# Update target for smooth animation
|
# Update target for smooth animation
|
||||||
self._target_percent = max(0, self.file_progress.percent)
|
self._target_percent = max(0, self.file_progress.percent)
|
||||||
|
|
||||||
# Start animation timer if not already running
|
# Start animation timer if not already running
|
||||||
if not self._animation_timer.isActive():
|
if not self._animation_timer.isActive():
|
||||||
self._animation_timer.start()
|
self._animation_timer.start()
|
||||||
|
|
||||||
# Update speed label immediately (doesn't need animation)
|
# Update speed label immediately (doesn't need animation)
|
||||||
self.speed_label.setText(self.file_progress.speed_display)
|
self.speed_label.setText(self.file_progress.speed_display)
|
||||||
self.progress_bar.setRange(0, 100)
|
self.progress_bar.setRange(0, 100)
|
||||||
# Progress bar value will be updated by animation timer
|
# Progress bar value will be updated by animation timer
|
||||||
else:
|
else:
|
||||||
# No progress data (e.g., texture conversions) - Qt's indeterminate mode
|
# No progress data (e.g., texture conversions, BSA building) - use custom animated spinner
|
||||||
self._animation_timer.stop() # Stop animation for indeterminate items
|
self._is_indeterminate = True
|
||||||
self.percent_label.setText("") # No percentage
|
self.percent_label.setText("") # Clear percent label
|
||||||
self.speed_label.setText("") # No speed
|
self.speed_label.setText("") # No speed
|
||||||
self.progress_bar.setRange(0, 0) # Qt handles smooth indeterminate animation
|
self.progress_bar.setRange(0, 100) # Use determinate range for custom animation
|
||||||
|
# Start animation timer for custom spinner
|
||||||
|
if not self._animation_timer.isActive():
|
||||||
|
self._animation_timer.start()
|
||||||
|
|
||||||
def _animate_progress(self):
|
def _animate_progress(self):
|
||||||
"""Smoothly animate progress bar from current to target value."""
|
"""Smoothly animate progress bar from current to target value, or animate spinner."""
|
||||||
# Calculate difference
|
if self._is_indeterminate:
|
||||||
diff = self._target_percent - self._current_display_percent
|
# Custom indeterminate spinner animation
|
||||||
|
# Use a bouncing/pulsing effect: position moves 0-100-0 smoothly
|
||||||
# If very close, snap to target and stop animation
|
# Increment by 4 units per frame for fast animation (full cycle in ~0.8s at 60fps)
|
||||||
if abs(diff) < 0.1:
|
self._spinner_position = (self._spinner_position + 4) % 200
|
||||||
self._current_display_percent = self._target_percent
|
|
||||||
self._animation_timer.stop()
|
# Create bouncing effect: 0->100->0
|
||||||
|
if self._spinner_position < 100:
|
||||||
|
display_value = self._spinner_position
|
||||||
|
else:
|
||||||
|
display_value = 200 - self._spinner_position
|
||||||
|
|
||||||
|
self.progress_bar.setValue(display_value)
|
||||||
else:
|
else:
|
||||||
# Smooth interpolation (ease-out for natural feel)
|
# Normal progress animation
|
||||||
# Move 20% of remaining distance per frame (~60fps = smooth)
|
# Calculate difference
|
||||||
self._current_display_percent += diff * 0.2
|
diff = self._target_percent - self._current_display_percent
|
||||||
|
|
||||||
# Update display
|
# If very close, snap to target and stop animation
|
||||||
display_percent = max(0, min(100, self._current_display_percent))
|
if abs(diff) < 0.1:
|
||||||
self.progress_bar.setValue(int(display_percent))
|
self._current_display_percent = self._target_percent
|
||||||
|
self._animation_timer.stop()
|
||||||
# Update percentage label
|
else:
|
||||||
if self.file_progress.percent > 0:
|
# Smooth interpolation (ease-out for natural feel)
|
||||||
self.percent_label.setText(f"{display_percent:.0f}%")
|
# Move 20% of remaining distance per frame (~60fps = smooth)
|
||||||
else:
|
self._current_display_percent += diff * 0.2
|
||||||
self.percent_label.setText("")
|
|
||||||
|
# Update display
|
||||||
|
display_percent = max(0, min(100, self._current_display_percent))
|
||||||
|
self.progress_bar.setValue(int(display_percent))
|
||||||
|
|
||||||
|
# Update percentage label
|
||||||
|
if self.file_progress.percent > 0:
|
||||||
|
self.percent_label.setText(f"{display_percent:.0f}%")
|
||||||
|
else:
|
||||||
|
self.percent_label.setText("")
|
||||||
|
|
||||||
def update_progress(self, file_progress: FileProgress):
|
def update_progress(self, file_progress: FileProgress):
|
||||||
"""Update with new progress data."""
|
"""Update with new progress data."""
|
||||||
@@ -558,30 +602,35 @@ class FileProgressList(QWidget):
|
|||||||
item_key = file_progress.filename
|
item_key = file_progress.filename
|
||||||
|
|
||||||
if item_key in self._file_items:
|
if item_key in self._file_items:
|
||||||
# Update existing - ensure it's in the right position
|
# Update existing widget - DO NOT reorder items (causes segfaults)
|
||||||
|
# Reordering with takeItem/insertItem can delete widgets and cause crashes
|
||||||
|
# Order is less important than stability - just update the widget in place
|
||||||
item_widget = self._file_items[item_key]
|
item_widget = self._file_items[item_key]
|
||||||
item_widget.update_progress(file_progress)
|
# CRITICAL: Check widget is still valid before updating
|
||||||
|
if shiboken6.isValid(item_widget):
|
||||||
# Find the item in the list and move it if needed
|
try:
|
||||||
for i in range(self.list_widget.count()):
|
item_widget.update_progress(file_progress)
|
||||||
item = self.list_widget.item(i)
|
except RuntimeError:
|
||||||
if item and item.data(Qt.UserRole) == item_key:
|
# Widget was deleted - remove from dict and create new one below
|
||||||
# Item is at position i, should be at position idx
|
del self._file_items[item_key]
|
||||||
if i != idx:
|
# Fall through to create new widget
|
||||||
# Take item from current position and insert at correct position
|
else:
|
||||||
taken_item = self.list_widget.takeItem(i)
|
# Update successful - skip creating new widget
|
||||||
self.list_widget.insertItem(idx, taken_item)
|
continue
|
||||||
self.list_widget.setItemWidget(taken_item, item_widget)
|
else:
|
||||||
break
|
# Widget invalid - remove from dict and create new one
|
||||||
else:
|
del self._file_items[item_key]
|
||||||
# Add new - insert at specific position (idx) to maintain order
|
# Fall through to create new widget
|
||||||
item_widget = FileProgressItem(file_progress)
|
# Create new widget (either because it didn't exist or was invalid)
|
||||||
list_item = QListWidgetItem()
|
# CRITICAL: Use addItem instead of insertItem to avoid position conflicts
|
||||||
list_item.setSizeHint(item_widget.sizeHint())
|
# Order is less important than stability - addItem is safer than insertItem
|
||||||
list_item.setData(Qt.UserRole, item_key) # Use stable key
|
item_widget = FileProgressItem(file_progress)
|
||||||
self.list_widget.insertItem(idx, list_item) # Insert at specific position
|
list_item = QListWidgetItem()
|
||||||
self.list_widget.setItemWidget(list_item, item_widget)
|
list_item.setSizeHint(item_widget.sizeHint())
|
||||||
self._file_items[item_key] = item_widget
|
list_item.setData(Qt.UserRole, item_key) # Use stable key
|
||||||
|
self.list_widget.addItem(list_item) # Use addItem for safety (avoids segfaults)
|
||||||
|
self.list_widget.setItemWidget(list_item, item_widget)
|
||||||
|
self._file_items[item_key] = item_widget
|
||||||
|
|
||||||
# Update last phase tracker
|
# Update last phase tracker
|
||||||
if current_phase:
|
if current_phase:
|
||||||
|
|||||||
@@ -117,8 +117,9 @@ class OverallProgressIndicator(QWidget):
|
|||||||
from jackify.shared.progress_models import InstallationPhase
|
from jackify.shared.progress_models import InstallationPhase
|
||||||
is_bsa_building = progress.get_phase_label() == "Building BSAs"
|
is_bsa_building = progress.get_phase_label() == "Building BSAs"
|
||||||
|
|
||||||
# For install/extract/BSA building phases, prefer step-based progress (more accurate)
|
# For install/extract/download/BSA building phases, prefer step-based progress (more accurate)
|
||||||
if progress.phase in (InstallationPhase.INSTALL, InstallationPhase.EXTRACT) or is_bsa_building:
|
# This prevents carrying over 100% from previous phases (e.g., .wabbajack download)
|
||||||
|
if progress.phase in (InstallationPhase.INSTALL, InstallationPhase.EXTRACT, InstallationPhase.DOWNLOAD) or is_bsa_building:
|
||||||
if progress.phase_max_steps > 0:
|
if progress.phase_max_steps > 0:
|
||||||
display_percent = (progress.phase_step / progress.phase_max_steps) * 100.0
|
display_percent = (progress.phase_step / progress.phase_max_steps) * 100.0
|
||||||
elif progress.data_total > 0 and progress.data_processed > 0:
|
elif progress.data_total > 0 and progress.data_processed > 0:
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class InstallationProgress:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_overall_speed_display(self) -> str:
|
def get_overall_speed_display(self) -> str:
|
||||||
"""Get overall speed display from speeds dict or sum of active files."""
|
"""Get overall speed display from aggregate speeds reported by engine."""
|
||||||
def _fresh_speed(op_key: str) -> float:
|
def _fresh_speed(op_key: str) -> float:
|
||||||
"""Return speed if recently updated, else 0."""
|
"""Return speed if recently updated, else 0."""
|
||||||
if op_key not in self.speeds:
|
if op_key not in self.speeds:
|
||||||
@@ -134,8 +134,14 @@ class InstallationProgress:
|
|||||||
if time.time() - updated_at > 2.0:
|
if time.time() - updated_at > 2.0:
|
||||||
return 0.0
|
return 0.0
|
||||||
return max(0.0, self.speeds.get(op_key, 0.0))
|
return max(0.0, self.speeds.get(op_key, 0.0))
|
||||||
|
|
||||||
# Prefer aggregate speeds that match the current phase
|
# CRITICAL FIX: Use aggregate speeds from engine status lines
|
||||||
|
# The engine reports accurate total speeds in lines like:
|
||||||
|
# "[00:00:10] Downloading Mod Archives (17/214) - 6.8MB/s"
|
||||||
|
# These aggregate speeds are stored in self.speeds dict and are the source of truth
|
||||||
|
# DO NOT sum individual file speeds - that inflates the total incorrectly
|
||||||
|
|
||||||
|
# Try to get speed for current phase first
|
||||||
phase_operation_map = {
|
phase_operation_map = {
|
||||||
InstallationPhase.DOWNLOAD: 'download',
|
InstallationPhase.DOWNLOAD: 'download',
|
||||||
InstallationPhase.EXTRACT: 'extract',
|
InstallationPhase.EXTRACT: 'extract',
|
||||||
@@ -147,22 +153,13 @@ class InstallationProgress:
|
|||||||
op_speed = _fresh_speed(active_op)
|
op_speed = _fresh_speed(active_op)
|
||||||
if op_speed > 0:
|
if op_speed > 0:
|
||||||
return FileProgress._format_bytes(int(op_speed)) + "/s"
|
return FileProgress._format_bytes(int(op_speed)) + "/s"
|
||||||
|
|
||||||
# Otherwise check other operations in priority order
|
# Otherwise check other operations in priority order
|
||||||
for op_key in ['download', 'extract', 'validate', 'install']:
|
for op_key in ['download', 'extract', 'validate', 'install']:
|
||||||
op_speed = _fresh_speed(op_key)
|
op_speed = _fresh_speed(op_key)
|
||||||
if op_speed > 0:
|
if op_speed > 0:
|
||||||
return FileProgress._format_bytes(int(op_speed)) + "/s"
|
return FileProgress._format_bytes(int(op_speed)) + "/s"
|
||||||
|
|
||||||
# If engine didn't report aggregate speed, fall back to summing active file speeds
|
|
||||||
if self.active_files:
|
|
||||||
total_speed = sum(
|
|
||||||
fp.speed
|
|
||||||
for fp in self.active_files
|
|
||||||
if fp.speed > 0 and not fp.is_complete
|
|
||||||
)
|
|
||||||
if total_speed > 0:
|
|
||||||
return FileProgress._format_bytes(int(total_speed)) + "/s"
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_phase_label(self) -> str:
|
def get_phase_label(self) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user