Compare commits
10 Commits
e4c0a0c05a
...
0.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
f1cd7eadb9
|
|||
|
49ba0a4602
|
|||
|
958fe26cad
|
|||
|
44fe6245b9
|
|||
|
ea6eb1fd5f
|
|||
|
24271abcd8
|
|||
|
f6addb9710
|
|||
|
3a5f071786
|
|||
| 303c7d8d6e | |||
|
634543eea6
|
7
.env
7
.env
@@ -1,7 +0,0 @@
|
|||||||
# Default Collection URLs
|
|
||||||
CORE_COLLECTION_URL=https://steamcommunity.com/workshop/filedetails/?id=3521297585
|
|
||||||
CONTENT_COLLECTION_URL=https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712
|
|
||||||
COSMETICS_COLLECTION_URL=https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646
|
|
||||||
|
|
||||||
# ModsConfig.xml Path Template
|
|
||||||
MODSCONFIG_PATH_TEMPLATE=%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml
|
|
||||||
@@ -16,7 +16,22 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Create .env file from secrets
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$envContent = @"
|
||||||
|
# Default Collection URLs
|
||||||
|
CORE_COLLECTION_URL=${{ secrets.CORE_COLLECTION_URL || 'https://steamcommunity.com/workshop/filedetails/?id=3521297585' }}
|
||||||
|
CONTENT_COLLECTION_URL=${{ secrets.CONTENT_COLLECTION_URL || 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712' }}
|
||||||
|
COSMETICS_COLLECTION_URL=${{ secrets.COSMETICS_COLLECTION_URL || 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646' }}
|
||||||
|
|
||||||
|
# ModsConfig.xml Path Template
|
||||||
|
MODSCONFIG_PATH_TEMPLATE=${{ secrets.MODSCONFIG_PATH_TEMPLATE || '%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml' }}
|
||||||
|
"@
|
||||||
|
$envContent | Out-File -FilePath .env -Encoding UTF8 -Force
|
||||||
|
Write-Host "Created .env file with configuration"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
shell: powershell
|
shell: powershell
|
||||||
@@ -29,28 +44,14 @@ jobs:
|
|||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
# Stamp version into sealoader_version.py from release tag
|
# Build standalone executable with all assets bundled
|
||||||
if ($env:GITHUB_EVENT_NAME -eq 'release') {
|
pyinstaller --clean --onefile --windowed --icon=art/Progression.ico --add-data "art;art" --add-data ".env;." --name ProgressionLoader steam_workshop_gui.py
|
||||||
$tag = '${{ github.event.release.tag_name }}'
|
|
||||||
} else {
|
|
||||||
$tag = (git describe --tags --always) 2>$null
|
|
||||||
if (-not $tag) { $tag = "0.0.0-dev" }
|
|
||||||
}
|
|
||||||
("__version__ = '" + $tag + "'") | Out-File -FilePath sealoader_version.py -Encoding UTF8 -Force
|
|
||||||
# Bundle PNG resources referenced at runtime
|
|
||||||
pyinstaller --noconfirm --onefile --windowed sealoader_gui.py --name SeaLoader `
|
|
||||||
--add-data "SeaLoader.png;." `
|
|
||||||
--add-data "hrsys.png;." `
|
|
||||||
--icon SeaLoader.ico
|
|
||||||
|
|
||||||
- name: Prepare artifact
|
- name: Prepare artifact
|
||||||
shell: powershell
|
shell: powershell
|
||||||
run: |
|
run: |
|
||||||
New-Item -ItemType Directory -Force -Path dist_upload | Out-Null
|
# Just upload the standalone .exe file
|
||||||
Copy-Item dist\SeaLoader.exe dist_upload\SeaLoader.exe
|
Copy-Item dist\ProgressionLoader.exe ProgressionLoader.exe
|
||||||
if (Test-Path README.md) { Copy-Item README.md dist_upload\ }
|
|
||||||
if (Test-Path LICENSE) { Copy-Item LICENSE dist_upload\ }
|
|
||||||
Compress-Archive -Path dist_upload\* -DestinationPath SeaLoader_Windows_x64.zip -Force
|
|
||||||
|
|
||||||
- name: Upload asset to Release
|
- name: Upload asset to Release
|
||||||
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
if: ${{ github.event_name == 'release' && github.event.action == 'published' }}
|
||||||
@@ -62,9 +63,27 @@ jobs:
|
|||||||
SERVER_URL: ${{ github.server_url }}
|
SERVER_URL: ${{ github.server_url }}
|
||||||
run: |
|
run: |
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$uploadUrl = "$env:SERVER_URL/api/v1/repos/$env:REPO/releases/$env:RELEASE_ID/assets?name=SeaLoader_Windows_x64.zip"
|
# Gitea API endpoint for uploading release assets
|
||||||
Write-Host "Uploading asset to $uploadUrl"
|
$uploadUrl = "$env:SERVER_URL/api/v1/repos/$env:REPO/releases/$env:RELEASE_ID/assets"
|
||||||
Invoke-RestMethod -Method Post -Uri $uploadUrl -Headers @{ Authorization = "token $env:TOKEN" } -ContentType "application/zip" -InFile "SeaLoader_Windows_x64.zip"
|
Write-Host "Uploading ProgressionLoader.exe to $uploadUrl"
|
||||||
|
|
||||||
# CI artifact upload removed for GHES compatibility
|
# 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
|
||||||
|
|
||||||
|
|||||||
203
.gitignore
vendored
Normal file
203
.gitignore
vendored
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be added to the global gitignore or merged into this project gitignore. For a PyCharm
|
||||||
|
# project, it is recommended to ignore the entire .idea directory.
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Application specific
|
||||||
|
# Keep art assets but ignore any generated thumbnails or cache
|
||||||
|
art/cache/
|
||||||
|
art/thumbnails/
|
||||||
|
|
||||||
|
# Steam Workshop related (if any cache files are generated)
|
||||||
|
workshop_cache/
|
||||||
|
*.cache
|
||||||
82
README.md
82
README.md
@@ -1,80 +1,18 @@
|
|||||||
# Steam Workshop Collection Manager
|
# Progression: Loader
|
||||||
|
|
||||||
A Python GUI application for extracting Steam Workshop mod IDs from RimWorld mod collections with configurable settings.
|
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.
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Dark themed Windows-style GUI
|
DLC detection is based on NotOwned_x.png if the user doesnt the same package NAME as the not owned png it doesnt proceed.
|
||||||
- Configurable collection URLs via .env file
|
.envs populate pre loaded data, only change if their is an update to collection url
|
||||||
- Automatic workshop path derivation from RimWorld game path
|
Doesnt sort so the current guidence to autosort from base game mod manager will still apply.
|
||||||
- ModsConfig.xml integration for active mod tracking
|
Art by ferny
|
||||||
- Real-time output display with scrollable text areas
|
|
||||||
- Local mod folder listing and comparison
|
|
||||||
|
|
||||||
## Installation
|
to bump the version
|
||||||
|
|
||||||
1. Make sure you have Python 3.6+ installed
|
python version_manager.py 1.0.1
|
||||||
2. Install required dependencies:
|
|
||||||
```
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The application uses a `.env` file for configuration. You can modify the default settings:
|
build
|
||||||
|
|
||||||
```env
|
pyinstaller --clean --onefile --windowed --icon=art/Progression.ico --add-data "art;art" --name ProgressionLoader steam_workshop_gui.py
|
||||||
# Default Collection URLs
|
|
||||||
CORE_COLLECTION_URL=https://steamcommunity.com/workshop/filedetails/?id=3521297585
|
|
||||||
CONTENT_COLLECTION_URL=steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712
|
|
||||||
COSMETICS_COLLECTION_URL=steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646
|
|
||||||
|
|
||||||
# ModsConfig.xml Path Template
|
|
||||||
MODSCONFIG_PATH_TEMPLATE=%USERPROFILE%\AppData\LocalLow\Ludeon Studios\RimWorld by Ludeon Studios\Config\ModsConfig.xml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
1. Run the application:
|
|
||||||
```
|
|
||||||
python steam_workshop_gui.py
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Set RimWorld Path**:
|
|
||||||
- Right-click RimWorld in Steam → Manage → Browse local files
|
|
||||||
- Copy and paste that path into the "RimWorld Game Folder" field
|
|
||||||
- The workshop path will automatically derive to `steamapps\workshop\content\294100`
|
|
||||||
|
|
||||||
3. **ModsConfig.xml**:
|
|
||||||
- The path is automatically detected
|
|
||||||
- You can manually edit if needed
|
|
||||||
- Click "Load Active Mods" to see currently enabled mods
|
|
||||||
|
|
||||||
4. **Collection Analysis**:
|
|
||||||
- Modify collection URLs or use defaults from .env
|
|
||||||
- Click "Extract Workshop IDs" to process collections
|
|
||||||
- See which mods are [INSTALLED] or [MISSING]
|
|
||||||
|
|
||||||
5. **Local Mod Management**:
|
|
||||||
- Click "List Local Mod Folders" to see downloaded workshop mods
|
|
||||||
- Compare with collection requirements and active mods
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
- `steam_workshop_gui.py` - Main application
|
|
||||||
- `.env` - Configuration file
|
|
||||||
- `requirements.txt` - Python dependencies
|
|
||||||
- `README.md` - This file
|
|
||||||
|
|
||||||
## URL Formats Supported
|
|
||||||
|
|
||||||
- Standard Steam Workshop URLs: `https://steamcommunity.com/workshop/filedetails/?id=XXXXXXXXX`
|
|
||||||
- Steam protocol URLs: `steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=XXXXXXXXX`
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- All paths are editable in the GUI
|
|
||||||
- Configuration URLs are loaded from .env file on startup
|
|
||||||
- Workshop path auto-derives from RimWorld path (steamapps\common\RimWorld → steamapps\workshop\content\294100)
|
|
||||||
- Processing may take a few seconds depending on collection size
|
|
||||||
- Workshop IDs are extracted from HTML content of collection pages
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB |
BIN
art/Progression.ico
Normal file
BIN
art/Progression.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
art/ProgressionICO.png
Normal file
BIN
art/ProgressionICO.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
art/ProgressionICO.psd
Normal file
BIN
art/ProgressionICO.psd
Normal file
Binary file not shown.
BIN
art/ProgressionLogo.png
Normal file
BIN
art/ProgressionLogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
@@ -1,3 +1,5 @@
|
|||||||
requests>=2.25.1
|
requests>=2.25.1
|
||||||
python-dotenv>=0.19.0
|
python-dotenv>=0.19.0
|
||||||
pyglet>=1.5.0
|
pyglet>=1.5.0
|
||||||
|
Pillow>=8.0.0
|
||||||
|
packaging>=21.0
|
||||||
File diff suppressed because it is too large
Load Diff
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
|
||||||
50
update_config.py
Normal file
50
update_config.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Configuration settings for the update checker
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Update checker configuration
|
||||||
|
UPDATE_CONFIG = {
|
||||||
|
# Current version of the application
|
||||||
|
"current_version": "0.0.3",
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
|
||||||
|
# Whether updates are required (blocks app if true)
|
||||||
|
"updates_required": True,
|
||||||
|
|
||||||
|
# Whether to auto-restart after successful update
|
||||||
|
"auto_restart_after_update": True,
|
||||||
|
|
||||||
|
# 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