Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
f1cd7eadb9
|
|||
|
49ba0a4602
|
|||
|
958fe26cad
|
@@ -12,3 +12,7 @@ to bump the version
|
|||||||
|
|
||||||
python version_manager.py 1.0.1
|
python version_manager.py 1.0.1
|
||||||
|
|
||||||
|
|
||||||
|
build
|
||||||
|
|
||||||
|
pyinstaller --clean --onefile --windowed --icon=art/Progression.ico --add-data "art;art" --name ProgressionLoader steam_workshop_gui.py
|
||||||
@@ -1160,7 +1160,7 @@ class SteamWorkshopGUI:
|
|||||||
self.log_to_output("🔍 Please provide a valid RimWorld installation path to continue.\n")
|
self.log_to_output("🔍 Please provide a valid RimWorld installation path to continue.\n")
|
||||||
self.log_to_output(" The 'RimWorld Game Folder' label will stop blinking once a valid path is detected.\n\n")
|
self.log_to_output(" The 'RimWorld Game Folder' label will stop blinking once a valid path is detected.\n\n")
|
||||||
|
|
||||||
# Initialize update checker
|
# Initialize update checker (for manual checks only)
|
||||||
config = get_update_config()
|
config = get_update_config()
|
||||||
self.update_checker = UpdateChecker(config["current_version"])
|
self.update_checker = UpdateChecker(config["current_version"])
|
||||||
|
|
||||||
@@ -1173,10 +1173,6 @@ class SteamWorkshopGUI:
|
|||||||
# Start blinking animation for RimWorld label (with slight delay to ensure GUI is ready)
|
# Start blinking animation for RimWorld label (with slight delay to ensure GUI is ready)
|
||||||
self.root.after(1000, self.start_rimworld_label_blink)
|
self.root.after(1000, self.start_rimworld_label_blink)
|
||||||
|
|
||||||
# Check for updates on startup if enabled
|
|
||||||
if config["check_on_startup"]:
|
|
||||||
self.root.after(config["startup_check_delay"], self.check_for_updates_on_startup)
|
|
||||||
|
|
||||||
def create_header_frame(self, parent):
|
def create_header_frame(self, parent):
|
||||||
"""Create header frame with progression logo"""
|
"""Create header frame with progression logo"""
|
||||||
header_frame = tk.Frame(parent, bg='#2b2b2b', height=100)
|
header_frame = tk.Frame(parent, bg='#2b2b2b', height=100)
|
||||||
@@ -2127,7 +2123,13 @@ class SteamWorkshopGUI:
|
|||||||
def generate_rml_xml(self, mod_data):
|
def generate_rml_xml(self, mod_data):
|
||||||
"""Generate the XML content for the .rml file"""
|
"""Generate the XML content for the .rml file"""
|
||||||
# Get core mods from ModsConfig.xml and RimWorld Data folder
|
# Get core mods from ModsConfig.xml and RimWorld Data folder
|
||||||
|
msg = "=== GENERATING RML XML ===\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
core_mods = self.get_core_mods_from_config()
|
core_mods = self.get_core_mods_from_config()
|
||||||
|
msg = f"Core mods returned: {core_mods}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
|
||||||
# Extract mod names from About.xml files for workshop mods
|
# Extract mod names from About.xml files for workshop mods
|
||||||
mod_names = []
|
mod_names = []
|
||||||
@@ -2144,8 +2146,14 @@ class SteamWorkshopGUI:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Add core mods first
|
# Add core mods first
|
||||||
|
msg = "Adding core mods to XML:\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
for package_id, mod_name in core_mods:
|
for package_id, mod_name in core_mods:
|
||||||
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
|
xml_lines.append(f'\t\t\t<li>{package_id}</li>')
|
||||||
|
msg = f" Added core mod: {package_id} ({mod_name})\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
|
||||||
# Add workshop mod package IDs to meta section
|
# Add workshop mod package IDs to meta section
|
||||||
for workshop_id, package_id in mod_data:
|
for workshop_id, package_id in mod_data:
|
||||||
@@ -2214,14 +2222,20 @@ class SteamWorkshopGUI:
|
|||||||
return '\n'.join(xml_lines)
|
return '\n'.join(xml_lines)
|
||||||
|
|
||||||
def get_core_mods_from_config(self):
|
def get_core_mods_from_config(self):
|
||||||
"""Get core mods from ModsConfig.xml knownExpansions section and their info from RimWorld Data folder"""
|
"""Get core mods from ModsConfig.xml knownExpansions section - includes ALL DLC the user owns"""
|
||||||
core_mods = []
|
core_mods = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not self.modsconfig_path or not os.path.exists(self.modsconfig_path):
|
if not self.modsconfig_path or not os.path.exists(self.modsconfig_path):
|
||||||
self._safe_update_output("ModsConfig.xml not found, using default core mods\n")
|
msg = "ModsConfig.xml not found, using default core mods\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
return [('ludeon.rimworld', 'RimWorld')]
|
return [('ludeon.rimworld', 'RimWorld')]
|
||||||
|
|
||||||
|
msg = f"Reading ModsConfig.xml from: {self.modsconfig_path}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
|
||||||
# Parse ModsConfig.xml
|
# Parse ModsConfig.xml
|
||||||
tree = ET.parse(self.modsconfig_path)
|
tree = ET.parse(self.modsconfig_path)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
@@ -2231,31 +2245,122 @@ class SteamWorkshopGUI:
|
|||||||
if self.rimworld_var.get().strip():
|
if self.rimworld_var.get().strip():
|
||||||
rimworld_game_path = self.rimworld_var.get().strip()
|
rimworld_game_path = self.rimworld_var.get().strip()
|
||||||
rimworld_data_path = os.path.join(rimworld_game_path, "Data")
|
rimworld_data_path = os.path.join(rimworld_game_path, "Data")
|
||||||
|
msg = f"RimWorld Data path: {rimworld_data_path}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
else:
|
||||||
|
msg = "No RimWorld path set, using fallback names\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
|
||||||
# Find knownExpansions section
|
# Find knownExpansions section and include ALL expansions found
|
||||||
known_expansions_element = root.find('knownExpansions')
|
known_expansions_element = root.find('knownExpansions')
|
||||||
if known_expansions_element is not None:
|
if known_expansions_element is not None:
|
||||||
|
msg = "Found knownExpansions section, processing all DLC...\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
for li in known_expansions_element.findall('li'):
|
for li in known_expansions_element.findall('li'):
|
||||||
expansion_id = li.text
|
expansion_id = li.text
|
||||||
if expansion_id:
|
if expansion_id:
|
||||||
|
expansion_id = expansion_id.strip()
|
||||||
|
msg = f"Processing DLC: {expansion_id}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
# Use the same method as DLC detection to get real names
|
# Use the same method as DLC detection to get real names
|
||||||
mod_name = self.get_expansion_real_name(expansion_id.strip(), rimworld_data_path)
|
mod_name = self.get_expansion_real_name(expansion_id, rimworld_data_path)
|
||||||
|
msg = f"get_expansion_real_name returned: {mod_name}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
if not mod_name:
|
if not mod_name:
|
||||||
mod_name = self.get_default_mod_name(expansion_id.strip())
|
mod_name = self.get_default_mod_name(expansion_id)
|
||||||
core_mods.append((expansion_id.strip(), mod_name))
|
msg = f"Using fallback name: {mod_name}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
core_mods.append((expansion_id, mod_name))
|
||||||
|
msg = f"Added to core_mods: {expansion_id} -> {mod_name}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
else:
|
||||||
|
msg = "No knownExpansions section found in ModsConfig.xml\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
|
||||||
if not core_mods:
|
if not core_mods:
|
||||||
# Fallback if no expansions found
|
# Fallback if no expansions found - just base game
|
||||||
core_mods = [('ludeon.rimworld', 'RimWorld')]
|
core_mods = [('ludeon.rimworld', 'RimWorld')]
|
||||||
|
msg = "No expansions found, using fallback base game only\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
|
||||||
self._safe_update_output(f"Found {len(core_mods)} core expansions from ModsConfig.xml\n")
|
msg = f"Final core_mods list: {core_mods}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
msg = f"Found {len(core_mods)} core expansions from knownExpansions in ModsConfig.xml\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._safe_update_output(f"Error reading core mods: {str(e)}\n")
|
msg = f"Error reading core mods: {str(e)}\n"
|
||||||
|
self._safe_update_output(msg)
|
||||||
|
print(msg.strip())
|
||||||
core_mods = [('ludeon.rimworld', 'RimWorld')]
|
core_mods = [('ludeon.rimworld', 'RimWorld')]
|
||||||
|
|
||||||
return core_mods
|
return core_mods
|
||||||
|
|
||||||
|
def get_expansion_real_name(self, expansion_id, rimworld_data_path):
|
||||||
|
"""Get the real expansion name from RimWorld Data folder About.xml"""
|
||||||
|
try:
|
||||||
|
if not rimworld_data_path or not os.path.exists(rimworld_data_path):
|
||||||
|
# Fallback to extracting from ID if no data path
|
||||||
|
return self.extract_name_from_id(expansion_id)
|
||||||
|
|
||||||
|
# Look for expansion folder in Data directory
|
||||||
|
# Core expansions are usually in subfolders like Royalty, Ideology, etc.
|
||||||
|
possible_folders = [
|
||||||
|
expansion_id.replace('ludeon.rimworld.', '').capitalize(),
|
||||||
|
expansion_id.split('.')[-1].capitalize() if '.' in expansion_id else expansion_id,
|
||||||
|
expansion_id.replace('ludeon.rimworld.', ''),
|
||||||
|
expansion_id.replace('ludeon.rimworld', 'Core')
|
||||||
|
]
|
||||||
|
|
||||||
|
for folder_name in possible_folders:
|
||||||
|
about_xml_path = os.path.join(rimworld_data_path, folder_name, "About", "About.xml")
|
||||||
|
if os.path.exists(about_xml_path):
|
||||||
|
try:
|
||||||
|
tree = ET.parse(about_xml_path)
|
||||||
|
root = tree.getroot()
|
||||||
|
|
||||||
|
name_element = root.find('name')
|
||||||
|
if name_element is not None and name_element.text:
|
||||||
|
return name_element.text.strip()
|
||||||
|
except ET.ParseError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If not found in Data folder, use fallback
|
||||||
|
return self.extract_name_from_id(expansion_id)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return self.extract_name_from_id(expansion_id)
|
||||||
|
|
||||||
|
def extract_name_from_id(self, expansion_id):
|
||||||
|
"""Extract expansion name from ID as fallback"""
|
||||||
|
if 'royalty' in expansion_id.lower():
|
||||||
|
return 'Royalty'
|
||||||
|
elif 'ideology' in expansion_id.lower():
|
||||||
|
return 'Ideology'
|
||||||
|
elif 'biotech' in expansion_id.lower():
|
||||||
|
return 'Biotech'
|
||||||
|
elif 'anomaly' in expansion_id.lower():
|
||||||
|
return 'Anomaly'
|
||||||
|
elif 'odyssey' in expansion_id.lower():
|
||||||
|
return 'Odyssey'
|
||||||
|
else:
|
||||||
|
# Extract the last part after the last dot and capitalize
|
||||||
|
parts = expansion_id.split('.')
|
||||||
|
if len(parts) > 1:
|
||||||
|
return parts[-1].capitalize()
|
||||||
|
return expansion_id
|
||||||
|
|
||||||
def get_default_mod_name(self, mod_id):
|
def get_default_mod_name(self, mod_id):
|
||||||
"""Get default display name for core mods"""
|
"""Get default display name for core mods"""
|
||||||
default_names = {
|
default_names = {
|
||||||
@@ -2603,20 +2708,6 @@ you must auto sort after this!"""
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Could not add update button: {e}")
|
print(f"Could not add update button: {e}")
|
||||||
|
|
||||||
def check_for_updates_on_startup(self):
|
|
||||||
"""Check for updates when the application starts"""
|
|
||||||
def update_callback(release_info, error):
|
|
||||||
if error or not release_info:
|
|
||||||
return # Silently fail on startup
|
|
||||||
|
|
||||||
latest_version = release_info['version']
|
|
||||||
if (self.update_checker.is_newer_version(latest_version) and
|
|
||||||
not self.update_checker.should_skip_version(latest_version)):
|
|
||||||
# Show update dialog
|
|
||||||
self.update_checker.show_update_dialog(self.root, release_info)
|
|
||||||
|
|
||||||
self.update_checker.check_for_updates_async(update_callback)
|
|
||||||
|
|
||||||
def manual_update_check(self):
|
def manual_update_check(self):
|
||||||
"""Manually triggered update check"""
|
"""Manually triggered update check"""
|
||||||
self.update_checker.manual_check_for_updates(self.root)
|
self.update_checker.manual_check_for_updates(self.root)
|
||||||
@@ -2625,6 +2716,9 @@ def main():
|
|||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
root.withdraw() # Hide main window initially
|
root.withdraw() # Hide main window initially
|
||||||
|
|
||||||
|
# Import config
|
||||||
|
from update_config import get_update_config
|
||||||
|
|
||||||
# Set window icon
|
# Set window icon
|
||||||
try:
|
try:
|
||||||
icon_path = get_resource_path(os.path.join("art", "Progression.ico"))
|
icon_path = get_resource_path(os.path.join("art", "Progression.ico"))
|
||||||
@@ -2638,10 +2732,356 @@ def main():
|
|||||||
root.deiconify() # Show main window
|
root.deiconify() # Show main window
|
||||||
app = SteamWorkshopGUI(root)
|
app = SteamWorkshopGUI(root)
|
||||||
|
|
||||||
# Show loading screen first
|
# Check for updates SYNCHRONOUSLY before starting anything
|
||||||
|
config = get_update_config()
|
||||||
|
update_info = check_for_updates_before_startup(root)
|
||||||
|
|
||||||
|
if update_info and config.get("updates_required", True):
|
||||||
|
# Show blocking update dialog - app won't continue until user updates
|
||||||
|
show_blocking_update_dialog(root, update_info['checker'], update_info['release'])
|
||||||
|
else:
|
||||||
|
# No update needed or updates not required, start loading screen
|
||||||
loading_screen = LoadingScreen(root, show_main_app)
|
loading_screen = LoadingScreen(root, show_main_app)
|
||||||
|
|
||||||
|
# If update available but not required, show non-blocking dialog
|
||||||
|
if update_info:
|
||||||
|
root.after(1000, lambda: show_optional_update_dialog(root, update_info['checker'], update_info['release']))
|
||||||
|
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
|
def check_for_updates_before_startup(root):
|
||||||
|
"""Check for updates synchronously before starting the application"""
|
||||||
|
try:
|
||||||
|
from update_checker import UpdateChecker
|
||||||
|
from update_config import get_update_config
|
||||||
|
|
||||||
|
print("Checking for updates...")
|
||||||
|
|
||||||
|
config = get_update_config()
|
||||||
|
update_checker = UpdateChecker(config["current_version"])
|
||||||
|
|
||||||
|
# Use synchronous check to block until complete
|
||||||
|
release_info, error = update_checker.check_for_updates_sync()
|
||||||
|
|
||||||
|
if error:
|
||||||
|
print(f"Update check failed: {error}")
|
||||||
|
return None
|
||||||
|
elif release_info:
|
||||||
|
latest_version = release_info['version']
|
||||||
|
if update_checker.is_newer_version(latest_version):
|
||||||
|
print(f"Update required: {latest_version}")
|
||||||
|
return {
|
||||||
|
'checker': update_checker,
|
||||||
|
'release': release_info
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
print(f"Already up to date: {latest_version}")
|
||||||
|
else:
|
||||||
|
print("No release info available")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Update check failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def show_blocking_update_dialog(root, update_checker, release_info):
|
||||||
|
"""Show a blocking update dialog that prevents app from continuing"""
|
||||||
|
# Create a new window for the update dialog
|
||||||
|
update_window = tk.Toplevel(root)
|
||||||
|
update_window.title("Update Required - Progression Loader")
|
||||||
|
update_window.configure(bg='#2b2b2b')
|
||||||
|
update_window.resizable(False, False)
|
||||||
|
update_window.attributes('-topmost', True)
|
||||||
|
|
||||||
|
# Make it modal - user can't interact with other windows
|
||||||
|
update_window.transient(root)
|
||||||
|
update_window.grab_set()
|
||||||
|
|
||||||
|
# Set window icon
|
||||||
|
try:
|
||||||
|
icon_path = get_resource_path(os.path.join("art", "Progression.ico"))
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
update_window.iconbitmap(icon_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Calculate window size and center it
|
||||||
|
window_width = 600
|
||||||
|
window_height = 600 # Increased height to accommodate buttons
|
||||||
|
|
||||||
|
# Get screen dimensions
|
||||||
|
screen_width = update_window.winfo_screenwidth()
|
||||||
|
screen_height = update_window.winfo_screenheight()
|
||||||
|
|
||||||
|
# Calculate center position
|
||||||
|
x = (screen_width - window_width) // 2
|
||||||
|
y = (screen_height - window_height) // 2
|
||||||
|
|
||||||
|
update_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_label = tk.Label(update_window,
|
||||||
|
text="🔄 Update Required",
|
||||||
|
fg='#ff6b6b', bg='#2b2b2b',
|
||||||
|
font=('Arial', 20, 'bold'))
|
||||||
|
title_label.pack(pady=20)
|
||||||
|
|
||||||
|
# Message
|
||||||
|
message_label = tk.Label(update_window,
|
||||||
|
text="A new version is available and required to continue.",
|
||||||
|
fg='white', bg='#2b2b2b',
|
||||||
|
font=('Arial', 14))
|
||||||
|
message_label.pack(pady=10)
|
||||||
|
|
||||||
|
# Version info
|
||||||
|
version_frame = tk.Frame(update_window, bg='#2b2b2b')
|
||||||
|
version_frame.pack(pady=15)
|
||||||
|
|
||||||
|
current_label = tk.Label(version_frame,
|
||||||
|
text=f"Current Version: {update_checker.current_version}",
|
||||||
|
fg='#cccccc', bg='#2b2b2b',
|
||||||
|
font=('Arial', 12))
|
||||||
|
current_label.pack()
|
||||||
|
|
||||||
|
latest_label = tk.Label(version_frame,
|
||||||
|
text=f"Required Version: {release_info['version']}",
|
||||||
|
fg='#4ecdc4', bg='#2b2b2b',
|
||||||
|
font=('Arial', 12, 'bold'))
|
||||||
|
latest_label.pack(pady=5)
|
||||||
|
|
||||||
|
# Release notes
|
||||||
|
if release_info.get('body'):
|
||||||
|
notes_label = tk.Label(update_window,
|
||||||
|
text="What's New:",
|
||||||
|
fg='white', bg='#2b2b2b',
|
||||||
|
font=('Arial', 14, 'bold'))
|
||||||
|
notes_label.pack(pady=(20, 10))
|
||||||
|
|
||||||
|
# Create scrollable text widget for release notes
|
||||||
|
notes_frame = tk.Frame(update_window, bg='#2b2b2b')
|
||||||
|
notes_frame.pack(fill='both', expand=True, padx=40, pady=(0, 10))
|
||||||
|
|
||||||
|
notes_text = tk.Text(notes_frame,
|
||||||
|
height=6, # Reduced height to make room for buttons
|
||||||
|
bg='#404040', fg='#ffffff',
|
||||||
|
font=('Arial', 11),
|
||||||
|
wrap=tk.WORD,
|
||||||
|
state='disabled',
|
||||||
|
relief='flat',
|
||||||
|
bd=0)
|
||||||
|
|
||||||
|
scrollbar = tk.Scrollbar(notes_frame, bg='#404040')
|
||||||
|
scrollbar.pack(side='right', fill='y')
|
||||||
|
|
||||||
|
notes_text.pack(side='left', fill='both', expand=True)
|
||||||
|
notes_text.config(yscrollcommand=scrollbar.set)
|
||||||
|
scrollbar.config(command=notes_text.yview)
|
||||||
|
|
||||||
|
# Insert release notes
|
||||||
|
notes_text.config(state='normal')
|
||||||
|
notes_text.insert('1.0', release_info['body'])
|
||||||
|
notes_text.config(state='disabled')
|
||||||
|
|
||||||
|
# Buttons - fixed at bottom
|
||||||
|
button_frame = tk.Frame(update_window, bg='#2b2b2b')
|
||||||
|
button_frame.pack(side='bottom', pady=20) # Pack at bottom with padding
|
||||||
|
|
||||||
|
def auto_update():
|
||||||
|
"""Download and install the update automatically"""
|
||||||
|
try:
|
||||||
|
# Disable buttons during download
|
||||||
|
auto_btn.config(state='disabled', text='Downloading...')
|
||||||
|
download_btn.config(state='disabled')
|
||||||
|
exit_btn.config(state='disabled')
|
||||||
|
|
||||||
|
# Update the window to show progress
|
||||||
|
update_window.update()
|
||||||
|
|
||||||
|
# Start download in a separate thread
|
||||||
|
import threading
|
||||||
|
threading.Thread(target=perform_auto_update, daemon=True).start()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Auto update failed: {e}")
|
||||||
|
# Re-enable buttons on error
|
||||||
|
auto_btn.config(state='normal', text='Auto Update')
|
||||||
|
download_btn.config(state='normal')
|
||||||
|
exit_btn.config(state='normal')
|
||||||
|
|
||||||
|
def perform_auto_update():
|
||||||
|
"""Perform the actual auto update process"""
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Find the EXE asset in the release
|
||||||
|
exe_asset = None
|
||||||
|
for asset in release_info.get('assets', []):
|
||||||
|
if asset.get('name', '').endswith('.exe'):
|
||||||
|
exe_asset = asset
|
||||||
|
break
|
||||||
|
|
||||||
|
if not exe_asset:
|
||||||
|
raise Exception("No EXE file found in release assets")
|
||||||
|
|
||||||
|
download_url = exe_asset.get('browser_download_url')
|
||||||
|
if not download_url:
|
||||||
|
raise Exception("No download URL found for EXE asset")
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
root.after(0, lambda: auto_btn.config(text='Downloading...'))
|
||||||
|
|
||||||
|
# Download the new EXE
|
||||||
|
response = requests.get(download_url, stream=True)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Save to temporary file
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix='.exe') as temp_file:
|
||||||
|
temp_path = temp_file.name
|
||||||
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
|
temp_file.write(chunk)
|
||||||
|
|
||||||
|
# Get current executable path
|
||||||
|
if getattr(sys, 'frozen', False):
|
||||||
|
# Running as PyInstaller bundle
|
||||||
|
current_exe = sys.executable
|
||||||
|
else:
|
||||||
|
# Running as script - for testing
|
||||||
|
current_exe = sys.argv[0]
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
root.after(0, lambda: auto_btn.config(text='Installing...'))
|
||||||
|
|
||||||
|
# Get configuration
|
||||||
|
from update_config import get_update_config
|
||||||
|
config = get_update_config()
|
||||||
|
auto_restart = config.get("auto_restart_after_update", True)
|
||||||
|
|
||||||
|
# Create a batch script to replace the executable and optionally restart
|
||||||
|
if auto_restart:
|
||||||
|
restart_command = f'start "" /B "{current_exe}"'
|
||||||
|
restart_message = "Starting new version..."
|
||||||
|
else:
|
||||||
|
restart_command = 'echo Please manually start the application.'
|
||||||
|
restart_message = "Please manually start the updated application."
|
||||||
|
|
||||||
|
batch_script = f'''@echo off
|
||||||
|
echo Updating Progression Loader...
|
||||||
|
timeout /t 3 /nobreak >nul
|
||||||
|
|
||||||
|
REM Replace the executable
|
||||||
|
move "{temp_path}" "{current_exe}"
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Failed to replace executable
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Update complete! {restart_message}
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
|
||||||
|
REM Start the new executable (if configured)
|
||||||
|
{restart_command}
|
||||||
|
|
||||||
|
REM Clean up this batch file
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
del "%~f0"
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Write batch script to temp file
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.bat') as batch_file:
|
||||||
|
batch_file.write(batch_script)
|
||||||
|
batch_path = batch_file.name
|
||||||
|
|
||||||
|
# Update status
|
||||||
|
root.after(0, lambda: auto_btn.config(text='Restarting...'))
|
||||||
|
|
||||||
|
# Show success message
|
||||||
|
root.after(0, lambda: tk.messagebox.showinfo(
|
||||||
|
"Update Complete",
|
||||||
|
"Update downloaded successfully!\nThe application will restart automatically.",
|
||||||
|
parent=update_window
|
||||||
|
))
|
||||||
|
|
||||||
|
# Execute the batch script and exit
|
||||||
|
subprocess.Popen([batch_path], shell=True)
|
||||||
|
|
||||||
|
# Exit the current application
|
||||||
|
root.after(2000, root.quit)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Auto update failed: {e}")
|
||||||
|
# Re-enable buttons on error
|
||||||
|
root.after(0, lambda: auto_btn.config(state='normal', text='Auto Update'))
|
||||||
|
root.after(0, lambda: download_btn.config(state='normal'))
|
||||||
|
root.after(0, lambda: exit_btn.config(state='normal'))
|
||||||
|
|
||||||
|
# Show error message
|
||||||
|
root.after(0, lambda: tk.messagebox.showerror(
|
||||||
|
"Auto Update Failed",
|
||||||
|
f"Failed to automatically update:\n{str(e)}\n\nPlease use 'Download Page' to update manually.",
|
||||||
|
parent=update_window
|
||||||
|
))
|
||||||
|
|
||||||
|
def download_page():
|
||||||
|
"""Open the release download page"""
|
||||||
|
webbrowser.open(release_info['html_url'])
|
||||||
|
print("Please download and install the update manually, then restart the application.")
|
||||||
|
root.quit() # Exit the entire application
|
||||||
|
|
||||||
|
def exit_app():
|
||||||
|
"""Exit the application without updating"""
|
||||||
|
print("Application cannot continue without updating.")
|
||||||
|
root.quit()
|
||||||
|
|
||||||
|
# Auto Update button (primary action)
|
||||||
|
auto_btn = tk.Button(button_frame,
|
||||||
|
text="Auto Update",
|
||||||
|
command=auto_update,
|
||||||
|
bg='#4ecdc4', fg='white',
|
||||||
|
font=('Arial', 14, 'bold'),
|
||||||
|
padx=30, pady=10,
|
||||||
|
cursor='hand2')
|
||||||
|
auto_btn.pack(side='left', padx=10)
|
||||||
|
|
||||||
|
# Download Page button (secondary action)
|
||||||
|
download_btn = tk.Button(button_frame,
|
||||||
|
text="Download Page",
|
||||||
|
command=download_page,
|
||||||
|
bg='#45b7d1', fg='white',
|
||||||
|
font=('Arial', 12),
|
||||||
|
padx=20, pady=10,
|
||||||
|
cursor='hand2')
|
||||||
|
download_btn.pack(side='left', padx=10)
|
||||||
|
|
||||||
|
# Exit button (tertiary action)
|
||||||
|
exit_btn = tk.Button(button_frame,
|
||||||
|
text="Exit Application",
|
||||||
|
command=exit_app,
|
||||||
|
bg='#666666', fg='white',
|
||||||
|
font=('Arial', 12),
|
||||||
|
padx=20, pady=10,
|
||||||
|
cursor='hand2')
|
||||||
|
exit_btn.pack(side='left', padx=10)
|
||||||
|
|
||||||
|
# Handle window close (same as exit)
|
||||||
|
update_window.protocol("WM_DELETE_WINDOW", exit_app)
|
||||||
|
|
||||||
|
# Focus on the auto update button
|
||||||
|
auto_btn.focus_set()
|
||||||
|
|
||||||
|
print("Blocking update dialog shown - app will not continue until user updates")
|
||||||
|
|
||||||
|
def show_optional_update_dialog(root, update_checker, release_info):
|
||||||
|
"""Show a non-blocking update dialog"""
|
||||||
|
try:
|
||||||
|
update_checker.show_update_dialog(root, release_info)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not show update dialog: {e}")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Configuration settings for the update checker
|
|||||||
# Update checker configuration
|
# Update checker configuration
|
||||||
UPDATE_CONFIG = {
|
UPDATE_CONFIG = {
|
||||||
# Current version of the application
|
# Current version of the application
|
||||||
"current_version": "0.0.1",
|
"current_version": "0.0.3",
|
||||||
|
|
||||||
# Repository information
|
# Repository information
|
||||||
"repo_owner": "HRiggs",
|
"repo_owner": "HRiggs",
|
||||||
@@ -18,8 +18,11 @@ UPDATE_CONFIG = {
|
|||||||
# Whether to check for updates on startup
|
# Whether to check for updates on startup
|
||||||
"check_on_startup": True,
|
"check_on_startup": True,
|
||||||
|
|
||||||
# Delay before checking for updates on startup (in milliseconds)
|
# Whether updates are required (blocks app if true)
|
||||||
"startup_check_delay": 5000,
|
"updates_required": True,
|
||||||
|
|
||||||
|
# Whether to auto-restart after successful update
|
||||||
|
"auto_restart_after_update": True,
|
||||||
|
|
||||||
# Whether to include pre-releases in update checks
|
# Whether to include pre-releases in update checks
|
||||||
"include_prereleases": True,
|
"include_prereleases": True,
|
||||||
|
|||||||
Reference in New Issue
Block a user