mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-01-17 19:47:00 +01:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9bd6f60e6 | ||
|
|
28cde64887 | ||
|
|
64c76046ce | ||
|
|
4eb1d63de7 | ||
|
|
8131e23057 | ||
|
|
1cd4caf04b | ||
|
|
e005f56bdb | ||
|
|
1f84fc7c68 |
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,5 +1,75 @@
|
||||
# Jackify Changelog
|
||||
|
||||
## v0.1.4 - GE-Proton Support and Performance Optimization
|
||||
**Release Date:** September 22, 2025
|
||||
|
||||
### New Features
|
||||
- **GE-Proton Detection**: Automatic detection and prioritization of GE-Proton versions
|
||||
- **User-selectable Proton version**: Settings dialog displays all available Proton versions with type indicators
|
||||
|
||||
### Engine Updates
|
||||
- **jackify-engine v0.3.15**: Reads Proton configuration from config.json, adds degree symbol handling for special characters, removes Wine fallback (Proton now required)
|
||||
|
||||
### Technical Improvements
|
||||
- **Smart Priority**: GE-Proton 10+ → Proton Experimental → Proton 10 → Proton 9
|
||||
- **Auto-Configuration**: Fresh installations automatically select optimal Proton version
|
||||
|
||||
### Bug Fixes
|
||||
- **Steam VDF Compatibility**: Fixed case-sensitivity issues with Steam shortcuts.vdf parsing for Configure Existing Modlist workflows
|
||||
|
||||
---
|
||||
|
||||
## v0.1.3 - Enhanced Proton Support and System Compatibility
|
||||
**Release Date:** September 21, 2025
|
||||
|
||||
### New Features
|
||||
- **Enhanced Proton Detection**: Automatic fallback system with priority: Experimental → Proton 10 → Proton 9
|
||||
- **Guided Proton Installation**: Professional auto-install dialog with Steam protocol integration for missing Proton versions
|
||||
- **Enderal Game Support**: Added Enderal to supported games list with special handling for Somnium modlist structure
|
||||
- **Proton Version Leniency**: Accept any Proton version 9+ instead of requiring Experimental
|
||||
|
||||
### UX Improvements
|
||||
- **Resolution System Overhaul**: Eliminated hardcoded 2560x1600 fallbacks across all screens
|
||||
- **Steam Deck Detection**: Proper 1280x800 default resolution with 1920x1080 fallback for desktop
|
||||
- **Leave Unchanged Logic**: Fixed resolution setting to actually preserve existing user configurations
|
||||
|
||||
### Technical Improvements
|
||||
- **Resolution Utilities**: New `shared/resolution_utils.py` with centralized resolution management
|
||||
- **Protontricks Detection**: Enhanced detection for both native and Flatpak protontricks installations
|
||||
- **Real-time Monitoring**: Progress tracking for Proton installation with directory stability detection
|
||||
|
||||
### Bug Fixes
|
||||
- **Somnium Support**: Automatic detection of `files/ModOrganizer.exe` structure in edge-case modlists
|
||||
- **Steam Protocol Integration**: Reliable triggering of Proton installation via `steam://install/` URLs
|
||||
- **Manual Fallback**: Clear instructions and recheck functionality when auto-install fails
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[Wiki](https://github.com/Omni-guides/Jackify/wiki) | [Nexus](https://www.nexusmods.com/site/mods/1427) | [Download](https://www.nexusmods.com/Core/Libs/Common/Widgets/DownloadPopUp?id=5807&game_id=2295) | [Wabbajack Discord](https://discord.gg/wabbajack) | [Jackify Issues](https://github.com/Omni-guides/Jackify/issues) | [Legacy Guides](https://github.com/Omni-guides/Jackify/tree/master/Legacy) | [Ko-fi](https://ko-fi.com/omni1)
|
||||
[Wiki](https://github.com/Omni-guides/Jackify/wiki) | [Nexus](https://www.nexusmods.com/site/mods/1427) | [Download](https://www.nexusmods.com/site/mods/1427?tab=files) | [Wabbajack Discord](https://discord.gg/wabbajack) | [Jackify Issues](https://github.com/Omni-guides/Jackify/issues) | [Legacy Guides](https://github.com/Omni-guides/Jackify/tree/master/Legacy) | [Ko-fi](https://ko-fi.com/omni1)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.4"
|
||||
|
||||
@@ -23,6 +23,44 @@ from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
# UI Colors already imported above
|
||||
|
||||
def _get_user_proton_version():
|
||||
"""Get user's preferred Proton version from config, with fallback to auto-detection"""
|
||||
try:
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
|
||||
config_handler = ConfigHandler()
|
||||
user_proton_path = config_handler.get('proton_path', 'auto')
|
||||
|
||||
if user_proton_path == 'auto':
|
||||
# Use enhanced fallback logic with GE-Proton preference
|
||||
logging.info("User selected auto-detect, using GE-Proton → Experimental → Proton precedence")
|
||||
return WineUtils.select_best_proton()
|
||||
else:
|
||||
# User has selected a specific Proton version
|
||||
# Use the exact directory name for Steam config.vdf
|
||||
try:
|
||||
proton_version = os.path.basename(user_proton_path)
|
||||
# GE-Proton uses exact directory name, Valve Proton needs lowercase conversion
|
||||
if proton_version.startswith('GE-Proton'):
|
||||
# Keep GE-Proton name exactly as-is
|
||||
steam_proton_name = proton_version
|
||||
else:
|
||||
# Convert Valve Proton names to Steam's format
|
||||
steam_proton_name = proton_version.lower().replace(' - ', '_').replace(' ', '_').replace('-', '_')
|
||||
if not steam_proton_name.startswith('proton'):
|
||||
steam_proton_name = f"proton_{steam_proton_name}"
|
||||
|
||||
logging.info(f"Using user-selected Proton: {steam_proton_name}")
|
||||
return steam_proton_name
|
||||
except Exception as e:
|
||||
logging.warning(f"Invalid user Proton path '{user_proton_path}', falling back to auto: {e}")
|
||||
return WineUtils.select_best_proton()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to get user Proton preference, using default: {e}")
|
||||
return "proton_experimental"
|
||||
|
||||
# Attempt to import readline for tab completion
|
||||
READLINE_AVAILABLE = False
|
||||
try:
|
||||
@@ -104,8 +142,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 +152,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 +955,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 +1012,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 +1039,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 +1055,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 +1114,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 +1191,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 +1204,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
|
||||
}
|
||||
|
||||
@@ -1258,13 +1286,16 @@ class ModlistInstallCLI:
|
||||
from jackify.backend.services.native_steam_service import NativeSteamService
|
||||
steam_service = NativeSteamService()
|
||||
|
||||
# Get user's preferred Proton version
|
||||
proton_version = _get_user_proton_version()
|
||||
|
||||
success, app_id = steam_service.create_shortcut_with_proton(
|
||||
app_name=config_context['name'],
|
||||
exe_path=config_context['mo2_exe_path'],
|
||||
start_dir=os.path.dirname(config_context['mo2_exe_path']),
|
||||
launch_options="%command%",
|
||||
tags=["Jackify"],
|
||||
proton_version="proton_experimental"
|
||||
proton_version=proton_version
|
||||
)
|
||||
|
||||
if not success or not app_id:
|
||||
|
||||
@@ -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
|
||||
@@ -46,6 +47,14 @@ class ConfigHandler:
|
||||
# If steam_path is not set, detect it
|
||||
if not self.settings["steam_path"]:
|
||||
self.settings["steam_path"] = self._detect_steam_path()
|
||||
|
||||
# Auto-detect and set Proton version on first run
|
||||
if not self.settings.get("proton_path"):
|
||||
self._auto_detect_proton()
|
||||
|
||||
# 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()
|
||||
|
||||
@@ -487,4 +496,28 @@ class ConfigHandler:
|
||||
logger.error(f"Error saving modlist downloads base directory: {e}")
|
||||
return False
|
||||
|
||||
def _auto_detect_proton(self):
|
||||
"""Auto-detect and set best Proton version (includes GE-Proton and Valve Proton)"""
|
||||
try:
|
||||
from .wine_utils import WineUtils
|
||||
best_proton = WineUtils.select_best_proton()
|
||||
|
||||
if best_proton:
|
||||
self.settings["proton_path"] = str(best_proton['path'])
|
||||
self.settings["proton_version"] = best_proton['name']
|
||||
proton_type = best_proton.get('type', 'Unknown')
|
||||
logger.info(f"Auto-detected Proton: {best_proton['name']} ({proton_type})")
|
||||
self.save_config()
|
||||
else:
|
||||
# Fallback to auto-detect mode
|
||||
self.settings["proton_path"] = "auto"
|
||||
self.settings["proton_version"] = "auto"
|
||||
logger.info("No compatible Proton versions found, using auto-detect mode")
|
||||
self.save_config()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to auto-detect Proton: {e}")
|
||||
self.settings["proton_path"] = "auto"
|
||||
self.settings["proton_version"] = "auto"
|
||||
|
||||
|
||||
@@ -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.")
|
||||
@@ -1151,7 +1163,7 @@ class ModlistHandler:
|
||||
# Determine game type
|
||||
game = (game_var_full or modlist_name or "").lower().replace(" ", "")
|
||||
# Add game-specific extras
|
||||
if "skyrim" in game or "fallout4" in game or "starfield" in game or "oblivion_remastered" in game:
|
||||
if "skyrim" in game or "fallout4" in game or "starfield" in game or "oblivion_remastered" in game or "enderal" in game:
|
||||
extras += ["d3dcompiler_47", "d3dx11_43", "d3dcompiler_43", "dotnet6", "dotnet7"]
|
||||
elif "falloutnewvegas" in game or "fnv" in game or "oblivion" in game:
|
||||
extras += ["d3dx9_43", "d3dx9"]
|
||||
@@ -1226,6 +1238,12 @@ class ModlistHandler:
|
||||
# Check ModOrganizer.ini for indicators (nvse/enderal) as an early, robust signal
|
||||
try:
|
||||
mo2_ini = modlist_path / "ModOrganizer.ini"
|
||||
# Also check Somnium's non-standard location
|
||||
if not mo2_ini.exists():
|
||||
somnium_mo2_ini = modlist_path / "files" / "ModOrganizer.ini"
|
||||
if somnium_mo2_ini.exists():
|
||||
mo2_ini = somnium_mo2_ini
|
||||
|
||||
if mo2_ini.exists():
|
||||
try:
|
||||
content = mo2_ini.read_text(errors='ignore').lower()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -988,8 +988,8 @@ class ShortcutHandler:
|
||||
shortcuts_data = VDFHandler.load(shortcuts_vdf_path, binary=True)
|
||||
if shortcuts_data and 'shortcuts' in shortcuts_data:
|
||||
for idx, shortcut in shortcuts_data['shortcuts'].items():
|
||||
app_name = shortcut.get('AppName', '').strip()
|
||||
exe = shortcut.get('Exe', '').strip('"').strip()
|
||||
app_name = shortcut.get('AppName', shortcut.get('appname', '')).strip()
|
||||
exe = shortcut.get('Exe', shortcut.get('exe', '')).strip('"').strip()
|
||||
vdf_shortcuts.append((app_name, exe, idx))
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error parsing shortcuts.vdf for exe path matching: {e}")
|
||||
@@ -1054,9 +1054,9 @@ class ShortcutHandler:
|
||||
self.logger.warning(f"Skipping invalid shortcut entry (not a dict) at index {shortcut_id} in {shortcuts_file}")
|
||||
continue
|
||||
|
||||
app_name = shortcut.get('AppName')
|
||||
exe_path = shortcut.get('Exe', '').strip('"')
|
||||
start_dir = shortcut.get('StartDir', '').strip('"')
|
||||
app_name = shortcut.get('AppName', shortcut.get('appname'))
|
||||
exe_path = shortcut.get('Exe', shortcut.get('exe', '')).strip('"')
|
||||
start_dir = shortcut.get('StartDir', shortcut.get('startdir', '')).strip('"')
|
||||
|
||||
# Check if the base name of the exe_path matches the target
|
||||
if app_name and start_dir and os.path.basename(exe_path) == executable_name:
|
||||
|
||||
@@ -132,7 +132,8 @@ class WabbajackParser:
|
||||
'falloutnv': 'Fallout New Vegas',
|
||||
'oblivion': 'Oblivion',
|
||||
'starfield': 'Starfield',
|
||||
'oblivion_remastered': 'Oblivion Remastered'
|
||||
'oblivion_remastered': 'Oblivion Remastered',
|
||||
'enderal': 'Enderal'
|
||||
}
|
||||
return [display_names.get(game, game) for game in self.supported_games]
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
import glob
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional, Tuple, List, Dict
|
||||
from .subprocess_utils import get_clean_subprocess_env
|
||||
|
||||
# Initialize logger
|
||||
@@ -643,7 +643,20 @@ class WineUtils:
|
||||
wine_bin = subdir / "files/bin/wine"
|
||||
if wine_bin.is_file():
|
||||
return str(wine_bin)
|
||||
# Fallback: Try 'Proton - Experimental' if present
|
||||
# Fallback: Try user's configured Proton version
|
||||
try:
|
||||
from .config_handler import ConfigHandler
|
||||
config = ConfigHandler()
|
||||
fallback_path = config.get('proton_path', 'auto')
|
||||
if fallback_path != 'auto':
|
||||
fallback_wine_bin = Path(fallback_path) / "files/bin/wine"
|
||||
if fallback_wine_bin.is_file():
|
||||
logger.warning(f"Requested Proton version '{proton_version}' not found. Falling back to user's configured version.")
|
||||
return str(fallback_wine_bin)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Final fallback: Try 'Proton - Experimental' if present
|
||||
for base_path in steam_common_paths:
|
||||
wine_bin = base_path / "Proton - Experimental" / "files/bin/wine"
|
||||
if wine_bin.is_file():
|
||||
@@ -698,4 +711,276 @@ class WineUtils:
|
||||
proton_path = str(Path(wine_bin).parent.parent)
|
||||
logger.debug(f"Found Proton path: {proton_path}")
|
||||
|
||||
return compatdata_path, proton_path, wine_bin
|
||||
return compatdata_path, proton_path, wine_bin
|
||||
|
||||
@staticmethod
|
||||
def get_steam_library_paths() -> List[Path]:
|
||||
"""
|
||||
Get all Steam library paths including standard locations.
|
||||
|
||||
Returns:
|
||||
List of Path objects for Steam library directories
|
||||
"""
|
||||
steam_paths = [
|
||||
Path.home() / ".steam/steam/steamapps/common",
|
||||
Path.home() / ".local/share/Steam/steamapps/common",
|
||||
Path.home() / ".steam/root/steamapps/common"
|
||||
]
|
||||
|
||||
# Return only existing paths
|
||||
return [path for path in steam_paths if path.exists()]
|
||||
|
||||
@staticmethod
|
||||
def get_compatibility_tool_paths() -> List[Path]:
|
||||
"""
|
||||
Get all compatibility tool paths for GE-Proton and other custom Proton versions.
|
||||
|
||||
Returns:
|
||||
List of Path objects for compatibility tool directories
|
||||
"""
|
||||
compat_paths = [
|
||||
Path.home() / ".steam/steam/compatibilitytools.d",
|
||||
Path.home() / ".local/share/Steam/compatibilitytools.d"
|
||||
]
|
||||
|
||||
# Return only existing paths
|
||||
return [path for path in compat_paths if path.exists()]
|
||||
|
||||
@staticmethod
|
||||
def scan_ge_proton_versions() -> List[Dict[str, any]]:
|
||||
"""
|
||||
Scan for available GE-Proton versions in compatibilitytools.d directories.
|
||||
|
||||
Returns:
|
||||
List of dicts with version info, sorted by priority (newest first)
|
||||
"""
|
||||
logger.info("Scanning for available GE-Proton versions...")
|
||||
|
||||
found_versions = []
|
||||
compat_paths = WineUtils.get_compatibility_tool_paths()
|
||||
|
||||
if not compat_paths:
|
||||
logger.warning("No compatibility tool paths found")
|
||||
return []
|
||||
|
||||
for compat_path in compat_paths:
|
||||
logger.debug(f"Scanning compatibility tools: {compat_path}")
|
||||
|
||||
try:
|
||||
# Look for GE-Proton directories
|
||||
for proton_dir in compat_path.iterdir():
|
||||
if not proton_dir.is_dir():
|
||||
continue
|
||||
|
||||
dir_name = proton_dir.name
|
||||
if not dir_name.startswith("GE-Proton"):
|
||||
continue
|
||||
|
||||
# Check for wine binary
|
||||
wine_bin = proton_dir / "files" / "bin" / "wine"
|
||||
if not wine_bin.exists() or not wine_bin.is_file():
|
||||
logger.debug(f"Skipping {dir_name} - no wine binary found")
|
||||
continue
|
||||
|
||||
# Parse version from directory name (e.g., "GE-Proton10-16")
|
||||
version_match = re.match(r'GE-Proton(\d+)-(\d+)', dir_name)
|
||||
if version_match:
|
||||
major_ver = int(version_match.group(1))
|
||||
minor_ver = int(version_match.group(2))
|
||||
|
||||
# Calculate priority: GE-Proton gets highest priority
|
||||
# Priority format: 200 (base) + major*10 + minor (e.g., 200 + 100 + 16 = 316)
|
||||
priority = 200 + (major_ver * 10) + minor_ver
|
||||
|
||||
found_versions.append({
|
||||
'name': dir_name,
|
||||
'path': proton_dir,
|
||||
'wine_bin': wine_bin,
|
||||
'priority': priority,
|
||||
'major_version': major_ver,
|
||||
'minor_version': minor_ver,
|
||||
'type': 'GE-Proton'
|
||||
})
|
||||
logger.debug(f"Found {dir_name} at {proton_dir} (priority: {priority})")
|
||||
else:
|
||||
logger.debug(f"Skipping {dir_name} - unknown GE-Proton version format")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error scanning {compat_path}: {e}")
|
||||
|
||||
# Sort by priority (highest first, so newest GE-Proton versions come first)
|
||||
found_versions.sort(key=lambda x: x['priority'], reverse=True)
|
||||
|
||||
logger.info(f"Found {len(found_versions)} GE-Proton version(s)")
|
||||
return found_versions
|
||||
|
||||
@staticmethod
|
||||
def scan_valve_proton_versions() -> List[Dict[str, any]]:
|
||||
"""
|
||||
Scan for available Valve Proton versions with fallback priority.
|
||||
|
||||
Returns:
|
||||
List of dicts with version info, sorted by priority (best first)
|
||||
"""
|
||||
logger.info("Scanning for available Valve Proton versions...")
|
||||
|
||||
found_versions = []
|
||||
steam_libs = WineUtils.get_steam_library_paths()
|
||||
|
||||
if not steam_libs:
|
||||
logger.warning("No Steam library paths found")
|
||||
return []
|
||||
|
||||
# Priority order for Valve Proton versions
|
||||
# Note: GE-Proton uses 200+ range, so Valve Proton gets 100+ range
|
||||
preferred_versions = [
|
||||
("Proton - Experimental", 150), # Higher priority than regular Valve Proton
|
||||
("Proton 10.0", 140),
|
||||
("Proton 9.0", 130),
|
||||
("Proton 9.0 (Beta)", 125)
|
||||
]
|
||||
|
||||
for steam_path in steam_libs:
|
||||
logger.debug(f"Scanning Steam library: {steam_path}")
|
||||
|
||||
for version_name, priority in preferred_versions:
|
||||
proton_path = steam_path / version_name
|
||||
wine_bin = proton_path / "files" / "bin" / "wine"
|
||||
|
||||
if wine_bin.exists() and wine_bin.is_file():
|
||||
found_versions.append({
|
||||
'name': version_name,
|
||||
'path': proton_path,
|
||||
'wine_bin': wine_bin,
|
||||
'priority': priority,
|
||||
'type': 'Valve-Proton'
|
||||
})
|
||||
logger.debug(f"Found {version_name} at {proton_path}")
|
||||
|
||||
# Sort by priority (highest first)
|
||||
found_versions.sort(key=lambda x: x['priority'], reverse=True)
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
unique_versions = []
|
||||
seen_names = set()
|
||||
for version in found_versions:
|
||||
if version['name'] not in seen_names:
|
||||
unique_versions.append(version)
|
||||
seen_names.add(version['name'])
|
||||
|
||||
logger.info(f"Found {len(unique_versions)} unique Valve Proton version(s)")
|
||||
return unique_versions
|
||||
|
||||
@staticmethod
|
||||
def scan_all_proton_versions() -> List[Dict[str, any]]:
|
||||
"""
|
||||
Scan for all available Proton versions (GE-Proton + Valve Proton) with unified priority.
|
||||
|
||||
Priority Chain (highest to lowest):
|
||||
1. GE-Proton10-16+ (priority 316+)
|
||||
2. GE-Proton10-* (priority 200+)
|
||||
3. Proton - Experimental (priority 150)
|
||||
4. Proton 10.0 (priority 140)
|
||||
5. Proton 9.0 (priority 130)
|
||||
6. Proton 9.0 (Beta) (priority 125)
|
||||
|
||||
Returns:
|
||||
List of dicts with version info, sorted by priority (best first)
|
||||
"""
|
||||
logger.info("Scanning for all available Proton versions...")
|
||||
|
||||
all_versions = []
|
||||
|
||||
# Scan GE-Proton versions (highest priority)
|
||||
ge_versions = WineUtils.scan_ge_proton_versions()
|
||||
all_versions.extend(ge_versions)
|
||||
|
||||
# Scan Valve Proton versions
|
||||
valve_versions = WineUtils.scan_valve_proton_versions()
|
||||
all_versions.extend(valve_versions)
|
||||
|
||||
# Sort by priority (highest first)
|
||||
all_versions.sort(key=lambda x: x['priority'], reverse=True)
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
unique_versions = []
|
||||
seen_names = set()
|
||||
for version in all_versions:
|
||||
if version['name'] not in seen_names:
|
||||
unique_versions.append(version)
|
||||
seen_names.add(version['name'])
|
||||
|
||||
if unique_versions:
|
||||
logger.info(f"Found {len(unique_versions)} total Proton version(s)")
|
||||
logger.info(f"Best available: {unique_versions[0]['name']} ({unique_versions[0]['type']})")
|
||||
else:
|
||||
logger.warning("No Proton versions found")
|
||||
|
||||
return unique_versions
|
||||
|
||||
@staticmethod
|
||||
def select_best_proton() -> Optional[Dict[str, any]]:
|
||||
"""
|
||||
Select the best available Proton version (GE-Proton or Valve Proton) using unified precedence.
|
||||
|
||||
Returns:
|
||||
Dict with version info for the best Proton, or None if none found
|
||||
"""
|
||||
available_versions = WineUtils.scan_all_proton_versions()
|
||||
|
||||
if not available_versions:
|
||||
logger.warning("No compatible Proton versions found")
|
||||
return None
|
||||
|
||||
# Return the highest priority version (first in sorted list)
|
||||
best_version = available_versions[0]
|
||||
logger.info(f"Selected best Proton version: {best_version['name']} ({best_version['type']})")
|
||||
return best_version
|
||||
|
||||
@staticmethod
|
||||
def select_best_valve_proton() -> Optional[Dict[str, any]]:
|
||||
"""
|
||||
Select the best available Valve Proton version using fallback precedence.
|
||||
Note: This method is kept for backward compatibility. Consider using select_best_proton() instead.
|
||||
|
||||
Returns:
|
||||
Dict with version info for the best Proton, or None if none found
|
||||
"""
|
||||
available_versions = WineUtils.scan_valve_proton_versions()
|
||||
|
||||
if not available_versions:
|
||||
logger.warning("No compatible Valve Proton versions found")
|
||||
return None
|
||||
|
||||
# Return the highest priority version (first in sorted list)
|
||||
best_version = available_versions[0]
|
||||
logger.info(f"Selected Valve Proton version: {best_version['name']}")
|
||||
return best_version
|
||||
|
||||
@staticmethod
|
||||
def check_proton_requirements() -> Tuple[bool, str, Optional[Dict[str, any]]]:
|
||||
"""
|
||||
Check if compatible Proton version is available for workflows.
|
||||
|
||||
Returns:
|
||||
tuple: (requirements_met, status_message, proton_info)
|
||||
- requirements_met: True if compatible Proton found
|
||||
- status_message: Human-readable status for display to user
|
||||
- proton_info: Dict with Proton details if found, None otherwise
|
||||
"""
|
||||
logger.info("Checking Proton requirements for workflow...")
|
||||
|
||||
# Scan for available Proton versions (includes GE-Proton + Valve Proton)
|
||||
best_proton = WineUtils.select_best_proton()
|
||||
|
||||
if best_proton:
|
||||
# Compatible Proton found
|
||||
proton_type = best_proton.get('type', 'Unknown')
|
||||
status_msg = f"✓ Using {best_proton['name']} ({proton_type}) for this workflow"
|
||||
logger.info(f"Proton requirements satisfied: {best_proton['name']} ({proton_type})")
|
||||
return True, status_msg, best_proton
|
||||
else:
|
||||
# No compatible Proton found
|
||||
status_msg = "✗ No compatible Proton version found (GE-Proton 10+, Proton 9+, 10, or Experimental required)"
|
||||
logger.warning("Proton requirements not met - no compatible version found")
|
||||
return False, status_msg, None
|
||||
@@ -38,6 +38,44 @@ class AutomatedPrefixService:
|
||||
"""Get consistent progress timestamp"""
|
||||
from jackify.shared.timing import get_timestamp
|
||||
return get_timestamp()
|
||||
|
||||
def _get_user_proton_version(self):
|
||||
"""Get user's preferred Proton version from config, with fallback to auto-detection"""
|
||||
try:
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
|
||||
config_handler = ConfigHandler()
|
||||
user_proton_path = config_handler.get('proton_path', 'auto')
|
||||
|
||||
if user_proton_path == 'auto':
|
||||
# Use enhanced fallback logic with GE-Proton preference
|
||||
logger.info("User selected auto-detect, using GE-Proton → Experimental → Proton precedence")
|
||||
return WineUtils.select_best_proton()
|
||||
else:
|
||||
# User has selected a specific Proton version
|
||||
# Use the exact directory name for Steam config.vdf
|
||||
try:
|
||||
proton_version = os.path.basename(user_proton_path)
|
||||
# GE-Proton uses exact directory name, Valve Proton needs lowercase conversion
|
||||
if proton_version.startswith('GE-Proton'):
|
||||
# Keep GE-Proton name exactly as-is
|
||||
steam_proton_name = proton_version
|
||||
else:
|
||||
# Convert Valve Proton names to Steam's format
|
||||
steam_proton_name = proton_version.lower().replace(' - ', '_').replace(' ', '_').replace('-', '_')
|
||||
if not steam_proton_name.startswith('proton'):
|
||||
steam_proton_name = f"proton_{steam_proton_name}"
|
||||
|
||||
logger.info(f"Using user-selected Proton: {steam_proton_name}")
|
||||
return steam_proton_name
|
||||
except Exception as e:
|
||||
logger.warning(f"Invalid user Proton path '{user_proton_path}', falling back to auto: {e}")
|
||||
return WineUtils.select_best_proton()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get user Proton preference, using default: {e}")
|
||||
return "proton_experimental"
|
||||
|
||||
|
||||
def create_shortcut_with_native_service(self, shortcut_name: str, exe_path: str,
|
||||
@@ -87,6 +125,9 @@ class AutomatedPrefixService:
|
||||
logger.warning(f"Could not generate STEAM_COMPAT_MOUNTS, using default: {e}")
|
||||
launch_options = "%command%"
|
||||
|
||||
# Get user's preferred Proton version
|
||||
proton_version = self._get_user_proton_version()
|
||||
|
||||
# Create shortcut with Proton using native service
|
||||
success, app_id = steam_service.create_shortcut_with_proton(
|
||||
app_name=shortcut_name,
|
||||
@@ -94,7 +135,7 @@ class AutomatedPrefixService:
|
||||
start_dir=modlist_install_dir,
|
||||
launch_options=launch_options,
|
||||
tags=["Jackify"],
|
||||
proton_version="proton_experimental"
|
||||
proton_version=proton_version
|
||||
)
|
||||
|
||||
if success and app_id:
|
||||
@@ -292,13 +333,13 @@ class AutomatedPrefixService:
|
||||
logger.error(f"Steam userdata directory not found: {userdata_dir}")
|
||||
return None
|
||||
|
||||
# Find the first user directory (most systems have only one user)
|
||||
user_dirs = [d for d in userdata_dir.iterdir() if d.is_dir() and d.name.isdigit()]
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in userdata_dir.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if not user_dirs:
|
||||
logger.error("No Steam user directories found in userdata")
|
||||
logger.error("No valid Steam user directories found in userdata (user 0 is not valid)")
|
||||
return None
|
||||
|
||||
# Use the first user directory found
|
||||
# Use the first valid user directory found
|
||||
user_dir = user_dirs[0]
|
||||
shortcuts_path = user_dir / "config" / "shortcuts.vdf"
|
||||
|
||||
@@ -2499,8 +2540,8 @@ echo Prefix creation complete.
|
||||
# Try the standard Steam userdata path
|
||||
steam_userdata_path = Path.home() / ".steam" / "steam" / "userdata"
|
||||
if steam_userdata_path.exists():
|
||||
# Find the first user directory (usually only one on Steam Deck)
|
||||
user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit()]
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in steam_userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if user_dirs:
|
||||
localconfig_path = user_dirs[0] / "config" / "localconfig.vdf"
|
||||
if localconfig_path.exists():
|
||||
@@ -2601,8 +2642,11 @@ echo Prefix creation complete.
|
||||
env = os.environ.copy()
|
||||
env['STEAM_COMPAT_CLIENT_INSTALL_PATH'] = str(steam_root)
|
||||
env['STEAM_COMPAT_DATA_PATH'] = str(compatdata_dir / str(abs(appid)))
|
||||
# Suppress GUI windows by unsetting DISPLAY
|
||||
# Suppress GUI windows using jackify-engine's proven approach
|
||||
env['DISPLAY'] = ''
|
||||
env['WAYLAND_DISPLAY'] = ''
|
||||
env['WINEDEBUG'] = '-all'
|
||||
env['WINEDLLOVERRIDES'] = 'msdia80.dll=n;conhost.exe=d;cmd.exe=d'
|
||||
|
||||
# Create the compatdata directory
|
||||
compat_dir = compatdata_dir / str(abs(appid))
|
||||
@@ -2616,7 +2660,9 @@ echo Prefix creation complete.
|
||||
cmd = [str(proton_path), 'run', 'wineboot', '-u']
|
||||
logger.info(f"Running: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=60)
|
||||
# Use jackify-engine's approach: UseShellExecute=false, CreateNoWindow=true equivalent
|
||||
result = subprocess.run(cmd, env=env, capture_output=True, text=True, timeout=60,
|
||||
shell=False, creationflags=getattr(subprocess, 'CREATE_NO_WINDOW', 0))
|
||||
logger.info(f"Proton exit code: {result.returncode}")
|
||||
|
||||
if result.stdout:
|
||||
@@ -2718,26 +2764,39 @@ echo Prefix creation complete.
|
||||
|
||||
def verify_compatibility_tool_persists(self, appid: int) -> bool:
|
||||
"""
|
||||
Verify that the compatibility tool setting persists.
|
||||
|
||||
Verify that the compatibility tool setting persists with correct Proton version.
|
||||
|
||||
Args:
|
||||
appid: The AppID to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if compatibility tool is set, False otherwise
|
||||
True if compatibility tool is correctly set, False otherwise
|
||||
"""
|
||||
try:
|
||||
config_path = Path.home() / ".steam/steam/config/config.vdf"
|
||||
with open(config_path, 'r') as f:
|
||||
if not config_path.exists():
|
||||
logger.warning("Steam config.vdf not found")
|
||||
return False
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
|
||||
# Check if AppID exists and has a Proton version set
|
||||
if f'"{appid}"' in content:
|
||||
logger.info(" Compatibility tool persists")
|
||||
return True
|
||||
# Get the expected Proton version
|
||||
expected_proton = self._get_user_proton_version()
|
||||
|
||||
# Look for the Proton version in the compatibility tool mapping
|
||||
if expected_proton in content:
|
||||
logger.info(f" Compatibility tool persists: {expected_proton}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"AppID {appid} found but Proton version '{expected_proton}' not set")
|
||||
return False
|
||||
else:
|
||||
logger.warning("Compatibility tool not found")
|
||||
return False
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying compatibility tool: {e}")
|
||||
return False
|
||||
|
||||
@@ -40,13 +40,13 @@ class NativeSteamService:
|
||||
logger.error("Steam userdata directory not found")
|
||||
return False
|
||||
|
||||
# Find the first user directory (usually there's only one)
|
||||
user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit()]
|
||||
# Find user directories (excluding user 0 which is a system account)
|
||||
user_dirs = [d for d in self.userdata_path.iterdir() if d.is_dir() and d.name.isdigit() and d.name != "0"]
|
||||
if not user_dirs:
|
||||
logger.error("No Steam user directories found")
|
||||
logger.error("No valid Steam user directories found (user 0 is not valid for shortcuts)")
|
||||
return False
|
||||
|
||||
# Use the first user directory
|
||||
# Use the first valid user directory
|
||||
user_dir = user_dirs[0]
|
||||
self.user_id = user_dir.name
|
||||
self.user_config_path = user_dir / "config"
|
||||
@@ -327,17 +327,27 @@ class NativeSteamService:
|
||||
logger.error(f"Error setting Proton version: {e}")
|
||||
return False
|
||||
|
||||
def create_shortcut_with_proton(self, app_name: str, exe_path: str, start_dir: str = None,
|
||||
def create_shortcut_with_proton(self, app_name: str, exe_path: str, start_dir: str = None,
|
||||
launch_options: str = "%command%", tags: List[str] = None,
|
||||
proton_version: str = "proton_experimental") -> Tuple[bool, Optional[int]]:
|
||||
proton_version: str = None) -> Tuple[bool, Optional[int]]:
|
||||
"""
|
||||
Complete workflow: Create shortcut and set Proton version.
|
||||
|
||||
|
||||
This is the main method that replaces STL entirely.
|
||||
|
||||
|
||||
Returns:
|
||||
(success, app_id) - Success status and the AppID
|
||||
"""
|
||||
# Auto-detect best Proton version if none provided
|
||||
if proton_version is None:
|
||||
try:
|
||||
from jackify.backend.core.modlist_operations import _get_user_proton_version
|
||||
proton_version = _get_user_proton_version()
|
||||
logger.info(f"Auto-detected Proton version: {proton_version}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to auto-detect Proton, falling back to experimental: {e}")
|
||||
proton_version = "proton_experimental"
|
||||
|
||||
logger.info(f"Creating shortcut with Proton: '{app_name}' -> '{proton_version}'")
|
||||
|
||||
# Step 1: Create the shortcut
|
||||
|
||||
@@ -103,15 +103,33 @@ class UpdateService:
|
||||
# Determine if this is a delta update
|
||||
is_delta = '.delta' in download_url or 'delta' in download_url.lower()
|
||||
|
||||
return UpdateInfo(
|
||||
version=latest_version,
|
||||
tag_name=release_data['tag_name'],
|
||||
release_date=release_data['published_at'],
|
||||
changelog=release_data.get('body', ''),
|
||||
download_url=download_url,
|
||||
file_size=file_size,
|
||||
is_delta_update=is_delta
|
||||
)
|
||||
# Safety checks to prevent segfault
|
||||
try:
|
||||
# Sanitize string fields
|
||||
safe_version = str(latest_version) if latest_version else ""
|
||||
safe_tag = str(release_data.get('tag_name', ''))
|
||||
safe_date = str(release_data.get('published_at', ''))
|
||||
safe_changelog = str(release_data.get('body', ''))[:1000] # Limit size
|
||||
safe_url = str(download_url)
|
||||
|
||||
logger.debug(f"Creating UpdateInfo for version {safe_version}")
|
||||
|
||||
update_info = UpdateInfo(
|
||||
version=safe_version,
|
||||
tag_name=safe_tag,
|
||||
release_date=safe_date,
|
||||
changelog=safe_changelog,
|
||||
download_url=safe_url,
|
||||
file_size=file_size,
|
||||
is_delta_update=is_delta
|
||||
)
|
||||
|
||||
logger.debug(f"UpdateInfo created successfully")
|
||||
return update_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create UpdateInfo: {e}")
|
||||
return None
|
||||
else:
|
||||
logger.warning(f"No AppImage found in release {latest_version}")
|
||||
|
||||
@@ -173,9 +191,14 @@ class UpdateService:
|
||||
def check_worker():
|
||||
try:
|
||||
update_info = self.check_for_updates()
|
||||
logger.debug(f"check_worker: Received update_info: {update_info}")
|
||||
logger.debug(f"check_worker: About to call callback...")
|
||||
callback(update_info)
|
||||
logger.debug(f"check_worker: Callback completed")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in background update check: {e}")
|
||||
import traceback
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
callback(None)
|
||||
|
||||
thread = threading.Thread(target=check_worker, daemon=True)
|
||||
@@ -331,42 +354,69 @@ class UpdateService:
|
||||
|
||||
script_content = f'''#!/bin/bash
|
||||
# Jackify Update Helper Script
|
||||
# This script replaces the current AppImage with the new version
|
||||
# This script safely replaces the current AppImage with the new version
|
||||
|
||||
CURRENT_APPIMAGE="{current_appimage}"
|
||||
NEW_APPIMAGE="{new_appimage}"
|
||||
TEMP_NAME="$CURRENT_APPIMAGE.updating"
|
||||
|
||||
echo "Jackify Update Helper"
|
||||
echo "Waiting for Jackify to exit..."
|
||||
|
||||
# Wait for Jackify to exit (give it a few seconds)
|
||||
sleep 3
|
||||
# Wait longer for Jackify to fully exit and unmount
|
||||
sleep 5
|
||||
|
||||
echo "Replacing AppImage..."
|
||||
echo "Validating new AppImage..."
|
||||
|
||||
# Backup current version (optional)
|
||||
# Validate new AppImage exists and is executable
|
||||
if [ ! -f "$NEW_APPIMAGE" ]; then
|
||||
echo "ERROR: New AppImage not found: $NEW_APPIMAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test that new AppImage can execute --version
|
||||
if ! timeout 10 "$NEW_APPIMAGE" --version >/dev/null 2>&1; then
|
||||
echo "ERROR: New AppImage failed validation test"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "New AppImage validated successfully"
|
||||
echo "Performing safe replacement..."
|
||||
|
||||
# Backup current version
|
||||
if [ -f "$CURRENT_APPIMAGE" ]; then
|
||||
cp "$CURRENT_APPIMAGE" "$CURRENT_APPIMAGE.backup"
|
||||
fi
|
||||
|
||||
# Replace with new version
|
||||
if cp "$NEW_APPIMAGE" "$CURRENT_APPIMAGE"; then
|
||||
chmod +x "$CURRENT_APPIMAGE"
|
||||
echo "Update completed successfully!"
|
||||
# Safe replacement: copy to temp name first, then atomic move
|
||||
if cp "$NEW_APPIMAGE" "$TEMP_NAME"; then
|
||||
chmod +x "$TEMP_NAME"
|
||||
|
||||
# Clean up temporary file
|
||||
rm -f "$NEW_APPIMAGE"
|
||||
|
||||
# Restart Jackify
|
||||
echo "Restarting Jackify..."
|
||||
exec "$CURRENT_APPIMAGE"
|
||||
else
|
||||
echo "Update failed - could not replace AppImage"
|
||||
# Restore backup if replacement failed
|
||||
if [ -f "$CURRENT_APPIMAGE.backup" ]; then
|
||||
mv "$CURRENT_APPIMAGE.backup" "$CURRENT_APPIMAGE"
|
||||
echo "Restored original AppImage"
|
||||
# Atomic move to replace
|
||||
if mv "$TEMP_NAME" "$CURRENT_APPIMAGE"; then
|
||||
echo "Update completed successfully!"
|
||||
|
||||
# Clean up
|
||||
rm -f "$NEW_APPIMAGE"
|
||||
rm -f "$CURRENT_APPIMAGE.backup"
|
||||
|
||||
# Restart Jackify
|
||||
echo "Restarting Jackify..."
|
||||
sleep 1
|
||||
exec "$CURRENT_APPIMAGE"
|
||||
else
|
||||
echo "ERROR: Failed to move updated AppImage"
|
||||
rm -f "$TEMP_NAME"
|
||||
# Restore backup
|
||||
if [ -f "$CURRENT_APPIMAGE.backup" ]; then
|
||||
mv "$CURRENT_APPIMAGE.backup" "$CURRENT_APPIMAGE"
|
||||
echo "Restored original AppImage"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "ERROR: Failed to copy new AppImage"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up this script
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {},
|
||||
".NETCoreApp,Version=v8.0/linux-x64": {
|
||||
"jackify-engine/0.3.13": {
|
||||
"jackify-engine/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Bethesda": "0.3.15",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Networking.Discord": "0.3.15",
|
||||
"Wabbajack.Networking.GitHub": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15",
|
||||
"Wabbajack.Server.Lib": "0.3.15",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.15",
|
||||
"Wabbajack.VFS": "0.3.15",
|
||||
"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.15": {
|
||||
"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.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.CLI.Builder.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Common/0.3.13": {
|
||||
"Wabbajack.Common/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Common.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compiler/0.3.13": {
|
||||
"Wabbajack.Compiler/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Installer": "0.3.15",
|
||||
"Wabbajack.VFS": "0.3.15",
|
||||
"ini-parser-netstandard": "2.5.2"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compiler.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compression.BSA/0.3.13": {
|
||||
"Wabbajack.Compression.BSA/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compression.BSA.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Compression.Zip/0.3.13": {
|
||||
"Wabbajack.Compression.Zip/0.3.15": {
|
||||
"dependencies": {
|
||||
"Wabbajack.IO.Async": "0.3.13"
|
||||
"Wabbajack.IO.Async": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Compression.Zip.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Configuration/0.3.13": {
|
||||
"Wabbajack.Configuration/0.3.15": {
|
||||
"runtime": {
|
||||
"Wabbajack.Configuration.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.13": {
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Bethesda.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.13": {
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.15",
|
||||
"Wabbajack.Downloaders.GoogleDrive": "0.3.15",
|
||||
"Wabbajack.Downloaders.Http": "0.3.15",
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader": "0.3.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Downloaders.Manual": "0.3.15",
|
||||
"Wabbajack.Downloaders.MediaFire": "0.3.15",
|
||||
"Wabbajack.Downloaders.Mega": "0.3.15",
|
||||
"Wabbajack.Downloaders.ModDB": "0.3.15",
|
||||
"Wabbajack.Downloaders.Nexus": "0.3.15",
|
||||
"Wabbajack.Downloaders.VerificationCache": "0.3.15",
|
||||
"Wabbajack.Downloaders.WabbajackCDN": "0.3.15",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Dispatcher.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.GameFile/0.3.13": {
|
||||
"Wabbajack.Downloaders.GameFile/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.VFS": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.GameFile.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.13": {
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.GoogleDrive.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Http/0.3.13": {
|
||||
"Wabbajack.Downloaders.Http/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Http.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.13": {
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Interfaces.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.13": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Manual/0.3.13": {
|
||||
"Wabbajack.Downloaders.Manual/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Manual.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.13": {
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.MediaFire.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Mega/0.3.13": {
|
||||
"Wabbajack.Downloaders.Mega/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Mega.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.ModDB/0.3.13": {
|
||||
"Wabbajack.Downloaders.ModDB/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.ModDB.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.Nexus/0.3.13": {
|
||||
"Wabbajack.Downloaders.Nexus/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.NexusApi": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.Nexus.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.13": {
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.VerificationCache.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.13": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.RateLimiter": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.DTOs/0.3.13": {
|
||||
"Wabbajack.DTOs/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Paths": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.DTOs.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.FileExtractor/0.3.13": {
|
||||
"Wabbajack.FileExtractor/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Compression.BSA": "0.3.15",
|
||||
"Wabbajack.Hashing.PHash": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.FileExtractor.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Hashing.PHash/0.3.13": {
|
||||
"Wabbajack.Hashing.PHash/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Hashing.PHash.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Hashing.xxHash64/0.3.13": {
|
||||
"Wabbajack.Hashing.xxHash64/0.3.15": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Paths": "0.3.13",
|
||||
"Wabbajack.RateLimiter": "0.3.13"
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.RateLimiter": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Hashing.xxHash64.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Installer/0.3.13": {
|
||||
"Wabbajack.Installer/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||
"Wabbajack.Downloaders.GameFile": "0.3.15",
|
||||
"Wabbajack.FileExtractor": "0.3.15",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15",
|
||||
"Wabbajack.VFS": "0.3.15",
|
||||
"ini-parser-netstandard": "2.5.2"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Installer.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.IO.Async/0.3.13": {
|
||||
"Wabbajack.IO.Async/0.3.15": {
|
||||
"runtime": {
|
||||
"Wabbajack.IO.Async.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.13": {
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.BethesdaNet.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Discord/0.3.13": {
|
||||
"Wabbajack.Networking.Discord/0.3.15": {
|
||||
"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.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Discord.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.GitHub/0.3.13": {
|
||||
"Wabbajack.Networking.GitHub/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.GitHub.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Http/0.3.13": {
|
||||
"Wabbajack.Networking.Http/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Interfaces": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Http.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.13": {
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.15": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.13"
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.Http.Interfaces.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.NexusApi/0.3.13": {
|
||||
"Wabbajack.Networking.NexusApi/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http": "0.3.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Networking.WabbajackClientApi": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.NexusApi.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.13": {
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.DTOs": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.15",
|
||||
"YamlDotNet": "16.3.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Networking.WabbajackClientApi.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Paths/0.3.13": {
|
||||
"Wabbajack.Paths/0.3.15": {
|
||||
"runtime": {
|
||||
"Wabbajack.Paths.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Paths.IO/0.3.13": {
|
||||
"Wabbajack.Paths.IO/0.3.15": {
|
||||
"dependencies": {
|
||||
"Wabbajack.Paths": "0.3.13",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"shortid": "4.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Paths.IO.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.RateLimiter/0.3.13": {
|
||||
"Wabbajack.RateLimiter/0.3.15": {
|
||||
"runtime": {
|
||||
"Wabbajack.RateLimiter.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Server.Lib/0.3.13": {
|
||||
"Wabbajack.Server.Lib/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Networking.Http.Interfaces": "0.3.15",
|
||||
"Wabbajack.Services.OSIntegrated": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Server.Lib.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.Services.OSIntegrated/0.3.13": {
|
||||
"Wabbajack.Services.OSIntegrated/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Downloaders.Dispatcher": "0.3.15",
|
||||
"Wabbajack.Installer": "0.3.15",
|
||||
"Wabbajack.Networking.BethesdaNet": "0.3.15",
|
||||
"Wabbajack.Networking.Discord": "0.3.15",
|
||||
"Wabbajack.VFS": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.Services.OSIntegrated.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.VFS/0.3.13": {
|
||||
"Wabbajack.VFS/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.FileExtractor": "0.3.15",
|
||||
"Wabbajack.Hashing.PHash": "0.3.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15",
|
||||
"Wabbajack.Paths.IO": "0.3.15",
|
||||
"Wabbajack.VFS.Interfaces": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.VFS.dll": {}
|
||||
}
|
||||
},
|
||||
"Wabbajack.VFS.Interfaces/0.3.13": {
|
||||
"Wabbajack.VFS.Interfaces/0.3.15": {
|
||||
"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.15",
|
||||
"Wabbajack.Hashing.xxHash64": "0.3.15",
|
||||
"Wabbajack.Paths": "0.3.15"
|
||||
},
|
||||
"runtime": {
|
||||
"Wabbajack.VFS.Interfaces.dll": {}
|
||||
@@ -2332,7 +2332,7 @@
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"jackify-engine/0.3.13": {
|
||||
"jackify-engine/0.3.15": {
|
||||
"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.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Common/0.3.13": {
|
||||
"Wabbajack.Common/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compiler/0.3.13": {
|
||||
"Wabbajack.Compiler/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compression.BSA/0.3.13": {
|
||||
"Wabbajack.Compression.BSA/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Compression.Zip/0.3.13": {
|
||||
"Wabbajack.Compression.Zip/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Configuration/0.3.13": {
|
||||
"Wabbajack.Configuration/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.13": {
|
||||
"Wabbajack.Downloaders.Bethesda/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.13": {
|
||||
"Wabbajack.Downloaders.Dispatcher/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.GameFile/0.3.13": {
|
||||
"Wabbajack.Downloaders.GameFile/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.13": {
|
||||
"Wabbajack.Downloaders.GoogleDrive/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Http/0.3.13": {
|
||||
"Wabbajack.Downloaders.Http/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.13": {
|
||||
"Wabbajack.Downloaders.Interfaces/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.13": {
|
||||
"Wabbajack.Downloaders.IPS4OAuth2Downloader/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Manual/0.3.13": {
|
||||
"Wabbajack.Downloaders.Manual/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.13": {
|
||||
"Wabbajack.Downloaders.MediaFire/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Mega/0.3.13": {
|
||||
"Wabbajack.Downloaders.Mega/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.ModDB/0.3.13": {
|
||||
"Wabbajack.Downloaders.ModDB/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.Nexus/0.3.13": {
|
||||
"Wabbajack.Downloaders.Nexus/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.13": {
|
||||
"Wabbajack.Downloaders.VerificationCache/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.13": {
|
||||
"Wabbajack.Downloaders.WabbajackCDN/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.DTOs/0.3.13": {
|
||||
"Wabbajack.DTOs/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.FileExtractor/0.3.13": {
|
||||
"Wabbajack.FileExtractor/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Hashing.PHash/0.3.13": {
|
||||
"Wabbajack.Hashing.PHash/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Hashing.xxHash64/0.3.13": {
|
||||
"Wabbajack.Hashing.xxHash64/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Installer/0.3.13": {
|
||||
"Wabbajack.Installer/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.IO.Async/0.3.13": {
|
||||
"Wabbajack.IO.Async/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.13": {
|
||||
"Wabbajack.Networking.BethesdaNet/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Discord/0.3.13": {
|
||||
"Wabbajack.Networking.Discord/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.GitHub/0.3.13": {
|
||||
"Wabbajack.Networking.GitHub/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Http/0.3.13": {
|
||||
"Wabbajack.Networking.Http/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.13": {
|
||||
"Wabbajack.Networking.Http.Interfaces/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.NexusApi/0.3.13": {
|
||||
"Wabbajack.Networking.NexusApi/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.13": {
|
||||
"Wabbajack.Networking.WabbajackClientApi/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Paths/0.3.13": {
|
||||
"Wabbajack.Paths/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Paths.IO/0.3.13": {
|
||||
"Wabbajack.Paths.IO/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.RateLimiter/0.3.13": {
|
||||
"Wabbajack.RateLimiter/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Server.Lib/0.3.13": {
|
||||
"Wabbajack.Server.Lib/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.Services.OSIntegrated/0.3.13": {
|
||||
"Wabbajack.Services.OSIntegrated/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.VFS/0.3.13": {
|
||||
"Wabbajack.VFS/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
},
|
||||
"Wabbajack.VFS.Interfaces/0.3.13": {
|
||||
"Wabbajack.VFS.Interfaces/0.3.15": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
400
jackify/frontends/gui/dialogs/about_dialog.py
Normal file
400
jackify/frontends/gui/dialogs/about_dialog.py
Normal file
@@ -0,0 +1,400 @@
|
||||
"""
|
||||
About dialog for Jackify.
|
||||
|
||||
This dialog displays system information, version details, and provides
|
||||
access to update checking and external links.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
|
||||
QGroupBox, QTextEdit, QApplication
|
||||
)
|
||||
from PySide6.QtCore import Qt, QThread, Signal
|
||||
from PySide6.QtGui import QFont, QClipboard
|
||||
|
||||
from ....backend.services.update_service import UpdateService
|
||||
from ....backend.models.configuration import SystemInfo
|
||||
from .... import __version__
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateCheckThread(QThread):
|
||||
"""Background thread for checking updates."""
|
||||
|
||||
update_check_finished = Signal(object) # UpdateInfo or None
|
||||
|
||||
def __init__(self, update_service: UpdateService):
|
||||
super().__init__()
|
||||
self.update_service = update_service
|
||||
|
||||
def run(self):
|
||||
"""Check for updates in background."""
|
||||
try:
|
||||
update_info = self.update_service.check_for_updates()
|
||||
self.update_check_finished.emit(update_info)
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking for updates: {e}")
|
||||
self.update_check_finished.emit(None)
|
||||
|
||||
|
||||
class AboutDialog(QDialog):
|
||||
"""About dialog showing system info and app details."""
|
||||
|
||||
def __init__(self, system_info: SystemInfo, parent=None):
|
||||
super().__init__(parent)
|
||||
self.system_info = system_info
|
||||
self.update_service = UpdateService(__version__)
|
||||
self.update_check_thread = None
|
||||
|
||||
self.setup_ui()
|
||||
self.setup_connections()
|
||||
|
||||
def setup_ui(self):
|
||||
"""Set up the dialog UI."""
|
||||
self.setWindowTitle("About Jackify")
|
||||
self.setModal(True)
|
||||
self.setFixedSize(520, 520)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
|
||||
# Header
|
||||
header_layout = QVBoxLayout()
|
||||
|
||||
# App icon/name
|
||||
title_label = QLabel("Jackify")
|
||||
title_font = QFont()
|
||||
title_font.setPointSize(18)
|
||||
title_font.setBold(True)
|
||||
title_label.setFont(title_font)
|
||||
title_label.setAlignment(Qt.AlignCenter)
|
||||
title_label.setStyleSheet("color: #3fd0ea; margin: 10px;")
|
||||
header_layout.addWidget(title_label)
|
||||
|
||||
subtitle_label = QLabel(f"v{__version__}")
|
||||
subtitle_font = QFont()
|
||||
subtitle_font.setPointSize(12)
|
||||
subtitle_label.setFont(subtitle_font)
|
||||
subtitle_label.setAlignment(Qt.AlignCenter)
|
||||
subtitle_label.setStyleSheet("color: #666; margin-bottom: 10px;")
|
||||
header_layout.addWidget(subtitle_label)
|
||||
|
||||
tagline_label = QLabel("Simplifying Wabbajack modlist installation and configuration on Linux")
|
||||
tagline_label.setAlignment(Qt.AlignCenter)
|
||||
tagline_label.setStyleSheet("color: #888; margin-bottom: 20px;")
|
||||
header_layout.addWidget(tagline_label)
|
||||
|
||||
layout.addLayout(header_layout)
|
||||
|
||||
# System Information Group
|
||||
system_group = QGroupBox("System Information")
|
||||
system_layout = QVBoxLayout(system_group)
|
||||
|
||||
system_info_text = self._get_system_info_text()
|
||||
system_info_label = QLabel(system_info_text)
|
||||
system_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
|
||||
system_info_label.setWordWrap(True)
|
||||
system_layout.addWidget(system_info_label)
|
||||
|
||||
layout.addWidget(system_group)
|
||||
|
||||
# Jackify Information Group
|
||||
jackify_group = QGroupBox("Jackify Information")
|
||||
jackify_layout = QVBoxLayout(jackify_group)
|
||||
|
||||
jackify_info_text = self._get_jackify_info_text()
|
||||
jackify_info_label = QLabel(jackify_info_text)
|
||||
jackify_info_label.setStyleSheet("font-family: monospace; font-size: 10pt; color: #ccc;")
|
||||
jackify_layout.addWidget(jackify_info_label)
|
||||
|
||||
layout.addWidget(jackify_group)
|
||||
|
||||
# Update status
|
||||
self.update_status_label = QLabel("")
|
||||
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
|
||||
self.update_status_label.setAlignment(Qt.AlignCenter)
|
||||
layout.addWidget(self.update_status_label)
|
||||
|
||||
# Buttons
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
# Update check button
|
||||
self.update_button = QPushButton("Check for Updates")
|
||||
self.update_button.clicked.connect(self.check_for_updates)
|
||||
self.update_button.setStyleSheet("""
|
||||
QPushButton {
|
||||
background-color: #23272e;
|
||||
color: #3fd0ea;
|
||||
font-weight: bold;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #3fd0ea;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #3fd0ea;
|
||||
color: #23272e;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #2bb8d6;
|
||||
color: #23272e;
|
||||
}
|
||||
QPushButton:disabled {
|
||||
background-color: #444;
|
||||
color: #666;
|
||||
border-color: #666;
|
||||
}
|
||||
""")
|
||||
button_layout.addWidget(self.update_button)
|
||||
|
||||
button_layout.addStretch()
|
||||
|
||||
# Copy Info button
|
||||
copy_button = QPushButton("Copy Info")
|
||||
copy_button.clicked.connect(self.copy_system_info)
|
||||
button_layout.addWidget(copy_button)
|
||||
|
||||
# External links
|
||||
github_button = QPushButton("GitHub")
|
||||
github_button.clicked.connect(self.open_github)
|
||||
button_layout.addWidget(github_button)
|
||||
|
||||
nexus_button = QPushButton("Nexus")
|
||||
nexus_button.clicked.connect(self.open_nexus)
|
||||
button_layout.addWidget(nexus_button)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Close button
|
||||
close_layout = QHBoxLayout()
|
||||
close_layout.addStretch()
|
||||
close_button = QPushButton("Close")
|
||||
close_button.setDefault(True)
|
||||
close_button.clicked.connect(self.accept)
|
||||
close_layout.addWidget(close_button)
|
||||
layout.addLayout(close_layout)
|
||||
|
||||
def setup_connections(self):
|
||||
"""Set up signal connections."""
|
||||
pass
|
||||
|
||||
def _get_system_info_text(self) -> str:
|
||||
"""Get formatted system information."""
|
||||
try:
|
||||
# OS info
|
||||
os_info = self._get_os_info()
|
||||
kernel = platform.release()
|
||||
|
||||
# Desktop environment
|
||||
desktop = self._get_desktop_environment()
|
||||
|
||||
# Display server
|
||||
display_server = self._get_display_server()
|
||||
|
||||
return f"• OS: {os_info}\n• Kernel: {kernel}\n• Desktop: {desktop}\n• Display: {display_server}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting system info: {e}")
|
||||
return "• System info unavailable"
|
||||
|
||||
def _get_jackify_info_text(self) -> str:
|
||||
"""Get formatted Jackify information."""
|
||||
try:
|
||||
# Engine version
|
||||
engine_version = self._get_engine_version()
|
||||
|
||||
# Python version
|
||||
python_version = platform.python_version()
|
||||
|
||||
return f"• Engine: {engine_version}\n• Python: {python_version}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting Jackify info: {e}")
|
||||
return "• Jackify info unavailable"
|
||||
|
||||
def _get_os_info(self) -> str:
|
||||
"""Get OS distribution name and version."""
|
||||
try:
|
||||
if os.path.exists("/etc/os-release"):
|
||||
with open("/etc/os-release", "r") as f:
|
||||
lines = f.readlines()
|
||||
pretty_name = None
|
||||
name = None
|
||||
version = None
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("PRETTY_NAME="):
|
||||
pretty_name = line.split("=", 1)[1].strip('"')
|
||||
elif line.startswith("NAME="):
|
||||
name = line.split("=", 1)[1].strip('"')
|
||||
elif line.startswith("VERSION="):
|
||||
version = line.split("=", 1)[1].strip('"')
|
||||
|
||||
# Prefer PRETTY_NAME, fallback to NAME + VERSION
|
||||
if pretty_name:
|
||||
return pretty_name
|
||||
elif name and version:
|
||||
return f"{name} {version}"
|
||||
elif name:
|
||||
return name
|
||||
|
||||
# Fallback to platform info
|
||||
return f"{platform.system()} {platform.release()}"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting OS info: {e}")
|
||||
return "Unknown Linux"
|
||||
|
||||
def _get_desktop_environment(self) -> str:
|
||||
"""Get desktop environment."""
|
||||
try:
|
||||
# Try XDG_CURRENT_DESKTOP first
|
||||
desktop = os.environ.get("XDG_CURRENT_DESKTOP")
|
||||
if desktop:
|
||||
return desktop
|
||||
|
||||
# Fallback to DESKTOP_SESSION
|
||||
desktop = os.environ.get("DESKTOP_SESSION")
|
||||
if desktop:
|
||||
return desktop
|
||||
|
||||
# Try detecting common DEs
|
||||
if os.environ.get("KDE_FULL_SESSION"):
|
||||
return "KDE"
|
||||
elif os.environ.get("GNOME_DESKTOP_SESSION_ID"):
|
||||
return "GNOME"
|
||||
elif os.environ.get("XFCE4_SESSION"):
|
||||
return "XFCE"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting desktop environment: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def _get_display_server(self) -> str:
|
||||
"""Get display server type (Wayland or X11)."""
|
||||
try:
|
||||
# Check XDG_SESSION_TYPE first
|
||||
session_type = os.environ.get("XDG_SESSION_TYPE")
|
||||
if session_type:
|
||||
return session_type.capitalize()
|
||||
|
||||
# Check for Wayland display
|
||||
if os.environ.get("WAYLAND_DISPLAY"):
|
||||
return "Wayland"
|
||||
|
||||
# Check for X11 display
|
||||
if os.environ.get("DISPLAY"):
|
||||
return "X11"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting display server: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def _get_engine_version(self) -> str:
|
||||
"""Get jackify-engine version."""
|
||||
try:
|
||||
# Try to execute jackify-engine --version
|
||||
engine_path = Path(__file__).parent.parent.parent.parent / "engine" / "jackify-engine"
|
||||
if engine_path.exists():
|
||||
result = subprocess.run([str(engine_path), "--version"],
|
||||
capture_output=True, text=True, timeout=5)
|
||||
if result.returncode == 0:
|
||||
version = result.stdout.strip()
|
||||
# Extract just the version number (before the +commit hash)
|
||||
if '+' in version:
|
||||
version = version.split('+')[0]
|
||||
return f"v{version}"
|
||||
|
||||
return "Unknown"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting engine version: {e}")
|
||||
return "Unknown"
|
||||
|
||||
def check_for_updates(self):
|
||||
"""Check for updates in background."""
|
||||
if self.update_check_thread and self.update_check_thread.isRunning():
|
||||
return
|
||||
|
||||
self.update_button.setEnabled(False)
|
||||
self.update_button.setText("Checking...")
|
||||
self.update_status_label.setText("Checking for updates...")
|
||||
|
||||
self.update_check_thread = UpdateCheckThread(self.update_service)
|
||||
self.update_check_thread.update_check_finished.connect(self.update_check_finished)
|
||||
self.update_check_thread.start()
|
||||
|
||||
def update_check_finished(self, update_info):
|
||||
"""Handle update check completion."""
|
||||
self.update_button.setEnabled(True)
|
||||
self.update_button.setText("Check for Updates")
|
||||
|
||||
if update_info:
|
||||
self.update_status_label.setText(f"Update available: v{update_info.version}")
|
||||
self.update_status_label.setStyleSheet("color: #3fd0ea; font-size: 10pt; margin: 5px;")
|
||||
|
||||
# Show update dialog
|
||||
from .update_dialog import UpdateDialog
|
||||
update_dialog = UpdateDialog(update_info, self.update_service, self)
|
||||
update_dialog.exec()
|
||||
else:
|
||||
self.update_status_label.setText("You're running the latest version")
|
||||
self.update_status_label.setStyleSheet("color: #666; font-size: 10pt; margin: 5px;")
|
||||
|
||||
def copy_system_info(self):
|
||||
"""Copy system information to clipboard."""
|
||||
try:
|
||||
info_text = f"""Jackify v{__version__} (Engine {self._get_engine_version()})
|
||||
OS: {self._get_os_info()} ({platform.release()})
|
||||
Desktop: {self._get_desktop_environment()} ({self._get_display_server()})
|
||||
Python: {platform.python_version()}"""
|
||||
|
||||
clipboard = QApplication.clipboard()
|
||||
clipboard.setText(info_text)
|
||||
|
||||
# Briefly update button text
|
||||
sender = self.sender()
|
||||
original_text = sender.text()
|
||||
sender.setText("Copied!")
|
||||
|
||||
# Reset button text after delay
|
||||
from PySide6.QtCore import QTimer
|
||||
QTimer.singleShot(1000, lambda: sender.setText(original_text))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error copying system info: {e}")
|
||||
|
||||
def open_github(self):
|
||||
"""Open GitHub repository."""
|
||||
try:
|
||||
import webbrowser
|
||||
webbrowser.open("https://github.com/Omni-guides/Jackify")
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening GitHub: {e}")
|
||||
|
||||
def open_nexus(self):
|
||||
"""Open Nexus Mods page."""
|
||||
try:
|
||||
import webbrowser
|
||||
webbrowser.open("https://www.nexusmods.com/site/mods/1427")
|
||||
except Exception as e:
|
||||
logger.error(f"Error opening Nexus: {e}")
|
||||
|
||||
def closeEvent(self, event):
|
||||
"""Handle dialog close event."""
|
||||
if self.update_check_thread and self.update_check_thread.isRunning():
|
||||
self.update_check_thread.terminate()
|
||||
self.update_check_thread.wait()
|
||||
|
||||
event.accept()
|
||||
@@ -7,6 +7,7 @@ This replaces the legacy jackify_gui implementation with a refactored architectu
|
||||
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
# Suppress xkbcommon locale errors (harmless but annoying)
|
||||
@@ -81,6 +82,9 @@ if '--env-diagnostic' in sys.argv:
|
||||
|
||||
from jackify import __version__ as jackify_version
|
||||
|
||||
# Initialize logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if '--help' in sys.argv or '-h' in sys.argv:
|
||||
print("""Jackify - Native Linux Modlist Manager\n\nUsage:\n jackify [--cli] [--debug] [--version] [--help]\n\nOptions:\n --cli Launch CLI frontend\n --debug Enable debug logging\n --version Show version and exit\n --help, -h Show this help message and exit\n\nIf no options are given, the GUI will launch by default.\n""")
|
||||
sys.exit(0)
|
||||
@@ -98,7 +102,7 @@ sys.path.insert(0, str(src_dir))
|
||||
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QLabel, QVBoxLayout, QPushButton,
|
||||
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle
|
||||
QStackedWidget, QHBoxLayout, QDialog, QFormLayout, QLineEdit, QCheckBox, QSpinBox, QMessageBox, QGroupBox, QGridLayout, QFileDialog, QToolButton, QStyle, QComboBox
|
||||
)
|
||||
from PySide6.QtCore import Qt, QEvent
|
||||
from PySide6.QtGui import QIcon
|
||||
@@ -298,6 +302,33 @@ class SettingsDialog(QDialog):
|
||||
main_layout.addWidget(api_group)
|
||||
main_layout.addSpacing(12)
|
||||
|
||||
# --- Proton Version Section ---
|
||||
proton_group = QGroupBox("Proton Version")
|
||||
proton_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
|
||||
proton_layout = QHBoxLayout()
|
||||
proton_group.setLayout(proton_layout)
|
||||
|
||||
self.proton_dropdown = QComboBox()
|
||||
self.proton_dropdown.setToolTip("Select Proton version for shortcut creation and texture processing")
|
||||
self.proton_dropdown.setMinimumWidth(200)
|
||||
|
||||
# Populate Proton dropdown
|
||||
self._populate_proton_dropdown()
|
||||
|
||||
# Refresh button for Proton detection
|
||||
refresh_btn = QPushButton("↻")
|
||||
refresh_btn.setFixedSize(30, 30)
|
||||
refresh_btn.setToolTip("Refresh Proton version list")
|
||||
refresh_btn.clicked.connect(self._refresh_proton_dropdown)
|
||||
|
||||
proton_layout.addWidget(QLabel("Proton Version:"))
|
||||
proton_layout.addWidget(self.proton_dropdown)
|
||||
proton_layout.addWidget(refresh_btn)
|
||||
proton_layout.addStretch()
|
||||
|
||||
main_layout.addWidget(proton_group)
|
||||
main_layout.addSpacing(12)
|
||||
|
||||
# --- Directories & Paths Section ---
|
||||
dir_group = QGroupBox("Directories & Paths")
|
||||
dir_group.setStyleSheet("QGroupBox { border: 1px solid #555; border-radius: 6px; margin-top: 8px; padding: 8px; background: #23282d; } QGroupBox:title { subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; font-weight: bold; color: #fff; }")
|
||||
@@ -325,6 +356,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)
|
||||
|
||||
@@ -424,6 +478,85 @@ class SettingsDialog(QDialog):
|
||||
api_key = text.strip()
|
||||
self.config_handler.save_api_key(api_key)
|
||||
|
||||
def _get_proton_10_path(self):
|
||||
"""Get Proton 10 path if available, fallback to auto"""
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
available_protons = WineUtils.scan_valve_proton_versions()
|
||||
|
||||
# Look for Proton 10.x
|
||||
for proton in available_protons:
|
||||
if proton['version'].startswith('10.'):
|
||||
return proton['path']
|
||||
|
||||
# Fallback to auto if no Proton 10 found
|
||||
return 'auto'
|
||||
except:
|
||||
return 'auto'
|
||||
|
||||
def _populate_proton_dropdown(self):
|
||||
"""Populate Proton version dropdown with detected versions (includes GE-Proton and Valve Proton)"""
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
|
||||
# Get all available Proton versions (GE-Proton + Valve Proton)
|
||||
available_protons = WineUtils.scan_all_proton_versions()
|
||||
|
||||
# Add "Auto" option first
|
||||
self.proton_dropdown.addItem("Auto", "auto")
|
||||
|
||||
# Add detected Proton versions with type indicators
|
||||
for proton in available_protons:
|
||||
proton_name = proton.get('name', 'Unknown Proton')
|
||||
proton_type = proton.get('type', 'Unknown')
|
||||
|
||||
# Format display name to show type for clarity
|
||||
if proton_type == 'GE-Proton':
|
||||
display_name = f"{proton_name} (GE)"
|
||||
elif proton_type == 'Valve-Proton':
|
||||
display_name = f"{proton_name}"
|
||||
else:
|
||||
display_name = proton_name
|
||||
|
||||
self.proton_dropdown.addItem(display_name, str(proton['path']))
|
||||
|
||||
# Load saved preference and determine UI selection
|
||||
saved_proton = self.config_handler.get('proton_path', self._get_proton_10_path())
|
||||
|
||||
# Check if saved path matches any specific Proton in dropdown
|
||||
found_match = False
|
||||
for i in range(self.proton_dropdown.count()):
|
||||
if self.proton_dropdown.itemData(i) == saved_proton:
|
||||
self.proton_dropdown.setCurrentIndex(i)
|
||||
found_match = True
|
||||
break
|
||||
|
||||
# If no exact match found, check if it's a resolved auto-selection
|
||||
if not found_match and saved_proton != "auto":
|
||||
# This means config has a resolved path from previous "Auto" selection
|
||||
# Show "Auto" in UI since user chose auto-detection
|
||||
for i in range(self.proton_dropdown.count()):
|
||||
if self.proton_dropdown.itemData(i) == "auto":
|
||||
self.proton_dropdown.setCurrentIndex(i)
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to populate Proton dropdown: {e}")
|
||||
# Fallback: just show auto
|
||||
self.proton_dropdown.addItem("Auto", "auto")
|
||||
|
||||
def _refresh_proton_dropdown(self):
|
||||
"""Refresh Proton dropdown with latest detected versions"""
|
||||
current_selection = self.proton_dropdown.currentData()
|
||||
self.proton_dropdown.clear()
|
||||
self._populate_proton_dropdown()
|
||||
|
||||
# Restore selection if still available
|
||||
for i in range(self.proton_dropdown.count()):
|
||||
if self.proton_dropdown.itemData(i) == current_selection:
|
||||
self.proton_dropdown.setCurrentIndex(i)
|
||||
break
|
||||
|
||||
def _save(self):
|
||||
# Validate values
|
||||
for k, (multithreading_checkbox, max_tasks_spin) in self.resource_edits.items():
|
||||
@@ -464,7 +597,41 @@ 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)
|
||||
|
||||
# Save Proton selection - resolve "auto" to actual path
|
||||
selected_proton_path = self.proton_dropdown.currentData()
|
||||
if selected_proton_path == "auto":
|
||||
# Resolve "auto" to actual best Proton path using unified detection
|
||||
try:
|
||||
from jackify.backend.handlers.wine_utils import WineUtils
|
||||
best_proton = WineUtils.select_best_proton()
|
||||
|
||||
if best_proton:
|
||||
resolved_path = str(best_proton['path'])
|
||||
resolved_version = best_proton['name']
|
||||
else:
|
||||
resolved_path = "auto"
|
||||
resolved_version = "auto"
|
||||
except:
|
||||
resolved_path = "auto"
|
||||
resolved_version = "auto"
|
||||
else:
|
||||
# User selected specific Proton version
|
||||
resolved_path = selected_proton_path
|
||||
# Extract version from dropdown text
|
||||
resolved_version = self.proton_dropdown.currentText()
|
||||
|
||||
self.config_handler.set("proton_path", resolved_path)
|
||||
self.config_handler.set("proton_version", resolved_version)
|
||||
|
||||
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 +651,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 +845,7 @@ class JackifyMainWindow(QMainWindow):
|
||||
# Spacer
|
||||
bottom_bar_layout.addStretch(1)
|
||||
|
||||
# Settings button (right)
|
||||
# Settings button (right side)
|
||||
settings_btn = QLabel('<a href="#" style="color:#6cf; text-decoration:none;">Settings</a>')
|
||||
settings_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
|
||||
settings_btn.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
@@ -663,6 +853,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('<a href="#" style="color:#6cf; text-decoration:none;">About</a>')
|
||||
about_btn.setStyleSheet("color: #6cf; font-size: 13px; padding-right: 8px;")
|
||||
about_btn.setTextInteractionFlags(Qt.TextBrowserInteraction)
|
||||
about_btn.setOpenExternalLinks(False)
|
||||
about_btn.linkActivated.connect(self.open_about_dialog)
|
||||
bottom_bar_layout.addWidget(about_btn, alignment=Qt.AlignRight)
|
||||
|
||||
# --- Main Layout ---
|
||||
central_widget = QWidget()
|
||||
main_layout = QVBoxLayout()
|
||||
@@ -740,27 +938,56 @@ class JackifyMainWindow(QMainWindow):
|
||||
# Continue anyway - don't block startup on detection errors
|
||||
|
||||
def _check_for_updates_on_startup(self):
|
||||
"""Check for updates on startup in background thread"""
|
||||
"""Check for updates on startup - SIMPLE VERSION"""
|
||||
try:
|
||||
debug_print("Checking for updates on startup...")
|
||||
|
||||
def update_check_callback(update_info):
|
||||
"""Handle update check results"""
|
||||
try:
|
||||
if update_info:
|
||||
debug_print(f"Update available: v{update_info.version}")
|
||||
# Show update dialog
|
||||
# Do it synchronously and simply
|
||||
update_info = self.update_service.check_for_updates()
|
||||
if update_info:
|
||||
debug_print(f"Update available: v{update_info.version}")
|
||||
|
||||
# Simple QMessageBox - no complex dialogs
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtCore import QTimer
|
||||
|
||||
def show_update_dialog():
|
||||
try:
|
||||
debug_print("Creating UpdateDialog...")
|
||||
from jackify.frontends.gui.dialogs.update_dialog import UpdateDialog
|
||||
dialog = UpdateDialog(update_info, self.update_service, self)
|
||||
debug_print("UpdateDialog created, showing...")
|
||||
dialog.show() # Non-blocking
|
||||
else:
|
||||
debug_print("No updates available")
|
||||
except Exception as e:
|
||||
debug_print(f"Error showing update dialog: {e}")
|
||||
|
||||
# Check for updates in background
|
||||
self.update_service.check_for_updates_async(update_check_callback)
|
||||
|
||||
debug_print("UpdateDialog shown successfully")
|
||||
except Exception as e:
|
||||
debug_print(f"UpdateDialog failed: {e}, falling back to simple dialog")
|
||||
# Fallback to simple dialog
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"Update Available",
|
||||
f"Jackify v{update_info.version} is available.\n\nDownload and install now?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.Yes
|
||||
)
|
||||
if reply == QMessageBox.Yes:
|
||||
# Simple download and replace
|
||||
try:
|
||||
new_appimage = self.update_service.download_update(update_info)
|
||||
if new_appimage:
|
||||
if self.update_service.apply_update(new_appimage):
|
||||
debug_print("Update applied successfully")
|
||||
else:
|
||||
QMessageBox.warning(self, "Update Failed", "Failed to apply update.")
|
||||
else:
|
||||
QMessageBox.warning(self, "Update Failed", "Failed to download update.")
|
||||
except Exception as e:
|
||||
QMessageBox.warning(self, "Update Failed", f"Update failed: {e}")
|
||||
|
||||
# Use QTimer to show dialog after GUI is fully loaded
|
||||
QTimer.singleShot(1000, show_update_dialog)
|
||||
else:
|
||||
debug_print("No updates available")
|
||||
|
||||
except Exception as e:
|
||||
debug_print(f"Error checking for updates on startup: {e}")
|
||||
# Continue anyway - don't block startup on update check errors
|
||||
@@ -808,6 +1035,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'):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -22,6 +22,7 @@ from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
from ..dialogs import SuccessDialog
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from jackify.frontends.gui.services.message_service import MessageService
|
||||
from jackify.shared.resolution_utils import get_resolution_fallback
|
||||
|
||||
def debug_print(message):
|
||||
"""Print debug message only if debug mode is enabled"""
|
||||
@@ -106,8 +107,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 +211,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 +235,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 +358,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 +580,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 +635,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 +651,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 +794,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"""
|
||||
@@ -962,7 +1034,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
try:
|
||||
# Get resolution from UI
|
||||
resolution = self.resolution_combo.currentText()
|
||||
resolution_value = resolution.split()[0] if resolution != "Leave unchanged" else '2560x1600'
|
||||
resolution_value = resolution.split()[0] if resolution != "Leave unchanged" else None
|
||||
|
||||
# Update the context with the new AppID (same format as manual steps)
|
||||
mo2_exe_path = self.install_dir_edit.text().strip()
|
||||
@@ -1011,7 +1083,7 @@ class ConfigureNewModlistScreen(QWidget):
|
||||
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', '2560x1600'),
|
||||
resolution=self.context.get('resolution') or get_resolution_fallback(None),
|
||||
skip_confirmation=True
|
||||
)
|
||||
|
||||
@@ -1162,8 +1234,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 +1257,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")
|
||||
|
||||
@@ -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
|
||||
@@ -368,6 +367,10 @@ class InstallModlistScreen(QWidget):
|
||||
self.resolution_service = ResolutionService()
|
||||
self.config_handler = ConfigHandler()
|
||||
self.protontricks_service = ProtontricksDetectionService()
|
||||
|
||||
# Somnium guidance tracking
|
||||
self._show_somnium_guidance = False
|
||||
self._somnium_install_dir = None
|
||||
|
||||
# Scroll tracking for professional auto-scroll behavior
|
||||
self._user_manually_scrolled = False
|
||||
@@ -459,11 +462,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 +487,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 +606,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 +744,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 +1193,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 +1203,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 +1236,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
|
||||
@@ -1282,7 +1360,8 @@ class InstallModlistScreen(QWidget):
|
||||
'oblivion': 'oblivion',
|
||||
'starfield': 'starfield',
|
||||
'oblivion_remastered': 'oblivion_remastered',
|
||||
'enderal': 'enderal'
|
||||
'enderal': 'enderal',
|
||||
'enderal special edition': 'enderal'
|
||||
}
|
||||
game_type = game_mapping.get(game_name.lower())
|
||||
debug_print(f"DEBUG: Mapped game_name '{game_name}' to game_type: '{game_type}'")
|
||||
@@ -1299,6 +1378,7 @@ class InstallModlistScreen(QWidget):
|
||||
|
||||
# Check if game is supported
|
||||
debug_print(f"DEBUG: Checking if game_type '{game_type}' is supported")
|
||||
debug_print(f"DEBUG: game_type='{game_type}', game_name='{game_name}'")
|
||||
is_supported = self.wabbajack_parser.is_supported_game(game_type) if game_type else False
|
||||
debug_print(f"DEBUG: is_supported_game('{game_type}') returned: {is_supported}")
|
||||
|
||||
@@ -1324,14 +1404,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 +1578,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 +1608,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 +1699,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 +1736,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,15 +1759,33 @@ 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")
|
||||
|
||||
if not os.path.exists(final_exe_path):
|
||||
self._safe_append_text(f"ERROR: ModOrganizer.exe not found at {final_exe_path}")
|
||||
MessageService.critical(self, "ModOrganizer.exe Not Found",
|
||||
f"ModOrganizer.exe not found at:\n{final_exe_path}\n\nCannot proceed with automated setup.")
|
||||
return
|
||||
# Check if this is Somnium specifically (uses files/ subdirectory)
|
||||
modlist_name_lower = modlist_name.lower()
|
||||
if "somnium" in modlist_name_lower:
|
||||
somnium_exe_path = os.path.join(install_dir, "files", "ModOrganizer.exe")
|
||||
if os.path.exists(somnium_exe_path):
|
||||
final_exe_path = somnium_exe_path
|
||||
self._safe_append_text(f"Detected Somnium modlist - will proceed with automated setup")
|
||||
# Show Somnium guidance popup after automated workflow completes
|
||||
self._show_somnium_guidance = True
|
||||
self._somnium_install_dir = install_dir
|
||||
else:
|
||||
self._safe_append_text(f"ERROR: Somnium ModOrganizer.exe not found at {somnium_exe_path}")
|
||||
MessageService.critical(self, "Somnium ModOrganizer.exe Not Found",
|
||||
f"Expected Somnium ModOrganizer.exe not found at:\n{somnium_exe_path}\n\nCannot proceed with automated setup.")
|
||||
return
|
||||
else:
|
||||
self._safe_append_text(f"ERROR: ModOrganizer.exe not found at {final_exe_path}")
|
||||
MessageService.critical(self, "ModOrganizer.exe Not Found",
|
||||
f"ModOrganizer.exe not found at:\n{final_exe_path}\n\nCannot proceed with automated setup.")
|
||||
return
|
||||
|
||||
# Run automated prefix creation in separate thread
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
@@ -1784,33 +1885,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 +1942,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 +1953,57 @@ 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:
|
||||
# Check if we need to show Somnium guidance
|
||||
if self._show_somnium_guidance:
|
||||
self._show_somnium_post_install_guidance()
|
||||
|
||||
# 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,16 +2062,25 @@ 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)
|
||||
|
||||
def _get_mo2_path(self, install_dir, modlist_name):
|
||||
"""Get ModOrganizer.exe path, handling Somnium's non-standard structure"""
|
||||
mo2_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
|
||||
if not os.path.exists(mo2_exe_path) and "somnium" in modlist_name.lower():
|
||||
somnium_path = os.path.join(install_dir, "files", "ModOrganizer.exe")
|
||||
if os.path.exists(somnium_path):
|
||||
mo2_exe_path = somnium_path
|
||||
return mo2_exe_path
|
||||
|
||||
def validate_manual_steps_completion(self):
|
||||
"""Validate that manual steps were actually completed and handle retry logic"""
|
||||
modlist_name = self.modlist_name_edit.text().strip()
|
||||
install_dir = self.install_dir_edit.text().strip()
|
||||
mo2_exe_path = os.path.join(install_dir, "ModOrganizer.exe")
|
||||
mo2_exe_path = self._get_mo2_path(install_dir, modlist_name)
|
||||
|
||||
# Add delay to allow Steam filesystem updates to complete
|
||||
self._safe_append_text("Waiting for Steam filesystem updates to complete...")
|
||||
@@ -2187,7 +2318,7 @@ class InstallModlistScreen(QWidget):
|
||||
updated_context = {
|
||||
'name': modlist_name,
|
||||
'path': install_dir,
|
||||
'mo2_exe_path': os.path.join(install_dir, "ModOrganizer.exe"),
|
||||
'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name),
|
||||
'modlist_value': None,
|
||||
'modlist_source': None,
|
||||
'resolution': getattr(self, '_current_resolution', '2560x1600'),
|
||||
@@ -2285,7 +2416,7 @@ class InstallModlistScreen(QWidget):
|
||||
updated_context = {
|
||||
'name': modlist_name,
|
||||
'path': install_dir,
|
||||
'mo2_exe_path': os.path.join(install_dir, "ModOrganizer.exe"),
|
||||
'mo2_exe_path': self._get_mo2_path(install_dir, modlist_name),
|
||||
'modlist_value': None,
|
||||
'modlist_source': None,
|
||||
'resolution': getattr(self, '_current_resolution', '2560x1600'),
|
||||
@@ -2513,13 +2644,33 @@ 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)
|
||||
|
||||
self._safe_append_text("Installation cancelled by user.")
|
||||
|
||||
def _show_somnium_post_install_guidance(self):
|
||||
"""Show guidance popup for Somnium post-installation steps"""
|
||||
from ..widgets.message_service import MessageService
|
||||
|
||||
guidance_text = f"""<b>Somnium Post-Installation Required</b><br><br>
|
||||
Due to Somnium's non-standard folder structure, you need to manually update the binary paths in ModOrganizer:<br><br>
|
||||
<b>1.</b> Launch the Steam shortcut created for Somnium<br>
|
||||
<b>2.</b> In ModOrganizer, go to Settings → Executables<br>
|
||||
<b>3.</b> For each executable entry (SKSE64, etc.), update the binary path to point to:<br>
|
||||
<code>{self._somnium_install_dir}/files/root/Enderal Special Edition/skse64_loader.exe</code><br><br>
|
||||
<b>Note:</b> Full Somnium support will be added in a future Jackify update.<br><br>
|
||||
<i>You can also refer to the Somnium installation guide at:<br>
|
||||
https://wiki.scenicroute.games/Somnium/1_Installation.html</i>"""
|
||||
|
||||
MessageService.information(self, "Somnium Setup Required", guidance_text)
|
||||
|
||||
# Reset the guidance flag
|
||||
self._show_somnium_guidance = False
|
||||
self._somnium_install_dir = None
|
||||
|
||||
def cancel_and_cleanup(self):
|
||||
"""Handle Cancel button - clean up processes and go back"""
|
||||
self.cleanup_processes()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -94,6 +94,7 @@ class UnsupportedGameDialog(QDialog):
|
||||
<li><strong>Oblivion</strong></li>
|
||||
<li><strong>Starfield</strong></li>
|
||||
<li><strong>Oblivion Remastered</strong></li>
|
||||
<li><strong>Enderal</strong></li>
|
||||
</ul>
|
||||
|
||||
<p>For unsupported games, you will need to manually configure Steam shortcuts and other post-install steps.</p>
|
||||
@@ -113,6 +114,7 @@ class UnsupportedGameDialog(QDialog):
|
||||
<li><strong>Oblivion</strong></li>
|
||||
<li><strong>Starfield</strong></li>
|
||||
<li><strong>Oblivion Remastered</strong></li>
|
||||
<li><strong>Enderal</strong></li>
|
||||
</ul>
|
||||
|
||||
<p>For unsupported games, you will need to manually configure Steam shortcuts and other post-install steps.</p>
|
||||
|
||||
@@ -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."""
|
||||
|
||||
65
jackify/shared/paths.py
Normal file
65
jackify/shared/paths.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Path utilities for Jackify.
|
||||
|
||||
This module provides standardized path resolution for Jackify directories,
|
||||
supporting configurable data directory while keeping config in a fixed location.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def get_jackify_data_dir() -> Path:
|
||||
"""
|
||||
Get the configurable Jackify data directory.
|
||||
|
||||
This directory contains:
|
||||
- downloaded_mod_lists/
|
||||
- logs/
|
||||
- temporary proton prefixes during installation
|
||||
|
||||
Returns:
|
||||
Path: The Jackify data directory (always set in config)
|
||||
"""
|
||||
try:
|
||||
# Import here to avoid circular imports
|
||||
from jackify.backend.handlers.config_handler import ConfigHandler
|
||||
|
||||
config_handler = ConfigHandler()
|
||||
jackify_data_dir = config_handler.get('jackify_data_dir')
|
||||
|
||||
# Config handler now always ensures this is set, but fallback just in case
|
||||
if jackify_data_dir:
|
||||
return Path(jackify_data_dir).expanduser()
|
||||
else:
|
||||
return Path.home() / "Jackify"
|
||||
|
||||
except Exception:
|
||||
# Emergency fallback if config system fails
|
||||
return Path.home() / "Jackify"
|
||||
|
||||
|
||||
def get_jackify_logs_dir() -> Path:
|
||||
"""Get the logs directory within the Jackify data directory."""
|
||||
return get_jackify_data_dir() / "logs"
|
||||
|
||||
|
||||
def get_jackify_downloads_dir() -> Path:
|
||||
"""Get the downloaded modlists directory within the Jackify data directory."""
|
||||
return get_jackify_data_dir() / "downloaded_mod_lists"
|
||||
|
||||
|
||||
def get_jackify_config_dir() -> Path:
|
||||
"""
|
||||
Get the Jackify configuration directory (always ~/.config/jackify).
|
||||
|
||||
This directory contains:
|
||||
- config.json (settings)
|
||||
- API keys and credentials
|
||||
- Resource settings
|
||||
|
||||
Returns:
|
||||
Path: Always ~/.config/jackify
|
||||
"""
|
||||
return Path.home() / ".config" / "jackify"
|
||||
113
jackify/shared/resolution_utils.py
Normal file
113
jackify/shared/resolution_utils.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Resolution Utilities Module
|
||||
Provides utility functions for handling resolution across GUI and CLI frontends
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_default_resolution() -> str:
|
||||
"""
|
||||
Get the appropriate default resolution based on system detection and user preferences.
|
||||
|
||||
Returns:
|
||||
str: Resolution string (e.g., '1920x1080', '1280x800')
|
||||
"""
|
||||
try:
|
||||
# First try to get saved resolution from config
|
||||
from ..backend.services.resolution_service import ResolutionService
|
||||
resolution_service = ResolutionService()
|
||||
|
||||
saved_resolution = resolution_service.get_saved_resolution()
|
||||
if saved_resolution and saved_resolution != 'Leave unchanged':
|
||||
logger.debug(f"Using saved resolution: {saved_resolution}")
|
||||
return saved_resolution
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load ResolutionService: {e}")
|
||||
|
||||
try:
|
||||
# Check for Steam Deck
|
||||
if _is_steam_deck():
|
||||
logger.debug("Steam Deck detected, using 1280x800")
|
||||
return "1280x800"
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error detecting Steam Deck: {e}")
|
||||
|
||||
# Fallback to common 1080p instead of arbitrary resolution
|
||||
logger.debug("Using fallback resolution: 1920x1080")
|
||||
return "1920x1080"
|
||||
|
||||
|
||||
def _is_steam_deck() -> bool:
|
||||
"""
|
||||
Detect if running on Steam Deck
|
||||
|
||||
Returns:
|
||||
bool: True if Steam Deck detected
|
||||
"""
|
||||
try:
|
||||
if os.path.exists("/etc/os-release"):
|
||||
with open("/etc/os-release", "r") as f:
|
||||
content = f.read().lower()
|
||||
return "steamdeck" in content or "steamos" in content
|
||||
except Exception as e:
|
||||
logger.debug(f"Error reading /etc/os-release: {e}")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_resolution_fallback(current_resolution: Optional[str]) -> str:
|
||||
"""
|
||||
Get appropriate resolution fallback when current resolution is invalid or None
|
||||
|
||||
Args:
|
||||
current_resolution: Current resolution value that might be None/invalid
|
||||
|
||||
Returns:
|
||||
str: Valid resolution string
|
||||
"""
|
||||
if current_resolution and current_resolution != 'Leave unchanged':
|
||||
# Validate format
|
||||
if _validate_resolution_format(current_resolution):
|
||||
return current_resolution
|
||||
|
||||
# Use proper default resolution logic
|
||||
return get_default_resolution()
|
||||
|
||||
|
||||
def _validate_resolution_format(resolution: str) -> bool:
|
||||
"""
|
||||
Validate resolution format
|
||||
|
||||
Args:
|
||||
resolution: Resolution string to validate
|
||||
|
||||
Returns:
|
||||
bool: True if valid WxH format
|
||||
"""
|
||||
import re
|
||||
|
||||
if not resolution:
|
||||
return False
|
||||
|
||||
# Handle Steam Deck format
|
||||
clean_resolution = resolution.replace(' (Steam Deck)', '')
|
||||
|
||||
# Check WxH format
|
||||
if re.match(r'^[0-9]+x[0-9]+$', clean_resolution):
|
||||
try:
|
||||
width, height = clean_resolution.split('x')
|
||||
width_int, height_int = int(width), int(height)
|
||||
return 0 < width_int <= 10000 and 0 < height_int <= 10000
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return False
|
||||
@@ -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 ║
|
||||
|
||||
Reference in New Issue
Block a user