Add update system
This commit is contained in:
@@ -63,7 +63,27 @@ jobs:
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$uploadUrl = "$env:SERVER_URL/api/v1/repos/$env:REPO/releases/$env:RELEASE_ID/assets?name=ProgressionLoader.exe"
|
||||
# Gitea API endpoint for uploading release assets
|
||||
$uploadUrl = "$env:SERVER_URL/api/v1/repos/$env:REPO/releases/$env:RELEASE_ID/assets"
|
||||
Write-Host "Uploading ProgressionLoader.exe to $uploadUrl"
|
||||
Invoke-RestMethod -Method Post -Uri $uploadUrl -Headers @{ Authorization = "token $env:TOKEN" } -ContentType "application/octet-stream" -InFile "ProgressionLoader.exe"
|
||||
|
||||
# Use multipart form data for Gitea
|
||||
$boundary = [System.Guid]::NewGuid().ToString()
|
||||
$LF = "`r`n"
|
||||
|
||||
$fileBytes = [System.IO.File]::ReadAllBytes("ProgressionLoader.exe")
|
||||
$fileEnc = [System.Text.Encoding]::GetEncoding('iso-8859-1').GetString($fileBytes)
|
||||
|
||||
$bodyLines = (
|
||||
"--$boundary",
|
||||
"Content-Disposition: form-data; name=`"attachment`"; filename=`"ProgressionLoader.exe`"",
|
||||
"Content-Type: application/octet-stream$LF",
|
||||
$fileEnc,
|
||||
"--$boundary--$LF"
|
||||
) -join $LF
|
||||
|
||||
Invoke-RestMethod -Method Post -Uri $uploadUrl -Headers @{
|
||||
Authorization = "token $env:TOKEN"
|
||||
"Content-Type" = "multipart/form-data; boundary=$boundary"
|
||||
} -Body $bodyLines
|
||||
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
# Progression: Loader
|
||||
|
||||
This project pulls from the three steam workshop collections of Progression, then it searches through your installed mods, in the workshop folder and matches the steam id to a package id, and Formal name, then it takes those names, package ids and steam ids and creates the modloader modslist, thats a mouth full, and save it to your modslist fodler so all you have to do is launch the game. The merge button will take your currently ENABLED mods and merge them with the latest progression pack to give you a new modslist called progresisonhomebrew which you can sort and load.
|
||||
This project pulls from the three steam workshop collections of Progression, then it searches through your installed mods, in the workshop folder and matches the steam id to a package id, and Formal name, then it takes those names, package ids and steam ids and creates the modloader modslist, thats a mouth full, and save it to your modslist folder so all you have to do is launch the game. The merge button will take your currently ENABLED mods and merge them with the latest progression pack to give you a new modslist called progresisonhomebrew which you can sort and load.
|
||||
|
||||
|
||||
DLC detection is based on NotOwned_x.png if the user doesnt the same package NAME as the not owned png it doesnt proceed.
|
||||
.envs populate pre loaded data, only change if their is an update to collection url
|
||||
Doesnt sort so the current guidence to autosort from base game mod manager will still apply.
|
||||
Art by ferny
|
||||
|
||||
to bump the version
|
||||
|
||||
python version_manager.py 1.0.1
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ requests>=2.25.1
|
||||
python-dotenv>=0.19.0
|
||||
pyglet>=1.5.0
|
||||
Pillow>=8.0.0
|
||||
packaging>=21.0
|
||||
@@ -12,6 +12,8 @@ from dotenv import load_dotenv
|
||||
from PIL import Image, ImageTk, ImageDraw, ImageFilter
|
||||
import time
|
||||
import webbrowser
|
||||
from update_checker import UpdateChecker
|
||||
from update_config import get_update_config
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
@@ -1158,12 +1160,23 @@ class SteamWorkshopGUI:
|
||||
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")
|
||||
|
||||
# Initialize update checker
|
||||
config = get_update_config()
|
||||
self.update_checker = UpdateChecker(config["current_version"])
|
||||
|
||||
# Add update check functionality
|
||||
self.add_update_check_button()
|
||||
|
||||
# Check initial state now that all GUI elements are created
|
||||
self.on_rimworld_path_change()
|
||||
|
||||
# Start blinking animation for RimWorld label (with slight delay to ensure GUI is ready)
|
||||
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):
|
||||
"""Create header frame with progression logo"""
|
||||
header_frame = tk.Frame(parent, bg='#2b2b2b', height=100)
|
||||
@@ -1200,12 +1213,12 @@ class SteamWorkshopGUI:
|
||||
|
||||
def create_footer_frame(self, parent):
|
||||
"""Create footer frame with HR Systems logo in bottom right"""
|
||||
footer_frame = tk.Frame(parent, bg='#2b2b2b', height=100) # Increased height for better visibility
|
||||
footer_frame.pack(fill='x', side=tk.BOTTOM, pady=(10, 0))
|
||||
footer_frame.pack_propagate(False)
|
||||
self.footer_frame = tk.Frame(parent, bg='#2b2b2b', height=100) # Increased height for better visibility
|
||||
self.footer_frame.pack(fill='x', side=tk.BOTTOM, pady=(10, 0))
|
||||
self.footer_frame.pack_propagate(False)
|
||||
|
||||
# Create clickable HR Systems logo in bottom right
|
||||
self.create_hr_logo(footer_frame)
|
||||
self.create_hr_logo(self.footer_frame)
|
||||
|
||||
def create_hr_logo(self, parent):
|
||||
"""Create clickable HR Systems logo with radial glow hover effects"""
|
||||
@@ -1479,6 +1492,16 @@ class SteamWorkshopGUI:
|
||||
self.workshop_display.pack(fill='x', pady=(5, 0))
|
||||
self.workshop_display.bind('<KeyRelease>', self.on_workshop_path_change)
|
||||
|
||||
# White line separator above ModsConfig section
|
||||
separator_line = tk.Frame(parent, bg='#ffffff', height=1)
|
||||
separator_line.pack(fill='x', pady=(15, 5))
|
||||
|
||||
# Warning text above ModsConfig section
|
||||
warning_label = tk.Label(parent, text="Don't edit anything below unless you know what you are doing",
|
||||
font=self.get_font(8), bg='#2b2b2b', fg='#ffaa00', # Orange warning color
|
||||
anchor='w')
|
||||
warning_label.pack(anchor='w', pady=(0, 10))
|
||||
|
||||
# ModsConfig.xml folder display
|
||||
modsconfig_frame = tk.Frame(parent, bg='#2b2b2b')
|
||||
modsconfig_frame.pack(fill='x', pady=(0, 15))
|
||||
@@ -1494,16 +1517,6 @@ class SteamWorkshopGUI:
|
||||
self.modsconfig_display.pack(fill='x', pady=(5, 0))
|
||||
self.modsconfig_display.bind('<KeyRelease>', self.on_modsconfig_path_change)
|
||||
|
||||
# White line separator
|
||||
separator_line = tk.Frame(modsconfig_frame, bg='#ffffff', height=1)
|
||||
separator_line.pack(fill='x', pady=(10, 5))
|
||||
|
||||
# Warning text
|
||||
warning_label = tk.Label(modsconfig_frame, text="Don't edit unless you know what you are doing",
|
||||
font=self.get_font(8), bg='#2b2b2b', fg='#ffaa00', # Orange warning color
|
||||
anchor='w')
|
||||
warning_label.pack(anchor='w', pady=(0, 5))
|
||||
|
||||
# Initialize ModsConfig path
|
||||
self.find_modsconfig_path()
|
||||
|
||||
@@ -1624,7 +1637,7 @@ class SteamWorkshopGUI:
|
||||
self.disable_buttons()
|
||||
self.workshop_var.set("Invalid RimWorld path")
|
||||
self.log_to_output(f"✗ Invalid RimWorld installation at: {rimworld_path}\n")
|
||||
self.log_to_output(" Please ensure the path contains RimWorld.exe and Data folder\n")
|
||||
self.log_to_output(" Please ensure the path contains RimWorld.exe (or RimWorldWin64.exe) and Data folder\n")
|
||||
else:
|
||||
# Empty path - keep blinking and buttons disabled
|
||||
self.is_rimworld_valid = False
|
||||
@@ -1636,9 +1649,10 @@ class SteamWorkshopGUI:
|
||||
if not path or not os.path.exists(path):
|
||||
return False
|
||||
|
||||
# Check for RimWorld.exe
|
||||
# Check for RimWorld executable (can be RimWorld.exe or RimWorldWin64.exe)
|
||||
rimworld_exe = os.path.join(path, "RimWorld.exe")
|
||||
if not os.path.exists(rimworld_exe):
|
||||
rimworld_win64_exe = os.path.join(path, "RimWorldWin64.exe")
|
||||
if not (os.path.exists(rimworld_exe) or os.path.exists(rimworld_win64_exe)):
|
||||
return False
|
||||
|
||||
# Check for Data folder (contains core game data)
|
||||
@@ -2574,6 +2588,39 @@ you must auto sort after this!"""
|
||||
# Exit the entire application since main window is hidden
|
||||
self.root.quit()
|
||||
|
||||
def add_update_check_button(self):
|
||||
"""Add an update check button to the footer"""
|
||||
try:
|
||||
if hasattr(self, 'footer_frame'):
|
||||
update_btn = tk.Button(self.footer_frame,
|
||||
text="Check for Updates",
|
||||
command=self.manual_update_check,
|
||||
bg='#404040', fg='white',
|
||||
font=('Arial', 10),
|
||||
padx=15, pady=5,
|
||||
cursor='hand2')
|
||||
update_btn.pack(side='left', padx=10, pady=10)
|
||||
except Exception as 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):
|
||||
"""Manually triggered update check"""
|
||||
self.update_checker.manual_check_for_updates(self.root)
|
||||
|
||||
def main():
|
||||
root = tk.Tk()
|
||||
root.withdraw() # Hide main window initially
|
||||
|
||||
487
update_checker.py
Normal file
487
update_checker.py
Normal file
@@ -0,0 +1,487 @@
|
||||
"""
|
||||
Update Checker Module for Progression Loader
|
||||
Checks for new releases from https://git.hudsonriggs.systems/HRiggs/ProgressionMods/releases
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
from packaging import version
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
import webbrowser
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
from pathlib import Path
|
||||
from update_config import get_update_config
|
||||
|
||||
class UpdateChecker:
|
||||
def __init__(self, current_version=None):
|
||||
# Load configuration
|
||||
self.config = get_update_config()
|
||||
|
||||
# Set version
|
||||
self.current_version = current_version or self.config["current_version"]
|
||||
|
||||
# API configuration
|
||||
self.api_base_url = self.config["api_base_url"]
|
||||
self.repo_owner = self.config["repo_owner"]
|
||||
self.repo_name = self.config["repo_name"]
|
||||
self.releases_url = f"{self.api_base_url}/repos/{self.repo_owner}/{self.repo_name}/releases"
|
||||
|
||||
# Check configuration
|
||||
self.last_check_file = Path(self.config["last_check_file"]) if self.config["last_check_file"] else None
|
||||
self.check_interval_hours = self.config["check_interval_hours"]
|
||||
self.include_prereleases = self.config["include_prereleases"]
|
||||
|
||||
# Request configuration
|
||||
self.user_agent = self.config["user_agent"]
|
||||
self.request_timeout = self.config["request_timeout"]
|
||||
|
||||
def get_current_version(self):
|
||||
"""Get the current version of the application"""
|
||||
return self.current_version
|
||||
|
||||
def set_current_version(self, version_string):
|
||||
"""Set the current version of the application"""
|
||||
self.current_version = version_string
|
||||
|
||||
def should_check_for_updates(self):
|
||||
"""Check if enough time has passed since last update check"""
|
||||
# If no persistent file or check interval is 0, always check
|
||||
if not self.last_check_file or self.check_interval_hours == 0:
|
||||
return True
|
||||
|
||||
if not self.last_check_file.exists():
|
||||
return True
|
||||
|
||||
try:
|
||||
with open(self.last_check_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
last_check = datetime.fromisoformat(data.get('last_check', '2000-01-01'))
|
||||
return datetime.now() - last_check > timedelta(hours=self.check_interval_hours)
|
||||
except (json.JSONDecodeError, ValueError, KeyError):
|
||||
return True
|
||||
|
||||
def save_last_check_time(self):
|
||||
"""Save the current time as the last check time"""
|
||||
# Skip saving if no persistent file is configured
|
||||
if not self.last_check_file:
|
||||
return
|
||||
|
||||
try:
|
||||
data = {'last_check': datetime.now().isoformat()}
|
||||
with open(self.last_check_file, 'w') as f:
|
||||
json.dump(data, f)
|
||||
except Exception as e:
|
||||
print(f"Could not save last check time: {e}")
|
||||
|
||||
def fetch_latest_release(self):
|
||||
"""Fetch the latest release information from the API"""
|
||||
try:
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'User-Agent': self.user_agent
|
||||
}
|
||||
|
||||
response = requests.get(self.releases_url, headers=headers, timeout=self.request_timeout)
|
||||
response.raise_for_status()
|
||||
|
||||
releases = response.json()
|
||||
|
||||
if not releases:
|
||||
return None
|
||||
|
||||
# Filter releases based on prerelease setting
|
||||
if not self.include_prereleases:
|
||||
releases = [r for r in releases if not r.get('prerelease', False)]
|
||||
|
||||
# Filter out draft releases
|
||||
releases = [r for r in releases if not r.get('draft', False)]
|
||||
|
||||
if not releases:
|
||||
return None
|
||||
|
||||
# Get the latest release (first in the filtered list)
|
||||
latest_release = releases[0]
|
||||
|
||||
return {
|
||||
'version': latest_release.get('tag_name', '').lstrip('v'),
|
||||
'name': latest_release.get('name', ''),
|
||||
'body': latest_release.get('body', ''),
|
||||
'html_url': latest_release.get('html_url', ''),
|
||||
'published_at': latest_release.get('published_at', ''),
|
||||
'assets': latest_release.get('assets', []),
|
||||
'prerelease': latest_release.get('prerelease', False),
|
||||
'draft': latest_release.get('draft', False)
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Network error checking for updates: {e}")
|
||||
return None
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing release data: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Unexpected error checking for updates: {e}")
|
||||
return None
|
||||
|
||||
def is_newer_version(self, latest_version):
|
||||
"""Compare versions to see if the latest is newer than current"""
|
||||
try:
|
||||
current = version.parse(self.current_version)
|
||||
latest = version.parse(latest_version)
|
||||
return latest > current
|
||||
except Exception as e:
|
||||
print(f"Error comparing versions: {e}")
|
||||
# Fallback to string comparison
|
||||
return latest_version != self.current_version
|
||||
|
||||
def check_for_updates_async(self, callback=None):
|
||||
"""Check for updates in a background thread"""
|
||||
def check_thread():
|
||||
try:
|
||||
if not self.should_check_for_updates():
|
||||
if callback:
|
||||
callback(None, "No check needed")
|
||||
return
|
||||
|
||||
latest_release = self.fetch_latest_release()
|
||||
self.save_last_check_time()
|
||||
|
||||
if callback:
|
||||
callback(latest_release, None)
|
||||
|
||||
except Exception as e:
|
||||
if callback:
|
||||
callback(None, str(e))
|
||||
|
||||
thread = threading.Thread(target=check_thread, daemon=True)
|
||||
thread.start()
|
||||
|
||||
def check_for_updates_sync(self):
|
||||
"""Check for updates synchronously"""
|
||||
if not self.should_check_for_updates():
|
||||
return None, "No check needed"
|
||||
|
||||
latest_release = self.fetch_latest_release()
|
||||
self.save_last_check_time()
|
||||
|
||||
return latest_release, None
|
||||
|
||||
def show_update_dialog(self, parent_window, release_info):
|
||||
"""Show an update notification dialog"""
|
||||
if not release_info:
|
||||
return
|
||||
|
||||
latest_version = release_info['version']
|
||||
|
||||
if not self.is_newer_version(latest_version):
|
||||
return # No update needed
|
||||
|
||||
# Create update dialog
|
||||
dialog = tk.Toplevel(parent_window)
|
||||
dialog.title("Update Available - Progression Loader")
|
||||
dialog.configure(bg='#2b2b2b')
|
||||
dialog.resizable(False, False)
|
||||
dialog.attributes('-topmost', True)
|
||||
|
||||
# Set window icon if available
|
||||
try:
|
||||
if hasattr(parent_window, 'iconbitmap'):
|
||||
dialog.iconbitmap(parent_window.iconbitmap())
|
||||
except:
|
||||
pass
|
||||
|
||||
# Calculate dialog size
|
||||
dialog_width = 500
|
||||
dialog_height = 400
|
||||
|
||||
# Center the dialog
|
||||
x = parent_window.winfo_x() + (parent_window.winfo_width() // 2) - (dialog_width // 2)
|
||||
y = parent_window.winfo_y() + (parent_window.winfo_height() // 2) - (dialog_height // 2)
|
||||
dialog.geometry(f"{dialog_width}x{dialog_height}+{x}+{y}")
|
||||
|
||||
# Title
|
||||
title_label = tk.Label(dialog,
|
||||
text="🔄 Update Available!",
|
||||
fg='#00ff00', bg='#2b2b2b',
|
||||
font=('Arial', 16, 'bold'))
|
||||
title_label.pack(pady=20)
|
||||
|
||||
# Version info
|
||||
version_frame = tk.Frame(dialog, bg='#2b2b2b')
|
||||
version_frame.pack(pady=10)
|
||||
|
||||
current_label = tk.Label(version_frame,
|
||||
text=f"Current Version: {self.current_version}",
|
||||
fg='#cccccc', bg='#2b2b2b',
|
||||
font=('Arial', 12))
|
||||
current_label.pack()
|
||||
|
||||
latest_label = tk.Label(version_frame,
|
||||
text=f"Latest Version: {latest_version}",
|
||||
fg='#00ff00', bg='#2b2b2b',
|
||||
font=('Arial', 12, 'bold'))
|
||||
latest_label.pack()
|
||||
|
||||
# Release notes
|
||||
if release_info.get('body'):
|
||||
notes_label = tk.Label(dialog,
|
||||
text="Release Notes:",
|
||||
fg='#cccccc', bg='#2b2b2b',
|
||||
font=('Arial', 12, 'bold'))
|
||||
notes_label.pack(pady=(20, 5))
|
||||
|
||||
# Create scrollable text widget for release notes
|
||||
notes_frame = tk.Frame(dialog, bg='#2b2b2b')
|
||||
notes_frame.pack(fill='both', expand=True, padx=20, pady=(0, 20))
|
||||
|
||||
notes_text = tk.Text(notes_frame,
|
||||
height=8,
|
||||
bg='#404040', fg='#ffffff',
|
||||
font=('Arial', 10),
|
||||
wrap=tk.WORD,
|
||||
state='disabled')
|
||||
|
||||
scrollbar = tk.Scrollbar(notes_frame)
|
||||
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
|
||||
button_frame = tk.Frame(dialog, bg='#2b2b2b')
|
||||
button_frame.pack(pady=20)
|
||||
|
||||
def download_update():
|
||||
webbrowser.open(release_info['html_url'])
|
||||
dialog.destroy()
|
||||
|
||||
def remind_later():
|
||||
# If no persistent file, just close the dialog
|
||||
if not self.last_check_file:
|
||||
dialog.destroy()
|
||||
return
|
||||
|
||||
# Reset last check time to check again sooner
|
||||
try:
|
||||
data = {'last_check': (datetime.now() - timedelta(hours=self.check_interval_hours - 4)).isoformat()}
|
||||
with open(self.last_check_file, 'w') as f:
|
||||
json.dump(data, f)
|
||||
except:
|
||||
pass
|
||||
dialog.destroy()
|
||||
|
||||
def skip_version():
|
||||
# If no persistent file, just close the dialog
|
||||
if not self.last_check_file:
|
||||
dialog.destroy()
|
||||
return
|
||||
|
||||
# Save this version as skipped
|
||||
try:
|
||||
data = {
|
||||
'last_check': datetime.now().isoformat(),
|
||||
'skipped_version': latest_version
|
||||
}
|
||||
with open(self.last_check_file, 'w') as f:
|
||||
json.dump(data, f)
|
||||
except:
|
||||
pass
|
||||
dialog.destroy()
|
||||
|
||||
download_btn = tk.Button(button_frame,
|
||||
text="Download Update",
|
||||
command=download_update,
|
||||
bg='#00aa00', fg='white',
|
||||
font=('Arial', 12, 'bold'),
|
||||
padx=20, pady=5)
|
||||
download_btn.pack(side='left', padx=5)
|
||||
|
||||
# Only show remind/skip buttons if persistence is enabled
|
||||
if self.last_check_file:
|
||||
later_btn = tk.Button(button_frame,
|
||||
text="Remind Later",
|
||||
command=remind_later,
|
||||
bg='#0078d4', fg='white',
|
||||
font=('Arial', 12),
|
||||
padx=20, pady=5)
|
||||
later_btn.pack(side='left', padx=5)
|
||||
|
||||
skip_btn = tk.Button(button_frame,
|
||||
text="Skip Version",
|
||||
command=skip_version,
|
||||
bg='#666666', fg='white',
|
||||
font=('Arial', 12),
|
||||
padx=20, pady=5)
|
||||
skip_btn.pack(side='left', padx=5)
|
||||
else:
|
||||
close_btn = tk.Button(button_frame,
|
||||
text="Close",
|
||||
command=dialog.destroy,
|
||||
bg='#666666', fg='white',
|
||||
font=('Arial', 12),
|
||||
padx=20, pady=5)
|
||||
close_btn.pack(side='left', padx=5)
|
||||
|
||||
# Handle window close
|
||||
dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
|
||||
|
||||
return dialog
|
||||
|
||||
def should_skip_version(self, version_string):
|
||||
"""Check if this version should be skipped"""
|
||||
# Skip version checking if no persistent file is configured
|
||||
if not self.last_check_file:
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(self.last_check_file, 'r') as f:
|
||||
data = json.load(f)
|
||||
return data.get('skipped_version') == version_string
|
||||
except:
|
||||
return False
|
||||
|
||||
def manual_check_for_updates(self, parent_window):
|
||||
"""Manually check for updates and show result"""
|
||||
def check_complete(release_info, error):
|
||||
if error:
|
||||
messagebox.showerror("Update Check Failed",
|
||||
f"Could not check for updates:\n{error}",
|
||||
parent=parent_window)
|
||||
return
|
||||
|
||||
if not release_info:
|
||||
messagebox.showinfo("No Updates",
|
||||
"Could not retrieve release information.",
|
||||
parent=parent_window)
|
||||
return
|
||||
|
||||
latest_version = release_info['version']
|
||||
|
||||
if self.is_newer_version(latest_version) and not self.should_skip_version(latest_version):
|
||||
self.show_update_dialog(parent_window, release_info)
|
||||
else:
|
||||
messagebox.showinfo("Up to Date",
|
||||
f"You are running the latest version ({self.current_version}).",
|
||||
parent=parent_window)
|
||||
|
||||
# Show checking message
|
||||
checking_dialog = tk.Toplevel(parent_window)
|
||||
checking_dialog.title("Checking for Updates")
|
||||
checking_dialog.configure(bg='#2b2b2b')
|
||||
checking_dialog.resizable(False, False)
|
||||
checking_dialog.attributes('-topmost', True)
|
||||
|
||||
# Center the dialog
|
||||
checking_dialog.geometry("300x100")
|
||||
x = parent_window.winfo_x() + (parent_window.winfo_width() // 2) - 150
|
||||
y = parent_window.winfo_y() + (parent_window.winfo_height() // 2) - 50
|
||||
checking_dialog.geometry(f"300x100+{x}+{y}")
|
||||
|
||||
label = tk.Label(checking_dialog,
|
||||
text="Checking for updates...",
|
||||
fg='white', bg='#2b2b2b',
|
||||
font=('Arial', 12))
|
||||
label.pack(expand=True)
|
||||
|
||||
def check_and_close():
|
||||
release_info, error = self.check_for_updates_sync()
|
||||
checking_dialog.destroy()
|
||||
check_complete(release_info, error)
|
||||
|
||||
# Start check in background
|
||||
threading.Thread(target=check_and_close, daemon=True).start()
|
||||
|
||||
|
||||
def integrate_update_checker_with_gui(gui_class):
|
||||
"""
|
||||
Decorator to integrate update checker with the main GUI class
|
||||
"""
|
||||
original_init = gui_class.__init__
|
||||
|
||||
def new_init(self, *args, **kwargs):
|
||||
# Call original init
|
||||
original_init(self, *args, **kwargs)
|
||||
|
||||
# Initialize update checker
|
||||
self.update_checker = UpdateChecker()
|
||||
|
||||
# Add update check to menu if it exists
|
||||
self.add_update_menu()
|
||||
|
||||
# Check for updates on startup (after a short delay)
|
||||
self.root.after(5000, self.check_for_updates_on_startup)
|
||||
|
||||
def add_update_menu(self):
|
||||
"""Add update check option to the application"""
|
||||
try:
|
||||
# Try to add to existing menu bar
|
||||
if hasattr(self, 'menubar'):
|
||||
help_menu = tk.Menu(self.menubar, tearoff=0)
|
||||
help_menu.add_command(label="Check for Updates", command=self.manual_update_check)
|
||||
self.menubar.add_cascade(label="Help", menu=help_menu)
|
||||
else:
|
||||
# Create a simple button in the GUI
|
||||
self.add_update_button()
|
||||
except Exception as e:
|
||||
print(f"Could not add update menu: {e}")
|
||||
|
||||
def add_update_button(self):
|
||||
"""Add an update check button to the GUI"""
|
||||
try:
|
||||
# Find a suitable parent frame (try footer first, then main frame)
|
||||
parent_frame = None
|
||||
if hasattr(self, 'footer_frame'):
|
||||
parent_frame = self.footer_frame
|
||||
elif hasattr(self, 'main_frame'):
|
||||
parent_frame = self.main_frame
|
||||
elif hasattr(self, 'root'):
|
||||
parent_frame = self.root
|
||||
|
||||
if parent_frame:
|
||||
update_btn = tk.Button(parent_frame,
|
||||
text="Check Updates",
|
||||
command=self.manual_update_check,
|
||||
bg='#404040', fg='white',
|
||||
font=('Arial', 10),
|
||||
padx=10, pady=2)
|
||||
update_btn.pack(side='right', padx=5, pady=5)
|
||||
except Exception as 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):
|
||||
"""Manually triggered update check"""
|
||||
self.update_checker.manual_check_for_updates(self.root)
|
||||
|
||||
# Add methods to the class
|
||||
gui_class.add_update_menu = add_update_menu
|
||||
gui_class.add_update_button = add_update_button
|
||||
gui_class.check_for_updates_on_startup = check_for_updates_on_startup
|
||||
gui_class.manual_update_check = manual_update_check
|
||||
gui_class.__init__ = new_init
|
||||
|
||||
return gui_class
|
||||
47
update_config.py
Normal file
47
update_config.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
Configuration settings for the update checker
|
||||
"""
|
||||
|
||||
# Update checker configuration
|
||||
UPDATE_CONFIG = {
|
||||
# Current version of the application
|
||||
"current_version": "0.0.1",
|
||||
|
||||
# Repository information
|
||||
"repo_owner": "HRiggs",
|
||||
"repo_name": "ProgressionMods",
|
||||
"api_base_url": "https://git.hudsonriggs.systems/api/v1",
|
||||
|
||||
# Update check frequency (in hours) - set to 0 to check every startup
|
||||
"check_interval_hours": 0,
|
||||
|
||||
# Whether to check for updates on startup
|
||||
"check_on_startup": True,
|
||||
|
||||
# Delay before checking for updates on startup (in milliseconds)
|
||||
"startup_check_delay": 5000,
|
||||
|
||||
# Whether to include pre-releases in update checks
|
||||
"include_prereleases": True,
|
||||
|
||||
# User agent string for API requests
|
||||
"user_agent": "ProgressionLoader-UpdateChecker/1.0",
|
||||
|
||||
# Request timeout in seconds
|
||||
"request_timeout": 10,
|
||||
|
||||
# File to store last check time and skipped versions (set to None to disable persistence)
|
||||
"last_check_file": None
|
||||
}
|
||||
|
||||
def get_update_config():
|
||||
"""Get the update configuration"""
|
||||
return UPDATE_CONFIG.copy()
|
||||
|
||||
def set_current_version(version):
|
||||
"""Set the current version"""
|
||||
UPDATE_CONFIG["current_version"] = version
|
||||
|
||||
def get_current_version():
|
||||
"""Get the current version"""
|
||||
return UPDATE_CONFIG["current_version"]
|
||||
104
version_manager.py
Normal file
104
version_manager.py
Normal file
@@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Version Manager for Progression Loader
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
|
||||
def get_current_version():
|
||||
"""Get the current version from update_config.py"""
|
||||
try:
|
||||
with open('update_config.py', 'r') as f:
|
||||
content = f.read()
|
||||
match = re.search(r'"current_version":\s*"([^"]+)"', content)
|
||||
if match:
|
||||
return match.group(1)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def set_version(new_version):
|
||||
"""Set the version in update_config.py"""
|
||||
try:
|
||||
with open('update_config.py', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
updated_content = re.sub(
|
||||
r'"current_version":\s*"[^"]+"',
|
||||
f'"current_version": "{new_version}"',
|
||||
content
|
||||
)
|
||||
|
||||
with open('update_config.py', 'w') as f:
|
||||
f.write(updated_content)
|
||||
|
||||
print(f"✅ Updated version to {new_version}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Error updating version: {e}")
|
||||
return False
|
||||
|
||||
def validate_version(version_string):
|
||||
"""Validate semantic versioning format"""
|
||||
pattern = r'^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9\-\.]+))?$'
|
||||
return re.match(pattern, version_string) is not None
|
||||
|
||||
def suggest_next_version(current_version):
|
||||
"""Suggest next version numbers"""
|
||||
if not current_version:
|
||||
return ["1.0.0"]
|
||||
|
||||
try:
|
||||
parts = current_version.split('.')
|
||||
if len(parts) >= 3:
|
||||
major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
|
||||
return [
|
||||
f"{major}.{minor}.{patch + 1}", # Patch
|
||||
f"{major}.{minor + 1}.0", # Minor
|
||||
f"{major + 1}.0.0" # Major
|
||||
]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return ["1.0.0"]
|
||||
|
||||
def main():
|
||||
"""Main interface"""
|
||||
if len(sys.argv) < 2:
|
||||
current = get_current_version()
|
||||
print(f"Current Version: {current or 'Not found'}")
|
||||
|
||||
if current:
|
||||
suggestions = suggest_next_version(current)
|
||||
print("Suggested versions:", ", ".join(suggestions))
|
||||
|
||||
print("\nUsage:")
|
||||
print(" python version_manager.py <version> # Set version")
|
||||
print(" python version_manager.py current # Show current")
|
||||
print("\nExample: python version_manager.py 1.0.1")
|
||||
return
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "current":
|
||||
current = get_current_version()
|
||||
print(f"Current version: {current or 'Not found'}")
|
||||
return
|
||||
|
||||
# Set version
|
||||
new_version = command
|
||||
|
||||
if not validate_version(new_version):
|
||||
print(f"❌ Invalid version format: {new_version}")
|
||||
print("Use format: MAJOR.MINOR.PATCH (e.g., 1.0.1)")
|
||||
return
|
||||
|
||||
if set_version(new_version):
|
||||
print("Next steps:")
|
||||
print(f" git add update_config.py")
|
||||
print(f" git commit -m 'Bump version to {new_version}'")
|
||||
print(f" git tag v{new_version} && git push --tags")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user