Sync from development - prepare for v0.1.2

This commit is contained in:
Omni
2025-09-18 08:18:59 +01:00
parent 70b18004e1
commit 1cd4caf04b
61 changed files with 1349 additions and 503 deletions

View File

@@ -1,5 +1,30 @@
# Jackify Changelog # Jackify Changelog
## v0.1.2 - About Dialog and System Information
**Release Date:** September 16, 2025
### New Features
- **About Dialog**: System information display with OS, kernel, desktop environment, and display server detection
- **Engine Version Detection**: Real-time jackify-engine version reporting
- **Update Integration**: Check for Updates functionality within About dialog
- **Support Tools**: Copy system info for troubleshooting
- **Configurable Jackify Directory**: Users can now customize the Jackify data directory location via Settings
### UX Improvements
- **Control Management**: Form controls are now disabled during install/configure workflows to prevent user conflicts (only Cancel remains active)
- **Auto-Accept Steam Restart**: Optional checkbox to automatically accept Steam restart dialogs for unattended workflows
- **Layout Optimization**: Resolution dropdown and Steam restart option share the same line for better space utilization
### Bug Fixes
- **Resolution Handler**: Fixed regression in resolution setting for Fallout 4 and other games when modlists use vanilla game directories instead of traditional "Stock Game" folders
- **DXVK Configuration**: Fixed dxvk.conf creation failure when modlists point directly to vanilla game installations
- **CLI Resolution Setting**: Fixed missing resolution prompting in CLI Install workflow
### Engine Updates
- **jackify-engine v0.3.14**: Updated to support configurable Jackify data directory, improved Nexus API error handling with better 404/403 responses, and enhanced error logging for troubleshooting
---
## v0.1.1 - Self-Updater Implementation ## v0.1.1 - Self-Updater Implementation
**Release Date:** September 17, 2025 **Release Date:** September 17, 2025

View File

@@ -2,7 +2,7 @@
<div align="center"> <div align="center">
[Wiki](https://github.com/Omni-guides/Jackify/wiki) | [Nexus](https://www.nexusmods.com/site/mods/1427) | [Download](https://www.nexusmods.com/Core/Libs/Common/Widgets/DownloadPopUp?id=5807&game_id=2295) | [Wabbajack Discord](https://discord.gg/wabbajack) | [Jackify Issues](https://github.com/Omni-guides/Jackify/issues) | [Legacy Guides](https://github.com/Omni-guides/Jackify/tree/master/Legacy) | [Ko-fi](https://ko-fi.com/omni1) [Wiki](https://github.com/Omni-guides/Jackify/wiki) | [Nexus](https://www.nexusmods.com/site/mods/1427) | [Download](https://www.nexusmods.com/site/mods/1427?tab=files) | [Wabbajack Discord](https://discord.gg/wabbajack) | [Jackify Issues](https://github.com/Omni-guides/Jackify/issues) | [Legacy Guides](https://github.com/Omni-guides/Jackify/tree/master/Legacy) | [Ko-fi](https://ko-fi.com/omni1)
</div> </div>
@@ -93,7 +93,7 @@ For a complete step-by-step guide with screenshots, see the [User Guide](https:/
### Quick Start ### Quick Start
1. **Download**: Get the latest release from [NexusMods](https://www.nexusmods.com/Core/Libs/Common/Widgets/DownloadPopUp?id=5807&game_id=2295) 1. **Download**: Get the latest release from [NexusMods](https://www.nexusmods.com/site/mods/1427?tab=files)
2. **Extract**: Unzip the .7z archive to get `Jackify.AppImage` 2. **Extract**: Unzip the .7z archive to get `Jackify.AppImage`
3. **Run**: `chmod +x Jackify.AppImage && ./Jackify.AppImage` 3. **Run**: `chmod +x Jackify.AppImage && ./Jackify.AppImage`
4. **Install**: Choose "Install a Modlist", select your game and modlist, configure directories and API key 4. **Install**: Choose "Install a Modlist", select your game and modlist, configure directories and API key

View File

@@ -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.1.1" __version__ = "0.1.2"

View File

@@ -104,8 +104,8 @@ class ModlistInstallCLI:
if isinstance(menu_handler_or_system_info, SystemInfo): if isinstance(menu_handler_or_system_info, SystemInfo):
# GUI frontend initialization pattern # GUI frontend initialization pattern
system_info = menu_handler_or_system_info self.system_info = menu_handler_or_system_info
self.steamdeck = system_info.is_steamdeck self.steamdeck = self.system_info.is_steamdeck
# Initialize menu_handler for GUI mode # Initialize menu_handler for GUI mode
from ..handlers.menu_handler import MenuHandler from ..handlers.menu_handler import MenuHandler
@@ -114,6 +114,9 @@ class ModlistInstallCLI:
# CLI frontend initialization pattern # CLI frontend initialization pattern
self.menu_handler = menu_handler_or_system_info self.menu_handler = menu_handler_or_system_info
self.steamdeck = steamdeck self.steamdeck = steamdeck
# Create system_info for CLI mode
from ..models.configuration import SystemInfo
self.system_info = SystemInfo(is_steamdeck=steamdeck)
self.protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck) self.protontricks_handler = ProtontricksHandler(steamdeck=self.steamdeck)
self.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck) self.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck)
@@ -914,6 +917,20 @@ class ModlistInstallCLI:
self.logger.debug("configuration_phase: Proceeding with Steam configuration...") self.logger.debug("configuration_phase: Proceeding with Steam configuration...")
# Add resolution prompting for CLI mode (before Steam operations)
if not is_gui_mode:
from jackify.backend.handlers.resolution_handler import ResolutionHandler
resolution_handler = ResolutionHandler()
# Check if Steam Deck
is_steamdeck = self.steamdeck if hasattr(self, 'steamdeck') else False
# Prompt for resolution in CLI mode
selected_resolution = resolution_handler.select_resolution(steamdeck=is_steamdeck)
if selected_resolution:
self.context['resolution'] = selected_resolution
self.logger.info(f"Resolution set to: {selected_resolution}")
# Proceed with Steam configuration # Proceed with Steam configuration
self.logger.info(f"Starting Steam configuration for '{shortcut_name}'") self.logger.info(f"Starting Steam configuration for '{shortcut_name}'")
@@ -957,8 +974,8 @@ class ModlistInstallCLI:
shortcut_name, install_dir_str, mo2_exe_path, progress_callback, steamdeck=_is_steamdeck shortcut_name, install_dir_str, mo2_exe_path, progress_callback, steamdeck=_is_steamdeck
) )
# Handle the result # Handle the result (same logic as GUI)
if isinstance(result, tuple) and len(result) == 3: if isinstance(result, tuple) and len(result) == 4:
if result[0] == "CONFLICT": if result[0] == "CONFLICT":
# Handle conflict # Handle conflict
conflicts = result[1] conflicts = result[1]
@@ -984,8 +1001,8 @@ class ModlistInstallCLI:
result = prefix_service.continue_workflow_after_conflict_resolution( result = prefix_service.continue_workflow_after_conflict_resolution(
shortcut_name, install_dir_str, mo2_exe_path, app_id, progress_callback shortcut_name, install_dir_str, mo2_exe_path, app_id, progress_callback
) )
if isinstance(result, tuple) and len(result) == 3: if isinstance(result, tuple) and len(result) >= 3:
success, prefix_path, app_id = result success, prefix_path, app_id = result[0], result[1], result[2]
else: else:
success, prefix_path, app_id = False, None, None success, prefix_path, app_id = False, None, None
else: else:
@@ -1000,10 +1017,58 @@ class ModlistInstallCLI:
print(f"{COLOR_ERROR}Invalid choice. Cancelling.{COLOR_RESET}") print(f"{COLOR_ERROR}Invalid choice. Cancelling.{COLOR_RESET}")
return return
else: else:
# Normal result # Normal result with timestamp (4-tuple)
success, prefix_path, app_id, last_timestamp = result
elif isinstance(result, tuple) and len(result) == 3:
if result[0] == "CONFLICT":
# Handle conflict (3-tuple format)
conflicts = result[1]
print(f"\n{COLOR_WARNING}Found existing Steam shortcut(s) with the same name and path:{COLOR_RESET}")
for i, conflict in enumerate(conflicts, 1):
print(f" {i}. Name: {conflict['name']}")
print(f" Executable: {conflict['exe']}")
print(f" Start Directory: {conflict['startdir']}")
print(f"\n{COLOR_PROMPT}Options:{COLOR_RESET}")
print(" • Replace - Remove the existing shortcut and create a new one")
print(" • Cancel - Keep the existing shortcut and stop the installation")
print(" • Skip - Continue without creating a Steam shortcut")
choice = input(f"\n{COLOR_PROMPT}Choose an option (replace/cancel/skip): {COLOR_RESET}").strip().lower()
if choice == 'replace':
print(f"{COLOR_INFO}Replacing existing shortcut...{COLOR_RESET}")
success, app_id = prefix_service.replace_existing_shortcut(shortcut_name, mo2_exe_path, install_dir_str)
if success and app_id:
# Continue the workflow after replacement
result = prefix_service.continue_workflow_after_conflict_resolution(
shortcut_name, install_dir_str, mo2_exe_path, app_id, progress_callback
)
if isinstance(result, tuple) and len(result) >= 3:
success, prefix_path, app_id = result[0], result[1], result[2]
else:
success, prefix_path, app_id = False, None, None
else:
success, prefix_path, app_id = False, None, None
elif choice == 'cancel':
print(f"{COLOR_INFO}Cancelling installation.{COLOR_RESET}")
return
elif choice == 'skip':
print(f"{COLOR_INFO}Skipping Steam shortcut creation.{COLOR_RESET}")
success, prefix_path, app_id = True, None, None
else:
print(f"{COLOR_ERROR}Invalid choice. Cancelling.{COLOR_RESET}")
return
else:
# Normal result (3-tuple format)
success, prefix_path, app_id = result success, prefix_path, app_id = result
else: else:
success, prefix_path, app_id = False, None, None # Result is not a tuple, check if it's just a boolean success
if result is True:
success, prefix_path, app_id = True, None, None
else:
success, prefix_path, app_id = False, None, None
if success: if success:
print(f"{COLOR_SUCCESS}Automated Steam setup completed successfully!{COLOR_RESET}") print(f"{COLOR_SUCCESS}Automated Steam setup completed successfully!{COLOR_RESET}")
@@ -1011,128 +1076,54 @@ class ModlistInstallCLI:
print(f"{COLOR_INFO}Proton prefix created at: {prefix_path}{COLOR_RESET}") print(f"{COLOR_INFO}Proton prefix created at: {prefix_path}{COLOR_RESET}")
if app_id: if app_id:
print(f"{COLOR_INFO}Steam AppID: {app_id}{COLOR_RESET}") print(f"{COLOR_INFO}Steam AppID: {app_id}{COLOR_RESET}")
return # Continue to configuration phase
else: else:
print(f"{COLOR_WARNING}Automated Steam setup failed. Falling back to manual setup...{COLOR_RESET}") print(f"{COLOR_ERROR}Automated Steam setup failed. Result: {result}{COLOR_RESET}")
print(f"{COLOR_ERROR}Steam integration was not completed. Please check the logs for details.{COLOR_RESET}")
return
# Fallback to manual shortcut creation process # Step 3: Use SAME backend service as GUI
print(f"\n{COLOR_INFO}Using manual Steam setup workflow...{COLOR_RESET}") from jackify.backend.services.modlist_service import ModlistService
from jackify.backend.models.modlist import ModlistContext
from pathlib import Path
# Use the working shortcut creation process from legacy code # Create ModlistContext with engine_installed=True (same as GUI)
from ..handlers.shortcut_handler import ShortcutHandler modlist_context = ModlistContext(
shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck, verbose=False) name=shortcut_name,
install_dir=Path(install_dir_str),
# Create nxmhandler.ini to suppress NXM popup download_dir=Path(install_dir_str) / "downloads", # Standard location
shortcut_handler.write_nxmhandler_ini(install_dir_str, mo2_exe_path) game_type=self.context.get('detected_game', 'Unknown'),
nexus_api_key='', # Not needed for configuration
# Create shortcut with working NativeSteamService modlist_value=self.context.get('modlist_value', ''),
from ..services.native_steam_service import NativeSteamService modlist_source=self.context.get('modlist_source', 'identifier'),
steam_service = NativeSteamService() resolution=self.context.get('resolution'),
mo2_exe_path=Path(mo2_exe_path),
success, app_id = steam_service.create_shortcut_with_proton( skip_confirmation=True, # Always skip confirmation in CLI
app_name=shortcut_name, engine_installed=True # Skip path manipulation for engine workflows
exe_path=mo2_exe_path,
start_dir=os.path.dirname(mo2_exe_path),
launch_options="%command%",
tags=["Jackify"],
proton_version="proton_experimental"
) )
if not success or not app_id: # Add app_id to context
self.logger.error("Failed to create Steam shortcut") modlist_context.app_id = app_id
print(f"{COLOR_ERROR}Failed to create Steam shortcut. Check logs for details.{COLOR_RESET}")
return
# Step 2: Handle Steam restart and manual steps (if not in GUI mode) # Step 4: Configure modlist using SAME service as GUI
if not is_gui_mode: modlist_service = ModlistService(self.system_info)
print(f"\n{COLOR_INFO}Steam shortcut created successfully!{COLOR_RESET}")
print("Steam needs to restart to detect the new shortcut. WARNING: This will close all running Steam instances, and games.")
restart_choice = input("\nRestart Steam automatically now? (Y/n): ").strip().lower()
if restart_choice == 'n':
print("\nPlease restart Steam manually and complete the Proton setup steps.")
print("You can configure this modlist later using 'Configure Existing Modlist'.")
return
# Restart Steam
print("\nRestarting Steam...")
if shortcut_handler.secure_steam_restart():
print(f"{COLOR_INFO}Steam restarted successfully.{COLOR_RESET}")
# Display manual Proton steps
from ..handlers.menu_handler import ModlistMenuHandler
from ..handlers.config_handler import ConfigHandler
config_handler = ConfigHandler()
menu_handler = ModlistMenuHandler(config_handler)
menu_handler._display_manual_proton_steps(shortcut_name)
retry_count = 0
max_retries = 3
while retry_count < max_retries:
input(f"\n{COLOR_PROMPT}Once you have completed ALL the steps above, press Enter to continue...{COLOR_RESET}")
print(f"\n{COLOR_INFO}Verifying manual steps...{COLOR_RESET}")
new_app_id = shortcut_handler.get_appid_for_shortcut(shortcut_name, mo2_exe_path)
if new_app_id and new_app_id.isdigit() and int(new_app_id) > 0:
app_id = new_app_id
from ..handlers.modlist_handler import ModlistHandler
modlist_handler = ModlistHandler({}, steamdeck=self.steamdeck)
verified, status_code = modlist_handler.verify_proton_setup(app_id)
if verified:
print(f"{COLOR_SUCCESS}Manual steps verification successful!{COLOR_RESET}")
break
else:
retry_count += 1
if retry_count < max_retries:
print(f"\n{COLOR_ERROR}Verification failed: {status_code}{COLOR_RESET}")
print(f"{COLOR_WARNING}Please ensure you have completed all manual steps correctly.{COLOR_RESET}")
menu_handler._display_manual_proton_steps(shortcut_name)
else:
print(f"\n{COLOR_ERROR}Manual steps verification failed after {max_retries} attempts.{COLOR_RESET}")
print(f"{COLOR_WARNING}Configuration may not work properly.{COLOR_RESET}")
return
else:
retry_count += 1
if retry_count < max_retries:
print(f"\n{COLOR_ERROR}Could not find valid AppID after launch.{COLOR_RESET}")
print(f"{COLOR_WARNING}Please ensure you have launched the shortcut from Steam.{COLOR_RESET}")
menu_handler._display_manual_proton_steps(shortcut_name)
else:
print(f"\n{COLOR_ERROR}Could not find valid AppID after {max_retries} attempts.{COLOR_RESET}")
print(f"{COLOR_WARNING}Configuration may not work properly.{COLOR_RESET}")
return
else:
print(f"{COLOR_ERROR}Steam restart failed. Please restart manually and configure later.{COLOR_RESET}")
return
# Step 3: Build configuration context with the AppID
config_context = {
'name': shortcut_name,
'appid': app_id,
'path': install_dir_str,
'mo2_exe_path': mo2_exe_path,
'resolution': self.context.get('resolution'),
'skip_confirmation': is_gui_mode,
'manual_steps_completed': not is_gui_mode # True if we did manual steps above
}
# Step 4: Use ModlistMenuHandler to run the complete configuration
from ..handlers.menu_handler import ModlistMenuHandler
from ..handlers.config_handler import ConfigHandler
config_handler = ConfigHandler()
modlist_menu = ModlistMenuHandler(config_handler)
# Add section header for configuration phase if progress callback is available # Add section header for configuration phase if progress callback is available
if 'progress_callback' in locals() and progress_callback: if 'progress_callback' in locals() and progress_callback:
progress_callback("") # Blank line for spacing progress_callback("") # Blank line for spacing
progress_callback("=== Configuring Modlist ===") progress_callback("=== Configuration Phase ===")
self.logger.info("Running post-installation configuration phase") print(f"\n{COLOR_INFO}=== Configuration Phase ==={COLOR_RESET}")
configuration_success = modlist_menu.run_modlist_configuration_phase(config_context) self.logger.info("Running post-installation configuration phase using ModlistService")
# Configure modlist using SAME method as GUI
configuration_success = modlist_service.configure_modlist_post_steam(modlist_context)
if configuration_success: if configuration_success:
print(f"{COLOR_SUCCESS}Configuration completed successfully!{COLOR_RESET}")
self.logger.info("Post-installation configuration completed successfully") self.logger.info("Post-installation configuration completed successfully")
else: else:
print(f"{COLOR_WARNING}Configuration had some issues but completed.{COLOR_RESET}")
self.logger.warning("Post-installation configuration had issues") self.logger.warning("Post-installation configuration had issues")
else: else:
# Game not supported for automated configuration # Game not supported for automated configuration
@@ -1162,10 +1153,9 @@ class ModlistInstallCLI:
# Section header now provided by GUI layer to avoid duplication # Section header now provided by GUI layer to avoid duplication
try: try:
# Set GUI mode for backend operations # CLI Install: keep original GUI mode (don't force GUI mode)
import os import os
original_gui_mode = os.environ.get('JACKIFY_GUI_MODE') original_gui_mode = os.environ.get('JACKIFY_GUI_MODE')
os.environ['JACKIFY_GUI_MODE'] = '1'
try: try:
# Build context for configuration # Build context for configuration
@@ -1176,7 +1166,7 @@ class ModlistInstallCLI:
'modlist_value': context.get('modlist_value'), 'modlist_value': context.get('modlist_value'),
'modlist_source': context.get('modlist_source'), 'modlist_source': context.get('modlist_source'),
'resolution': context.get('resolution'), 'resolution': context.get('resolution'),
'skip_confirmation': True, # GUI mode is non-interactive 'skip_confirmation': True, # CLI Install is non-interactive
'manual_steps_completed': False 'manual_steps_completed': False
} }

View File

@@ -37,7 +37,8 @@ class ConfigHandler:
"default_install_parent_dir": None, # Parent directory for modlist installations "default_install_parent_dir": None, # Parent directory for modlist installations
"default_download_parent_dir": None, # Parent directory for downloads "default_download_parent_dir": None, # Parent directory for downloads
"modlist_install_base_dir": os.path.expanduser("~/Games"), # Configurable base directory for modlist installations "modlist_install_base_dir": os.path.expanduser("~/Games"), # Configurable base directory for modlist installations
"modlist_downloads_base_dir": os.path.expanduser("~/Games/Modlist_Downloads") # Configurable base directory for downloads "modlist_downloads_base_dir": os.path.expanduser("~/Games/Modlist_Downloads"), # Configurable base directory for downloads
"jackify_data_dir": None # Configurable Jackify data directory (default: ~/Jackify)
} }
# Load configuration if exists # Load configuration if exists
@@ -49,6 +50,12 @@ class ConfigHandler:
# Save the updated settings # Save the updated settings
self.save_config() self.save_config()
# If jackify_data_dir is not set, initialize it to default
if not self.settings.get("jackify_data_dir"):
self.settings["jackify_data_dir"] = os.path.expanduser("~/Jackify")
# Save the updated settings
self.save_config()
def _detect_steam_path(self): def _detect_steam_path(self):
""" """
Detect the Steam installation path Detect the Steam installation path

View File

@@ -788,10 +788,16 @@ class ModlistHandler:
status_callback(f"{self._get_progress_timestamp()} Updating resolution settings") status_callback(f"{self._get_progress_timestamp()} Updating resolution settings")
# Ensure resolution_handler call uses correct args if needed # Ensure resolution_handler call uses correct args if needed
# Assuming it uses modlist_dir (str) and game_var_full (str) # Assuming it uses modlist_dir (str) and game_var_full (str)
# Construct vanilla game directory path for fallback
vanilla_game_dir = None
if self.steam_library and self.game_var_full:
vanilla_game_dir = str(Path(self.steam_library) / "steamapps" / "common" / self.game_var_full)
if not self.resolution_handler.update_ini_resolution( if not self.resolution_handler.update_ini_resolution(
modlist_dir=self.modlist_dir, modlist_dir=self.modlist_dir,
game_var=self.game_var_full, game_var=self.game_var_full,
set_res=self.selected_resolution set_res=self.selected_resolution,
vanilla_game_dir=vanilla_game_dir
): ):
self.logger.warning("Failed to update resolution settings in some INI files.") self.logger.warning("Failed to update resolution settings in some INI files.")
print("Warning: Failed to update resolution settings.") print("Warning: Failed to update resolution settings.")
@@ -818,12 +824,18 @@ class ModlistHandler:
status_callback(f"{self._get_progress_timestamp()} Creating dxvk.conf file") status_callback(f"{self._get_progress_timestamp()} Creating dxvk.conf file")
self.logger.info("Step 10: Creating dxvk.conf file...") self.logger.info("Step 10: Creating dxvk.conf file...")
# Assuming create_dxvk_conf still uses string paths # Assuming create_dxvk_conf still uses string paths
# Construct vanilla game directory path for fallback
vanilla_game_dir = None
if self.steam_library and self.game_var_full:
vanilla_game_dir = str(Path(self.steam_library) / "steamapps" / "common" / self.game_var_full)
if not self.path_handler.create_dxvk_conf( if not self.path_handler.create_dxvk_conf(
modlist_dir=self.modlist_dir, modlist_dir=self.modlist_dir,
modlist_sdcard=self.modlist_sdcard, modlist_sdcard=self.modlist_sdcard,
steam_library=str(self.steam_library) if self.steam_library else None, # Pass as string or None steam_library=str(self.steam_library) if self.steam_library else None, # Pass as string or None
basegame_sdcard=self.basegame_sdcard, basegame_sdcard=self.basegame_sdcard,
game_var_full=self.game_var_full game_var_full=self.game_var_full,
vanilla_game_dir=vanilla_game_dir
): ):
self.logger.warning("Failed to create dxvk.conf file.") self.logger.warning("Failed to create dxvk.conf file.")
print("Warning: Failed to create dxvk.conf file.") print("Warning: Failed to create dxvk.conf file.")

View File

@@ -616,7 +616,8 @@ class ModlistInstallCLI:
if machineid: if machineid:
# Convert machineid to filename (e.g., "Tuxborn/Tuxborn" -> "Tuxborn.wabbajack") # Convert machineid to filename (e.g., "Tuxborn/Tuxborn" -> "Tuxborn.wabbajack")
modlist_name = machineid.split('/')[-1] if '/' in machineid else machineid modlist_name = machineid.split('/')[-1] if '/' in machineid else machineid
cached_wabbajack_path = os.path.expanduser(f"~/Jackify/downloaded_mod_lists/{modlist_name}.wabbajack") from jackify.shared.paths import get_jackify_downloads_dir
cached_wabbajack_path = get_jackify_downloads_dir() / f"{modlist_name}.wabbajack"
self.logger.debug(f"Checking for cached .wabbajack file: {cached_wabbajack_path}") self.logger.debug(f"Checking for cached .wabbajack file: {cached_wabbajack_path}")
if modlist_value and modlist_value.endswith('.wabbajack') and os.path.isfile(modlist_value): if modlist_value and modlist_value.endswith('.wabbajack') and os.path.isfile(modlist_value):

View File

@@ -251,7 +251,7 @@ class PathHandler:
return False return False
@staticmethod @staticmethod
def create_dxvk_conf(modlist_dir, modlist_sdcard, steam_library, basegame_sdcard, game_var_full): def create_dxvk_conf(modlist_dir, modlist_sdcard, steam_library, basegame_sdcard, game_var_full, vanilla_game_dir=None):
""" """
Create dxvk.conf file in the appropriate location Create dxvk.conf file in the appropriate location
@@ -261,6 +261,7 @@ class PathHandler:
steam_library (str): Path to the Steam library steam_library (str): Path to the Steam library
basegame_sdcard (bool): Whether the base game is on an SD card basegame_sdcard (bool): Whether the base game is on an SD card
game_var_full (str): Full name of the game (e.g., "Skyrim Special Edition") game_var_full (str): Full name of the game (e.g., "Skyrim Special Edition")
vanilla_game_dir (str): Optional path to vanilla game directory for fallback
Returns: Returns:
bool: True on success, False on failure bool: True on success, False on failure
@@ -271,25 +272,35 @@ class PathHandler:
# Determine the location for dxvk.conf # Determine the location for dxvk.conf
dxvk_conf_path = None dxvk_conf_path = None
# Check for common stock game directories # Check for common stock game directories first, then vanilla as fallback
stock_game_paths = [ stock_game_paths = [
os.path.join(modlist_dir, "Stock Game"), os.path.join(modlist_dir, "Stock Game"),
os.path.join(modlist_dir, "STOCK GAME"),
os.path.join(modlist_dir, "Game Root"), os.path.join(modlist_dir, "Game Root"),
os.path.join(modlist_dir, "STOCK GAME"),
os.path.join(modlist_dir, "Stock Game Folder"),
os.path.join(modlist_dir, "Stock Folder"), os.path.join(modlist_dir, "Stock Folder"),
os.path.join(modlist_dir, "Skyrim Stock"), os.path.join(modlist_dir, "Skyrim Stock"),
os.path.join(modlist_dir, "root", "Skyrim Special Edition"), os.path.join(modlist_dir, "root", "Skyrim Special Edition")
os.path.join(steam_library, game_var_full)
] ]
# Add vanilla game directory as fallback if steam_library and game_var_full are provided
if steam_library and game_var_full:
stock_game_paths.append(os.path.join(steam_library, "steamapps", "common", game_var_full))
for path in stock_game_paths: for path in stock_game_paths:
if os.path.exists(path): if os.path.exists(path):
dxvk_conf_path = os.path.join(path, "dxvk.conf") dxvk_conf_path = os.path.join(path, "dxvk.conf")
break break
if not dxvk_conf_path: if not dxvk_conf_path:
logger.error("Could not determine location for dxvk.conf") # Fallback: Try vanilla game directory if provided
return False if vanilla_game_dir and os.path.exists(vanilla_game_dir):
logger.info(f"Attempting fallback to vanilla game directory: {vanilla_game_dir}")
dxvk_conf_path = os.path.join(vanilla_game_dir, "dxvk.conf")
logger.info(f"Using vanilla game directory for dxvk.conf: {dxvk_conf_path}")
else:
logger.error("Could not determine location for dxvk.conf")
return False
# The required line that Jackify needs # The required line that Jackify needs
required_line = "dxvk.enableGraphicsPipelineLibrary = False" required_line = "dxvk.enableGraphicsPipelineLibrary = False"
@@ -773,6 +784,21 @@ class PathHandler:
return False return False
with open(modlist_ini_path, 'r', encoding='utf-8') as f: with open(modlist_ini_path, 'r', encoding='utf-8') as f:
lines = f.readlines() lines = f.readlines()
# Extract existing gamePath to use as source of truth for vanilla game location
existing_game_path = None
for line in lines:
if re.match(r'^\s*gamepath\s*=.*@ByteArray\(([^)]+)\)', line, re.IGNORECASE):
match = re.search(r'@ByteArray\(([^)]+)\)', line)
if match:
raw_path = match.group(1)
# Convert Windows path back to Linux path
if raw_path.startswith(('Z:', 'D:')):
linux_path = raw_path[2:].replace('\\\\', '/').replace('\\', '/')
existing_game_path = linux_path
logger.debug(f"Extracted existing gamePath: {existing_game_path}")
break
game_path_updated = False game_path_updated = False
binary_paths_updated = 0 binary_paths_updated = 0
working_dirs_updated = 0 working_dirs_updated = 0
@@ -791,9 +817,16 @@ class PathHandler:
backslash_style = wd_match.group(2) backslash_style = wd_match.group(2)
working_dir_lines.append((i, stripped, index, backslash_style)) working_dir_lines.append((i, stripped, index, backslash_style))
binary_paths_by_index = {} binary_paths_by_index = {}
# Use provided steam_libraries if available, else detect # Use existing gamePath to determine correct Steam library, fallback to detection
if steam_libraries is None or not steam_libraries: if existing_game_path and '/steamapps/common/' in existing_game_path:
# Extract the Steam library root from the existing gamePath
steamapps_index = existing_game_path.find('/steamapps/common/')
steam_lib_root = existing_game_path[:steamapps_index]
steam_libraries = [Path(steam_lib_root)]
logger.info(f"Using Steam library from existing gamePath: {steam_lib_root}")
elif steam_libraries is None or not steam_libraries:
steam_libraries = PathHandler.get_all_steam_library_paths() steam_libraries = PathHandler.get_all_steam_library_paths()
logger.debug(f"Fallback to detected Steam libraries: {steam_libraries}")
for i, line, index, backslash_style in binary_lines: for i, line, index, backslash_style in binary_lines:
parts = line.split('=', 1) parts = line.split('=', 1)
if len(parts) != 2: if len(parts) != 2:

View File

@@ -149,7 +149,7 @@ class ResolutionHandler:
return ["1280x720", "1280x800", "1920x1080", "1920x1200", "2560x1440"] return ["1280x720", "1280x800", "1920x1080", "1920x1200", "2560x1440"]
@staticmethod @staticmethod
def update_ini_resolution(modlist_dir: str, game_var: str, set_res: str) -> bool: def update_ini_resolution(modlist_dir: str, game_var: str, set_res: str, vanilla_game_dir: str = None) -> bool:
""" """
Updates the resolution in relevant INI files for the specified game. Updates the resolution in relevant INI files for the specified game.
@@ -157,6 +157,7 @@ class ResolutionHandler:
modlist_dir (str): Path to the modlist directory. modlist_dir (str): Path to the modlist directory.
game_var (str): The game identifier (e.g., "Skyrim Special Edition", "Fallout 4"). game_var (str): The game identifier (e.g., "Skyrim Special Edition", "Fallout 4").
set_res (str): The desired resolution (e.g., "1920x1080"). set_res (str): The desired resolution (e.g., "1920x1080").
vanilla_game_dir (str): Optional path to vanilla game directory for fallback.
Returns: Returns:
bool: True if successful or not applicable, False on error. bool: True if successful or not applicable, False on error.
@@ -211,22 +212,30 @@ class ResolutionHandler:
logger.debug(f"Processing {prefs_filenames}...") logger.debug(f"Processing {prefs_filenames}...")
prefs_files_found = [] prefs_files_found = []
# Search common locations: profiles/, stock game dirs # Search entire modlist directory recursively for all target files
search_dirs = [modlist_path / "profiles"] logger.debug(f"Searching entire modlist directory for: {prefs_filenames}")
# Add potential stock game directories dynamically (case-insensitive) for fname in prefs_filenames:
potential_stock_dirs = [d for d in modlist_path.iterdir() if d.is_dir() and found_files = list(modlist_path.rglob(fname))
d.name.lower() in ["stock game", "game root", "stock folder", "skyrim stock"]] # Add more if needed prefs_files_found.extend(found_files)
search_dirs.extend(potential_stock_dirs) if found_files:
logger.debug(f"Found {len(found_files)} {fname} files: {[str(f) for f in found_files]}")
for search_dir in search_dirs:
if search_dir.is_dir():
for fname in prefs_filenames:
prefs_files_found.extend(list(search_dir.rglob(fname)))
if not prefs_files_found: if not prefs_files_found:
logger.warning(f"No preference files ({prefs_filenames}) found in standard locations ({search_dirs}). Manual INI edit might be needed.") logger.warning(f"No preference files ({prefs_filenames}) found in modlist directory.")
# Consider this success as the main operation didn't fail?
return True # Fallback: Try vanilla game directory if provided
if vanilla_game_dir:
logger.info(f"Attempting fallback to vanilla game directory: {vanilla_game_dir}")
vanilla_path = Path(vanilla_game_dir)
for fname in prefs_filenames:
vanilla_files = list(vanilla_path.rglob(fname))
prefs_files_found.extend(vanilla_files)
if vanilla_files:
logger.info(f"Found {len(vanilla_files)} {fname} files in vanilla game directory")
if not prefs_files_found:
logger.warning("No preference files found in modlist or vanilla game directory. Manual INI edit might be needed.")
return True
for ini_file in prefs_files_found: for ini_file in prefs_files_found:
files_processed += 1 files_processed += 1
@@ -314,19 +323,23 @@ class ResolutionHandler:
new_lines = [] new_lines = []
modified = False modified = False
# Prepare the replacement strings for width and height
# Ensure correct spacing for Oblivion vs other games
# Corrected f-string syntax for conditional expression
equals_operator = "=" if is_oblivion else " = "
width_replace = f"iSize W{equals_operator}{width}\n"
height_replace = f"iSize H{equals_operator}{height}\n"
for line in lines: for line in lines:
stripped_line = line.strip() stripped_line = line.strip()
if stripped_line.lower().endswith("isize w"): if stripped_line.lower().startswith("isize w"):
# Preserve original spacing around equals sign
if " = " in stripped_line:
width_replace = f"iSize W = {width}\n"
else:
width_replace = f"iSize W={width}\n"
new_lines.append(width_replace) new_lines.append(width_replace)
modified = True modified = True
elif stripped_line.lower().endswith("isize h"): elif stripped_line.lower().startswith("isize h"):
# Preserve original spacing around equals sign
if " = " in stripped_line:
height_replace = f"iSize H = {height}\n"
else:
height_replace = f"iSize H={height}\n"
new_lines.append(height_replace) new_lines.append(height_replace)
modified = True modified = True
else: else:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

Binary file not shown.

View File

@@ -32,9 +32,9 @@ class WabbajackMenuHandler:
print_section_header("Modlist and Wabbajack Tasks") print_section_header("Modlist and Wabbajack Tasks")
print(f"{COLOR_SELECTION}1.{COLOR_RESET} Install a Modlist (Automated)") print(f"{COLOR_SELECTION}1.{COLOR_RESET} Install a Modlist (Automated)")
print(f" {COLOR_ACTION}Uses jackify-engine for a full install flow{COLOR_RESET}") print(f" {COLOR_ACTION}Install a modlist in full: Select from a list or provide a .wabbajack file{COLOR_RESET}")
print(f"{COLOR_SELECTION}2.{COLOR_RESET} Configure New Modlist (Post-Download)") print(f"{COLOR_SELECTION}2.{COLOR_RESET} Configure New Modlist (Post-Download)")
print(f" {COLOR_ACTION}→ Modlist .wabbajack file downloaded? Configure it for Steam{COLOR_RESET}") print(f" {COLOR_ACTION}→ Modlist already downloaded? Configure and add to Steam{COLOR_RESET}")
print(f"{COLOR_SELECTION}3.{COLOR_RESET} Configure Existing Modlist (In Steam)") print(f"{COLOR_SELECTION}3.{COLOR_RESET} Configure Existing Modlist (In Steam)")
print(f" {COLOR_ACTION}→ Modlist already in Steam? Re-configure it here{COLOR_RESET}") print(f" {COLOR_ACTION}→ Modlist already in Steam? Re-configure it here{COLOR_RESET}")
# HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY # HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY

View File

@@ -0,0 +1,400 @@
"""
About dialog for Jackify.
This dialog displays system information, version details, and provides
access to update checking and external links.
"""
import logging
import os
import platform
import subprocess
from pathlib import Path
from typing import Optional
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QGroupBox, QTextEdit, QApplication
)
from PySide6.QtCore import Qt, QThread, Signal
from PySide6.QtGui import QFont, QClipboard
from ....backend.services.update_service import UpdateService
from ....backend.models.configuration import SystemInfo
from .... import __version__
logger = logging.getLogger(__name__)
class UpdateCheckThread(QThread):
"""Background thread for checking updates."""
update_check_finished = Signal(object) # UpdateInfo or None
def __init__(self, update_service: UpdateService):
super().__init__()
self.update_service = update_service
def run(self):
"""Check for updates in background."""
try:
update_info = self.update_service.check_for_updates()
self.update_check_finished.emit(update_info)
except Exception as e:
logger.error(f"Error checking for updates: {e}")
self.update_check_finished.emit(None)
class AboutDialog(QDialog):
"""About dialog showing system info and app details."""
def __init__(self, system_info: SystemInfo, parent=None):
super().__init__(parent)
self.system_info = system_info
self.update_service = UpdateService(__version__)
self.update_check_thread = None
self.setup_ui()
self.setup_connections()
def setup_ui(self):
"""Set up the dialog UI."""
self.setWindowTitle("About Jackify")
self.setModal(True)
self.setFixedSize(520, 520)
layout = QVBoxLayout(self)
# Header
header_layout = QVBoxLayout()
# App icon/name
title_label = QLabel("Jackify")
title_font = QFont()
title_font.setPointSize(18)
title_font.setBold(True)
title_label.setFont(title_font)
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #3fd0ea; margin: 10px;")
header_layout.addWidget(title_label)
subtitle_label = QLabel(f"v{__version__}")
subtitle_font = QFont()
subtitle_font.setPointSize(12)
subtitle_label.setFont(subtitle_font)
subtitle_label.setAlignment(Qt.AlignCenter)
subtitle_label.setStyleSheet("color: #666; margin-bottom: 10px;")
header_layout.addWidget(subtitle_label)
tagline_label = QLabel("Simplifying Wabbajack modlist installation and configuration on Linux")
tagline_label.setAlignment(Qt.AlignCenter)
tagline_label.setStyleSheet("color: #888; margin-bottom: 20px;")
header_layout.addWidget(tagline_label)
layout.addLayout(header_layout)
# System Information Group
system_group = QGroupBox("System Information")
system_layout = QVBoxLayout(system_group)
system_info_text = self._get_system_info_text()
system_info_label = QLabel(system_info_text)
system_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
system_info_label.setWordWrap(True)
system_layout.addWidget(system_info_label)
layout.addWidget(system_group)
# Jackify Information Group
jackify_group = QGroupBox("Jackify Information")
jackify_layout = QVBoxLayout(jackify_group)
jackify_info_text = self._get_jackify_info_text()
jackify_info_label = QLabel(jackify_info_text)
jackify_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
jackify_layout.addWidget(jackify_info_label)
layout.addWidget(jackify_group)
# Update status
self.update_status_label = QLabel("")
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
self.update_status_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.update_status_label)
# Buttons
button_layout = QHBoxLayout()
# Update check button
self.update_button = QPushButton("Check for Updates")
self.update_button.clicked.connect(self.check_for_updates)
self.update_button.setStyleSheet("""
QPushButton {
background-color: #23272e;
color: #3fd0ea;
font-weight: bold;
padding: 8px 16px;
border-radius: 4px;
border: 2px solid #3fd0ea;
}
QPushButton:hover {
background-color: #3fd0ea;
color: #23272e;
}
QPushButton:pressed {
background-color: #2bb8d6;
color: #23272e;
}
QPushButton:disabled {
background-color: #444;
color: #666;
border-color: #666;
}
""")
button_layout.addWidget(self.update_button)
button_layout.addStretch()
# Copy Info button
copy_button = QPushButton("Copy Info")
copy_button.clicked.connect(self.copy_system_info)
button_layout.addWidget(copy_button)
# External links
github_button = QPushButton("GitHub")
github_button.clicked.connect(self.open_github)
button_layout.addWidget(github_button)
nexus_button = QPushButton("Nexus")
nexus_button.clicked.connect(self.open_nexus)
button_layout.addWidget(nexus_button)
layout.addLayout(button_layout)
# Close button
close_layout = QHBoxLayout()
close_layout.addStretch()
close_button = QPushButton("Close")
close_button.setDefault(True)
close_button.clicked.connect(self.accept)
close_layout.addWidget(close_button)
layout.addLayout(close_layout)
def setup_connections(self):
"""Set up signal connections."""
pass
def _get_system_info_text(self) -> str:
"""Get formatted system information."""
try:
# OS info
os_info = self._get_os_info()
kernel = platform.release()
# Desktop environment
desktop = self._get_desktop_environment()
# Display server
display_server = self._get_display_server()
return f"• OS: {os_info}\n• Kernel: {kernel}\n• Desktop: {desktop}\n• Display: {display_server}"
except Exception as e:
logger.error(f"Error getting system info: {e}")
return "• System info unavailable"
def _get_jackify_info_text(self) -> str:
"""Get formatted Jackify information."""
try:
# Engine version
engine_version = self._get_engine_version()
# Python version
python_version = platform.python_version()
return f"• Engine: {engine_version}\n• Python: {python_version}"
except Exception as e:
logger.error(f"Error getting Jackify info: {e}")
return "• Jackify info unavailable"
def _get_os_info(self) -> str:
"""Get OS distribution name and version."""
try:
if os.path.exists("/etc/os-release"):
with open("/etc/os-release", "r") as f:
lines = f.readlines()
pretty_name = None
name = None
version = None
for line in lines:
line = line.strip()
if line.startswith("PRETTY_NAME="):
pretty_name = line.split("=", 1)[1].strip('"')
elif line.startswith("NAME="):
name = line.split("=", 1)[1].strip('"')
elif line.startswith("VERSION="):
version = line.split("=", 1)[1].strip('"')
# Prefer PRETTY_NAME, fallback to NAME + VERSION
if pretty_name:
return pretty_name
elif name and version:
return f"{name} {version}"
elif name:
return name
# Fallback to platform info
return f"{platform.system()} {platform.release()}"
except Exception as e:
logger.error(f"Error getting OS info: {e}")
return "Unknown Linux"
def _get_desktop_environment(self) -> str:
"""Get desktop environment."""
try:
# Try XDG_CURRENT_DESKTOP first
desktop = os.environ.get("XDG_CURRENT_DESKTOP")
if desktop:
return desktop
# Fallback to DESKTOP_SESSION
desktop = os.environ.get("DESKTOP_SESSION")
if desktop:
return desktop
# Try detecting common DEs
if os.environ.get("KDE_FULL_SESSION"):
return "KDE"
elif os.environ.get("GNOME_DESKTOP_SESSION_ID"):
return "GNOME"
elif os.environ.get("XFCE4_SESSION"):
return "XFCE"
return "Unknown"
except Exception as e:
logger.error(f"Error getting desktop environment: {e}")
return "Unknown"
def _get_display_server(self) -> str:
"""Get display server type (Wayland or X11)."""
try:
# Check XDG_SESSION_TYPE first
session_type = os.environ.get("XDG_SESSION_TYPE")
if session_type:
return session_type.capitalize()
# Check for Wayland display
if os.environ.get("WAYLAND_DISPLAY"):
return "Wayland"
# Check for X11 display
if os.environ.get("DISPLAY"):
return "X11"
return "Unknown"
except Exception as e:
logger.error(f"Error getting display server: {e}")
return "Unknown"
def _get_engine_version(self) -> str:
"""Get jackify-engine version."""
try:
# Try to execute jackify-engine --version
engine_path = Path(__file__).parent.parent.parent.parent / "engine" / "jackify-engine"
if engine_path.exists():
result = subprocess.run([str(engine_path), "--version"],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
version = result.stdout.strip()
# Extract just the version number (before the +commit hash)
if '+' in version:
version = version.split('+')[0]
return f"v{version}"
return "Unknown"
except Exception as e:
logger.error(f"Error getting engine version: {e}")
return "Unknown"
def check_for_updates(self):
"""Check for updates in background."""
if self.update_check_thread and self.update_check_thread.isRunning():
return
self.update_button.setEnabled(False)
self.update_button.setText("Checking...")
self.update_status_label.setText("Checking for updates...")
self.update_check_thread = UpdateCheckThread(self.update_service)
self.update_check_thread.update_check_finished.connect(self.update_check_finished)
self.update_check_thread.start()
def update_check_finished(self, update_info):
"""Handle update check completion."""
self.update_button.setEnabled(True)
self.update_button.setText("Check for Updates")
if update_info:
self.update_status_label.setText(f"Update available: v{update_info.version}")
self.update_status_label.setStyleSheet("color: #3fd0ea; font-size: 10pt; margin: 5px;")
# Show update dialog
from .update_dialog import UpdateDialog
update_dialog = UpdateDialog(update_info, self.update_service, self)
update_dialog.exec()
else:
self.update_status_label.setText("You're running the latest version")
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
def copy_system_info(self):
"""Copy system information to clipboard."""
try:
info_text = f"""Jackify v{__version__} (Engine {self._get_engine_version()})
OS: {self._get_os_info()} ({platform.release()})
Desktop: {self._get_desktop_environment()} ({self._get_display_server()})
Python: {platform.python_version()}"""
clipboard = QApplication.clipboard()
clipboard.setText(info_text)
# Briefly update button text
sender = self.sender()
original_text = sender.text()
sender.setText("Copied!")
# Reset button text after delay
from PySide6.QtCore import QTimer
QTimer.singleShot(1000, lambda: sender.setText(original_text))
except Exception as e:
logger.error(f"Error copying system info: {e}")
def open_github(self):
"""Open GitHub repository."""
try:
import webbrowser
webbrowser.open("https://github.com/Omni-guides/Jackify")
except Exception as e:
logger.error(f"Error opening GitHub: {e}")
def open_nexus(self):
"""Open Nexus Mods page."""
try:
import webbrowser
webbrowser.open("https://www.nexusmods.com/site/mods/1427")
except Exception as e:
logger.error(f"Error opening Nexus: {e}")
def closeEvent(self, event):
"""Handle dialog close event."""
if self.update_check_thread and self.update_check_thread.isRunning():
self.update_check_thread.terminate()
self.update_check_thread.wait()
event.accept()

View File

@@ -325,6 +325,29 @@ class SettingsDialog(QDialog):
download_dir_row.addWidget(self.download_dir_edit) download_dir_row.addWidget(self.download_dir_edit)
download_dir_row.addWidget(self.download_dir_btn) download_dir_row.addWidget(self.download_dir_btn)
dir_layout.addRow(QLabel("Downloads Base Dir:"), download_dir_row) dir_layout.addRow(QLabel("Downloads Base Dir:"), download_dir_row)
# Jackify Data Directory
from jackify.shared.paths import get_jackify_data_dir
current_jackify_dir = str(get_jackify_data_dir())
self.jackify_data_dir_edit = QLineEdit(current_jackify_dir)
self.jackify_data_dir_edit.setToolTip("Directory for Jackify data (logs, downloads, temp files). Default: ~/Jackify")
self.jackify_data_dir_btn = QPushButton()
self.jackify_data_dir_btn.setIcon(QIcon.fromTheme("folder-open"))
self.jackify_data_dir_btn.setToolTip("Browse for directory")
self.jackify_data_dir_btn.setFixedWidth(32)
self.jackify_data_dir_btn.clicked.connect(lambda: self._pick_directory(self.jackify_data_dir_edit))
jackify_data_dir_row = QHBoxLayout()
jackify_data_dir_row.addWidget(self.jackify_data_dir_edit)
jackify_data_dir_row.addWidget(self.jackify_data_dir_btn)
# Reset to default button
reset_jackify_dir_btn = QPushButton("Reset")
reset_jackify_dir_btn.setToolTip("Reset to default (~/ Jackify)")
reset_jackify_dir_btn.setFixedWidth(50)
reset_jackify_dir_btn.clicked.connect(lambda: self.jackify_data_dir_edit.setText(str(Path.home() / "Jackify")))
jackify_data_dir_row.addWidget(reset_jackify_dir_btn)
dir_layout.addRow(QLabel("Jackify Data Dir:"), jackify_data_dir_row)
main_layout.addWidget(dir_group) main_layout.addWidget(dir_group)
main_layout.addSpacing(12) main_layout.addSpacing(12)
@@ -464,7 +487,14 @@ class SettingsDialog(QDialog):
# Save modlist base dirs # Save modlist base dirs
self.config_handler.set("modlist_install_base_dir", self.install_dir_edit.text().strip()) self.config_handler.set("modlist_install_base_dir", self.install_dir_edit.text().strip())
self.config_handler.set("modlist_downloads_base_dir", self.download_dir_edit.text().strip()) self.config_handler.set("modlist_downloads_base_dir", self.download_dir_edit.text().strip())
# Save jackify data directory (always store actual path, never None)
jackify_data_dir = self.jackify_data_dir_edit.text().strip()
self.config_handler.set("jackify_data_dir", jackify_data_dir)
self.config_handler.save_config() self.config_handler.save_config()
# Refresh cached paths in GUI screens if Jackify directory changed
self._refresh_gui_paths()
# Check if debug mode changed and prompt for restart # Check if debug mode changed and prompt for restart
new_debug_mode = self.debug_checkbox.isChecked() new_debug_mode = self.debug_checkbox.isChecked()
if new_debug_mode != self._original_debug_mode: if new_debug_mode != self._original_debug_mode:
@@ -484,6 +514,29 @@ class SettingsDialog(QDialog):
MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low") MessageService.information(self, "Settings Saved", "Settings have been saved successfully.", safety_level="low")
self.accept() self.accept()
def _refresh_gui_paths(self):
"""Refresh cached paths in all GUI screens."""
try:
# Get the main window through parent relationship
main_window = self.parent()
if not main_window or not hasattr(main_window, 'stacked_widget'):
return
# Refresh paths in all screens that have the method
screens_to_refresh = [
getattr(main_window, 'install_modlist_screen', None),
getattr(main_window, 'configure_new_modlist_screen', None),
getattr(main_window, 'configure_existing_modlist_screen', None),
getattr(main_window, 'tuxborn_screen', None),
]
for screen in screens_to_refresh:
if screen and hasattr(screen, 'refresh_paths'):
screen.refresh_paths()
except Exception as e:
print(f"Warning: Could not refresh GUI paths: {e}")
def _bold_label(self, text): def _bold_label(self, text):
label = QLabel(text) label = QLabel(text)
label.setStyleSheet("font-weight: bold; color: #fff;") label.setStyleSheet("font-weight: bold; color: #fff;")
@@ -655,7 +708,7 @@ class JackifyMainWindow(QMainWindow):
# Spacer # Spacer
bottom_bar_layout.addStretch(1) bottom_bar_layout.addStretch(1)
# Settings button (right) # Settings button (right side)
settings_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">Settings</a>') settings_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">Settings</a>')
settings_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;") settings_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
settings_btn.setTextInteractionFlags(Qt.TextBrowserInteraction) settings_btn.setTextInteractionFlags(Qt.TextBrowserInteraction)
@@ -663,6 +716,14 @@ class JackifyMainWindow(QMainWindow):
settings_btn.linkActivated.connect(self.open_settings_dialog) settings_btn.linkActivated.connect(self.open_settings_dialog)
bottom_bar_layout.addWidget(settings_btn, alignment=Qt.AlignRight) bottom_bar_layout.addWidget(settings_btn, alignment=Qt.AlignRight)
# About button (right side)
about_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">About</a>')
about_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
about_btn.setTextInteractionFlags(Qt.TextBrowserInteraction)
about_btn.setOpenExternalLinks(False)
about_btn.linkActivated.connect(self.open_about_dialog)
bottom_bar_layout.addWidget(about_btn, alignment=Qt.AlignRight)
# --- Main Layout --- # --- Main Layout ---
central_widget = QWidget() central_widget = QWidget()
main_layout = QVBoxLayout() main_layout = QVBoxLayout()
@@ -808,6 +869,16 @@ class JackifyMainWindow(QMainWindow):
import traceback import traceback
traceback.print_exc() traceback.print_exc()
def open_about_dialog(self):
try:
from jackify.frontends.gui.dialogs.about_dialog import AboutDialog
dlg = AboutDialog(self.system_info, self)
dlg.exec()
except Exception as e:
print(f"[ERROR] Exception in open_about_dialog: {e}")
import traceback
traceback.print_exc()
def resource_path(relative_path): def resource_path(relative_path):
if hasattr(sys, '_MEIPASS'): if hasattr(sys, '_MEIPASS'):

View File

@@ -34,8 +34,7 @@ class ConfigureExistingModlistScreen(QWidget):
self.stacked_widget = stacked_widget self.stacked_widget = stacked_widget
self.main_menu_index = main_menu_index self.main_menu_index = main_menu_index
self.debug = DEBUG_BORDERS self.debug = DEBUG_BORDERS
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_Existing_Modlist_workflow.log') self.refresh_paths()
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
# --- Detect Steam Deck --- # --- Detect Steam Deck ---
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower() steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
@@ -298,6 +297,41 @@ class ConfigureExistingModlistScreen(QWidget):
# Time tracking for workflow completion # Time tracking for workflow completion
self._workflow_start_time = None self._workflow_start_time = None
# Initialize empty controls list - will be populated after UI is built
self._actionable_controls = []
# Now collect all actionable controls after UI is fully built
self._collect_actionable_controls()
def _collect_actionable_controls(self):
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
self._actionable_controls = [
# Main action button
self.start_btn,
# Form fields
self.shortcut_combo,
# Resolution controls
self.resolution_combo,
]
def _disable_controls_during_operation(self):
"""Disable all actionable controls during configure operations (except Cancel)"""
for control in self._actionable_controls:
if control:
control.setEnabled(False)
def _enable_controls_after_operation(self):
"""Re-enable all actionable controls after configure operations complete"""
for control in self._actionable_controls:
if control:
control.setEnabled(True)
def refresh_paths(self):
"""Refresh cached paths when config changes."""
from jackify.shared.paths import get_jackify_logs_dir
self.modlist_log_path = get_jackify_logs_dir() / 'Configure_Existing_Modlist_workflow.log'
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
def resizeEvent(self, event): def resizeEvent(self, event):
"""Handle window resize to prioritize form over console""" """Handle window resize to prioritize form over console"""
super().resizeEvent(event) super().resizeEvent(event)
@@ -382,17 +416,22 @@ class ConfigureExistingModlistScreen(QWidget):
log_handler = LoggingHandler() log_handler = LoggingHandler()
log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5) log_handler.rotate_log_file_per_run(Path(self.modlist_log_path), backup_count=5)
# Disable controls during configuration
self._disable_controls_during_operation()
# Get selected shortcut # Get selected shortcut
idx = self.shortcut_combo.currentIndex() - 1 # Account for 'Please Select...' idx = self.shortcut_combo.currentIndex() - 1 # Account for 'Please Select...'
from jackify.frontends.gui.services.message_service import MessageService from jackify.frontends.gui.services.message_service import MessageService
if idx < 0 or idx >= len(self.shortcut_map): if idx < 0 or idx >= len(self.shortcut_map):
MessageService.critical(self, "No Shortcut Selected", "Please select a ModOrganizer.exe Steam shortcut to configure.", safety_level="medium") MessageService.critical(self, "No Shortcut Selected", "Please select a ModOrganizer.exe Steam shortcut to configure.", safety_level="medium")
self._enable_controls_after_operation()
return return
shortcut = self.shortcut_map[idx] shortcut = self.shortcut_map[idx]
modlist_name = shortcut.get('AppName', '') modlist_name = shortcut.get('AppName', '')
install_dir = shortcut.get('StartDir', '') install_dir = shortcut.get('StartDir', '')
if not modlist_name or not install_dir: if not modlist_name or not install_dir:
MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium") MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium")
self._enable_controls_after_operation()
return return
resolution = self.resolution_combo.currentText() resolution = self.resolution_combo.currentText()
# Handle resolution saving # Handle resolution saving
@@ -505,6 +544,9 @@ class ConfigureExistingModlistScreen(QWidget):
def on_configuration_complete(self, success, message, modlist_name): def on_configuration_complete(self, success, message, modlist_name):
"""Handle configuration completion""" """Handle configuration completion"""
# Re-enable all controls when workflow completes
self._enable_controls_after_operation()
if success: if success:
# Calculate time taken # Calculate time taken
time_taken = self._calculate_time_taken() time_taken = self._calculate_time_taken()
@@ -525,6 +567,9 @@ class ConfigureExistingModlistScreen(QWidget):
def on_configuration_error(self, error_message): def on_configuration_error(self, error_message):
"""Handle configuration error""" """Handle configuration error"""
# Re-enable all controls on error
self._enable_controls_after_operation()
self._safe_append_text(f"Configuration error: {error_message}") self._safe_append_text(f"Configuration error: {error_message}")
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium") MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium")
@@ -559,8 +604,8 @@ class ConfigureExistingModlistScreen(QWidget):
if self.config_process and self.config_process.state() == QProcess.Running: if self.config_process and self.config_process.state() == QProcess.Running:
self.config_process.terminate() self.config_process.terminate()
self.config_process.waitForFinished(2000) self.config_process.waitForFinished(2000)
# Reset button states # Re-enable all controls
self.start_btn.setEnabled(True) self._enable_controls_after_operation()
self.cancel_btn.setVisible(True) self.cancel_btn.setVisible(True)
def show_next_steps_dialog(self, message): def show_next_steps_dialog(self, message):

View File

@@ -1,7 +1,7 @@
""" """
ConfigureNewModlistScreen for Jackify GUI ConfigureNewModlistScreen for Jackify GUI
""" """
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QComboBox, QHBoxLayout, QLineEdit, QPushButton, QGridLayout, QFileDialog, QTextEdit, QSizePolicy, QTabWidget, QDialog, QListWidget, QListWidgetItem, QMessageBox, QProgressDialog, QCheckBox
from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QProcess, QMetaObject from PySide6.QtCore import Qt, QSize, QThread, Signal, QTimer, QProcess, QMetaObject
from PySide6.QtGui import QPixmap, QTextCursor from PySide6.QtGui import QPixmap, QTextCursor
from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS
@@ -106,8 +106,7 @@ class ConfigureNewModlistScreen(QWidget):
self.protontricks_service = ProtontricksDetectionService() self.protontricks_service = ProtontricksDetectionService()
# Path for workflow log # Path for workflow log
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_New_Modlist_workflow.log') self.refresh_paths()
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
# Scroll tracking for professional auto-scroll behavior # Scroll tracking for professional auto-scroll behavior
self._user_manually_scrolled = False self._user_manually_scrolled = False
@@ -211,7 +210,6 @@ class ConfigureNewModlistScreen(QWidget):
"7680x4320" "7680x4320"
]) ])
form_grid.addWidget(resolution_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) form_grid.addWidget(resolution_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
form_grid.addWidget(self.resolution_combo, 2, 1)
# Load saved resolution if available # Load saved resolution if available
saved_resolution = self.resolution_service.get_saved_resolution() saved_resolution = self.resolution_service.get_saved_resolution()
@@ -236,6 +234,27 @@ class ConfigureNewModlistScreen(QWidget):
else: else:
self.resolution_combo.setCurrentIndex(0) self.resolution_combo.setCurrentIndex(0)
# Otherwise, default is 'Leave unchanged' (index 0) # Otherwise, default is 'Leave unchanged' (index 0)
# Horizontal layout for resolution dropdown and auto-restart checkbox
resolution_and_restart_layout = QHBoxLayout()
resolution_and_restart_layout.setSpacing(12)
# Resolution dropdown (made smaller)
self.resolution_combo.setMaximumWidth(280) # Constrain width but keep aesthetically pleasing
resolution_and_restart_layout.addWidget(self.resolution_combo)
# Add stretch to push checkbox to the right
resolution_and_restart_layout.addStretch()
# Auto-accept Steam restart checkbox (right-aligned)
self.auto_restart_checkbox = QCheckBox("Auto-accept Steam restart")
self.auto_restart_checkbox.setChecked(False) # Always default to unchecked per session
self.auto_restart_checkbox.setToolTip("When checked, Steam restart dialog will be automatically accepted, allowing unattended configuration")
resolution_and_restart_layout.addWidget(self.auto_restart_checkbox)
# Update the form grid to use the combined layout
form_grid.addLayout(resolution_and_restart_layout, 2, 1)
form_section_widget = QWidget() form_section_widget = QWidget()
form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
form_section_widget.setLayout(form_grid) form_section_widget.setLayout(form_grid)
@@ -339,6 +358,44 @@ class ConfigureNewModlistScreen(QWidget):
# --- Connect steam_restart_finished signal --- # --- Connect steam_restart_finished signal ---
self.steam_restart_finished.connect(self._on_steam_restart_finished) self.steam_restart_finished.connect(self._on_steam_restart_finished)
# Initialize empty controls list - will be populated after UI is built
self._actionable_controls = []
# Now collect all actionable controls after UI is fully built
self._collect_actionable_controls()
def _collect_actionable_controls(self):
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
self._actionable_controls = [
# Main action button
self.start_btn,
# Form fields
self.modlist_name_edit,
self.install_dir_edit,
# Resolution controls
self.resolution_combo,
# Checkboxes
self.auto_restart_checkbox,
]
def _disable_controls_during_operation(self):
"""Disable all actionable controls during configure operations (except Cancel)"""
for control in self._actionable_controls:
if control:
control.setEnabled(False)
def _enable_controls_after_operation(self):
"""Re-enable all actionable controls after configure operations complete"""
for control in self._actionable_controls:
if control:
control.setEnabled(True)
def refresh_paths(self):
"""Refresh cached paths when config changes."""
from jackify.shared.paths import get_jackify_logs_dir
self.modlist_log_path = get_jackify_logs_dir() / 'Configure_New_Modlist_workflow.log'
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
def resizeEvent(self, event): def resizeEvent(self, event):
"""Handle window resize to prioritize form over console""" """Handle window resize to prioritize form over console"""
super().resizeEvent(event) super().resizeEvent(event)
@@ -522,23 +579,38 @@ class ConfigureNewModlistScreen(QWidget):
# Start time tracking # Start time tracking
self._workflow_start_time = time.time() self._workflow_start_time = time.time()
# Disable controls during configuration (after validation passes)
self._disable_controls_during_operation()
# Validate modlist name # Validate modlist name
modlist_name = self.modlist_name_edit.text().strip() modlist_name = self.modlist_name_edit.text().strip()
if not modlist_name: if not modlist_name:
MessageService.warning(self, "Missing Name", "Please specify a name for your modlist", safety_level="low") MessageService.warning(self, "Missing Name", "Please specify a name for your modlist", safety_level="low")
self._enable_controls_after_operation()
return return
# --- Shortcut creation will be handled by automated workflow --- # --- Shortcut creation will be handled by automated workflow ---
from jackify.backend.handlers.shortcut_handler import ShortcutHandler from jackify.backend.handlers.shortcut_handler import ShortcutHandler
steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower() steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower()
shortcut_handler = ShortcutHandler(steamdeck=steamdeck) # Still needed for Steam restart shortcut_handler = ShortcutHandler(steamdeck=steamdeck) # Still needed for Steam restart
# --- User confirmation before restarting Steam ---
reply = MessageService.question( # Check if auto-restart is enabled
self, "Ready to Configure Modlist", auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked()
"Would you like to restart Steam and begin post-install configuration now? Restarting Steam could close any games you have open!",
safety_level="medium" if auto_restart_enabled:
) # Auto-accept Steam restart - proceed without dialog
print(f"DEBUG: Steam restart dialog returned: {reply!r}") self._safe_append_text("Auto-accepting Steam restart (unattended mode enabled)")
reply = QMessageBox.Yes # Simulate user clicking Yes
else:
# --- User confirmation before restarting Steam ---
reply = MessageService.question(
self, "Ready to Configure Modlist",
"Would you like to restart Steam and begin post-install configuration now? Restarting Steam could close any games you have open!",
safety_level="medium"
)
debug_print(f"DEBUG: Steam restart dialog returned: {reply!r}")
if reply not in (QMessageBox.Yes, QMessageBox.Ok, QMessageBox.AcceptRole): if reply not in (QMessageBox.Yes, QMessageBox.Ok, QMessageBox.AcceptRole):
self._enable_controls_after_operation()
if self.stacked_widget: if self.stacked_widget:
self.stacked_widget.setCurrentIndex(0) self.stacked_widget.setCurrentIndex(0)
return return
@@ -562,7 +634,6 @@ class ConfigureNewModlistScreen(QWidget):
progress.setMinimumDuration(0) progress.setMinimumDuration(0)
progress.setValue(0) progress.setValue(0)
progress.show() progress.show()
self.setEnabled(False)
def do_restart(): def do_restart():
try: try:
ok = shortcut_handler.secure_steam_restart() ok = shortcut_handler.secure_steam_restart()
@@ -579,7 +650,7 @@ class ConfigureNewModlistScreen(QWidget):
if hasattr(self, '_steam_restart_progress'): if hasattr(self, '_steam_restart_progress'):
self._steam_restart_progress.close() self._steam_restart_progress.close()
del self._steam_restart_progress del self._steam_restart_progress
self.setEnabled(True) self._enable_controls_after_operation()
if success: if success:
self._safe_append_text("Steam restarted successfully.") self._safe_append_text("Steam restarted successfully.")
@@ -722,7 +793,7 @@ class ConfigureNewModlistScreen(QWidget):
"""Handle error from the automated prefix workflow""" """Handle error from the automated prefix workflow"""
self._safe_append_text(f"Error during automated Steam setup: {error_message}") self._safe_append_text(f"Error during automated Steam setup: {error_message}")
self._safe_append_text("Please check the logs for details.") self._safe_append_text("Please check the logs for details.")
self.start_btn.setEnabled(True) self._enable_controls_after_operation()
def show_shortcut_conflict_dialog(self, conflicts): def show_shortcut_conflict_dialog(self, conflicts):
"""Show dialog to resolve shortcut name conflicts""" """Show dialog to resolve shortcut name conflicts"""
@@ -1162,8 +1233,8 @@ class ConfigureNewModlistScreen(QWidget):
def on_configuration_complete(self, success, message, modlist_name): def on_configuration_complete(self, success, message, modlist_name):
"""Handle configuration completion (same as Tuxborn)""" """Handle configuration completion (same as Tuxborn)"""
# Always re-enable the start button when workflow completes # Re-enable all controls when workflow completes
self.start_btn.setEnabled(True) self._enable_controls_after_operation()
if success: if success:
# Calculate time taken # Calculate time taken
@@ -1185,8 +1256,8 @@ class ConfigureNewModlistScreen(QWidget):
def on_configuration_error(self, error_message): def on_configuration_error(self, error_message):
"""Handle configuration error""" """Handle configuration error"""
# Re-enable the start button on error # Re-enable all controls on error
self.start_btn.setEnabled(True) self._enable_controls_after_operation()
self._safe_append_text(f"Configuration error: {error_message}") self._safe_append_text(f"Configuration error: {error_message}")
MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium") MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium")

View File

@@ -355,9 +355,8 @@ class InstallModlistScreen(QWidget):
self.online_modlists = {} # {game_type: [modlist_dict, ...]} self.online_modlists = {} # {game_type: [modlist_dict, ...]}
self.modlist_details = {} # {modlist_name: modlist_dict} self.modlist_details = {} # {modlist_name: modlist_dict}
# Path for workflow log # Initialize log path (can be refreshed via refresh_paths method)
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Modlist_Install_workflow.log') self.refresh_paths()
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
# Initialize services early # Initialize services early
from jackify.backend.services.api_key_service import APIKeyService from jackify.backend.services.api_key_service import APIKeyService
@@ -459,11 +458,11 @@ class InstallModlistScreen(QWidget):
file_layout.setContentsMargins(0, 0, 0, 0) file_layout.setContentsMargins(0, 0, 0, 0)
self.file_edit = QLineEdit() self.file_edit = QLineEdit()
self.file_edit.setMinimumWidth(400) self.file_edit.setMinimumWidth(400)
file_btn = QPushButton("Browse") self.file_btn = QPushButton("Browse")
file_btn.clicked.connect(self.browse_wabbajack_file) self.file_btn.clicked.connect(self.browse_wabbajack_file)
file_layout.addWidget(QLabel(".wabbajack File:")) file_layout.addWidget(QLabel(".wabbajack File:"))
file_layout.addWidget(self.file_edit) file_layout.addWidget(self.file_edit)
file_layout.addWidget(file_btn) file_layout.addWidget(self.file_btn)
self.file_group.setLayout(file_layout) self.file_group.setLayout(file_layout)
file_tab_vbox.addWidget(self.file_group) file_tab_vbox.addWidget(self.file_group)
file_tab.setLayout(file_tab_vbox) file_tab.setLayout(file_tab_vbox)
@@ -484,22 +483,22 @@ class InstallModlistScreen(QWidget):
install_dir_label = QLabel("Install Directory:") install_dir_label = QLabel("Install Directory:")
self.install_dir_edit = QLineEdit(self.config_handler.get_modlist_install_base_dir()) self.install_dir_edit = QLineEdit(self.config_handler.get_modlist_install_base_dir())
self.install_dir_edit.setMaximumHeight(25) # Force compact height self.install_dir_edit.setMaximumHeight(25) # Force compact height
browse_install_btn = QPushButton("Browse") self.browse_install_btn = QPushButton("Browse")
browse_install_btn.clicked.connect(self.browse_install_dir) self.browse_install_btn.clicked.connect(self.browse_install_dir)
install_dir_hbox = QHBoxLayout() install_dir_hbox = QHBoxLayout()
install_dir_hbox.addWidget(self.install_dir_edit) install_dir_hbox.addWidget(self.install_dir_edit)
install_dir_hbox.addWidget(browse_install_btn) install_dir_hbox.addWidget(self.browse_install_btn)
form_grid.addWidget(install_dir_label, 1, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) form_grid.addWidget(install_dir_label, 1, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
form_grid.addLayout(install_dir_hbox, 1, 1) form_grid.addLayout(install_dir_hbox, 1, 1)
# Downloads Dir # Downloads Dir
downloads_dir_label = QLabel("Downloads Directory:") downloads_dir_label = QLabel("Downloads Directory:")
self.downloads_dir_edit = QLineEdit(self.config_handler.get_modlist_downloads_base_dir()) self.downloads_dir_edit = QLineEdit(self.config_handler.get_modlist_downloads_base_dir())
self.downloads_dir_edit.setMaximumHeight(25) # Force compact height self.downloads_dir_edit.setMaximumHeight(25) # Force compact height
browse_downloads_btn = QPushButton("Browse") self.browse_downloads_btn = QPushButton("Browse")
browse_downloads_btn.clicked.connect(self.browse_downloads_dir) self.browse_downloads_btn.clicked.connect(self.browse_downloads_dir)
downloads_dir_hbox = QHBoxLayout() downloads_dir_hbox = QHBoxLayout()
downloads_dir_hbox.addWidget(self.downloads_dir_edit) downloads_dir_hbox.addWidget(self.downloads_dir_edit)
downloads_dir_hbox.addWidget(browse_downloads_btn) downloads_dir_hbox.addWidget(self.browse_downloads_btn)
form_grid.addWidget(downloads_dir_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) form_grid.addWidget(downloads_dir_label, 2, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
form_grid.addLayout(downloads_dir_hbox, 2, 1) form_grid.addLayout(downloads_dir_hbox, 2, 1)
# Nexus API Key # Nexus API Key
@@ -603,7 +602,25 @@ class InstallModlistScreen(QWidget):
self.resolution_combo.setCurrentIndex(0) self.resolution_combo.setCurrentIndex(0)
# Otherwise, default is 'Leave unchanged' (index 0) # Otherwise, default is 'Leave unchanged' (index 0)
form_grid.addWidget(resolution_label, 5, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter) form_grid.addWidget(resolution_label, 5, 0, alignment=Qt.AlignLeft | Qt.AlignVCenter)
form_grid.addWidget(self.resolution_combo, 5, 1)
# Horizontal layout for resolution dropdown and auto-restart checkbox
resolution_and_restart_layout = QHBoxLayout()
resolution_and_restart_layout.setSpacing(12)
# Resolution dropdown (made smaller)
self.resolution_combo.setMaximumWidth(280) # Constrain width but keep aesthetically pleasing
resolution_and_restart_layout.addWidget(self.resolution_combo)
# Add stretch to push checkbox to the right
resolution_and_restart_layout.addStretch()
# Auto-accept Steam restart checkbox (right-aligned)
self.auto_restart_checkbox = QCheckBox("Auto-accept Steam restart")
self.auto_restart_checkbox.setChecked(False) # Always default to unchecked per session
self.auto_restart_checkbox.setToolTip("When checked, Steam restart dialog will be automatically accepted, allowing unattended installation")
resolution_and_restart_layout.addWidget(self.auto_restart_checkbox)
form_grid.addLayout(resolution_and_restart_layout, 5, 1)
form_section_widget = QWidget() form_section_widget = QWidget()
form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) form_section_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
form_section_widget.setLayout(form_grid) form_section_widget.setLayout(form_grid)
@@ -724,6 +741,57 @@ class InstallModlistScreen(QWidget):
# Initialize process tracking # Initialize process tracking
self.process = None self.process = None
# Initialize empty controls list - will be populated after UI is built
self._actionable_controls = []
# Now collect all actionable controls after UI is fully built
self._collect_actionable_controls()
def _collect_actionable_controls(self):
"""Collect all actionable controls that should be disabled during operations (except Cancel)"""
self._actionable_controls = [
# Main action button
self.start_btn,
# Game/modlist selection
self.game_type_btn,
self.modlist_btn,
# Source tabs (entire tab widget)
self.source_tabs,
# Form fields
self.modlist_name_edit,
self.install_dir_edit,
self.downloads_dir_edit,
self.api_key_edit,
self.file_edit,
# Browse buttons
self.browse_install_btn,
self.browse_downloads_btn,
self.file_btn,
# Resolution controls
self.resolution_combo,
# Checkboxes
self.save_api_key_checkbox,
self.auto_restart_checkbox,
]
def _disable_controls_during_operation(self):
"""Disable all actionable controls during install/configure operations (except Cancel)"""
for control in self._actionable_controls:
if control:
control.setEnabled(False)
def _enable_controls_after_operation(self):
"""Re-enable all actionable controls after install/configure operations complete"""
for control in self._actionable_controls:
if control:
control.setEnabled(True)
def refresh_paths(self):
"""Refresh cached paths when config changes."""
from jackify.shared.paths import get_jackify_logs_dir
self.modlist_log_path = get_jackify_logs_dir() / 'Modlist_Install_workflow.log'
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
def _open_url_safe(self, url): def _open_url_safe(self, url):
"""Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller""" """Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller"""
import subprocess import subprocess
@@ -1121,6 +1189,9 @@ class InstallModlistScreen(QWidget):
if not self._check_protontricks(): if not self._check_protontricks():
return return
# Disable all controls during installation (except Cancel)
self._disable_controls_during_operation()
try: try:
tab_index = self.source_tabs.currentIndex() tab_index = self.source_tabs.currentIndex()
install_mode = 'online' install_mode = 'online'
@@ -1128,12 +1199,14 @@ class InstallModlistScreen(QWidget):
modlist = self.file_edit.text().strip() modlist = self.file_edit.text().strip()
if not modlist or not os.path.isfile(modlist) or not modlist.endswith('.wabbajack'): if not modlist or not os.path.isfile(modlist) or not modlist.endswith('.wabbajack'):
MessageService.warning(self, "Invalid Modlist", "Please select a valid .wabbajack file.") MessageService.warning(self, "Invalid Modlist", "Please select a valid .wabbajack file.")
self._enable_controls_after_operation()
return return
install_mode = 'file' install_mode = 'file'
else: else:
modlist = self.modlist_btn.text().strip() modlist = self.modlist_btn.text().strip()
if not modlist or modlist in ("Select Modlist", "Fetching modlists...", "No modlists found.", "Error fetching modlists."): if not modlist or modlist in ("Select Modlist", "Fetching modlists...", "No modlists found.", "Error fetching modlists."):
MessageService.warning(self, "Invalid Modlist", "Please select a valid modlist.") MessageService.warning(self, "Invalid Modlist", "Please select a valid modlist.")
self._enable_controls_after_operation()
return return
# For online modlists, use machine_url instead of display name # For online modlists, use machine_url instead of display name
@@ -1159,6 +1232,7 @@ class InstallModlistScreen(QWidget):
missing_fields.append("Nexus API Key") missing_fields.append("Nexus API Key")
if missing_fields: if missing_fields:
MessageService.warning(self, "Missing Required Fields", f"Please fill in all required fields before starting the install:\n- " + "\n- ".join(missing_fields)) MessageService.warning(self, "Missing Required Fields", f"Please fill in all required fields before starting the install:\n- " + "\n- ".join(missing_fields))
self._enable_controls_after_operation()
return return
validation_handler = ValidationHandler() validation_handler = ValidationHandler()
from pathlib import Path from pathlib import Path
@@ -1324,14 +1398,11 @@ class InstallModlistScreen(QWidget):
debug_print(f"DEBUG: Exception in validate_and_start_install: {e}") debug_print(f"DEBUG: Exception in validate_and_start_install: {e}")
import traceback import traceback
debug_print(f"DEBUG: Traceback: {traceback.format_exc()}") debug_print(f"DEBUG: Traceback: {traceback.format_exc()}")
# Re-enable the button in case of exception # Re-enable all controls after exception
self.start_btn.setEnabled(True) self._enable_controls_after_operation()
self.cancel_btn.setVisible(True) self.cancel_btn.setVisible(True)
self.cancel_install_btn.setVisible(False) self.cancel_install_btn.setVisible(False)
# Also re-enable the entire widget debug_print(f"DEBUG: Controls re-enabled in exception handler")
self.setEnabled(True)
debug_print(f"DEBUG: Widget re-enabled in exception handler, widget enabled: {self.isEnabled()}")
print(f"DEBUG: Widget re-enabled in exception handler, widget enabled: {self.isEnabled()}") # Always print
def run_modlist_installer(self, modlist, install_dir, downloads_dir, api_key, install_mode='online'): def run_modlist_installer(self, modlist, install_dir, downloads_dir, api_key, install_mode='online'):
debug_print('DEBUG: run_modlist_installer called - USING THREADED BACKEND WRAPPER') debug_print('DEBUG: run_modlist_installer called - USING THREADED BACKEND WRAPPER')
@@ -1501,12 +1572,21 @@ class InstallModlistScreen(QWidget):
self._safe_append_text(f"\nModlist installation completed successfully.") self._safe_append_text(f"\nModlist installation completed successfully.")
self._safe_append_text(f"\nWarning: Post-install configuration skipped for unsupported game: {game_name or game_type}") self._safe_append_text(f"\nWarning: Post-install configuration skipped for unsupported game: {game_name or game_type}")
else: else:
# Show the normal install complete dialog for supported games # Check if auto-restart is enabled
reply = MessageService.question( auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked()
self, "Modlist Install Complete!",
"Modlist install complete!\n\nWould you like to add this modlist to Steam and configure it now? Steam will restart, closing any game you have open!", if auto_restart_enabled:
critical=False # Non-critical, won't steal focus # Auto-accept Steam restart - proceed without dialog
) self._safe_append_text("\nAuto-accepting Steam restart (unattended mode enabled)")
reply = QMessageBox.Yes # Simulate user clicking Yes
else:
# Show the normal install complete dialog for supported games
reply = MessageService.question(
self, "Modlist Install Complete!",
"Modlist install complete!\n\nWould you like to add this modlist to Steam and configure it now? Steam will restart, closing any game you have open!",
critical=False # Non-critical, won't steal focus
)
if reply == QMessageBox.Yes: if reply == QMessageBox.Yes:
# --- Create Steam shortcut BEFORE restarting Steam --- # --- Create Steam shortcut BEFORE restarting Steam ---
# Proceed directly to automated prefix creation # Proceed directly to automated prefix creation
@@ -1522,6 +1602,8 @@ class InstallModlistScreen(QWidget):
"You can manually add the modlist to Steam later if desired.", "You can manually add the modlist to Steam later if desired.",
safety_level="medium" safety_level="medium"
) )
# Re-enable controls since operation is complete
self._enable_controls_after_operation()
else: else:
# Check for user cancellation first # Check for user cancellation first
last_output = self.console.toPlainText() last_output = self.console.toPlainText()
@@ -1611,9 +1693,6 @@ class InstallModlistScreen(QWidget):
progress.setMinimumDuration(0) progress.setMinimumDuration(0)
progress.setValue(0) progress.setValue(0)
progress.show() progress.show()
self.setEnabled(False)
debug_print(f"DEBUG: Widget disabled in restart_steam_and_configure, widget enabled: {self.isEnabled()}")
print(f"DEBUG: Widget disabled in restart_steam_and_configure, widget enabled: {self.isEnabled()}") # Always print
def do_restart(): def do_restart():
debug_print("DEBUG: do_restart thread started - using direct backend service") debug_print("DEBUG: do_restart thread started - using direct backend service")
@@ -1651,9 +1730,7 @@ class InstallModlistScreen(QWidget):
finally: finally:
self._steam_restart_progress = None self._steam_restart_progress = None
self.setEnabled(True) # Controls are managed by the proper control management system
debug_print(f"DEBUG: Widget re-enabled in _on_steam_restart_finished, widget enabled: {self.isEnabled()}")
print(f"DEBUG: Widget re-enabled in _on_steam_restart_finished, widget enabled: {self.isEnabled()}") # Always print
if success: if success:
self._safe_append_text("Steam restarted successfully.") self._safe_append_text("Steam restarted successfully.")
@@ -1676,6 +1753,8 @@ class InstallModlistScreen(QWidget):
def start_automated_prefix_workflow(self): def start_automated_prefix_workflow(self):
"""Start the automated prefix creation workflow""" """Start the automated prefix creation workflow"""
try: try:
# Disable controls during installation
self._disable_controls_during_operation()
modlist_name = self.modlist_name_edit.text().strip() modlist_name = self.modlist_name_edit.text().strip()
install_dir = self.install_dir_edit.text().strip() install_dir = self.install_dir_edit.text().strip()
final_exe_path = os.path.join(install_dir, "ModOrganizer.exe") final_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
@@ -1784,33 +1863,43 @@ class InstallModlistScreen(QWidget):
import traceback import traceback
debug_print(f"DEBUG: Traceback: {traceback.format_exc()}") debug_print(f"DEBUG: Traceback: {traceback.format_exc()}")
self._safe_append_text(f"ERROR: Failed to start automated workflow: {e}") self._safe_append_text(f"ERROR: Failed to start automated workflow: {e}")
# Re-enable controls on exception
self._enable_controls_after_operation()
def on_automated_prefix_finished(self, success, prefix_path, new_appid_str, last_timestamp=None): def on_automated_prefix_finished(self, success, prefix_path, new_appid_str, last_timestamp=None):
"""Handle completion of automated prefix creation""" """Handle completion of automated prefix creation"""
if success: try:
debug_print(f"SUCCESS: Automated prefix creation completed!") if success:
debug_print(f"Prefix created at: {prefix_path}") debug_print(f"SUCCESS: Automated prefix creation completed!")
if new_appid_str and new_appid_str != "0": debug_print(f"Prefix created at: {prefix_path}")
debug_print(f"AppID: {new_appid_str}") if new_appid_str and new_appid_str != "0":
debug_print(f"AppID: {new_appid_str}")
# Convert string AppID back to integer for configuration # Convert string AppID back to integer for configuration
new_appid = int(new_appid_str) if new_appid_str and new_appid_str != "0" else None new_appid = int(new_appid_str) if new_appid_str and new_appid_str != "0" else None
# Continue with configuration using the new AppID and timestamp # Continue with configuration using the new AppID and timestamp
modlist_name = self.modlist_name_edit.text().strip() modlist_name = self.modlist_name_edit.text().strip()
install_dir = self.install_dir_edit.text().strip() install_dir = self.install_dir_edit.text().strip()
self.continue_configuration_after_automated_prefix(new_appid, modlist_name, install_dir, last_timestamp) self.continue_configuration_after_automated_prefix(new_appid, modlist_name, install_dir, last_timestamp)
else: else:
self._safe_append_text(f"ERROR: Automated prefix creation failed") self._safe_append_text(f"ERROR: Automated prefix creation failed")
self._safe_append_text("Please check the logs for details") self._safe_append_text("Please check the logs for details")
MessageService.critical(self, "Automated Setup Failed", MessageService.critical(self, "Automated Setup Failed",
"Automated prefix creation failed. Please check the console output for details.") "Automated prefix creation failed. Please check the console output for details.")
# Re-enable controls on failure
self._enable_controls_after_operation()
finally:
# Always ensure controls are re-enabled when workflow truly completes
pass
def on_automated_prefix_error(self, error_msg): def on_automated_prefix_error(self, error_msg):
"""Handle error in automated prefix creation""" """Handle error in automated prefix creation"""
self._safe_append_text(f"ERROR: Error during automated prefix creation: {error_msg}") self._safe_append_text(f"ERROR: Error during automated prefix creation: {error_msg}")
MessageService.critical(self, "Automated Setup Error", MessageService.critical(self, "Automated Setup Error",
f"Error during automated prefix creation: {error_msg}") f"Error during automated prefix creation: {error_msg}")
# Re-enable controls on error
self._enable_controls_after_operation()
def on_automated_prefix_progress(self, progress_msg): def on_automated_prefix_progress(self, progress_msg):
"""Handle progress updates from automated prefix creation""" """Handle progress updates from automated prefix creation"""
@@ -1831,7 +1920,6 @@ class InstallModlistScreen(QWidget):
self.steam_restart_progress.setMinimumDuration(0) self.steam_restart_progress.setMinimumDuration(0)
self.steam_restart_progress.setValue(0) self.steam_restart_progress.setValue(0)
self.steam_restart_progress.show() self.steam_restart_progress.show()
self.setEnabled(False)
def hide_steam_restart_progress(self): def hide_steam_restart_progress(self):
"""Hide Steam restart progress dialog""" """Hide Steam restart progress dialog"""
@@ -1843,45 +1931,53 @@ class InstallModlistScreen(QWidget):
pass pass
finally: finally:
self.steam_restart_progress = None self.steam_restart_progress = None
self.setEnabled(True) # Controls are managed by the proper control management system
def on_configuration_complete(self, success, message, modlist_name): def on_configuration_complete(self, success, message, modlist_name):
"""Handle configuration completion on main thread""" """Handle configuration completion on main thread"""
if success: try:
# Show celebration SuccessDialog after the entire workflow # Re-enable controls now that installation/configuration is complete
from ..dialogs import SuccessDialog self._enable_controls_after_operation()
import time
if not hasattr(self, '_install_workflow_start_time'): if success:
self._install_workflow_start_time = time.time() # Show celebration SuccessDialog after the entire workflow
time_taken = int(time.time() - self._install_workflow_start_time) from ..dialogs import SuccessDialog
mins, secs = divmod(time_taken, 60) import time
time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds" if not hasattr(self, '_install_workflow_start_time'):
display_names = { self._install_workflow_start_time = time.time()
'skyrim': 'Skyrim', time_taken = int(time.time() - self._install_workflow_start_time)
'fallout4': 'Fallout 4', mins, secs = divmod(time_taken, 60)
'falloutnv': 'Fallout New Vegas', time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds"
'oblivion': 'Oblivion', display_names = {
'starfield': 'Starfield', 'skyrim': 'Skyrim',
'oblivion_remastered': 'Oblivion Remastered', 'fallout4': 'Fallout 4',
'enderal': 'Enderal' 'falloutnv': 'Fallout New Vegas',
} 'oblivion': 'Oblivion',
game_name = display_names.get(self._current_game_type, self._current_game_name) 'starfield': 'Starfield',
success_dialog = SuccessDialog( 'oblivion_remastered': 'Oblivion Remastered',
modlist_name=modlist_name, 'enderal': 'Enderal'
workflow_type="install", }
time_taken=time_str, game_name = display_names.get(self._current_game_type, self._current_game_name)
game_name=game_name, success_dialog = SuccessDialog(
parent=self modlist_name=modlist_name,
) workflow_type="install",
success_dialog.show() time_taken=time_str,
elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3: game_name=game_name,
# Max retries reached - show failure message parent=self
MessageService.critical(self, "Manual Steps Failed", )
"Manual steps validation failed after multiple attempts.") success_dialog.show()
else: elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3:
# Configuration failed for other reasons # Max retries reached - show failure message
MessageService.critical(self, "Configuration Failed", MessageService.critical(self, "Manual Steps Failed",
"Post-install configuration failed. Please check the console output.") "Manual steps validation failed after multiple attempts.")
else:
# Configuration failed for other reasons
MessageService.critical(self, "Configuration Failed",
"Post-install configuration failed. Please check the console output.")
except Exception as e:
# Ensure controls are re-enabled even on unexpected errors
self._enable_controls_after_operation()
raise
# Clean up thread # Clean up thread
if hasattr(self, 'config_thread') and self.config_thread is not None: if hasattr(self, 'config_thread') and self.config_thread is not None:
# Disconnect all signals to prevent "Internal C++ object already deleted" errors # Disconnect all signals to prevent "Internal C++ object already deleted" errors
@@ -1940,8 +2036,8 @@ class InstallModlistScreen(QWidget):
else: else:
# User clicked Cancel or closed the dialog - cancel the workflow # User clicked Cancel or closed the dialog - cancel the workflow
self._safe_append_text("\n🛑 Manual steps cancelled by user. Workflow stopped.") self._safe_append_text("\n🛑 Manual steps cancelled by user. Workflow stopped.")
# Reset button states # Re-enable all controls when workflow is cancelled
self.start_btn.setEnabled(True) self._enable_controls_after_operation()
self.cancel_btn.setVisible(True) self.cancel_btn.setVisible(True)
self.cancel_install_btn.setVisible(False) self.cancel_install_btn.setVisible(False)
@@ -2513,8 +2609,8 @@ class InstallModlistScreen(QWidget):
# Cleanup any remaining processes # Cleanup any remaining processes
self.cleanup_processes() self.cleanup_processes()
# Reset button states # Reset button states and re-enable all controls
self.start_btn.setEnabled(True) self._enable_controls_after_operation()
self.cancel_btn.setVisible(True) self.cancel_btn.setVisible(True)
self.cancel_install_btn.setVisible(False) self.cancel_install_btn.setVisible(False)

View File

@@ -106,8 +106,7 @@ class TuxbornInstallerScreen(QWidget):
self.modlist_details = {} # {modlist_name: modlist_dict} self.modlist_details = {} # {modlist_name: modlist_dict}
# Path for workflow log # Path for workflow log
self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Tuxborn_Installer_workflow.log') self.refresh_paths()
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
# Initialize services early # Initialize services early
from jackify.backend.services.api_key_service import APIKeyService from jackify.backend.services.api_key_service import APIKeyService
@@ -440,6 +439,12 @@ class TuxbornInstallerScreen(QWidget):
self.start_btn.clicked.connect(self.validate_and_start_install) self.start_btn.clicked.connect(self.validate_and_start_install)
self.steam_restart_finished.connect(self._on_steam_restart_finished) self.steam_restart_finished.connect(self._on_steam_restart_finished)
def refresh_paths(self):
"""Refresh cached paths when config changes."""
from jackify.shared.paths import get_jackify_logs_dir
self.modlist_log_path = get_jackify_logs_dir() / 'Tuxborn_Installer_workflow.log'
os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True)
def _open_url_safe(self, url): def _open_url_safe(self, url):
"""Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller""" """Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller"""
import subprocess import subprocess

View File

@@ -14,16 +14,22 @@ import shutil
class LoggingHandler: class LoggingHandler:
""" """
Central logging handler for Jackify. Central logging handler for Jackify.
- Uses ~/Jackify/logs/ as the log directory. - Uses configurable Jackify data directory for logs (default: ~/Jackify/logs/).
- Supports per-function log files (e.g., jackify-install-wabbajack.log). - Supports per-function log files (e.g., jackify-install-wabbajack.log).
- Handles log rotation and log directory creation. - Handles log rotation and log directory creation.
Usage: Usage:
logger = LoggingHandler().setup_logger('install_wabbajack', 'jackify-install-wabbajack.log') logger = LoggingHandler().setup_logger('install_wabbajack', 'jackify-install-wabbajack.log')
""" """
def __init__(self): def __init__(self):
self.log_dir = Path.home() / "Jackify" / "logs" # Don't cache log_dir - use property to get fresh path each time
self.ensure_log_directory() self.ensure_log_directory()
@property
def log_dir(self):
"""Get the current log directory (may change if config updated)."""
from jackify.shared.paths import get_jackify_logs_dir
return get_jackify_logs_dir()
def ensure_log_directory(self) -> None: def ensure_log_directory(self) -> None:
"""Ensure the log directory exists.""" """Ensure the log directory exists."""
try: try:

65
jackify/shared/paths.py Normal file
View File

@@ -0,0 +1,65 @@
"""
Path utilities for Jackify.
This module provides standardized path resolution for Jackify directories,
supporting configurable data directory while keeping config in a fixed location.
"""
import os
from pathlib import Path
from typing import Optional
def get_jackify_data_dir() -> Path:
"""
Get the configurable Jackify data directory.
This directory contains:
- downloaded_mod_lists/
- logs/
- temporary proton prefixes during installation
Returns:
Path: The Jackify data directory (always set in config)
"""
try:
# Import here to avoid circular imports
from jackify.backend.handlers.config_handler import ConfigHandler
config_handler = ConfigHandler()
jackify_data_dir = config_handler.get('jackify_data_dir')
# Config handler now always ensures this is set, but fallback just in case
if jackify_data_dir:
return Path(jackify_data_dir).expanduser()
else:
return Path.home() / "Jackify"
except Exception:
# Emergency fallback if config system fails
return Path.home() / "Jackify"
def get_jackify_logs_dir() -> Path:
"""Get the logs directory within the Jackify data directory."""
return get_jackify_data_dir() / "logs"
def get_jackify_downloads_dir() -> Path:
"""Get the downloaded modlists directory within the Jackify data directory."""
return get_jackify_data_dir() / "downloaded_mod_lists"
def get_jackify_config_dir() -> Path:
"""
Get the Jackify configuration directory (always ~/.config/jackify).
This directory contains:
- config.json (settings)
- API keys and credentials
- Resource settings
Returns:
Path: Always ~/.config/jackify
"""
return Path.home() / ".config" / "jackify"

View File

@@ -51,9 +51,15 @@ def _clear_screen_fallback():
def print_jackify_banner(): def print_jackify_banner():
"""Print the Jackify application banner""" """Print the Jackify application banner"""
print(""" from jackify import __version__
version_text = f"Jackify CLI ({__version__})"
# Center the version text in the banner (72 chars content width)
padding = (72 - len(version_text)) // 2
centered_version = " " * padding + version_text + " " * (72 - len(version_text) - padding)
print(f"""
╔════════════════════════════════════════════════════════════════════════╗ ╔════════════════════════════════════════════════════════════════════════╗
Jackify CLI (pre-alpha) {centered_version}
║ ║ ║ ║
║ A tool for installing and configuring modlists ║ ║ A tool for installing and configuring modlists ║
║ & associated utilities on Linux ║ ║ & associated utilities on Linux ║