diff --git a/CHANGELOG.md b/CHANGELOG.md index cdbffaf..95a87e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # 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 **Release Date:** September 17, 2025 diff --git a/README.md b/README.md index 2bd5b90..fd92e3b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@
-[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)
@@ -93,7 +93,7 @@ For a complete step-by-step guide with screenshots, see the [User Guide](https:/ ### 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` 3. **Run**: `chmod +x Jackify.AppImage && ./Jackify.AppImage` 4. **Install**: Choose "Install a Modlist", select your game and modlist, configure directories and API key diff --git a/jackify/__init__.py b/jackify/__init__.py index 5eb60ef..d5f1880 100644 --- a/jackify/__init__.py +++ b/jackify/__init__.py @@ -5,4 +5,4 @@ This package provides both CLI and GUI interfaces for managing Wabbajack modlists natively on Linux systems. """ -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/jackify/backend/core/modlist_operations.py b/jackify/backend/core/modlist_operations.py index 5b37a51..e3923ec 100644 --- a/jackify/backend/core/modlist_operations.py +++ b/jackify/backend/core/modlist_operations.py @@ -104,8 +104,8 @@ class ModlistInstallCLI: if isinstance(menu_handler_or_system_info, SystemInfo): # GUI frontend initialization pattern - system_info = menu_handler_or_system_info - self.steamdeck = system_info.is_steamdeck + self.system_info = menu_handler_or_system_info + self.steamdeck = self.system_info.is_steamdeck # Initialize menu_handler for GUI mode from ..handlers.menu_handler import MenuHandler @@ -114,6 +114,9 @@ class ModlistInstallCLI: # CLI frontend initialization pattern self.menu_handler = menu_handler_or_system_info 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.shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck) @@ -914,6 +917,20 @@ class ModlistInstallCLI: 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 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 ) - # Handle the result - if isinstance(result, tuple) and len(result) == 3: + # Handle the result (same logic as GUI) + if isinstance(result, tuple) and len(result) == 4: if result[0] == "CONFLICT": # Handle conflict conflicts = result[1] @@ -984,8 +1001,8 @@ class ModlistInstallCLI: 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 + 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: @@ -1000,10 +1017,58 @@ class ModlistInstallCLI: print(f"{COLOR_ERROR}Invalid choice. Cancelling.{COLOR_RESET}") return 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 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: 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}") if app_id: print(f"{COLOR_INFO}Steam AppID: {app_id}{COLOR_RESET}") - return + # Continue to configuration phase 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 - print(f"\n{COLOR_INFO}Using manual Steam setup workflow...{COLOR_RESET}") + # Step 3: Use SAME backend service as GUI + 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 - from ..handlers.shortcut_handler import ShortcutHandler - shortcut_handler = ShortcutHandler(steamdeck=self.steamdeck, verbose=False) - - # Create nxmhandler.ini to suppress NXM popup - shortcut_handler.write_nxmhandler_ini(install_dir_str, mo2_exe_path) - - # Create shortcut with working NativeSteamService - from ..services.native_steam_service import NativeSteamService - steam_service = NativeSteamService() - - success, app_id = steam_service.create_shortcut_with_proton( - app_name=shortcut_name, - exe_path=mo2_exe_path, - start_dir=os.path.dirname(mo2_exe_path), - launch_options="%command%", - tags=["Jackify"], - proton_version="proton_experimental" + # Create ModlistContext with engine_installed=True (same as GUI) + modlist_context = ModlistContext( + name=shortcut_name, + install_dir=Path(install_dir_str), + download_dir=Path(install_dir_str) / "downloads", # Standard location + game_type=self.context.get('detected_game', 'Unknown'), + nexus_api_key='', # Not needed for configuration + modlist_value=self.context.get('modlist_value', ''), + modlist_source=self.context.get('modlist_source', 'identifier'), + resolution=self.context.get('resolution'), + mo2_exe_path=Path(mo2_exe_path), + skip_confirmation=True, # Always skip confirmation in CLI + engine_installed=True # Skip path manipulation for engine workflows ) - if not success or not app_id: - self.logger.error("Failed to create Steam shortcut") - print(f"{COLOR_ERROR}Failed to create Steam shortcut. Check logs for details.{COLOR_RESET}") - return + # Add app_id to context + modlist_context.app_id = app_id - # Step 2: Handle Steam restart and manual steps (if not in GUI mode) - if not is_gui_mode: - 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) + # Step 4: Configure modlist using SAME service as GUI + modlist_service = ModlistService(self.system_info) # Add section header for configuration phase if progress callback is available if 'progress_callback' in locals() and progress_callback: progress_callback("") # Blank line for spacing - progress_callback("=== Configuring Modlist ===") + progress_callback("=== Configuration Phase ===") - self.logger.info("Running post-installation configuration phase") - configuration_success = modlist_menu.run_modlist_configuration_phase(config_context) + print(f"\n{COLOR_INFO}=== Configuration Phase ==={COLOR_RESET}") + 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: + print(f"{COLOR_SUCCESS}Configuration completed successfully!{COLOR_RESET}") self.logger.info("Post-installation configuration completed successfully") else: + print(f"{COLOR_WARNING}Configuration had some issues but completed.{COLOR_RESET}") self.logger.warning("Post-installation configuration had issues") else: # Game not supported for automated configuration @@ -1162,10 +1153,9 @@ class ModlistInstallCLI: # Section header now provided by GUI layer to avoid duplication try: - # Set GUI mode for backend operations + # CLI Install: keep original GUI mode (don't force GUI mode) import os original_gui_mode = os.environ.get('JACKIFY_GUI_MODE') - os.environ['JACKIFY_GUI_MODE'] = '1' try: # Build context for configuration @@ -1176,7 +1166,7 @@ class ModlistInstallCLI: 'modlist_value': context.get('modlist_value'), 'modlist_source': context.get('modlist_source'), 'resolution': context.get('resolution'), - 'skip_confirmation': True, # GUI mode is non-interactive + 'skip_confirmation': True, # CLI Install is non-interactive 'manual_steps_completed': False } diff --git a/jackify/backend/handlers/config_handler.py b/jackify/backend/handlers/config_handler.py index 6da81d8..65425b1 100644 --- a/jackify/backend/handlers/config_handler.py +++ b/jackify/backend/handlers/config_handler.py @@ -37,7 +37,8 @@ class ConfigHandler: "default_install_parent_dir": None, # Parent directory for modlist installations "default_download_parent_dir": None, # Parent directory for downloads "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 @@ -48,6 +49,12 @@ class ConfigHandler: self.settings["steam_path"] = self._detect_steam_path() # Save the updated settings 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): """ diff --git a/jackify/backend/handlers/modlist_handler.py b/jackify/backend/handlers/modlist_handler.py index 9741c88..dc88219 100644 --- a/jackify/backend/handlers/modlist_handler.py +++ b/jackify/backend/handlers/modlist_handler.py @@ -788,10 +788,16 @@ class ModlistHandler: status_callback(f"{self._get_progress_timestamp()} Updating resolution settings") # Ensure resolution_handler call uses correct args if needed # 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( modlist_dir=self.modlist_dir, 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.") print("Warning: Failed to update resolution settings.") @@ -818,12 +824,18 @@ class ModlistHandler: status_callback(f"{self._get_progress_timestamp()} Creating dxvk.conf file") self.logger.info("Step 10: Creating dxvk.conf file...") # 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( modlist_dir=self.modlist_dir, modlist_sdcard=self.modlist_sdcard, steam_library=str(self.steam_library) if self.steam_library else None, # Pass as string or None 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.") print("Warning: Failed to create dxvk.conf file.") diff --git a/jackify/backend/handlers/modlist_install_cli.py b/jackify/backend/handlers/modlist_install_cli.py index a0c2d0f..cd1dc99 100644 --- a/jackify/backend/handlers/modlist_install_cli.py +++ b/jackify/backend/handlers/modlist_install_cli.py @@ -616,7 +616,8 @@ class ModlistInstallCLI: if machineid: # Convert machineid to filename (e.g., "Tuxborn/Tuxborn" -> "Tuxborn.wabbajack") 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}") if modlist_value and modlist_value.endswith('.wabbajack') and os.path.isfile(modlist_value): diff --git a/jackify/backend/handlers/path_handler.py b/jackify/backend/handlers/path_handler.py index 2daab2c..67b1794 100644 --- a/jackify/backend/handlers/path_handler.py +++ b/jackify/backend/handlers/path_handler.py @@ -251,7 +251,7 @@ class PathHandler: return False @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 @@ -261,6 +261,7 @@ class PathHandler: steam_library (str): Path to the Steam library 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") + vanilla_game_dir (str): Optional path to vanilla game directory for fallback Returns: bool: True on success, False on failure @@ -271,25 +272,35 @@ class PathHandler: # Determine the location for dxvk.conf dxvk_conf_path = None - # Check for common stock game directories + # Check for common stock game directories first, then vanilla as fallback stock_game_paths = [ 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, "STOCK GAME"), + os.path.join(modlist_dir, "Stock Game Folder"), os.path.join(modlist_dir, "Stock Folder"), os.path.join(modlist_dir, "Skyrim Stock"), - os.path.join(modlist_dir, "root", "Skyrim Special Edition"), - os.path.join(steam_library, game_var_full) + os.path.join(modlist_dir, "root", "Skyrim Special Edition") ] + # 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: if os.path.exists(path): dxvk_conf_path = os.path.join(path, "dxvk.conf") break if not dxvk_conf_path: - logger.error("Could not determine location for dxvk.conf") - return False + # Fallback: Try vanilla game directory if provided + 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 required_line = "dxvk.enableGraphicsPipelineLibrary = False" @@ -773,6 +784,21 @@ class PathHandler: return False with open(modlist_ini_path, 'r', encoding='utf-8') as f: 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 binary_paths_updated = 0 working_dirs_updated = 0 @@ -791,9 +817,16 @@ class PathHandler: backslash_style = wd_match.group(2) working_dir_lines.append((i, stripped, index, backslash_style)) binary_paths_by_index = {} - # Use provided steam_libraries if available, else detect - if steam_libraries is None or not steam_libraries: + # Use existing gamePath to determine correct Steam library, fallback to detection + 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() + logger.debug(f"Fallback to detected Steam libraries: {steam_libraries}") for i, line, index, backslash_style in binary_lines: parts = line.split('=', 1) if len(parts) != 2: diff --git a/jackify/backend/handlers/resolution_handler.py b/jackify/backend/handlers/resolution_handler.py index 7abee18..799e6c0 100644 --- a/jackify/backend/handlers/resolution_handler.py +++ b/jackify/backend/handlers/resolution_handler.py @@ -149,7 +149,7 @@ class ResolutionHandler: return ["1280x720", "1280x800", "1920x1080", "1920x1200", "2560x1440"] @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. @@ -157,6 +157,7 @@ class ResolutionHandler: modlist_dir (str): Path to the modlist directory. game_var (str): The game identifier (e.g., "Skyrim Special Edition", "Fallout 4"). set_res (str): The desired resolution (e.g., "1920x1080"). + vanilla_game_dir (str): Optional path to vanilla game directory for fallback. Returns: bool: True if successful or not applicable, False on error. @@ -211,22 +212,30 @@ class ResolutionHandler: logger.debug(f"Processing {prefs_filenames}...") prefs_files_found = [] - # Search common locations: profiles/, stock game dirs - search_dirs = [modlist_path / "profiles"] - # Add potential stock game directories dynamically (case-insensitive) - potential_stock_dirs = [d for d in modlist_path.iterdir() if d.is_dir() and - d.name.lower() in ["stock game", "game root", "stock folder", "skyrim stock"]] # Add more if needed - search_dirs.extend(potential_stock_dirs) - - 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))) + # Search entire modlist directory recursively for all target files + logger.debug(f"Searching entire modlist directory for: {prefs_filenames}") + for fname in prefs_filenames: + found_files = list(modlist_path.rglob(fname)) + prefs_files_found.extend(found_files) + if found_files: + logger.debug(f"Found {len(found_files)} {fname} files: {[str(f) for f in found_files]}") 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.") - # Consider this success as the main operation didn't fail? - return True + logger.warning(f"No preference files ({prefs_filenames}) found in modlist directory.") + + # 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: files_processed += 1 @@ -314,19 +323,23 @@ class ResolutionHandler: new_lines = [] 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: 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) 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) modified = True else: diff --git a/jackify/engine/Wabbajack.CLI.Builder.dll b/jackify/engine/Wabbajack.CLI.Builder.dll index f24ff8d..e40be4c 100644 Binary files a/jackify/engine/Wabbajack.CLI.Builder.dll and b/jackify/engine/Wabbajack.CLI.Builder.dll differ diff --git a/jackify/engine/Wabbajack.Common.dll b/jackify/engine/Wabbajack.Common.dll index c763518..cc54e48 100644 Binary files a/jackify/engine/Wabbajack.Common.dll and b/jackify/engine/Wabbajack.Common.dll differ diff --git a/jackify/engine/Wabbajack.Compiler.dll b/jackify/engine/Wabbajack.Compiler.dll index 48f45d9..9a74999 100644 Binary files a/jackify/engine/Wabbajack.Compiler.dll and b/jackify/engine/Wabbajack.Compiler.dll differ diff --git a/jackify/engine/Wabbajack.Compression.BSA.dll b/jackify/engine/Wabbajack.Compression.BSA.dll index 5b490b2..6c9be8a 100644 Binary files a/jackify/engine/Wabbajack.Compression.BSA.dll and b/jackify/engine/Wabbajack.Compression.BSA.dll differ diff --git a/jackify/engine/Wabbajack.Compression.Zip.dll b/jackify/engine/Wabbajack.Compression.Zip.dll index 90ad292..a2d75a2 100644 Binary files a/jackify/engine/Wabbajack.Compression.Zip.dll and b/jackify/engine/Wabbajack.Compression.Zip.dll differ diff --git a/jackify/engine/Wabbajack.Configuration.dll b/jackify/engine/Wabbajack.Configuration.dll index f64e986..0855672 100644 Binary files a/jackify/engine/Wabbajack.Configuration.dll and b/jackify/engine/Wabbajack.Configuration.dll differ diff --git a/jackify/engine/Wabbajack.DTOs.dll b/jackify/engine/Wabbajack.DTOs.dll index fe84a4a..63a953a 100644 Binary files a/jackify/engine/Wabbajack.DTOs.dll and b/jackify/engine/Wabbajack.DTOs.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Bethesda.dll b/jackify/engine/Wabbajack.Downloaders.Bethesda.dll index 7225b3d..80b0eb3 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Bethesda.dll and b/jackify/engine/Wabbajack.Downloaders.Bethesda.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Dispatcher.dll b/jackify/engine/Wabbajack.Downloaders.Dispatcher.dll index ef4fe6f..915bcb7 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Dispatcher.dll and b/jackify/engine/Wabbajack.Downloaders.Dispatcher.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.GameFile.dll b/jackify/engine/Wabbajack.Downloaders.GameFile.dll index 9f3e15f..84faeee 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.GameFile.dll and b/jackify/engine/Wabbajack.Downloaders.GameFile.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll b/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll index ffc43d6..1f09c62 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll and b/jackify/engine/Wabbajack.Downloaders.GoogleDrive.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Http.dll b/jackify/engine/Wabbajack.Downloaders.Http.dll index afad480..b9f70ac 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Http.dll and b/jackify/engine/Wabbajack.Downloaders.Http.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll b/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll index b5a1881..f6853a7 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll and b/jackify/engine/Wabbajack.Downloaders.IPS4OAuth2Downloader.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Interfaces.dll b/jackify/engine/Wabbajack.Downloaders.Interfaces.dll index 9c9eb91..41cedad 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Interfaces.dll and b/jackify/engine/Wabbajack.Downloaders.Interfaces.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Manual.dll b/jackify/engine/Wabbajack.Downloaders.Manual.dll index 5a680af..dc2c504 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Manual.dll and b/jackify/engine/Wabbajack.Downloaders.Manual.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.MediaFire.dll b/jackify/engine/Wabbajack.Downloaders.MediaFire.dll index fe0dac6..963c733 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.MediaFire.dll and b/jackify/engine/Wabbajack.Downloaders.MediaFire.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Mega.dll b/jackify/engine/Wabbajack.Downloaders.Mega.dll index 411cfdf..b6c3388 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Mega.dll and b/jackify/engine/Wabbajack.Downloaders.Mega.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.ModDB.dll b/jackify/engine/Wabbajack.Downloaders.ModDB.dll index b16a7ff..f1d4c93 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.ModDB.dll and b/jackify/engine/Wabbajack.Downloaders.ModDB.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.Nexus.dll b/jackify/engine/Wabbajack.Downloaders.Nexus.dll index 169b90e..0f1aa3c 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.Nexus.dll and b/jackify/engine/Wabbajack.Downloaders.Nexus.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll b/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll index 1690780..0d32d9a 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll and b/jackify/engine/Wabbajack.Downloaders.VerificationCache.dll differ diff --git a/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll b/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll index b7c3ad9..7203a01 100644 Binary files a/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll and b/jackify/engine/Wabbajack.Downloaders.WabbajackCDN.dll differ diff --git a/jackify/engine/Wabbajack.FileExtractor.dll b/jackify/engine/Wabbajack.FileExtractor.dll index ac3cc15..eab09a9 100644 Binary files a/jackify/engine/Wabbajack.FileExtractor.dll and b/jackify/engine/Wabbajack.FileExtractor.dll differ diff --git a/jackify/engine/Wabbajack.Hashing.PHash.dll b/jackify/engine/Wabbajack.Hashing.PHash.dll index 440f92c..6746836 100644 Binary files a/jackify/engine/Wabbajack.Hashing.PHash.dll and b/jackify/engine/Wabbajack.Hashing.PHash.dll differ diff --git a/jackify/engine/Wabbajack.Hashing.xxHash64.dll b/jackify/engine/Wabbajack.Hashing.xxHash64.dll index 6d48caa..2c1459f 100644 Binary files a/jackify/engine/Wabbajack.Hashing.xxHash64.dll and b/jackify/engine/Wabbajack.Hashing.xxHash64.dll differ diff --git a/jackify/engine/Wabbajack.IO.Async.dll b/jackify/engine/Wabbajack.IO.Async.dll index 30039f6..94d05b6 100644 Binary files a/jackify/engine/Wabbajack.IO.Async.dll and b/jackify/engine/Wabbajack.IO.Async.dll differ diff --git a/jackify/engine/Wabbajack.Installer.dll b/jackify/engine/Wabbajack.Installer.dll index 509205f..1dd7ba0 100644 Binary files a/jackify/engine/Wabbajack.Installer.dll and b/jackify/engine/Wabbajack.Installer.dll differ diff --git a/jackify/engine/Wabbajack.Networking.BethesdaNet.dll b/jackify/engine/Wabbajack.Networking.BethesdaNet.dll index cc398bb..282661a 100644 Binary files a/jackify/engine/Wabbajack.Networking.BethesdaNet.dll and b/jackify/engine/Wabbajack.Networking.BethesdaNet.dll differ diff --git a/jackify/engine/Wabbajack.Networking.Discord.dll b/jackify/engine/Wabbajack.Networking.Discord.dll index 2c7f155..eb6ca84 100644 Binary files a/jackify/engine/Wabbajack.Networking.Discord.dll and b/jackify/engine/Wabbajack.Networking.Discord.dll differ diff --git a/jackify/engine/Wabbajack.Networking.GitHub.dll b/jackify/engine/Wabbajack.Networking.GitHub.dll index 0e8a74c..ff9c9c6 100644 Binary files a/jackify/engine/Wabbajack.Networking.GitHub.dll and b/jackify/engine/Wabbajack.Networking.GitHub.dll differ diff --git a/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll b/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll index 40f681d..76708eb 100644 Binary files a/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll and b/jackify/engine/Wabbajack.Networking.Http.Interfaces.dll differ diff --git a/jackify/engine/Wabbajack.Networking.Http.dll b/jackify/engine/Wabbajack.Networking.Http.dll index 9934bf9..e9b54aa 100644 Binary files a/jackify/engine/Wabbajack.Networking.Http.dll and b/jackify/engine/Wabbajack.Networking.Http.dll differ diff --git a/jackify/engine/Wabbajack.Networking.NexusApi.dll b/jackify/engine/Wabbajack.Networking.NexusApi.dll index 8be85e1..863cf3b 100644 Binary files a/jackify/engine/Wabbajack.Networking.NexusApi.dll and b/jackify/engine/Wabbajack.Networking.NexusApi.dll differ diff --git a/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll b/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll index 1340748..14a5ec6 100644 Binary files a/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll and b/jackify/engine/Wabbajack.Networking.WabbajackClientApi.dll differ diff --git a/jackify/engine/Wabbajack.Paths.IO.dll b/jackify/engine/Wabbajack.Paths.IO.dll index 8d36bcc..f25da12 100644 Binary files a/jackify/engine/Wabbajack.Paths.IO.dll and b/jackify/engine/Wabbajack.Paths.IO.dll differ diff --git a/jackify/engine/Wabbajack.Paths.dll b/jackify/engine/Wabbajack.Paths.dll index 8b4a8e8..debdb38 100644 Binary files a/jackify/engine/Wabbajack.Paths.dll and b/jackify/engine/Wabbajack.Paths.dll differ diff --git a/jackify/engine/Wabbajack.RateLimiter.dll b/jackify/engine/Wabbajack.RateLimiter.dll index c9d590a..f77cd11 100644 Binary files a/jackify/engine/Wabbajack.RateLimiter.dll and b/jackify/engine/Wabbajack.RateLimiter.dll differ diff --git a/jackify/engine/Wabbajack.Server.Lib.dll b/jackify/engine/Wabbajack.Server.Lib.dll index 28b4fac..3eab3e0 100644 Binary files a/jackify/engine/Wabbajack.Server.Lib.dll and b/jackify/engine/Wabbajack.Server.Lib.dll differ diff --git a/jackify/engine/Wabbajack.Services.OSIntegrated.dll b/jackify/engine/Wabbajack.Services.OSIntegrated.dll index ab1a9ad..86f5a2b 100644 Binary files a/jackify/engine/Wabbajack.Services.OSIntegrated.dll and b/jackify/engine/Wabbajack.Services.OSIntegrated.dll differ diff --git a/jackify/engine/Wabbajack.VFS.Interfaces.dll b/jackify/engine/Wabbajack.VFS.Interfaces.dll index 082ac65..9eb2c70 100644 Binary files a/jackify/engine/Wabbajack.VFS.Interfaces.dll and b/jackify/engine/Wabbajack.VFS.Interfaces.dll differ diff --git a/jackify/engine/Wabbajack.VFS.dll b/jackify/engine/Wabbajack.VFS.dll index bbf8192..44c1077 100644 Binary files a/jackify/engine/Wabbajack.VFS.dll and b/jackify/engine/Wabbajack.VFS.dll differ diff --git a/jackify/engine/jackify-engine.deps.json b/jackify/engine/jackify-engine.deps.json index 918a38d..0e07f79 100644 --- a/jackify/engine/jackify-engine.deps.json +++ b/jackify/engine/jackify-engine.deps.json @@ -7,7 +7,7 @@ "targets": { ".NETCoreApp,Version=v8.0": {}, ".NETCoreApp,Version=v8.0/linux-x64": { - "jackify-engine/0.3.13": { + "jackify-engine/0.3.14": { "dependencies": { "Markdig": "0.40.0", "Microsoft.Extensions.Configuration.Json": "9.0.1", @@ -22,16 +22,16 @@ "SixLabors.ImageSharp": "3.1.6", "System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", - "Wabbajack.CLI.Builder": "0.3.13", - "Wabbajack.Downloaders.Bethesda": "0.3.13", - "Wabbajack.Downloaders.Dispatcher": "0.3.13", - "Wabbajack.Hashing.xxHash64": "0.3.13", - "Wabbajack.Networking.Discord": "0.3.13", - "Wabbajack.Networking.GitHub": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13", - "Wabbajack.Server.Lib": "0.3.13", - "Wabbajack.Services.OSIntegrated": "0.3.13", - "Wabbajack.VFS": "0.3.13", + "Wabbajack.CLI.Builder": "0.3.14", + "Wabbajack.Downloaders.Bethesda": "0.3.14", + "Wabbajack.Downloaders.Dispatcher": "0.3.14", + "Wabbajack.Hashing.xxHash64": "0.3.14", + "Wabbajack.Networking.Discord": "0.3.14", + "Wabbajack.Networking.GitHub": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14", + "Wabbajack.Server.Lib": "0.3.14", + "Wabbajack.Services.OSIntegrated": "0.3.14", + "Wabbajack.VFS": "0.3.14", "MegaApiClient": "1.0.0.0", "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": { "Microsoft.Extensions.Configuration.Json": "9.0.1", "Microsoft.Extensions.DependencyInjection": "9.0.1", @@ -1791,109 +1791,109 @@ "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "System.CommandLine": "2.0.0-beta4.22272.1", "System.CommandLine.NamingConventionBinder": "2.0.0-beta4.22272.1", - "Wabbajack.Paths": "0.3.13" + "Wabbajack.Paths": "0.3.14" }, "runtime": { "Wabbajack.CLI.Builder.dll": {} } }, - "Wabbajack.Common/0.3.13": { + "Wabbajack.Common/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "System.Reactive": "6.0.1", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Networking.Http": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13" + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Networking.Http": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14" }, "runtime": { "Wabbajack.Common.dll": {} } }, - "Wabbajack.Compiler/0.3.13": { + "Wabbajack.Compiler/0.3.14": { "dependencies": { "F23.StringSimilarity": "6.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Dispatcher": "0.3.13", - "Wabbajack.Installer": "0.3.13", - "Wabbajack.VFS": "0.3.13", + "Wabbajack.Downloaders.Dispatcher": "0.3.14", + "Wabbajack.Installer": "0.3.14", + "Wabbajack.VFS": "0.3.14", "ini-parser-netstandard": "2.5.2" }, "runtime": { "Wabbajack.Compiler.dll": {} } }, - "Wabbajack.Compression.BSA/0.3.13": { + "Wabbajack.Compression.BSA/0.3.14": { "dependencies": { "K4os.Compression.LZ4.Streams": "1.3.8", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SharpZipLib": "1.4.2", - "Wabbajack.Common": "0.3.13", - "Wabbajack.DTOs": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.DTOs": "0.3.14" }, "runtime": { "Wabbajack.Compression.BSA.dll": {} } }, - "Wabbajack.Compression.Zip/0.3.13": { + "Wabbajack.Compression.Zip/0.3.14": { "dependencies": { - "Wabbajack.IO.Async": "0.3.13" + "Wabbajack.IO.Async": "0.3.14" }, "runtime": { "Wabbajack.Compression.Zip.dll": {} } }, - "Wabbajack.Configuration/0.3.13": { + "Wabbajack.Configuration/0.3.14": { "runtime": { "Wabbajack.Configuration.dll": {} } }, - "Wabbajack.Downloaders.Bethesda/0.3.13": { + "Wabbajack.Downloaders.Bethesda/0.3.14": { "dependencies": { "LibAES-CTR": "1.1.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SharpZipLib": "1.4.2", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Networking.BethesdaNet": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Networking.BethesdaNet": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.Bethesda.dll": {} } }, - "Wabbajack.Downloaders.Dispatcher/0.3.13": { + "Wabbajack.Downloaders.Dispatcher/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Bethesda": "0.3.13", - "Wabbajack.Downloaders.GameFile": "0.3.13", - "Wabbajack.Downloaders.GoogleDrive": "0.3.13", - "Wabbajack.Downloaders.Http": "0.3.13", - "Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Downloaders.Manual": "0.3.13", - "Wabbajack.Downloaders.MediaFire": "0.3.13", - "Wabbajack.Downloaders.Mega": "0.3.13", - "Wabbajack.Downloaders.ModDB": "0.3.13", - "Wabbajack.Downloaders.Nexus": "0.3.13", - "Wabbajack.Downloaders.VerificationCache": "0.3.13", - "Wabbajack.Downloaders.WabbajackCDN": "0.3.13", - "Wabbajack.Networking.WabbajackClientApi": "0.3.13" + "Wabbajack.Downloaders.Bethesda": "0.3.14", + "Wabbajack.Downloaders.GameFile": "0.3.14", + "Wabbajack.Downloaders.GoogleDrive": "0.3.14", + "Wabbajack.Downloaders.Http": "0.3.14", + "Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Downloaders.Manual": "0.3.14", + "Wabbajack.Downloaders.MediaFire": "0.3.14", + "Wabbajack.Downloaders.Mega": "0.3.14", + "Wabbajack.Downloaders.ModDB": "0.3.14", + "Wabbajack.Downloaders.Nexus": "0.3.14", + "Wabbajack.Downloaders.VerificationCache": "0.3.14", + "Wabbajack.Downloaders.WabbajackCDN": "0.3.14", + "Wabbajack.Networking.WabbajackClientApi": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.Dispatcher.dll": {} } }, - "Wabbajack.Downloaders.GameFile/0.3.13": { + "Wabbajack.Downloaders.GameFile/0.3.14": { "dependencies": { "GameFinder.StoreHandlers.EADesktop": "4.5.0", "GameFinder.StoreHandlers.EGS": "4.5.0", @@ -1903,360 +1903,360 @@ "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.VFS": "0.3.13" + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.VFS": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.GameFile.dll": {} } }, - "Wabbajack.Downloaders.GoogleDrive/0.3.13": { + "Wabbajack.Downloaders.GoogleDrive/0.3.14": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.AspNetCore.Http.Extensions": "2.3.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.13", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Networking.Http": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Networking.Http": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.GoogleDrive.dll": {} } }, - "Wabbajack.Downloaders.Http/0.3.13": { + "Wabbajack.Downloaders.Http/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.13", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Networking.BethesdaNet": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Networking.BethesdaNet": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.Http.dll": {} } }, - "Wabbajack.Downloaders.Interfaces/0.3.13": { + "Wabbajack.Downloaders.Interfaces/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.Compression.Zip": "0.3.13", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13" + "Wabbajack.Compression.Zip": "0.3.14", + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.Interfaces.dll": {} } }, - "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.13": { + "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.14": { "dependencies": { "F23.StringSimilarity": "6.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Networking.Http": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Networking.Http": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {} } }, - "Wabbajack.Downloaders.Manual/0.3.13": { + "Wabbajack.Downloaders.Manual/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.Manual.dll": {} } }, - "Wabbajack.Downloaders.MediaFire/0.3.13": { + "Wabbajack.Downloaders.MediaFire/0.3.14": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.MediaFire.dll": {} } }, - "Wabbajack.Downloaders.Mega/0.3.13": { + "Wabbajack.Downloaders.Mega/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.Mega.dll": {} } }, - "Wabbajack.Downloaders.ModDB/0.3.13": { + "Wabbajack.Downloaders.ModDB/0.3.14": { "dependencies": { "HtmlAgilityPack": "1.11.72", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Networking.Http": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Networking.Http": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.ModDB.dll": {} } }, - "Wabbajack.Downloaders.Nexus/0.3.13": { + "Wabbajack.Downloaders.Nexus/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Hashing.xxHash64": "0.3.13", - "Wabbajack.Networking.Http": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13", - "Wabbajack.Networking.NexusApi": "0.3.13", - "Wabbajack.Paths": "0.3.13" + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Hashing.xxHash64": "0.3.14", + "Wabbajack.Networking.Http": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14", + "Wabbajack.Networking.NexusApi": "0.3.14", + "Wabbajack.Paths": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.Nexus.dll": {} } }, - "Wabbajack.Downloaders.VerificationCache/0.3.13": { + "Wabbajack.Downloaders.VerificationCache/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Stub.System.Data.SQLite.Core.NetStandard": "1.0.119", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13" + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.VerificationCache.dll": {} } }, - "Wabbajack.Downloaders.WabbajackCDN/0.3.13": { + "Wabbajack.Downloaders.WabbajackCDN/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Microsoft.Toolkit.HighPerformance": "7.1.2", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Networking.Http": "0.3.13", - "Wabbajack.RateLimiter": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Networking.Http": "0.3.14", + "Wabbajack.RateLimiter": "0.3.14" }, "runtime": { "Wabbajack.Downloaders.WabbajackCDN.dll": {} } }, - "Wabbajack.DTOs/0.3.13": { + "Wabbajack.DTOs/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.Hashing.xxHash64": "0.3.13", - "Wabbajack.Paths": "0.3.13" + "Wabbajack.Hashing.xxHash64": "0.3.14", + "Wabbajack.Paths": "0.3.14" }, "runtime": { "Wabbajack.DTOs.dll": {} } }, - "Wabbajack.FileExtractor/0.3.13": { + "Wabbajack.FileExtractor/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "OMODFramework": "3.0.1", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Compression.BSA": "0.3.13", - "Wabbajack.Hashing.PHash": "0.3.13", - "Wabbajack.Paths": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Compression.BSA": "0.3.14", + "Wabbajack.Hashing.PHash": "0.3.14", + "Wabbajack.Paths": "0.3.14" }, "runtime": { "Wabbajack.FileExtractor.dll": {} } }, - "Wabbajack.Hashing.PHash/0.3.13": { + "Wabbajack.Hashing.PHash/0.3.14": { "dependencies": { "BCnEncoder.Net.ImageSharp": "1.1.1", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Shipwreck.Phash": "0.5.0", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Common": "0.3.13", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Paths": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Paths": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14" }, "runtime": { "Wabbajack.Hashing.PHash.dll": {} } }, - "Wabbajack.Hashing.xxHash64/0.3.13": { + "Wabbajack.Hashing.xxHash64/0.3.14": { "dependencies": { - "Wabbajack.Paths": "0.3.13", - "Wabbajack.RateLimiter": "0.3.13" + "Wabbajack.Paths": "0.3.14", + "Wabbajack.RateLimiter": "0.3.14" }, "runtime": { "Wabbajack.Hashing.xxHash64.dll": {} } }, - "Wabbajack.Installer/0.3.13": { + "Wabbajack.Installer/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "Octopus.Octodiff": "2.0.548", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Downloaders.Dispatcher": "0.3.13", - "Wabbajack.Downloaders.GameFile": "0.3.13", - "Wabbajack.FileExtractor": "0.3.13", - "Wabbajack.Networking.WabbajackClientApi": "0.3.13", - "Wabbajack.Paths": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13", - "Wabbajack.VFS": "0.3.13", + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Downloaders.Dispatcher": "0.3.14", + "Wabbajack.Downloaders.GameFile": "0.3.14", + "Wabbajack.FileExtractor": "0.3.14", + "Wabbajack.Networking.WabbajackClientApi": "0.3.14", + "Wabbajack.Paths": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14", + "Wabbajack.VFS": "0.3.14", "ini-parser-netstandard": "2.5.2" }, "runtime": { "Wabbajack.Installer.dll": {} } }, - "Wabbajack.IO.Async/0.3.13": { + "Wabbajack.IO.Async/0.3.14": { "runtime": { "Wabbajack.IO.Async.dll": {} } }, - "Wabbajack.Networking.BethesdaNet/0.3.13": { + "Wabbajack.Networking.BethesdaNet/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Networking.Http": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13" + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Networking.Http": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14" }, "runtime": { "Wabbajack.Networking.BethesdaNet.dll": {} } }, - "Wabbajack.Networking.Discord/0.3.13": { + "Wabbajack.Networking.Discord/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection.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": { "Wabbajack.Networking.Discord.dll": {} } }, - "Wabbajack.Networking.GitHub/0.3.13": { + "Wabbajack.Networking.GitHub/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Octokit": "14.0.0", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13" + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14" }, "runtime": { "Wabbajack.Networking.GitHub.dll": {} } }, - "Wabbajack.Networking.Http/0.3.13": { + "Wabbajack.Networking.Http/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Http": "9.0.1", "Microsoft.Extensions.Logging": "9.0.1", - "Wabbajack.Configuration": "0.3.13", - "Wabbajack.Downloaders.Interfaces": "0.3.13", - "Wabbajack.Hashing.xxHash64": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13", - "Wabbajack.Paths": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13" + "Wabbajack.Configuration": "0.3.14", + "Wabbajack.Downloaders.Interfaces": "0.3.14", + "Wabbajack.Hashing.xxHash64": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14", + "Wabbajack.Paths": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14" }, "runtime": { "Wabbajack.Networking.Http.dll": {} } }, - "Wabbajack.Networking.Http.Interfaces/0.3.13": { + "Wabbajack.Networking.Http.Interfaces/0.3.14": { "dependencies": { - "Wabbajack.Hashing.xxHash64": "0.3.13" + "Wabbajack.Hashing.xxHash64": "0.3.14" }, "runtime": { "Wabbajack.Networking.Http.Interfaces.dll": {} } }, - "Wabbajack.Networking.NexusApi/0.3.13": { + "Wabbajack.Networking.NexusApi/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Networking.Http": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13", - "Wabbajack.Networking.WabbajackClientApi": "0.3.13" + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Networking.Http": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14", + "Wabbajack.Networking.WabbajackClientApi": "0.3.14" }, "runtime": { "Wabbajack.Networking.NexusApi.dll": {} } }, - "Wabbajack.Networking.WabbajackClientApi/0.3.13": { + "Wabbajack.Networking.WabbajackClientApi/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "Octokit": "14.0.0", - "Wabbajack.Common": "0.3.13", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13", - "Wabbajack.VFS.Interfaces": "0.3.13", + "Wabbajack.Common": "0.3.14", + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14", + "Wabbajack.VFS.Interfaces": "0.3.14", "YamlDotNet": "16.3.0" }, "runtime": { "Wabbajack.Networking.WabbajackClientApi.dll": {} } }, - "Wabbajack.Paths/0.3.13": { + "Wabbajack.Paths/0.3.14": { "runtime": { "Wabbajack.Paths.dll": {} } }, - "Wabbajack.Paths.IO/0.3.13": { + "Wabbajack.Paths.IO/0.3.14": { "dependencies": { - "Wabbajack.Paths": "0.3.13", + "Wabbajack.Paths": "0.3.14", "shortid": "4.0.0" }, "runtime": { "Wabbajack.Paths.IO.dll": {} } }, - "Wabbajack.RateLimiter/0.3.13": { + "Wabbajack.RateLimiter/0.3.14": { "runtime": { "Wabbajack.RateLimiter.dll": {} } }, - "Wabbajack.Server.Lib/0.3.13": { + "Wabbajack.Server.Lib/0.3.14": { "dependencies": { "FluentFTP": "52.0.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", @@ -2264,58 +2264,58 @@ "Nettle": "3.0.0", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Common": "0.3.13", - "Wabbajack.Networking.Http.Interfaces": "0.3.13", - "Wabbajack.Services.OSIntegrated": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.Networking.Http.Interfaces": "0.3.14", + "Wabbajack.Services.OSIntegrated": "0.3.14" }, "runtime": { "Wabbajack.Server.Lib.dll": {} } }, - "Wabbajack.Services.OSIntegrated/0.3.13": { + "Wabbajack.Services.OSIntegrated/0.3.14": { "dependencies": { "DeviceId": "6.8.0", "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Newtonsoft.Json": "13.0.3", "SixLabors.ImageSharp": "3.1.6", - "Wabbajack.Compiler": "0.3.13", - "Wabbajack.Downloaders.Dispatcher": "0.3.13", - "Wabbajack.Installer": "0.3.13", - "Wabbajack.Networking.BethesdaNet": "0.3.13", - "Wabbajack.Networking.Discord": "0.3.13", - "Wabbajack.VFS": "0.3.13" + "Wabbajack.Compiler": "0.3.14", + "Wabbajack.Downloaders.Dispatcher": "0.3.14", + "Wabbajack.Installer": "0.3.14", + "Wabbajack.Networking.BethesdaNet": "0.3.14", + "Wabbajack.Networking.Discord": "0.3.14", + "Wabbajack.VFS": "0.3.14" }, "runtime": { "Wabbajack.Services.OSIntegrated.dll": {} } }, - "Wabbajack.VFS/0.3.13": { + "Wabbajack.VFS/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", "Microsoft.Extensions.Logging.Abstractions": "9.0.1", "SixLabors.ImageSharp": "3.1.6", "System.Data.SQLite.Core": "1.0.119", - "Wabbajack.Common": "0.3.13", - "Wabbajack.FileExtractor": "0.3.13", - "Wabbajack.Hashing.PHash": "0.3.13", - "Wabbajack.Hashing.xxHash64": "0.3.13", - "Wabbajack.Paths": "0.3.13", - "Wabbajack.Paths.IO": "0.3.13", - "Wabbajack.VFS.Interfaces": "0.3.13" + "Wabbajack.Common": "0.3.14", + "Wabbajack.FileExtractor": "0.3.14", + "Wabbajack.Hashing.PHash": "0.3.14", + "Wabbajack.Hashing.xxHash64": "0.3.14", + "Wabbajack.Paths": "0.3.14", + "Wabbajack.Paths.IO": "0.3.14", + "Wabbajack.VFS.Interfaces": "0.3.14" }, "runtime": { "Wabbajack.VFS.dll": {} } }, - "Wabbajack.VFS.Interfaces/0.3.13": { + "Wabbajack.VFS.Interfaces/0.3.14": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.1", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1", - "Wabbajack.DTOs": "0.3.13", - "Wabbajack.Hashing.xxHash64": "0.3.13", - "Wabbajack.Paths": "0.3.13" + "Wabbajack.DTOs": "0.3.14", + "Wabbajack.Hashing.xxHash64": "0.3.14", + "Wabbajack.Paths": "0.3.14" }, "runtime": { "Wabbajack.VFS.Interfaces.dll": {} @@ -2332,7 +2332,7 @@ } }, "libraries": { - "jackify-engine/0.3.13": { + "jackify-engine/0.3.14": { "type": "project", "serviceable": false, "sha512": "" @@ -3021,202 +3021,202 @@ "path": "yamldotnet/16.3.0", "hashPath": "yamldotnet.16.3.0.nupkg.sha512" }, - "Wabbajack.CLI.Builder/0.3.13": { + "Wabbajack.CLI.Builder/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Common/0.3.13": { + "Wabbajack.Common/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compiler/0.3.13": { + "Wabbajack.Compiler/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compression.BSA/0.3.13": { + "Wabbajack.Compression.BSA/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Compression.Zip/0.3.13": { + "Wabbajack.Compression.Zip/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Configuration/0.3.13": { + "Wabbajack.Configuration/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Bethesda/0.3.13": { + "Wabbajack.Downloaders.Bethesda/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Dispatcher/0.3.13": { + "Wabbajack.Downloaders.Dispatcher/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.GameFile/0.3.13": { + "Wabbajack.Downloaders.GameFile/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.GoogleDrive/0.3.13": { + "Wabbajack.Downloaders.GoogleDrive/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Http/0.3.13": { + "Wabbajack.Downloaders.Http/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Interfaces/0.3.13": { + "Wabbajack.Downloaders.Interfaces/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.13": { + "Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Manual/0.3.13": { + "Wabbajack.Downloaders.Manual/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.MediaFire/0.3.13": { + "Wabbajack.Downloaders.MediaFire/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Mega/0.3.13": { + "Wabbajack.Downloaders.Mega/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.ModDB/0.3.13": { + "Wabbajack.Downloaders.ModDB/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.Nexus/0.3.13": { + "Wabbajack.Downloaders.Nexus/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.VerificationCache/0.3.13": { + "Wabbajack.Downloaders.VerificationCache/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Downloaders.WabbajackCDN/0.3.13": { + "Wabbajack.Downloaders.WabbajackCDN/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.DTOs/0.3.13": { + "Wabbajack.DTOs/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.FileExtractor/0.3.13": { + "Wabbajack.FileExtractor/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Hashing.PHash/0.3.13": { + "Wabbajack.Hashing.PHash/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Hashing.xxHash64/0.3.13": { + "Wabbajack.Hashing.xxHash64/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Installer/0.3.13": { + "Wabbajack.Installer/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.IO.Async/0.3.13": { + "Wabbajack.IO.Async/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.BethesdaNet/0.3.13": { + "Wabbajack.Networking.BethesdaNet/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Discord/0.3.13": { + "Wabbajack.Networking.Discord/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.GitHub/0.3.13": { + "Wabbajack.Networking.GitHub/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Http/0.3.13": { + "Wabbajack.Networking.Http/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.Http.Interfaces/0.3.13": { + "Wabbajack.Networking.Http.Interfaces/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.NexusApi/0.3.13": { + "Wabbajack.Networking.NexusApi/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Networking.WabbajackClientApi/0.3.13": { + "Wabbajack.Networking.WabbajackClientApi/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Paths/0.3.13": { + "Wabbajack.Paths/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Paths.IO/0.3.13": { + "Wabbajack.Paths.IO/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.RateLimiter/0.3.13": { + "Wabbajack.RateLimiter/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Server.Lib/0.3.13": { + "Wabbajack.Server.Lib/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.Services.OSIntegrated/0.3.13": { + "Wabbajack.Services.OSIntegrated/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.VFS/0.3.13": { + "Wabbajack.VFS/0.3.14": { "type": "project", "serviceable": false, "sha512": "" }, - "Wabbajack.VFS.Interfaces/0.3.13": { + "Wabbajack.VFS.Interfaces/0.3.14": { "type": "project", "serviceable": false, "sha512": "" diff --git a/jackify/engine/jackify-engine.dll b/jackify/engine/jackify-engine.dll index 6c78cb2..14aa184 100644 Binary files a/jackify/engine/jackify-engine.dll and b/jackify/engine/jackify-engine.dll differ diff --git a/jackify/frontends/cli/menus/wabbajack_menu.py b/jackify/frontends/cli/menus/wabbajack_menu.py index f1f35fa..3530344 100644 --- a/jackify/frontends/cli/menus/wabbajack_menu.py +++ b/jackify/frontends/cli/menus/wabbajack_menu.py @@ -32,9 +32,9 @@ class WabbajackMenuHandler: print_section_header("Modlist and Wabbajack Tasks") 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_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_ACTION}→ Modlist already in Steam? Re-configure it here{COLOR_RESET}") # HIDDEN FOR FIRST RELEASE - UNCOMMENT WHEN READY diff --git a/jackify/frontends/gui/dialogs/about_dialog.py b/jackify/frontends/gui/dialogs/about_dialog.py new file mode 100644 index 0000000..5d8a0a2 --- /dev/null +++ b/jackify/frontends/gui/dialogs/about_dialog.py @@ -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() \ No newline at end of file diff --git a/jackify/frontends/gui/main.py b/jackify/frontends/gui/main.py index b613ca0..a131251 100644 --- a/jackify/frontends/gui/main.py +++ b/jackify/frontends/gui/main.py @@ -325,6 +325,29 @@ class SettingsDialog(QDialog): download_dir_row.addWidget(self.download_dir_edit) download_dir_row.addWidget(self.download_dir_btn) 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.addSpacing(12) @@ -464,7 +487,14 @@ class SettingsDialog(QDialog): # Save modlist base dirs 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()) + # 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() + + # Refresh cached paths in GUI screens if Jackify directory changed + self._refresh_gui_paths() + # Check if debug mode changed and prompt for restart new_debug_mode = self.debug_checkbox.isChecked() 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") 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): label = QLabel(text) label.setStyleSheet("font-weight: bold; color: #fff;") @@ -655,7 +708,7 @@ class JackifyMainWindow(QMainWindow): # Spacer bottom_bar_layout.addStretch(1) - # Settings button (right) + # Settings button (right side) settings_btn = QLabel('Settings') settings_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;") settings_btn.setTextInteractionFlags(Qt.TextBrowserInteraction) @@ -663,6 +716,14 @@ class JackifyMainWindow(QMainWindow): settings_btn.linkActivated.connect(self.open_settings_dialog) bottom_bar_layout.addWidget(settings_btn, alignment=Qt.AlignRight) + # About button (right side) + about_btn = QLabel('About') + 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 --- central_widget = QWidget() main_layout = QVBoxLayout() @@ -808,6 +869,16 @@ class JackifyMainWindow(QMainWindow): import traceback 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): if hasattr(sys, '_MEIPASS'): diff --git a/jackify/frontends/gui/screens/configure_existing_modlist.py b/jackify/frontends/gui/screens/configure_existing_modlist.py index a2f0546..83afe98 100644 --- a/jackify/frontends/gui/screens/configure_existing_modlist.py +++ b/jackify/frontends/gui/screens/configure_existing_modlist.py @@ -34,8 +34,7 @@ class ConfigureExistingModlistScreen(QWidget): self.stacked_widget = stacked_widget self.main_menu_index = main_menu_index self.debug = DEBUG_BORDERS - self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_Existing_Modlist_workflow.log') - os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True) + self.refresh_paths() # --- Detect Steam Deck --- steamdeck = os.path.exists('/etc/os-release') and 'steamdeck' in open('/etc/os-release').read().lower() @@ -297,6 +296,41 @@ class ConfigureExistingModlistScreen(QWidget): # Time tracking for workflow completion 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): """Handle window resize to prioritize form over console""" @@ -382,17 +416,22 @@ class ConfigureExistingModlistScreen(QWidget): log_handler = LoggingHandler() 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 idx = self.shortcut_combo.currentIndex() - 1 # Account for 'Please Select...' from jackify.frontends.gui.services.message_service import MessageService 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") + self._enable_controls_after_operation() return shortcut = self.shortcut_map[idx] modlist_name = shortcut.get('AppName', '') install_dir = shortcut.get('StartDir', '') if not modlist_name or not install_dir: MessageService.critical(self, "Invalid Shortcut", "The selected shortcut is missing required information.", safety_level="medium") + self._enable_controls_after_operation() return resolution = self.resolution_combo.currentText() # Handle resolution saving @@ -505,6 +544,9 @@ class ConfigureExistingModlistScreen(QWidget): def on_configuration_complete(self, success, message, modlist_name): """Handle configuration completion""" + # Re-enable all controls when workflow completes + self._enable_controls_after_operation() + if success: # Calculate time taken time_taken = self._calculate_time_taken() @@ -525,6 +567,9 @@ class ConfigureExistingModlistScreen(QWidget): def on_configuration_error(self, error_message): """Handle configuration error""" + # Re-enable all controls on error + self._enable_controls_after_operation() + self._safe_append_text(f"Configuration error: {error_message}") 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: self.config_process.terminate() self.config_process.waitForFinished(2000) - # Reset button states - self.start_btn.setEnabled(True) + # Re-enable all controls + self._enable_controls_after_operation() self.cancel_btn.setVisible(True) def show_next_steps_dialog(self, message): diff --git a/jackify/frontends/gui/screens/configure_new_modlist.py b/jackify/frontends/gui/screens/configure_new_modlist.py index 51ab087..43e2395 100644 --- a/jackify/frontends/gui/screens/configure_new_modlist.py +++ b/jackify/frontends/gui/screens/configure_new_modlist.py @@ -1,7 +1,7 @@ """ 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.QtGui import QPixmap, QTextCursor from ..shared_theme import JACKIFY_COLOR_BLUE, DEBUG_BORDERS @@ -106,8 +106,7 @@ class ConfigureNewModlistScreen(QWidget): self.protontricks_service = ProtontricksDetectionService() # Path for workflow log - self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Configure_New_Modlist_workflow.log') - os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True) + self.refresh_paths() # Scroll tracking for professional auto-scroll behavior self._user_manually_scrolled = False @@ -211,7 +210,6 @@ class ConfigureNewModlistScreen(QWidget): "7680x4320" ]) 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 saved_resolution = self.resolution_service.get_saved_resolution() @@ -236,6 +234,27 @@ class ConfigureNewModlistScreen(QWidget): else: self.resolution_combo.setCurrentIndex(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.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) form_section_widget.setLayout(form_grid) @@ -338,6 +357,44 @@ class ConfigureNewModlistScreen(QWidget): self.start_btn.clicked.connect(self.validate_and_start_configure) # --- Connect steam_restart_finished signal --- 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): """Handle window resize to prioritize form over console""" @@ -522,23 +579,38 @@ class ConfigureNewModlistScreen(QWidget): # Start time tracking self._workflow_start_time = time.time() + # Disable controls during configuration (after validation passes) + self._disable_controls_during_operation() + # Validate modlist name modlist_name = self.modlist_name_edit.text().strip() if not modlist_name: MessageService.warning(self, "Missing Name", "Please specify a name for your modlist", safety_level="low") + self._enable_controls_after_operation() return # --- Shortcut creation will be handled by automated workflow --- from jackify.backend.handlers.shortcut_handler import ShortcutHandler 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 - # --- 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" - ) - print(f"DEBUG: Steam restart dialog returned: {reply!r}") + + # Check if auto-restart is enabled + auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked() + + if auto_restart_enabled: + # Auto-accept Steam restart - proceed without dialog + 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): + self._enable_controls_after_operation() if self.stacked_widget: self.stacked_widget.setCurrentIndex(0) return @@ -562,7 +634,6 @@ class ConfigureNewModlistScreen(QWidget): progress.setMinimumDuration(0) progress.setValue(0) progress.show() - self.setEnabled(False) def do_restart(): try: ok = shortcut_handler.secure_steam_restart() @@ -579,7 +650,7 @@ class ConfigureNewModlistScreen(QWidget): if hasattr(self, '_steam_restart_progress'): self._steam_restart_progress.close() del self._steam_restart_progress - self.setEnabled(True) + self._enable_controls_after_operation() if success: self._safe_append_text("Steam restarted successfully.") @@ -722,7 +793,7 @@ class ConfigureNewModlistScreen(QWidget): """Handle error from the automated prefix workflow""" self._safe_append_text(f"Error during automated Steam setup: {error_message}") 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): """Show dialog to resolve shortcut name conflicts""" @@ -1162,8 +1233,8 @@ class ConfigureNewModlistScreen(QWidget): def on_configuration_complete(self, success, message, modlist_name): """Handle configuration completion (same as Tuxborn)""" - # Always re-enable the start button when workflow completes - self.start_btn.setEnabled(True) + # Re-enable all controls when workflow completes + self._enable_controls_after_operation() if success: # Calculate time taken @@ -1185,8 +1256,8 @@ class ConfigureNewModlistScreen(QWidget): def on_configuration_error(self, error_message): """Handle configuration error""" - # Re-enable the start button on error - self.start_btn.setEnabled(True) + # Re-enable all controls on error + self._enable_controls_after_operation() self._safe_append_text(f"Configuration error: {error_message}") MessageService.critical(self, "Configuration Error", f"Configuration failed: {error_message}", safety_level="medium") diff --git a/jackify/frontends/gui/screens/install_modlist.py b/jackify/frontends/gui/screens/install_modlist.py index db1c68d..c140ed5 100644 --- a/jackify/frontends/gui/screens/install_modlist.py +++ b/jackify/frontends/gui/screens/install_modlist.py @@ -355,9 +355,8 @@ class InstallModlistScreen(QWidget): self.online_modlists = {} # {game_type: [modlist_dict, ...]} self.modlist_details = {} # {modlist_name: modlist_dict} - # Path for workflow log - self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Modlist_Install_workflow.log') - os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True) + # Initialize log path (can be refreshed via refresh_paths method) + self.refresh_paths() # Initialize services early from jackify.backend.services.api_key_service import APIKeyService @@ -459,11 +458,11 @@ class InstallModlistScreen(QWidget): file_layout.setContentsMargins(0, 0, 0, 0) self.file_edit = QLineEdit() self.file_edit.setMinimumWidth(400) - file_btn = QPushButton("Browse") - file_btn.clicked.connect(self.browse_wabbajack_file) + self.file_btn = QPushButton("Browse") + self.file_btn.clicked.connect(self.browse_wabbajack_file) file_layout.addWidget(QLabel(".wabbajack File:")) file_layout.addWidget(self.file_edit) - file_layout.addWidget(file_btn) + file_layout.addWidget(self.file_btn) self.file_group.setLayout(file_layout) file_tab_vbox.addWidget(self.file_group) file_tab.setLayout(file_tab_vbox) @@ -484,22 +483,22 @@ class InstallModlistScreen(QWidget): install_dir_label = QLabel("Install Directory:") self.install_dir_edit = QLineEdit(self.config_handler.get_modlist_install_base_dir()) self.install_dir_edit.setMaximumHeight(25) # Force compact height - browse_install_btn = QPushButton("Browse") - browse_install_btn.clicked.connect(self.browse_install_dir) + self.browse_install_btn = QPushButton("Browse") + self.browse_install_btn.clicked.connect(self.browse_install_dir) install_dir_hbox = QHBoxLayout() 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.addLayout(install_dir_hbox, 1, 1) # Downloads Dir downloads_dir_label = QLabel("Downloads Directory:") self.downloads_dir_edit = QLineEdit(self.config_handler.get_modlist_downloads_base_dir()) self.downloads_dir_edit.setMaximumHeight(25) # Force compact height - browse_downloads_btn = QPushButton("Browse") - browse_downloads_btn.clicked.connect(self.browse_downloads_dir) + self.browse_downloads_btn = QPushButton("Browse") + self.browse_downloads_btn.clicked.connect(self.browse_downloads_dir) downloads_dir_hbox = QHBoxLayout() 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.addLayout(downloads_dir_hbox, 2, 1) # Nexus API Key @@ -603,7 +602,25 @@ class InstallModlistScreen(QWidget): self.resolution_combo.setCurrentIndex(0) # Otherwise, default is 'Leave unchanged' (index 0) 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.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) form_section_widget.setLayout(form_grid) @@ -723,6 +740,57 @@ class InstallModlistScreen(QWidget): # Initialize process tracking 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): """Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller""" @@ -1121,6 +1189,9 @@ class InstallModlistScreen(QWidget): if not self._check_protontricks(): return + # Disable all controls during installation (except Cancel) + self._disable_controls_during_operation() + try: tab_index = self.source_tabs.currentIndex() install_mode = 'online' @@ -1128,12 +1199,14 @@ class InstallModlistScreen(QWidget): modlist = self.file_edit.text().strip() 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.") + self._enable_controls_after_operation() return install_mode = 'file' else: modlist = self.modlist_btn.text().strip() 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.") + self._enable_controls_after_operation() return # For online modlists, use machine_url instead of display name @@ -1159,6 +1232,7 @@ class InstallModlistScreen(QWidget): missing_fields.append("Nexus API Key") 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)) + self._enable_controls_after_operation() return validation_handler = ValidationHandler() from pathlib import Path @@ -1324,14 +1398,11 @@ class InstallModlistScreen(QWidget): debug_print(f"DEBUG: Exception in validate_and_start_install: {e}") import traceback debug_print(f"DEBUG: Traceback: {traceback.format_exc()}") - # Re-enable the button in case of exception - self.start_btn.setEnabled(True) + # Re-enable all controls after exception + self._enable_controls_after_operation() self.cancel_btn.setVisible(True) self.cancel_install_btn.setVisible(False) - # Also re-enable the entire widget - 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 + debug_print(f"DEBUG: Controls re-enabled in exception handler") 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') @@ -1501,12 +1572,21 @@ class InstallModlistScreen(QWidget): 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}") 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 - ) + # Check if auto-restart is enabled + auto_restart_enabled = hasattr(self, 'auto_restart_checkbox') and self.auto_restart_checkbox.isChecked() + + if auto_restart_enabled: + # 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: # --- Create Steam shortcut BEFORE restarting Steam --- # Proceed directly to automated prefix creation @@ -1522,6 +1602,8 @@ class InstallModlistScreen(QWidget): "You can manually add the modlist to Steam later if desired.", safety_level="medium" ) + # Re-enable controls since operation is complete + self._enable_controls_after_operation() else: # Check for user cancellation first last_output = self.console.toPlainText() @@ -1611,9 +1693,6 @@ class InstallModlistScreen(QWidget): progress.setMinimumDuration(0) progress.setValue(0) 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(): debug_print("DEBUG: do_restart thread started - using direct backend service") @@ -1651,9 +1730,7 @@ class InstallModlistScreen(QWidget): finally: self._steam_restart_progress = None - self.setEnabled(True) - 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 + # Controls are managed by the proper control management system if success: self._safe_append_text("Steam restarted successfully.") @@ -1676,6 +1753,8 @@ class InstallModlistScreen(QWidget): def start_automated_prefix_workflow(self): """Start the automated prefix creation workflow""" try: + # Disable controls during installation + self._disable_controls_during_operation() modlist_name = self.modlist_name_edit.text().strip() install_dir = self.install_dir_edit.text().strip() final_exe_path = os.path.join(install_dir, "ModOrganizer.exe") @@ -1784,33 +1863,43 @@ class InstallModlistScreen(QWidget): import traceback debug_print(f"DEBUG: Traceback: {traceback.format_exc()}") 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): """Handle completion of automated prefix creation""" - if success: - debug_print(f"SUCCESS: Automated prefix creation completed!") - debug_print(f"Prefix created at: {prefix_path}") - if new_appid_str and new_appid_str != "0": - debug_print(f"AppID: {new_appid_str}") - - # 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 - - # Continue with configuration using the new AppID and timestamp - modlist_name = self.modlist_name_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) - else: - self._safe_append_text(f"ERROR: Automated prefix creation failed") - self._safe_append_text("Please check the logs for details") - MessageService.critical(self, "Automated Setup Failed", - "Automated prefix creation failed. Please check the console output for details.") + try: + if success: + debug_print(f"SUCCESS: Automated prefix creation completed!") + debug_print(f"Prefix created at: {prefix_path}") + if new_appid_str and new_appid_str != "0": + debug_print(f"AppID: {new_appid_str}") + + # 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 + + # Continue with configuration using the new AppID and timestamp + modlist_name = self.modlist_name_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) + else: + self._safe_append_text(f"ERROR: Automated prefix creation failed") + self._safe_append_text("Please check the logs for details") + MessageService.critical(self, "Automated Setup Failed", + "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): """Handle error in automated prefix creation""" self._safe_append_text(f"ERROR: Error during automated prefix creation: {error_msg}") MessageService.critical(self, "Automated Setup Error", 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): """Handle progress updates from automated prefix creation""" @@ -1831,7 +1920,6 @@ class InstallModlistScreen(QWidget): self.steam_restart_progress.setMinimumDuration(0) self.steam_restart_progress.setValue(0) self.steam_restart_progress.show() - self.setEnabled(False) def hide_steam_restart_progress(self): """Hide Steam restart progress dialog""" @@ -1843,45 +1931,53 @@ class InstallModlistScreen(QWidget): pass finally: 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): """Handle configuration completion on main thread""" - if success: - # Show celebration SuccessDialog after the entire workflow - from ..dialogs import SuccessDialog - import time - if not hasattr(self, '_install_workflow_start_time'): - self._install_workflow_start_time = time.time() - time_taken = int(time.time() - self._install_workflow_start_time) - mins, secs = divmod(time_taken, 60) - time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds" - display_names = { - 'skyrim': 'Skyrim', - 'fallout4': 'Fallout 4', - 'falloutnv': 'Fallout New Vegas', - 'oblivion': 'Oblivion', - 'starfield': 'Starfield', - 'oblivion_remastered': 'Oblivion Remastered', - 'enderal': 'Enderal' - } - game_name = display_names.get(self._current_game_type, self._current_game_name) - success_dialog = SuccessDialog( - modlist_name=modlist_name, - workflow_type="install", - time_taken=time_str, - game_name=game_name, - parent=self - ) - success_dialog.show() - elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3: - # Max retries reached - show failure message - MessageService.critical(self, "Manual Steps Failed", - "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.") + try: + # Re-enable controls now that installation/configuration is complete + self._enable_controls_after_operation() + + if success: + # Show celebration SuccessDialog after the entire workflow + from ..dialogs import SuccessDialog + import time + if not hasattr(self, '_install_workflow_start_time'): + self._install_workflow_start_time = time.time() + time_taken = int(time.time() - self._install_workflow_start_time) + mins, secs = divmod(time_taken, 60) + time_str = f"{mins} minutes, {secs} seconds" if mins else f"{secs} seconds" + display_names = { + 'skyrim': 'Skyrim', + 'fallout4': 'Fallout 4', + 'falloutnv': 'Fallout New Vegas', + 'oblivion': 'Oblivion', + 'starfield': 'Starfield', + 'oblivion_remastered': 'Oblivion Remastered', + 'enderal': 'Enderal' + } + game_name = display_names.get(self._current_game_type, self._current_game_name) + success_dialog = SuccessDialog( + modlist_name=modlist_name, + workflow_type="install", + time_taken=time_str, + game_name=game_name, + parent=self + ) + success_dialog.show() + elif hasattr(self, '_manual_steps_retry_count') and self._manual_steps_retry_count >= 3: + # Max retries reached - show failure message + MessageService.critical(self, "Manual Steps Failed", + "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 if hasattr(self, 'config_thread') and self.config_thread is not None: # Disconnect all signals to prevent "Internal C++ object already deleted" errors @@ -1940,8 +2036,8 @@ class InstallModlistScreen(QWidget): else: # User clicked Cancel or closed the dialog - cancel the workflow self._safe_append_text("\n🛑 Manual steps cancelled by user. Workflow stopped.") - # Reset button states - self.start_btn.setEnabled(True) + # Re-enable all controls when workflow is cancelled + self._enable_controls_after_operation() self.cancel_btn.setVisible(True) self.cancel_install_btn.setVisible(False) @@ -2513,8 +2609,8 @@ class InstallModlistScreen(QWidget): # Cleanup any remaining processes self.cleanup_processes() - # Reset button states - self.start_btn.setEnabled(True) + # Reset button states and re-enable all controls + self._enable_controls_after_operation() self.cancel_btn.setVisible(True) self.cancel_install_btn.setVisible(False) diff --git a/jackify/frontends/gui/screens/tuxborn_installer.py b/jackify/frontends/gui/screens/tuxborn_installer.py index 50fc114..e51be5b 100644 --- a/jackify/frontends/gui/screens/tuxborn_installer.py +++ b/jackify/frontends/gui/screens/tuxborn_installer.py @@ -106,8 +106,7 @@ class TuxbornInstallerScreen(QWidget): self.modlist_details = {} # {modlist_name: modlist_dict} # Path for workflow log - self.modlist_log_path = os.path.expanduser('~/Jackify/logs/Tuxborn_Installer_workflow.log') - os.makedirs(os.path.dirname(self.modlist_log_path), exist_ok=True) + self.refresh_paths() # Initialize services early 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.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): """Safely open URL using subprocess to avoid Qt library conflicts in PyInstaller""" import subprocess diff --git a/jackify/shared/logging.py b/jackify/shared/logging.py index 8b6dc76..f4056f8 100644 --- a/jackify/shared/logging.py +++ b/jackify/shared/logging.py @@ -14,15 +14,21 @@ import shutil class LoggingHandler: """ 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). - Handles log rotation and log directory creation. Usage: logger = LoggingHandler().setup_logger('install_wabbajack', 'jackify-install-wabbajack.log') """ 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() + + @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: """Ensure the log directory exists.""" diff --git a/jackify/shared/paths.py b/jackify/shared/paths.py new file mode 100644 index 0000000..7980a39 --- /dev/null +++ b/jackify/shared/paths.py @@ -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" \ No newline at end of file diff --git a/jackify/shared/ui_utils.py b/jackify/shared/ui_utils.py index 2ce875f..74d718b 100644 --- a/jackify/shared/ui_utils.py +++ b/jackify/shared/ui_utils.py @@ -51,9 +51,15 @@ def _clear_screen_fallback(): def print_jackify_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 ║ ║ & associated utilities on Linux ║