Compare commits
17 Commits
303c7d8d6e
...
0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
99d6d2a578
|
|||
|
603af4f378
|
|||
|
66e7ca4fc7
|
|||
|
a015908d44
|
|||
|
1c9598c00a
|
|||
|
4fb77ef0de
|
|||
|
df6c970112
|
|||
|
411ff15ea5
|
|||
|
269950cf4a
|
|||
|
f1cd7eadb9
|
|||
|
49ba0a4602
|
|||
|
958fe26cad
|
|||
|
44fe6245b9
|
|||
|
ea6eb1fd5f
|
|||
|
24271abcd8
|
|||
|
f6addb9710
|
|||
|
3a5f071786
|
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
inclusion: always
|
||||||
|
---
|
||||||
|
<!------------------------------------------------------------------------------------
|
||||||
|
Add rules to this file or a short description and have Kiro refine them for you.
|
||||||
|
|
||||||
|
Learn about inclusion modes: https://kiro.dev/docs/steering/#inclusion-modes
|
||||||
|
------------------------------------------------------------------------------------->
|
||||||
|
|
||||||
|
This project has a GUI and a TUI when fixing or implmenting features access and make changes to both.
|
||||||
|
After Width: | Height: | Size: 358 KiB |
@@ -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
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# Steam Collections Page Design Update Summary
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Updated all UI screens (except mod manager) to match Steam Collections page design with modern dark blue theme, improved typography, and Steam-inspired visual elements.
|
||||||
|
|
||||||
|
## Color Scheme Changes
|
||||||
|
|
||||||
|
### New Steam Collections Inspired Colors
|
||||||
|
```python
|
||||||
|
COLORS = {
|
||||||
|
'bg_primary': '#1b2838', # Steam dark blue background
|
||||||
|
'bg_secondary': '#2a475e', # Steam medium blue background
|
||||||
|
'bg_tertiary': '#1e2328', # Steam darker background for cards
|
||||||
|
'bg_card': '#16202d', # Steam card background
|
||||||
|
'bg_hover': '#2a475e', # Steam hover state
|
||||||
|
'text_primary': '#c7d5e0', # Steam primary text (light blue-gray)
|
||||||
|
'text_highlight': '#66c0f4', # Steam blue highlight color
|
||||||
|
'text_body': '#8f98a0', # Steam body text (muted blue-gray)
|
||||||
|
'text_secondary': '#acb2b8', # Steam secondary text
|
||||||
|
'text_muted': '#67707b', # Steam muted text
|
||||||
|
'accent_green': '#5ba32b', # Steam success green
|
||||||
|
'accent_red': '#cd5c5c', # Steam error red
|
||||||
|
'accent_yellow': '#ffa500', # Steam warning orange
|
||||||
|
'accent_blue': '#66c0f4', # Steam signature blue
|
||||||
|
'border_light': '#3c4043', # Steam light border
|
||||||
|
'border_dark': '#0e141b', # Steam dark border
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Previous Colors (Replaced)
|
||||||
|
- Old yellow-green highlights (#f5f5b5) → Steam blue (#66c0f4)
|
||||||
|
- Old gray backgrounds (#424041) → Steam dark blue (#1b2838)
|
||||||
|
- Old bright colors → Muted Steam palette
|
||||||
|
|
||||||
|
## GUI Changes (steam_workshop_gui.py)
|
||||||
|
|
||||||
|
### 1. Main Application Window
|
||||||
|
- **Background**: Updated to Steam dark blue (#1b2838)
|
||||||
|
- **Card System**: Implemented Steam-style cards with rounded corners (12px radius)
|
||||||
|
- **Typography**: Enhanced with Steam blue highlights for titles
|
||||||
|
- **Padding**: Increased from 10px to 15px for better spacing
|
||||||
|
|
||||||
|
### 2. Input Section Redesign
|
||||||
|
- **Card-based Layout**: Each section now uses Steam-style cards
|
||||||
|
- RimWorld Installation card
|
||||||
|
- Workshop Content card
|
||||||
|
- Advanced Configuration card
|
||||||
|
- Steam Workshop Collections card
|
||||||
|
- **Input Fields**:
|
||||||
|
- Dark backgrounds (#1e2328)
|
||||||
|
- Steam blue focus borders (#66c0f4)
|
||||||
|
- Improved padding and spacing
|
||||||
|
- **Buttons**:
|
||||||
|
- Primary button: Steam blue (#66c0f4) with dark text
|
||||||
|
- Secondary button: Steam green (#5ba32b)
|
||||||
|
- Disabled state: Muted colors
|
||||||
|
|
||||||
|
### 3. Output Section (Logs)
|
||||||
|
- **Card Container**: Logs now in Steam-style card
|
||||||
|
- **Color-coded Messages**:
|
||||||
|
- Success: Steam green
|
||||||
|
- Error: Steam red
|
||||||
|
- Warning: Steam orange
|
||||||
|
- Info: Steam blue
|
||||||
|
- **Timestamps**: Added with muted color
|
||||||
|
- **Background**: Dark Steam theme (#1e2328)
|
||||||
|
|
||||||
|
### 4. Loading Screen Updates
|
||||||
|
- **Card Backgrounds**: Updated to Steam styling with subtle borders
|
||||||
|
- **Corner Radius**: Reduced from 20px to 12px for modern look
|
||||||
|
- **Colors**: All text and UI elements use Steam palette
|
||||||
|
- **Error Cards**: Steam red accents for error states
|
||||||
|
|
||||||
|
### 5. Button Styling
|
||||||
|
- **Enable State**: Steam blue primary, Steam green secondary
|
||||||
|
- **Disable State**: Muted Steam colors
|
||||||
|
- **Hover Effects**: Lighter Steam blue on hover
|
||||||
|
- **Cursor**: Hand cursor for better UX
|
||||||
|
|
||||||
|
## TUI Changes (progression_tui.py)
|
||||||
|
|
||||||
|
### 1. Color Scheme
|
||||||
|
- Updated all TUI_COLORS to match GUI Steam palette
|
||||||
|
- Consistent blue theme throughout terminal interface
|
||||||
|
|
||||||
|
### 2. Header Styling
|
||||||
|
- **Border**: Steam blue double border
|
||||||
|
- **Title**: Added "PROGRESSION LOADER" title to panel
|
||||||
|
- **ASCII Art**: Highlighted with Steam blue
|
||||||
|
|
||||||
|
### 3. Menu System
|
||||||
|
- **Table Styling**: Rounded borders with Steam blue accents
|
||||||
|
- **Status Indicators**:
|
||||||
|
- Valid: ✓ with Steam green
|
||||||
|
- Invalid: ⚠ with Steam yellow
|
||||||
|
- Ready: ✓ Ready with Steam green
|
||||||
|
- Disabled: ⚠ Disabled with muted colors
|
||||||
|
|
||||||
|
### 4. Configuration Display
|
||||||
|
- **Path Display**: Added icons and Steam blue labels
|
||||||
|
- **Panel Styling**: Rounded borders with Steam theme
|
||||||
|
- **Status Colors**: Consistent with GUI
|
||||||
|
|
||||||
|
## Font and Styling (font_and_colors_update.py)
|
||||||
|
|
||||||
|
### 1. New Utility Functions
|
||||||
|
- `apply_steam_styling()`: Returns Steam-inspired style dictionaries
|
||||||
|
- `get_steam_button_colors()`: Provides button color schemes by type
|
||||||
|
- Updated color constants to match Steam theme
|
||||||
|
|
||||||
|
### 2. Button Color Schemes
|
||||||
|
- **Default**: Steam secondary background
|
||||||
|
- **Primary**: Steam blue with dark text
|
||||||
|
- **Success**: Steam green
|
||||||
|
- **Warning**: Steam orange
|
||||||
|
- **Danger**: Steam red
|
||||||
|
|
||||||
|
## Visual Improvements
|
||||||
|
|
||||||
|
### 1. Card System
|
||||||
|
- **Rounded Corners**: 12px radius for modern look
|
||||||
|
- **Subtle Borders**: Light Steam borders (#3c4043)
|
||||||
|
- **Inner Highlights**: Very subtle Steam blue inner glow
|
||||||
|
- **Proper Spacing**: Consistent 10-15px padding
|
||||||
|
|
||||||
|
### 2. Typography Hierarchy
|
||||||
|
- **Titles**: Steam blue (#66c0f4) with bold weight
|
||||||
|
- **Labels**: Steam secondary text (#acb2b8)
|
||||||
|
- **Body Text**: Steam body color (#8f98a0)
|
||||||
|
- **Muted Text**: Steam muted (#67707b)
|
||||||
|
|
||||||
|
### 3. Interactive Elements
|
||||||
|
- **Focus States**: Steam blue borders on input fields
|
||||||
|
- **Hover Effects**: Lighter Steam blue on buttons
|
||||||
|
- **Status Indicators**: Color-coded with Steam palette
|
||||||
|
- **Blinking Animation**: Steam blue/muted for invalid states
|
||||||
|
|
||||||
|
## Consistency Improvements
|
||||||
|
|
||||||
|
### 1. Cross-Platform
|
||||||
|
- GUI and TUI now use matching color schemes
|
||||||
|
- Consistent terminology and styling
|
||||||
|
- Same visual hierarchy in both interfaces
|
||||||
|
|
||||||
|
### 2. Error Handling
|
||||||
|
- Consistent error colors (Steam red)
|
||||||
|
- Warning colors (Steam orange)
|
||||||
|
- Success colors (Steam green)
|
||||||
|
- Info colors (Steam blue)
|
||||||
|
|
||||||
|
### 3. Accessibility
|
||||||
|
- Better contrast ratios with Steam's tested palette
|
||||||
|
- Consistent focus indicators
|
||||||
|
- Clear visual hierarchy
|
||||||
|
|
||||||
|
## Files Modified
|
||||||
|
|
||||||
|
1. **steam_workshop_gui.py** - Main GUI application
|
||||||
|
2. **progression_tui.py** - Terminal user interface
|
||||||
|
3. **font_and_colors_update.py** - Color and styling utilities
|
||||||
|
4. **update_checker.py** - Update dialog styling
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- ✅ Syntax validation passed for all Python files
|
||||||
|
- ✅ Color scheme consistency verified
|
||||||
|
- ✅ Both GUI and TUI updated per requirements
|
||||||
|
- ✅ Steam Collections design patterns implemented
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
The application now features a cohesive Steam Collections inspired design with:
|
||||||
|
- Modern dark blue theme matching Steam's visual identity
|
||||||
|
- Improved card-based layouts for better organization
|
||||||
|
- Consistent typography and color usage
|
||||||
|
- Enhanced user experience with better visual hierarchy
|
||||||
|
- Professional appearance matching Steam's design standards
|
||||||
|
|
||||||
|
All screens except the mod manager have been updated to match the Steam Collections page design as requested.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@ @@@@@@@@ @@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@ @ @@@@@@@@@@
|
||||||
|
@....................@@@ @....................%@@@ @@@...........*@@@ @@@+................@@ @....................@@@ @@......................@@ @@@...................@@ @@@..................@@@ @@.......@@ @@@@...........@@@ @..@@ @@.......@@
|
||||||
|
@@.......................@@ @*......................@@@ @@..................@@@ @@@....................@@@@@@.......................@@ @@......................@@@@ @@%......................@@@ @@.......................@@@ @@.......@@@@ @@..................@@@ @....@@ @@.......@@@@
|
||||||
|
@@.........................@@@ @@#........................@@@ @@......................@@@ @@.......................@@@@@@.........................@@@ @@@......................@@@@@@@.........................@@@@ @.........................%@@@@@@.......@@@@@ @@......................@@@ @=.....@@ @@.......@@@@@
|
||||||
|
@@..........................@@@@@@*.........................@@@@ @@..........................@@@ @@........................@@@@@@..........................@@@@@@@......................@@@@@@..........................@@@@@@@..........................@@@@@@.......@@@@@@@..........................@@@@@@=......@@@ @@@.......@@@@@
|
||||||
|
@@.......@@@@@@@@@@@........@@@@@@*.......@@@@@@@@@@@........@@@@@@.........#@@@@@@@@..........@@@ @@..........@@@@@@@@@@@@@@@@@@@@@.......@@@@@@@@@@@........@@@@@@@.......@@@@@@@@@@@@@@@@@@@@@.......@@@@@@@@@@@@@@@@@@@@@@@@@........@@@@@@@@@@@@@@@@@@@@@@@@@.......@@@@@@..........@@@@@@@@@.........@@@@@=........@@@@@@.......@@@@@
|
||||||
|
@@.......@@@@@@@@@@@@.......@@@@@@#.......@@@@@@@@@@@........@@@@@........@@@@@@@@@@@@@.........@@@@@.........@@@@@@@@@@@@@@@@@@@@@@@.......@@@@@@@@@@@@.......@@@@@@@.......@@@@@@@@@@@@@@@@@@@@@.......@@@@@@@@@@@@@@@@@@@@@@@@@........@@@@@@@@@@@@@@@@@@@@@@@@@.......@@@@@@........@@@@@@@@@@@@@........@@@@=..........@@@@.......@@@@@
|
||||||
|
@@.......@@@@@@@@@@@@.......@@@@@@#.......@@@@@@@@@@@........@@@@@.......@@@@@@@@@@@@@@@@.......@@@@@.......@@@@@@@@@@@@@@@@@@@@@@@@@.......@@@@@@@@@@@@.......@@@@@@@................@@@@@@@@@@@@..................@@@@@@@@@@@@@@#..................@@@@@@@@@@@@@@.......@@@@@........@@@@@@@@@@@@@@@........@@@+............@@.......@@@@@
|
||||||
|
@@.......@.......:..........@@@@@@#.........................@@@@@........@@@@@@@@@@@@@@@@.......*@@@........@@@@@@@@@@@@@@@@@@.@@@@@@..........................@@@@@@@................@@@@@@@@@@@@@.....................@@@@@@@@@@@......................@@@@@@@@@@.......@@@@@.......@@@@@@@@@@@@@@@@@.......@@@+.....................@@@@@
|
||||||
|
@@.........................@@@@@@@#........................:@@@@@........@@@@@@@ @@@@@........@@@........@@@@@@@@ @@@.....@@@@@@.........................@@@@@@@@................@@@@@ @@@@......................@@@@ @@@@@......................@@@ @@@.......@@@@@.......@@@@@@@@ @@@@@.......@@@+.....................@@@@@
|
||||||
|
@@........................@@@@@@@@#.......................@@@@@@@@.......@@@@@@ @@@.......@@@@*.......@@@@@@ @........@@@@@@........................@@@@@@@@@................@@@@@ @@@@@@@....................@@@@ @@@@@@@.....................@@@@@@@.......@@@@@.......@@@@@@ @@@........@@@+.......@.............@@@@@
|
||||||
|
@@......................@@@@@@@@@@#.....................@@@@@@@@@@........@@@@ @@........@@@@@........@@@@ @........@@@@@@.....................@@@@@@@@@@@@.......@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@........@@@@@@@@@@@@@@@@@@@@@@@@........@@@@@@@.......@@@@@........@@@@@ @@.......@@@@*.......@@............@@@@@
|
||||||
|
@@.......@*********@@@@@@@@@@@@@@@#.......@****@........@@@@@@@@@@@........@@@@ @@@........@@@@@@.........@@@@ @@........@@@@@@.......@****%.........@@@@@@@@@@@.......@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@.......@@@@@ @@@@@@@@@@@@@@@@@@@........@@@@@@.......@@@@@@........@@@@ @@.........@@@@*.......@@@@..........@@@@@
|
||||||
|
@@.......@@@@@@@@@@@@@@@@@@@@@@@@@%.......@@@@@@@........@@@@@@@@@@...........@@@@@@..........@@@@@@@@............@@@@@........@@@@@@.......@@@@@@@.........@@@@@@@@@@.......@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@........@@@@@@@@@@@@@@@@@@@@@@@@........@@@@@@@.......@@@@@@@..........@@@@@@...........@@@@@*.......@@@@@@........@@@@@
|
||||||
|
@@.......@@@@@@@@@@@@@@@@@@@@ @@#.......@@@@@@@@........@@@@@@@@@@@........................@@@@@@@@@@@.......................@@@@@@.......@@@@@@@@=........@@@@ @@@......................@@@ @@.........................@@@@@@@..........................@@@@@@@.......@@@@@@@@........................@@@@@@@*.......@@@@@@@@......@@@@@
|
||||||
|
@@.......@@@@@@@@@@@@@@@@@ @@@.......@@@@@@@@@........@@@@@@@@@@@@....................@@@@@@@@@@@@@@-.....................@@@@@@.......@@@@@@@@@@........@@@@ @@@......................@@@@@@........................@@@@@@@@.........................@@@@@@@@.......@@@@@@@@@@.....................@@@@@@@@@.......@@@@@@@@@@....@@@@@
|
||||||
|
@@@......@@@@@@@@@@@@ @@@*......@@@@@@@@@@.........@@@@@@@@@@@@...............-@@@@@@@@@@@@@@@@@@@...................@@@@@@@......@@@@@@@@@@@........@@@@@@@@.....................@@@@@@@.....................@@@@@@@@@@@:.....................@@@@@@@@@@@......@@@@@@@@@@@@@...............@@@@@@@@@@@@@......@@@@@@@@@@@@..@@@@@
|
||||||
|
@@@@@@@.+@@@@@@ @@@@@@@-.@@@@@@@@@@@@@@@@@@@#@@@@@@@@@@@@@@@@@@#..@@@@@@@@@@@@@@@@ @@@@@@@@@@@@-...........-@@@@@@@@@@@.+@@@@@@@@@@@@@@@@@@@*@@@@@@@@@@#................*@@@@@@@@@@+............@@@@@@@@@@@@@@@@@@@@............%@@@@@@@@@@@@@@@@@@@*.@@@@@@@@@@@@@@@@@@@@:.*@@@@@@@@@@@@@@@@@@@@@@#.@@@@@@@@@@@@@@@@@@@@
|
||||||
|
@@@@@@@@@@@@@ @@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@ @@@@@@@@@@@
|
||||||
|
@@@@@@@@@@@@@ @@@@@@@@@@@@@ @@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@ @@@@@@@@@
|
||||||
|
@@@@@@@@@@ @@@@@@@@@ @@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@@@@@@@@@@@@@@@@ @@@@@@@@@@ @@@@@@@@@@@@@@@@@@ @@@@@@@@@ @@@@@
|
||||||
|
|
||||||
|
@@@@@@@@%@@@@@@@@
|
||||||
|
@@@....@@@@.....@@@@@@
|
||||||
|
%@@.....@@@@..........@@@@
|
||||||
|
%@@......@@@@.............@@@%
|
||||||
|
:@@.......@@@@...............@@@-
|
||||||
|
@@@.......@@@@.................@@@
|
||||||
|
@@@........@@@@..................@@@
|
||||||
|
@@@.....@@@@@@@@@@@.................@@+
|
||||||
|
@@@......@@-. .+@@@@:..............@@-
|
||||||
|
@@@.......@@ @@@@.............@@
|
||||||
|
+@@........@@@@@@@@@@@ @@@............@@%
|
||||||
|
-@@.........@@-......@@@@ =@@............@@
|
||||||
|
@@:.........@@.........=@@. @@@...........@@@
|
||||||
|
@@@..........@@..........:@@ .@@...........#@@
|
||||||
|
@@@...........@@...........@@ @@............@@
|
||||||
|
@@@............@@...........@@ @@...........=@@
|
||||||
|
#@@.............@@..........@@@ :@@...........@@@
|
||||||
|
*@@..............@@.........@@@ @@:...........@@=
|
||||||
|
:@@...............@@@@....@@@@: @@@............@@
|
||||||
|
@@:...............@@#@@@@@@+ #@@%............@@*
|
||||||
|
@@@................@@ .@@@@.............@@%
|
||||||
|
@@@.............@@@@@@@@@@@@@@@@@@..............@@@
|
||||||
|
@@@..............@@ @@.-@@@@...................@@@
|
||||||
|
@@@...............@@ @@........................@@@
|
||||||
|
=@@................@@ @@......................@@@.
|
||||||
|
@@.................@@ @@....................@@@#
|
||||||
|
@@@.................@@ @@..................@@@%
|
||||||
|
@@-..................@@ @@..............@@@@@
|
||||||
|
-@@@@@@@@@@@@@@@@@@@@@@@ @@.......*@@@@@@@*
|
||||||
|
+@@@@@@@@@@@@@@@@@@@@@ %@@@@@@@@@@%=
|
||||||
|
|
||||||
|
█████████████████
|
||||||
|
███░░░░████░░░░░██████
|
||||||
|
███░░░░░████░░░░░░░░░░████
|
||||||
|
███░░░░░░████░░░░░░░░░░░░░████
|
||||||
|
▒██░░░░░░░████░░░░░░░░░░░░░░░███▒
|
||||||
|
███░░░░░░░████░░░░░░░░░░░░░░░░░███
|
||||||
|
███░░░░░░░░████░░░░░░░░░░░░░░░░░░███
|
||||||
|
███░░░░░███████████░░░░░░░░░░░░░░░░░██▓
|
||||||
|
███░░░░░░██▒░ ░▓████▒░░░░░░░░░░░░░░██▒
|
||||||
|
███░░░░░░░██ ████░░░░░░░░░░░░░██
|
||||||
|
▓██░░░░░░░░███████████ ███░░░░░░░░░░░░███
|
||||||
|
▒██░░░░░░░░░██▒░░░░░░████ ▓██░░░░░░░░░░░░██
|
||||||
|
██▒░░░░░░░░░██░░░░░░░░░▒██░ ███░░░░░░░░░░░███
|
||||||
|
███░░░░░░░░░░██░░░░░░░░░░▒██ ░██░░░░░░░░░░░▓██
|
||||||
|
███░░░░░░░░░░░██░░░░░░░░░░░██ ██░░░░░░░░░░░░██
|
||||||
|
███░░░░░░░░░░░░██░░░░░░░░░░░██ ██░░░░░░░░░░░▓██
|
||||||
|
███░░░░░░░░░░░░░██░░░░░░░░░░███ ▒██░░░░░░░░░░░███
|
||||||
|
▓██░░░░░░░░░░░░░░██░░░░░░░░░███ ██▒░░░░░░░░░░░██▓
|
||||||
|
▒██░░░░░░░░░░░░░░░████░░░░████▒ ███░░░░░░░░░░░░██
|
||||||
|
██▒░░░░░░░░░░░░░░░█████████▓ ████░░░░░░░░░░░░██▓
|
||||||
|
███░░░░░░░░░░░░░░░░██ ░████░░░░░░░░░░░░░███
|
||||||
|
███░░░░░░░░░░░░░██████████████████░░░░░░░░░░░░░░███
|
||||||
|
███░░░░░░░░░░░░░░██ ██░▒████░░░░░░░░░░░░░░░░░░░███
|
||||||
|
███░░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░░░░░░░░░░░███
|
||||||
|
▒██░░░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░░░░░░░░░███░
|
||||||
|
██░░░░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░░░░░░░████
|
||||||
|
███░░░░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░░░░░████
|
||||||
|
██▒░░░░░░░░░░░░░░░░░░██ ██░░░░░░░░░░░░░░█████
|
||||||
|
▒███████████████████████ ██░░░░░░░▓███████▓
|
||||||
|
▓█████████████████████ ████████████▒
|
||||||
|
|
||||||
|
``````` ````````````````````````````````````````´
|
||||||
|
``````` ``````````````````````````````````````````
|
||||||
|
``````´ ´``````````````````````````````````````````
|
||||||
|
``````` ``````````````````````````````````````````´
|
||||||
|
``````` ````````
|
||||||
|
``````` ``````
|
||||||
|
``````` ´`````´
|
||||||
|
``````` ´``````
|
||||||
|
``````` ``````´
|
||||||
|
``````` ´```````
|
||||||
|
``````´````````````````````````` ```````````````````````````````
|
||||||
|
`````````````````````````````````` ´```````````````````````````
|
||||||
|
```````````````````````````````````` ´``````````````````````´
|
||||||
|
`````````````````````````````````````` ```````````````````
|
||||||
|
``````` `````` `````````´
|
||||||
|
``````` `````` ``````````
|
||||||
|
``````` `````` `````````
|
||||||
|
``````` `````` ``````````
|
||||||
|
``````` `````` ``````````
|
||||||
|
``````` `````` ``````````
|
||||||
|
``````` `````` ``````````
|
||||||
|
``````` `````` `````````´
|
||||||
|
``````` `````` `````````
|
||||||
|
``````` `````` ´`````````
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 591 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 186 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,187 @@
|
|||||||
|
# Updated font loading and color scheme for Progression Loader
|
||||||
|
# Steam Collections page inspired design:
|
||||||
|
# - Dark blue theme matching Steam's signature colors
|
||||||
|
# - Steam blue (#66c0f4) for highlights and accents
|
||||||
|
# - Dark blue backgrounds (#1b2838, #2a475e) for depth
|
||||||
|
# - Light blue-gray text (#c7d5e0) for readability
|
||||||
|
# - Card-based layouts with rounded corners
|
||||||
|
# - Subtle shadows and glows for depth
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def get_resource_path(relative_path):
|
||||||
|
"""Get absolute path to resource, works for dev and for PyInstaller"""
|
||||||
|
try:
|
||||||
|
# PyInstaller creates a temp folder and stores path in _MEIPASS
|
||||||
|
base_path = sys._MEIPASS
|
||||||
|
except Exception:
|
||||||
|
base_path = os.path.abspath(".")
|
||||||
|
|
||||||
|
return os.path.join(base_path, relative_path)
|
||||||
|
|
||||||
|
# Steam Collections inspired color constants
|
||||||
|
COLORS = {
|
||||||
|
'bg_primary': '#1b2838', # Steam dark blue background
|
||||||
|
'bg_secondary': '#2a475e', # Steam medium blue background
|
||||||
|
'bg_tertiary': '#1e2328', # Steam darker background for cards
|
||||||
|
'bg_card': '#16202d', # Steam card background
|
||||||
|
'bg_hover': '#2a475e', # Steam hover state
|
||||||
|
'text_primary': '#c7d5e0', # Steam primary text (light blue-gray)
|
||||||
|
'text_highlight': '#66c0f4', # Steam blue highlight color
|
||||||
|
'text_body': '#8f98a0', # Steam body text (muted blue-gray)
|
||||||
|
'text_secondary': '#acb2b8', # Steam secondary text
|
||||||
|
'text_muted': '#67707b', # Steam muted text
|
||||||
|
'accent_green': '#5ba32b', # Steam success green
|
||||||
|
'accent_red': '#cd5c5c', # Steam error red
|
||||||
|
'accent_yellow': '#ffa500', # Steam warning orange
|
||||||
|
'accent_blue': '#66c0f4', # Steam signature blue
|
||||||
|
'border_light': '#3c4043', # Steam light border
|
||||||
|
'border_dark': '#0e141b', # Steam dark border
|
||||||
|
}
|
||||||
|
|
||||||
|
def load_all_georgia_fonts():
|
||||||
|
"""Load all Georgia font variants using Windows AddFontResourceEx with private flag"""
|
||||||
|
custom_font_available = False
|
||||||
|
custom_font_family = None
|
||||||
|
|
||||||
|
# List of Georgia font files to load
|
||||||
|
georgia_fonts = [
|
||||||
|
"georgia.ttf", # Regular
|
||||||
|
"georgiab.ttf", # Bold
|
||||||
|
"georgiai.ttf", # Italic
|
||||||
|
"georgiaz.ttf" # Bold Italic
|
||||||
|
]
|
||||||
|
|
||||||
|
fonts_loaded = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
for font_file in georgia_fonts:
|
||||||
|
font_path = get_resource_path(os.path.join("art", font_file))
|
||||||
|
if os.path.exists(font_path):
|
||||||
|
abs_font_path = os.path.abspath(font_path)
|
||||||
|
|
||||||
|
# Use the Stack Overflow method with AddFontResourceEx
|
||||||
|
success = _load_font_private(abs_font_path)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
fonts_loaded += 1
|
||||||
|
print(f"Successfully loaded font: {font_file}")
|
||||||
|
else:
|
||||||
|
print(f"Failed to load font: {font_file}")
|
||||||
|
else:
|
||||||
|
print(f"Font file not found: {font_path}")
|
||||||
|
|
||||||
|
if fonts_loaded > 0:
|
||||||
|
custom_font_available = True
|
||||||
|
custom_font_family = "Georgia"
|
||||||
|
print(f"Successfully loaded {fonts_loaded} Georgia font variants")
|
||||||
|
else:
|
||||||
|
print("No Georgia fonts could be loaded, using system fallback")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading fonts: {e}")
|
||||||
|
custom_font_available = False
|
||||||
|
custom_font_family = None
|
||||||
|
|
||||||
|
return custom_font_available, custom_font_family
|
||||||
|
|
||||||
|
def _load_font_private(fontpath):
|
||||||
|
"""
|
||||||
|
Load font privately using AddFontResourceEx
|
||||||
|
Based on Stack Overflow solution by Felipe
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from ctypes import windll, byref, create_unicode_buffer
|
||||||
|
|
||||||
|
# Constants for AddFontResourceEx
|
||||||
|
FR_PRIVATE = 0x10 # Font is private to this process
|
||||||
|
FR_NOT_ENUM = 0x20 # Font won't appear in font enumeration
|
||||||
|
|
||||||
|
# Create unicode buffer for the font path
|
||||||
|
pathbuf = create_unicode_buffer(fontpath)
|
||||||
|
|
||||||
|
# Use AddFontResourceExW for Unicode strings
|
||||||
|
AddFontResourceEx = windll.gdi32.AddFontResourceExW
|
||||||
|
|
||||||
|
# Set flags: private (unloaded when process dies) and not enumerable
|
||||||
|
flags = FR_PRIVATE | FR_NOT_ENUM
|
||||||
|
|
||||||
|
# Add the font resource
|
||||||
|
numFontsAdded = AddFontResourceEx(byref(pathbuf), flags, 0)
|
||||||
|
|
||||||
|
return numFontsAdded > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in _load_font_private: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def apply_steam_styling():
|
||||||
|
"""Apply Steam Collections page inspired styling to UI components"""
|
||||||
|
return {
|
||||||
|
'button_style': {
|
||||||
|
'relief': 'flat',
|
||||||
|
'borderwidth': 0,
|
||||||
|
'highlightthickness': 0,
|
||||||
|
'font': ('Georgia', 10, 'bold'),
|
||||||
|
'cursor': 'hand2'
|
||||||
|
},
|
||||||
|
'entry_style': {
|
||||||
|
'relief': 'flat',
|
||||||
|
'borderwidth': 2,
|
||||||
|
'highlightthickness': 0,
|
||||||
|
'font': ('Georgia', 9),
|
||||||
|
'insertbackground': COLORS['text_primary']
|
||||||
|
},
|
||||||
|
'label_style': {
|
||||||
|
'font': ('Georgia', 9),
|
||||||
|
'anchor': 'w'
|
||||||
|
},
|
||||||
|
'title_style': {
|
||||||
|
'font': ('Georgia', 14, 'bold'),
|
||||||
|
'anchor': 'center'
|
||||||
|
},
|
||||||
|
'card_style': {
|
||||||
|
'relief': 'flat',
|
||||||
|
'borderwidth': 1,
|
||||||
|
'highlightthickness': 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_steam_button_colors(button_type='default'):
|
||||||
|
"""Get Steam-inspired button color schemes"""
|
||||||
|
button_colors = {
|
||||||
|
'default': {
|
||||||
|
'bg': COLORS['bg_secondary'],
|
||||||
|
'fg': COLORS['text_primary'],
|
||||||
|
'activebackground': COLORS['bg_hover'],
|
||||||
|
'activeforeground': COLORS['text_highlight']
|
||||||
|
},
|
||||||
|
'primary': {
|
||||||
|
'bg': COLORS['accent_blue'],
|
||||||
|
'fg': COLORS['bg_primary'],
|
||||||
|
'activebackground': COLORS['text_highlight'],
|
||||||
|
'activeforeground': COLORS['bg_primary']
|
||||||
|
},
|
||||||
|
'success': {
|
||||||
|
'bg': COLORS['accent_green'],
|
||||||
|
'fg': COLORS['text_primary'],
|
||||||
|
'activebackground': '#6bb33f',
|
||||||
|
'activeforeground': COLORS['text_primary']
|
||||||
|
},
|
||||||
|
'warning': {
|
||||||
|
'bg': COLORS['accent_yellow'],
|
||||||
|
'fg': COLORS['bg_primary'],
|
||||||
|
'activebackground': '#ffb733',
|
||||||
|
'activeforeground': COLORS['bg_primary']
|
||||||
|
},
|
||||||
|
'danger': {
|
||||||
|
'bg': COLORS['accent_red'],
|
||||||
|
'fg': COLORS['text_primary'],
|
||||||
|
'activebackground': '#d66f6f',
|
||||||
|
'activeforeground': COLORS['text_primary']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return button_colors.get(button_type, button_colors['default'])
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
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
|
||||||
|
customtkinter>=5.0.0
|
||||||
|
rich>=13.0.0
|
||||||
|
After Width: | Height: | Size: 962 KiB |
@@ -0,0 +1,258 @@
|
|||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from PIL import Image, ImageColor, ImageDraw
|
||||||
|
|
||||||
|
|
||||||
|
FONT_FAMILY = "Georgia"
|
||||||
|
|
||||||
|
COLORS = {
|
||||||
|
"bg_primary": "#151515",
|
||||||
|
"bg_secondary": "#1d1d1d",
|
||||||
|
"bg_tertiary": "#212121",
|
||||||
|
"bg_card": "#2b2b2b",
|
||||||
|
"bg_hover": "#36312c",
|
||||||
|
"bg_error": "#2f2926",
|
||||||
|
"text_primary": "#f5f2ec",
|
||||||
|
"text_highlight": "#f0a621",
|
||||||
|
"text_body": "#c8c8c8",
|
||||||
|
"text_secondary": "#ffffff",
|
||||||
|
"text_muted": "#979797",
|
||||||
|
"accent_green": "#3a8e35",
|
||||||
|
"accent_red": "#ff4a46",
|
||||||
|
"accent_yellow": "#f0a621",
|
||||||
|
"accent_blue": "#d7b56a",
|
||||||
|
"border_light": "#4d453b",
|
||||||
|
"border_dark": "#0f0f0f",
|
||||||
|
"stripe": "#1d1d1d",
|
||||||
|
"stripe_soft": "#232323",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4)
|
||||||
|
def _load_rgba_source(image_path):
|
||||||
|
"""Load and cache a source image in RGBA form for repeated resizes."""
|
||||||
|
return Image.open(image_path).convert("RGBA")
|
||||||
|
|
||||||
|
|
||||||
|
def _rgba(color, alpha=255):
|
||||||
|
red, green, blue = ImageColor.getrgb(color)
|
||||||
|
return red, green, blue, alpha
|
||||||
|
|
||||||
|
|
||||||
|
def create_striped_texture(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
*,
|
||||||
|
base_color=None,
|
||||||
|
stripe_color=None,
|
||||||
|
stripe_width=16,
|
||||||
|
stripe_gap=18,
|
||||||
|
stripe_alpha=110,
|
||||||
|
corner_radius=0,
|
||||||
|
border_color=None,
|
||||||
|
border_width=0,
|
||||||
|
border_alpha=255,
|
||||||
|
):
|
||||||
|
"""Create a dark diagonal striped texture matching the pack UI style."""
|
||||||
|
width = max(1, int(width))
|
||||||
|
height = max(1, int(height))
|
||||||
|
|
||||||
|
base = Image.new(
|
||||||
|
"RGBA",
|
||||||
|
(width, height),
|
||||||
|
_rgba(base_color or COLORS["bg_card"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
stripes = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||||
|
stripe_draw = ImageDraw.Draw(stripes)
|
||||||
|
span = width + height
|
||||||
|
step = max(1, stripe_width + stripe_gap)
|
||||||
|
|
||||||
|
for offset in range(-height, span + height, step):
|
||||||
|
stripe_draw.line(
|
||||||
|
(offset, 0, offset + height, height),
|
||||||
|
fill=_rgba(stripe_color or COLORS["stripe"], stripe_alpha),
|
||||||
|
width=max(1, stripe_width),
|
||||||
|
)
|
||||||
|
|
||||||
|
image = Image.alpha_composite(base, stripes)
|
||||||
|
|
||||||
|
if border_color and border_width > 0:
|
||||||
|
border_draw = ImageDraw.Draw(image)
|
||||||
|
inset = max(1, border_width // 2)
|
||||||
|
bounds = (inset, inset, width - inset - 1, height - inset - 1)
|
||||||
|
if corner_radius > 0:
|
||||||
|
border_draw.rounded_rectangle(
|
||||||
|
bounds,
|
||||||
|
radius=max(1, corner_radius - inset),
|
||||||
|
outline=_rgba(border_color, border_alpha),
|
||||||
|
width=border_width,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
border_draw.rectangle(
|
||||||
|
bounds,
|
||||||
|
outline=_rgba(border_color, border_alpha),
|
||||||
|
width=border_width,
|
||||||
|
)
|
||||||
|
|
||||||
|
if corner_radius > 0:
|
||||||
|
mask = Image.new("L", (width, height), 0)
|
||||||
|
mask_draw = ImageDraw.Draw(mask)
|
||||||
|
mask_draw.rounded_rectangle(
|
||||||
|
(0, 0, width - 1, height - 1),
|
||||||
|
radius=corner_radius,
|
||||||
|
fill=255,
|
||||||
|
)
|
||||||
|
clipped = Image.new("RGBA", (width, height), (0, 0, 0, 0))
|
||||||
|
clipped.paste(image, (0, 0), mask)
|
||||||
|
image = clipped
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def create_striped_panel(
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
*,
|
||||||
|
panel_color=None,
|
||||||
|
stripe_color=None,
|
||||||
|
stripe_width=18,
|
||||||
|
stripe_gap=18,
|
||||||
|
stripe_alpha=110,
|
||||||
|
halo_padding=22,
|
||||||
|
halo_alpha=135,
|
||||||
|
panel_alpha=240,
|
||||||
|
corner_radius=18,
|
||||||
|
border_color=None,
|
||||||
|
border_width=1,
|
||||||
|
border_alpha=255,
|
||||||
|
):
|
||||||
|
"""Create a mostly solid panel with stripes that extend just beyond its edges."""
|
||||||
|
width = max(1, int(width))
|
||||||
|
height = max(1, int(height))
|
||||||
|
halo_padding = max(0, int(halo_padding))
|
||||||
|
total_width = width + (halo_padding * 2)
|
||||||
|
total_height = height + (halo_padding * 2)
|
||||||
|
|
||||||
|
image = Image.new("RGBA", (total_width, total_height), (0, 0, 0, 0))
|
||||||
|
|
||||||
|
stripes = Image.new("RGBA", (total_width, total_height), (0, 0, 0, 0))
|
||||||
|
stripe_draw = ImageDraw.Draw(stripes)
|
||||||
|
span = total_width + total_height
|
||||||
|
step = max(1, stripe_width + stripe_gap)
|
||||||
|
|
||||||
|
for offset in range(-total_height, span + total_height, step):
|
||||||
|
stripe_draw.line(
|
||||||
|
(offset, 0, offset + total_height, total_height),
|
||||||
|
fill=_rgba(stripe_color or COLORS["stripe"], halo_alpha),
|
||||||
|
width=max(1, stripe_width),
|
||||||
|
)
|
||||||
|
|
||||||
|
halo_mask = Image.new("L", (total_width, total_height), 0)
|
||||||
|
halo_draw = ImageDraw.Draw(halo_mask)
|
||||||
|
halo_draw.rounded_rectangle(
|
||||||
|
(0, 0, total_width - 1, total_height - 1),
|
||||||
|
radius=max(1, corner_radius + halo_padding),
|
||||||
|
fill=255,
|
||||||
|
)
|
||||||
|
halo_image = Image.new("RGBA", (total_width, total_height), (0, 0, 0, 0))
|
||||||
|
halo_image.paste(stripes, (0, 0), halo_mask)
|
||||||
|
image = Image.alpha_composite(image, halo_image)
|
||||||
|
|
||||||
|
panel_left = halo_padding
|
||||||
|
panel_top = halo_padding
|
||||||
|
panel_right = panel_left + width - 1
|
||||||
|
panel_bottom = panel_top + height - 1
|
||||||
|
|
||||||
|
panel = Image.new("RGBA", (total_width, total_height), (0, 0, 0, 0))
|
||||||
|
panel_draw = ImageDraw.Draw(panel)
|
||||||
|
panel_draw.rounded_rectangle(
|
||||||
|
(panel_left, panel_top, panel_right, panel_bottom),
|
||||||
|
radius=corner_radius,
|
||||||
|
fill=_rgba(panel_color or COLORS["bg_card"], panel_alpha),
|
||||||
|
)
|
||||||
|
|
||||||
|
if border_color and border_width > 0:
|
||||||
|
inset = max(1, border_width // 2)
|
||||||
|
panel_draw.rounded_rectangle(
|
||||||
|
(
|
||||||
|
panel_left + inset,
|
||||||
|
panel_top + inset,
|
||||||
|
panel_right - inset,
|
||||||
|
panel_bottom - inset,
|
||||||
|
),
|
||||||
|
radius=max(1, corner_radius - inset),
|
||||||
|
outline=_rgba(border_color, border_alpha),
|
||||||
|
width=border_width,
|
||||||
|
)
|
||||||
|
|
||||||
|
image = Image.alpha_composite(image, panel)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def load_cover_background(image_path, width, height, *, overlay_color=None, overlay_alpha=125):
|
||||||
|
"""Load an image, scale it to cover, and apply a dark overlay for readability."""
|
||||||
|
width = max(1, int(width))
|
||||||
|
height = max(1, int(height))
|
||||||
|
|
||||||
|
image = _load_rgba_source(image_path)
|
||||||
|
source_ratio = image.width / image.height
|
||||||
|
target_ratio = width / height
|
||||||
|
|
||||||
|
if source_ratio > target_ratio:
|
||||||
|
new_height = height
|
||||||
|
new_width = int(height * source_ratio)
|
||||||
|
else:
|
||||||
|
new_width = width
|
||||||
|
new_height = int(width / source_ratio)
|
||||||
|
|
||||||
|
image = image.resize((new_width, new_height), Image.Resampling.BICUBIC)
|
||||||
|
left = (new_width - width) // 2
|
||||||
|
top = (new_height - height) // 2
|
||||||
|
image = image.crop((left, top, left + width, top + height))
|
||||||
|
|
||||||
|
overlay = Image.new(
|
||||||
|
"RGBA",
|
||||||
|
(width, height),
|
||||||
|
_rgba(overlay_color or COLORS["bg_primary"], overlay_alpha),
|
||||||
|
)
|
||||||
|
return Image.alpha_composite(image, overlay)
|
||||||
|
|
||||||
|
|
||||||
|
def style_text_button(
|
||||||
|
button,
|
||||||
|
foreground,
|
||||||
|
background,
|
||||||
|
*,
|
||||||
|
hover_foreground=None,
|
||||||
|
disabled_foreground=None,
|
||||||
|
):
|
||||||
|
"""Style a tkinter Button to look like a color-coded text action."""
|
||||||
|
button.configure(
|
||||||
|
bg=background,
|
||||||
|
fg=foreground,
|
||||||
|
activebackground=background,
|
||||||
|
activeforeground=hover_foreground or COLORS["text_primary"],
|
||||||
|
relief="flat",
|
||||||
|
bd=0,
|
||||||
|
borderwidth=0,
|
||||||
|
highlightthickness=0,
|
||||||
|
disabledforeground=disabled_foreground or COLORS["text_muted"],
|
||||||
|
cursor="hand2",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def center_window(window, width, height, parent=None):
|
||||||
|
"""Center a window either on its parent or the current screen."""
|
||||||
|
width = int(width)
|
||||||
|
height = int(height)
|
||||||
|
window.update_idletasks()
|
||||||
|
|
||||||
|
if parent is not None and parent.winfo_exists():
|
||||||
|
x = parent.winfo_rootx() + (parent.winfo_width() // 2) - (width // 2)
|
||||||
|
y = parent.winfo_rooty() + (parent.winfo_height() // 2) - (height // 2)
|
||||||
|
else:
|
||||||
|
x = (window.winfo_screenwidth() // 2) - (width // 2)
|
||||||
|
y = (window.winfo_screenheight() // 2) - (height // 2)
|
||||||
|
|
||||||
|
window.geometry(f"{width}x{height}+{max(0, x)}+{max(0, y)}")
|
||||||
@@ -0,0 +1,577 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
from ui_theme import COLORS, FONT_FAMILY, style_text_button, center_window
|
||||||
|
|
||||||
|
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_notice_dialog(self, parent_window, title, message, accent_color):
|
||||||
|
"""Show a small themed notice dialog."""
|
||||||
|
dialog = tk.Toplevel(parent_window)
|
||||||
|
dialog.title(title)
|
||||||
|
dialog.configure(bg=COLORS['bg_primary'])
|
||||||
|
dialog.resizable(False, False)
|
||||||
|
dialog.attributes('-topmost', True)
|
||||||
|
|
||||||
|
panel = tk.Frame(
|
||||||
|
dialog,
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
highlightbackground=COLORS['border_light'],
|
||||||
|
highlightcolor=COLORS['border_light'],
|
||||||
|
highlightthickness=1,
|
||||||
|
)
|
||||||
|
panel.pack(fill='both', expand=True, padx=16, pady=16)
|
||||||
|
|
||||||
|
tk.Label(
|
||||||
|
panel,
|
||||||
|
text=title,
|
||||||
|
fg=accent_color,
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 18, 'bold italic'),
|
||||||
|
).pack(pady=(18, 10))
|
||||||
|
|
||||||
|
tk.Label(
|
||||||
|
panel,
|
||||||
|
text=message,
|
||||||
|
fg=COLORS['text_body'],
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 11),
|
||||||
|
justify='center',
|
||||||
|
wraplength=340,
|
||||||
|
).pack(padx=24, pady=(0, 18))
|
||||||
|
|
||||||
|
close_btn = tk.Button(
|
||||||
|
panel,
|
||||||
|
text="Close",
|
||||||
|
command=dialog.destroy,
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12, 'bold italic'),
|
||||||
|
padx=0,
|
||||||
|
pady=6,
|
||||||
|
)
|
||||||
|
style_text_button(close_btn, COLORS['accent_green'], COLORS['bg_card'])
|
||||||
|
close_btn.pack(pady=(0, 18))
|
||||||
|
|
||||||
|
center_window(dialog, 420, 220, parent_window)
|
||||||
|
dialog.protocol("WM_DELETE_WINDOW", dialog.destroy)
|
||||||
|
return dialog
|
||||||
|
|
||||||
|
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=COLORS['bg_primary'])
|
||||||
|
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
|
||||||
|
|
||||||
|
dialog_width = 500
|
||||||
|
dialog_height = 430
|
||||||
|
center_window(dialog, dialog_width, dialog_height, parent_window)
|
||||||
|
|
||||||
|
panel = tk.Frame(
|
||||||
|
dialog,
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
highlightbackground=COLORS['border_light'],
|
||||||
|
highlightcolor=COLORS['border_light'],
|
||||||
|
highlightthickness=1,
|
||||||
|
)
|
||||||
|
panel.pack(fill='both', expand=True, padx=16, pady=16)
|
||||||
|
|
||||||
|
title_label = tk.Label(
|
||||||
|
panel,
|
||||||
|
text="Update Available",
|
||||||
|
fg=COLORS['text_highlight'],
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 18, 'bold italic'),
|
||||||
|
)
|
||||||
|
title_label.pack(pady=20)
|
||||||
|
|
||||||
|
# Version info
|
||||||
|
version_frame = tk.Frame(panel, bg=COLORS['bg_card'])
|
||||||
|
version_frame.pack(pady=10)
|
||||||
|
|
||||||
|
current_label = tk.Label(version_frame,
|
||||||
|
text=f"Current Version: {self.current_version}",
|
||||||
|
fg=COLORS['text_body'], bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12))
|
||||||
|
current_label.pack()
|
||||||
|
|
||||||
|
latest_label = tk.Label(version_frame,
|
||||||
|
text=f"Latest Version: {latest_version}",
|
||||||
|
fg=COLORS['accent_green'], bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12, 'bold'))
|
||||||
|
latest_label.pack()
|
||||||
|
|
||||||
|
# Release notes
|
||||||
|
if release_info.get('body'):
|
||||||
|
notes_label = tk.Label(panel,
|
||||||
|
text="Release Notes:",
|
||||||
|
fg=COLORS['text_primary'], bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12, 'bold italic'))
|
||||||
|
notes_label.pack(pady=(20, 5))
|
||||||
|
|
||||||
|
# Create scrollable text widget for release notes
|
||||||
|
notes_frame = tk.Frame(panel, bg=COLORS['bg_card'])
|
||||||
|
notes_frame.pack(fill='both', expand=True, padx=20, pady=(0, 20))
|
||||||
|
|
||||||
|
notes_text = tk.Text(notes_frame,
|
||||||
|
height=8,
|
||||||
|
bg=COLORS['bg_tertiary'], fg=COLORS['text_body'],
|
||||||
|
font=(FONT_FAMILY, 10),
|
||||||
|
wrap=tk.WORD,
|
||||||
|
state='disabled',
|
||||||
|
relief='flat',
|
||||||
|
bd=0,
|
||||||
|
insertbackground=COLORS['text_primary'],
|
||||||
|
highlightthickness=1,
|
||||||
|
highlightbackground=COLORS['border_light'])
|
||||||
|
|
||||||
|
scrollbar = tk.Scrollbar(
|
||||||
|
notes_frame,
|
||||||
|
bg=COLORS['bg_tertiary'],
|
||||||
|
troughcolor=COLORS['bg_secondary'],
|
||||||
|
activebackground=COLORS['bg_hover'],
|
||||||
|
)
|
||||||
|
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(panel, bg=COLORS['bg_card'])
|
||||||
|
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=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12, 'bold italic'),
|
||||||
|
padx=0, pady=6)
|
||||||
|
style_text_button(download_btn, COLORS['accent_green'], COLORS['bg_card'])
|
||||||
|
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=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12, 'bold italic'),
|
||||||
|
padx=0, pady=6)
|
||||||
|
style_text_button(later_btn, COLORS['accent_yellow'], COLORS['bg_card'])
|
||||||
|
later_btn.pack(side='left', padx=5)
|
||||||
|
|
||||||
|
skip_btn = tk.Button(button_frame,
|
||||||
|
text="Skip Version",
|
||||||
|
command=skip_version,
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12, 'bold italic'),
|
||||||
|
padx=0, pady=6)
|
||||||
|
style_text_button(skip_btn, COLORS['accent_red'], COLORS['bg_card'])
|
||||||
|
skip_btn.pack(side='left', padx=5)
|
||||||
|
else:
|
||||||
|
close_btn = tk.Button(button_frame,
|
||||||
|
text="Close",
|
||||||
|
command=dialog.destroy,
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12, 'bold italic'),
|
||||||
|
padx=0, pady=6)
|
||||||
|
style_text_button(close_btn, COLORS['accent_red'], COLORS['bg_card'])
|
||||||
|
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:
|
||||||
|
self._show_notice_dialog(
|
||||||
|
parent_window,
|
||||||
|
"Update Check Failed",
|
||||||
|
f"Could not check for updates:\n{error}",
|
||||||
|
COLORS['accent_red'],
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not release_info:
|
||||||
|
self._show_notice_dialog(
|
||||||
|
parent_window,
|
||||||
|
"No Updates",
|
||||||
|
"Could not retrieve release information.",
|
||||||
|
COLORS['accent_yellow'],
|
||||||
|
)
|
||||||
|
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:
|
||||||
|
self._show_notice_dialog(
|
||||||
|
parent_window,
|
||||||
|
"Up To Date",
|
||||||
|
f"You are running the latest version ({self.current_version}).",
|
||||||
|
COLORS['accent_green'],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Show checking message
|
||||||
|
checking_dialog = tk.Toplevel(parent_window)
|
||||||
|
checking_dialog.title("Checking for Updates")
|
||||||
|
checking_dialog.configure(bg=COLORS['bg_primary'])
|
||||||
|
checking_dialog.resizable(False, False)
|
||||||
|
checking_dialog.attributes('-topmost', True)
|
||||||
|
|
||||||
|
panel = tk.Frame(
|
||||||
|
checking_dialog,
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
highlightbackground=COLORS['border_light'],
|
||||||
|
highlightcolor=COLORS['border_light'],
|
||||||
|
highlightthickness=1,
|
||||||
|
)
|
||||||
|
panel.pack(fill='both', expand=True, padx=14, pady=14)
|
||||||
|
|
||||||
|
label = tk.Label(
|
||||||
|
panel,
|
||||||
|
text="Checking for updates...",
|
||||||
|
fg=COLORS['text_primary'],
|
||||||
|
bg=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 12, 'bold italic'),
|
||||||
|
)
|
||||||
|
label.pack(expand=True)
|
||||||
|
|
||||||
|
center_window(checking_dialog, 320, 120, parent_window)
|
||||||
|
|
||||||
|
def check_and_close():
|
||||||
|
release_info, error = self.check_for_updates_sync()
|
||||||
|
parent_window.after(0, lambda: checking_dialog.destroy())
|
||||||
|
parent_window.after(0, lambda: 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=COLORS['bg_card'],
|
||||||
|
font=(FONT_FAMILY, 10, 'bold italic'),
|
||||||
|
padx=8, pady=3)
|
||||||
|
style_text_button(update_btn, COLORS['accent_yellow'], COLORS['bg_card'])
|
||||||
|
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
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
Configuration settings for the update checker
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Update checker configuration
|
||||||
|
UPDATE_CONFIG = {
|
||||||
|
# Current version of the application
|
||||||
|
"current_version": "0.2.0",
|
||||||
|
|
||||||
|
# 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"]
|
||||||
@@ -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()
|
||||||