diff --git a/Error UI.png b/Error UI.png new file mode 100644 index 0000000..9e46b0b Binary files /dev/null and b/Error UI.png differ diff --git a/STEAM_COLLECTIONS_UPDATE_SUMMARY.md b/STEAM_COLLECTIONS_UPDATE_SUMMARY.md new file mode 100644 index 0000000..8d7efb4 --- /dev/null +++ b/STEAM_COLLECTIONS_UPDATE_SUMMARY.md @@ -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. \ No newline at end of file diff --git a/art/InfoUI.png b/art/InfoUI.png new file mode 100644 index 0000000..a985dff Binary files /dev/null and b/art/InfoUI.png differ diff --git a/art/UIDrawup.psd b/art/UIDrawup.psd new file mode 100644 index 0000000..fd37bd9 Binary files /dev/null and b/art/UIDrawup.psd differ diff --git a/art/lines.png b/art/lines.png new file mode 100644 index 0000000..96e0f89 Binary files /dev/null and b/art/lines.png differ diff --git a/font_and_colors_update.py b/font_and_colors_update.py new file mode 100644 index 0000000..4889941 --- /dev/null +++ b/font_and_colors_update.py @@ -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']) \ No newline at end of file diff --git a/progression_tui.py b/progression_tui.py index 9a0f1a3..85a45dc 100644 --- a/progression_tui.py +++ b/progression_tui.py @@ -36,6 +36,23 @@ from update_config import get_update_config # Load environment variables load_dotenv() +# Steam Collections page inspired color scheme for TUI +TUI_COLORS = { + 'bg_primary': '#1b2838', # Steam dark blue background + 'bg_secondary': '#2a475e', # Steam medium blue background + 'bg_tertiary': '#1e2328', # Steam darker background for cards + '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 + 'accent_cyan': '#66c0f4', # Steam info/header color (same as blue) +} + console = Console() class ProgressionTUI: @@ -114,23 +131,26 @@ class ProgressionTUI: self.hr_systems_logo = "Hudson Riggs Systems" def display_header(self): - """Display the header with ASCII art""" - # Use the actual progression logo ASCII art instead of just text + """Display the header with ASCII art using Steam Collections inspired styling""" + # Use the actual progression logo ASCII art with Steam blue highlight header_panel = Panel( Align.center(self.progression_logo), box=box.DOUBLE, - style="cyan", - padding=(1, 2) + style=TUI_COLORS['accent_blue'], # Steam blue + border_style=TUI_COLORS['accent_blue'], + padding=(1, 2), + title="[bold]PROGRESSION LOADER[/bold]", + title_align="center" ) return header_panel def display_footer(self): - """Display footer with HR Systems info""" + """Display footer with HR Systems info using Steam Collections inspired styling""" footer_text = Text() - footer_text.append("Hudson Riggs Systems", style="dim white") - footer_text.append(" - ", style="dim white") - footer_text.append("https://hudsonriggs.systems", style="dim blue underline") + footer_text.append("Hudson Riggs Systems", style=f"dim {TUI_COLORS['text_secondary']}") + footer_text.append(" - ", style=f"dim {TUI_COLORS['text_secondary']}") + footer_text.append("https://hudsonriggs.systems", style=f"dim {TUI_COLORS['accent_blue']} underline") return Align.center(footer_text) @@ -147,40 +167,66 @@ class ProgressionTUI: self.console.print(self.display_header()) self.console.print() - # Create main menu options - menu_table = Table(show_header=False, box=box.SIMPLE, padding=(0, 2)) - menu_table.add_column("Option", style="cyan bold", width=4) - menu_table.add_column("Description", style="white") - menu_table.add_column("Status", style="green", width=15) + # Create main menu options with Steam Collections inspired styling + menu_table = Table(show_header=False, box=box.ROUNDED, padding=(0, 2)) + menu_table.add_column("Option", style=f"{TUI_COLORS['accent_blue']} bold", width=4) + menu_table.add_column("Description", style=TUI_COLORS['text_primary']) + menu_table.add_column("Status", style=TUI_COLORS['accent_green'], width=15) - # Menu options with status indicators - rimworld_status = "Valid" if self.is_rimworld_valid else "Not Set" - rimworld_color = "green" if self.is_rimworld_valid else "red" + # Menu options with Steam-styled status indicators + rimworld_status = "✓ Valid" if self.is_rimworld_valid else "⚠ Not Set" + rimworld_color = TUI_COLORS['accent_green'] if self.is_rimworld_valid else TUI_COLORS['accent_yellow'] menu_table.add_row("1", "Set RimWorld Game Folder", f"[{rimworld_color}]{rimworld_status}[/{rimworld_color}]") menu_table.add_row("2", "Check Steam Workshop Subscriptions", "") menu_table.add_row("3", "Check RimWorld Expansions", "") - menu_table.add_row("4", "Load Progression Pack Complete", "Enabled" if self.is_rimworld_valid else "[dim]Disabled[/dim]") - menu_table.add_row("5", "Merge with Current Mods Config", "Enabled" if self.is_rimworld_valid else "[dim]Disabled[/dim]") + menu_table.add_row("4", "Load Progression Pack Complete", f"[{TUI_COLORS['accent_green']}]✓ Ready[/{TUI_COLORS['accent_green']}]" if self.is_rimworld_valid else f"[{TUI_COLORS['text_muted']}]⚠ Disabled[/{TUI_COLORS['text_muted']}]") + menu_table.add_row("5", "Merge with Current Mods Config", f"[{TUI_COLORS['accent_green']}]✓ Ready[/{TUI_COLORS['accent_green']}]" if self.is_rimworld_valid else f"[{TUI_COLORS['text_muted']}]⚠ Disabled[/{TUI_COLORS['text_muted']}]") menu_table.add_row("6", "Check for Updates", "") menu_table.add_row("7", "View Configuration", "") menu_table.add_row("0", "Exit", "") + # Create Steam-styled menu panel menu_panel = Panel( menu_table, - title="Main Menu", + title="[bold]Main Menu[/bold]", + title_align="center", box=box.ROUNDED, - style="white" + style=TUI_COLORS['text_primary'], + border_style=TUI_COLORS['accent_blue'] ) self.console.print(menu_panel) self.console.print() - # Display current paths if set + # Display current paths if set with Steam Collections styling if self.rimworld_path or self.workshop_path: info_table = Table(show_header=False, box=None, padding=(0, 1)) - info_table.add_column("Label", style="cyan", width=25) - info_table.add_column("Path", style="white") + info_table.add_column("Label", style=f"{TUI_COLORS['accent_blue']} bold", width=25) + info_table.add_column("Path", style=TUI_COLORS['text_body']) + + if self.rimworld_path: + status_icon = "✓" if self.is_rimworld_valid else "⚠" + status_color = TUI_COLORS['accent_green'] if self.is_rimworld_valid else TUI_COLORS['accent_yellow'] + info_table.add_row(f"[{status_color}]{status_icon}[/{status_color}] RimWorld Path:", self.rimworld_path) + + if self.workshop_path: + info_table.add_row(f"[{TUI_COLORS['accent_blue']}]📁[/{TUI_COLORS['accent_blue']}] Workshop Path:", self.workshop_path) + + if self.modsconfig_path: + info_table.add_row(f"[{TUI_COLORS['accent_blue']}]⚙[/{TUI_COLORS['accent_blue']}] ModsConfig Path:", self.modsconfig_path) + + # Create Steam-styled info panel + info_panel = Panel( + info_table, + title="[bold]Current Configuration[/bold]", + title_align="center", + box=box.ROUNDED, + style=TUI_COLORS['text_secondary'], + border_style=TUI_COLORS['text_muted'] + ) + self.console.print(info_panel) + self.console.print() if self.rimworld_path: info_table.add_row("RimWorld Path:", self.rimworld_path) @@ -201,7 +247,7 @@ class ProgressionTUI: choice = Prompt.ask("Select an option", choices=["0", "1", "2", "3", "4", "5", "6", "7"], default="1") if choice == "0": - self.console.print("\n[yellow]Thank you for using Progression Loader![/yellow]") + self.console.print(f"\n[{TUI_COLORS['accent_yellow']}]Thank you for using Progression Loader![/{TUI_COLORS['accent_yellow']}]") break elif choice == "1": self.set_rimworld_path() @@ -213,13 +259,13 @@ class ProgressionTUI: if self.is_rimworld_valid: self.load_progression_pack() else: - self.console.print("\n[red]Please set a valid RimWorld path first![/red]") + self.console.print(f"\n[{TUI_COLORS['accent_red']}]Please set a valid RimWorld path first![/{TUI_COLORS['accent_red']}]") self.console.input("\nPress Enter to continue...") elif choice == "5": if self.is_rimworld_valid: self.merge_with_current_mods() else: - self.console.print("\n[red]Please set a valid RimWorld path first![/red]") + self.console.print(f"\n[{TUI_COLORS['accent_red']}]Please set a valid RimWorld path first![/{TUI_COLORS['accent_red']}]") self.console.input("\nPress Enter to continue...") elif choice == "6": self.check_for_updates() @@ -227,10 +273,10 @@ class ProgressionTUI: self.view_configuration() except KeyboardInterrupt: - self.console.print("\n\n[yellow]Goodbye![/yellow]") + self.console.print(f"\n\n[{TUI_COLORS['accent_yellow']}]Goodbye![/{TUI_COLORS['accent_yellow']}]") break except Exception as e: - self.console.print(f"\n[red]Error: {e}[/red]") + self.console.print(f"\n[{TUI_COLORS['accent_red']}]Error: {e}[/{TUI_COLORS['accent_red']}]") self.console.input("\nPress Enter to continue...") def set_rimworld_path(self): @@ -239,28 +285,30 @@ class ProgressionTUI: self.console.print(self.display_header()) self.console.print() - # Instructions + # Instructions with modern styling - bold titles, italic emphasis instructions = Panel( Text.from_markup( - "To find your RimWorld path:\n" + "[bold]To find your RimWorld path:[/bold]\n\n" "1. Open Steam\n" "2. Right-click RimWorld in your library\n" "3. Select 'Manage' > 'Browse local files'\n" "4. Copy the path from the address bar\n\n" - "Example: C:\\Steam\\steamapps\\common\\RimWorld" + "[italic dim]Example: C:\\Steam\\steamapps\\common\\RimWorld[/italic dim]" ), - title="Instructions", - style="cyan" + title=f"[bold {TUI_COLORS['text_highlight']}]Instructions[/bold {TUI_COLORS['text_highlight']}]", + title_align="center", + style=TUI_COLORS['accent_cyan'], + border_style="rounded" ) self.console.print(instructions) self.console.print() - # Current path if set + # Current path if set with new colors if self.rimworld_path: current_panel = Panel( f"Current path: {self.rimworld_path}", title="Current RimWorld Path", - style="green" if self.is_rimworld_valid else "red" + style=TUI_COLORS['accent_green'] if self.is_rimworld_valid else TUI_COLORS['accent_red'] ) self.console.print(current_panel) self.console.print() @@ -283,17 +331,17 @@ class ProgressionTUI: # Derive workshop path self.derive_workshop_path() - progress.update(task, description="[green]Valid RimWorld installation found![/green]") + progress.update(task, description=f"[{TUI_COLORS['accent_green']}]Valid RimWorld installation found![/{TUI_COLORS['accent_green']}]") time.sleep(1) - self.console.print(f"\n[green]RimWorld path set successfully![/green]") + self.console.print(f"\n[{TUI_COLORS['accent_green']}]RimWorld path set successfully![/{TUI_COLORS['accent_green']}]") self.console.print(f"Workshop path: {self.workshop_path}") else: - progress.update(task, description="[red]Invalid RimWorld path![/red]") + progress.update(task, description=f"[{TUI_COLORS['accent_red']}]Invalid RimWorld path![/{TUI_COLORS['accent_red']}]") time.sleep(1) - self.console.print(f"\n[red]Invalid RimWorld installation at: {new_path}[/red]") - self.console.print("[red]Please ensure the path contains RimWorld.exe and Data folder[/red]") + self.console.print(f"\n[{TUI_COLORS['accent_red']}]Invalid RimWorld installation at: {new_path}[/{TUI_COLORS['accent_red']}]") + self.console.print(f"[{TUI_COLORS['accent_red']}]Please ensure the path contains RimWorld.exe and Data folder[/{TUI_COLORS['accent_red']}]") self.console.input("\nPress Enter to continue...") @@ -339,26 +387,28 @@ class ProgressionTUI: self.console.print(self.display_header()) self.console.print() - # Display collections - collections_table = Table(show_header=True, box=box.ROUNDED) - collections_table.add_column("Collection", style="cyan bold", width=20) - collections_table.add_column("URL", style="blue", width=50) - collections_table.add_column("Subscribed", style="green", width=12) + # Display collections with modern styling + collections_table = Table(show_header=True, box=box.ROUNDED, border_style="bright_black") + collections_table.add_column("Collection", style=f"{TUI_COLORS['text_highlight']} bold", width=20) + collections_table.add_column("URL", style=TUI_COLORS['accent_blue'], width=50) + collections_table.add_column("Subscribed", style=TUI_COLORS['accent_green'], width=12) for name, url in self.collections.items(): status = "Yes" if self.subscription_status[name] else "No" - status_color = "green" if self.subscription_status[name] else "red" + status_color = TUI_COLORS['accent_green'] if self.subscription_status[name] else TUI_COLORS['accent_red'] collections_table.add_row(name, url, f"[{status_color}]{status}[/{status_color}]") collections_panel = Panel( collections_table, - title="Steam Workshop Collections", - style="white" + title="[bold]Steam Workshop Collections[/bold]", + title_align="center", + style=TUI_COLORS['text_primary'], + border_style="rounded" ) self.console.print(collections_panel) self.console.print() - # Instructions + # Instructions with new color scheme instructions = Panel( Text.from_markup( "To subscribe to collections:\n" @@ -367,10 +417,10 @@ class ProgressionTUI: "3. Click 'Subscribe' on the Steam Workshop page\n" "4. Wait for Steam to download the collection\n" "5. Return here and mark as subscribed\n\n" - "[yellow]All collections are required for the complete Progression experience.[/yellow]" + f"[{TUI_COLORS['accent_yellow']}]All collections are required for the complete Progression experience.[/{TUI_COLORS['accent_yellow']}]" ), title="Instructions", - style="cyan" + style=TUI_COLORS['accent_cyan'] ) self.console.print(instructions) self.console.print() @@ -378,8 +428,8 @@ class ProgressionTUI: # Menu options while True: menu_table = Table(show_header=False, box=box.SIMPLE, padding=(0, 2)) - menu_table.add_column("Option", style="cyan bold", width=4) - menu_table.add_column("Description", style="white") + menu_table.add_column("Option", style=f"{TUI_COLORS['accent_cyan']} bold", width=4) + menu_table.add_column("Description", style=TUI_COLORS['text_primary']) menu_table.add_row("1", "Open Core Collection in browser") menu_table.add_row("2", "Open Content Collection in browser") @@ -390,7 +440,7 @@ class ProgressionTUI: menu_table.add_row("7", "Check if all subscribed") menu_table.add_row("0", "Back to main menu") - self.console.print(Panel(menu_table, title="Subscription Menu", style="white")) + self.console.print(Panel(menu_table, title="Subscription Menu", style=TUI_COLORS['text_primary'])) self.console.print() choice = Prompt.ask("Select an option", choices=["0", "1", "2", "3", "4", "5", "6", "7"]) @@ -399,29 +449,29 @@ class ProgressionTUI: break elif choice == "1": webbrowser.open(self.collections["Core Collection"]) - self.console.print("[green]Opened Core Collection in browser[/green]") + self.console.print(f"[{TUI_COLORS['accent_green']}]Opened Core Collection in browser[/{TUI_COLORS['accent_green']}]") elif choice == "2": webbrowser.open(self.collections["Content Collection"]) - self.console.print("[green]Opened Content Collection in browser[/green]") + self.console.print(f"[{TUI_COLORS['accent_green']}]Opened Content Collection in browser[/{TUI_COLORS['accent_green']}]") elif choice == "3": webbrowser.open(self.collections["Cosmetics Collection"]) - self.console.print("[green]Opened Cosmetics Collection in browser[/green]") + self.console.print(f"[{TUI_COLORS['accent_green']}]Opened Cosmetics Collection in browser[/{TUI_COLORS['accent_green']}]") elif choice == "4": self.subscription_status["Core Collection"] = True - self.console.print("[green]Core Collection marked as subscribed[/green]") + self.console.print(f"[{TUI_COLORS['accent_green']}]Core Collection marked as subscribed[/{TUI_COLORS['accent_green']}]") elif choice == "5": self.subscription_status["Content Collection"] = True - self.console.print("[green]Content Collection marked as subscribed[/green]") + self.console.print(f"[{TUI_COLORS['accent_green']}]Content Collection marked as subscribed[/{TUI_COLORS['accent_green']}]") elif choice == "6": self.subscription_status["Cosmetics Collection"] = True - self.console.print("[green]Cosmetics Collection marked as subscribed[/green]") + self.console.print(f"[{TUI_COLORS['accent_green']}]Cosmetics Collection marked as subscribed[/{TUI_COLORS['accent_green']}]") elif choice == "7": all_subscribed = all(self.subscription_status.values()) if all_subscribed: - self.console.print("[green]All collections are subscribed! You can proceed.[/green]") + self.console.print(f"[{TUI_COLORS['accent_green']}]All collections are subscribed! You can proceed.[/{TUI_COLORS['accent_green']}]") else: missing = [name for name, status in self.subscription_status.items() if not status] - self.console.print(f"[red]Missing subscriptions: {', '.join(missing)}[/red]") + self.console.print(f"[{TUI_COLORS['accent_red']}]Missing subscriptions: {', '.join(missing)}[/{TUI_COLORS['accent_red']}]") self.console.print() @@ -441,44 +491,44 @@ class ProgressionTUI: # Check expansions self.check_expansions_thread() - progress.update(task, description="[green]Expansion check complete![/green]") + progress.update(task, description=f"[{TUI_COLORS['accent_green']}]Expansion check complete![/{TUI_COLORS['accent_green']}]") time.sleep(1) - # Display results + # Display results with new color scheme if self.expansion_check_results: expansion_table = Table(show_header=True, box=box.ROUNDED) - expansion_table.add_column("Expansion", style="cyan bold", width=15) - expansion_table.add_column("Status", style="white", width=10) + expansion_table.add_column("Expansion", style=f"{TUI_COLORS['accent_cyan']} bold", width=15) + expansion_table.add_column("Status", style=TUI_COLORS['text_primary'], width=10) missing_expansions = [] for expansion, owned in self.expansion_check_results.items(): status = "Owned" if owned else "Missing" - status_color = "green" if owned else "red" + status_color = TUI_COLORS['accent_green'] if owned else TUI_COLORS['accent_red'] expansion_table.add_row(expansion, f"[{status_color}]{status}[/{status_color}]") if not owned: missing_expansions.append(expansion) - self.console.print(Panel(expansion_table, title="RimWorld Expansions", style="white")) + self.console.print(Panel(expansion_table, title="RimWorld Expansions", style=TUI_COLORS['text_primary'])) self.console.print() if missing_expansions: warning_text = Text() - warning_text.append("WARNING: ", style="bold red") - warning_text.append(f"You don't own {', '.join(missing_expansions)}!\n", style="red") - warning_text.append("Some progression mods may not work without these expansions.", style="yellow") + warning_text.append("WARNING: ", style=f"bold {TUI_COLORS['accent_red']}") + warning_text.append(f"You don't own {', '.join(missing_expansions)}!\n", style=TUI_COLORS['accent_red']) + warning_text.append("Some progression mods may not work without these expansions.", style=TUI_COLORS['accent_yellow']) - warning_panel = Panel(warning_text, title="Missing Expansions", style="red") + warning_panel = Panel(warning_text, title="Missing Expansions", style=TUI_COLORS['accent_red']) self.console.print(warning_panel) else: success_panel = Panel( - "[green]All RimWorld expansions detected![/green]", + f"[{TUI_COLORS['accent_green']}]All RimWorld expansions detected![/{TUI_COLORS['accent_green']}]", title="Expansion Check", - style="green" + style=TUI_COLORS['accent_green'] ) self.console.print(success_panel) else: - self.console.print("[yellow]Could not check expansions. Please ensure RimWorld path is set correctly.[/yellow]") + self.console.print(f"[{TUI_COLORS['accent_yellow']}]Could not check expansions. Please ensure RimWorld path is set correctly.[/{TUI_COLORS['accent_yellow']}]") self.console.input("\nPress Enter to continue...") @@ -680,25 +730,25 @@ class ProgressionTUI: self.console.print(self.display_header()) self.console.print() - # Check prerequisites + # Check prerequisites with new colors all_subscribed = all(self.subscription_status.values()) if not all_subscribed: missing = [name for name, status in self.subscription_status.items() if not status] error_panel = Panel( - f"[red]Please subscribe to all collections first!\nMissing: {', '.join(missing)}[/red]", + f"[{TUI_COLORS['accent_red']}]Please subscribe to all collections first!\nMissing: {', '.join(missing)}[/{TUI_COLORS['accent_red']}]", title="Prerequisites Not Met", - style="red" + style=TUI_COLORS['accent_red'] ) self.console.print(error_panel) self.console.input("\nPress Enter to continue...") return - # Confirm action + # Confirm action with new colors confirm_text = Text() - confirm_text.append("This will replace your current ModsConfig.xml with the complete Progression pack.\n", style="yellow") - confirm_text.append("Your current mod configuration will be backed up.", style="white") + confirm_text.append("This will replace your current ModsConfig.xml with the complete Progression pack.\n", style=TUI_COLORS['accent_yellow']) + confirm_text.append("Your current mod configuration will be backed up.", style=TUI_COLORS['text_primary']) - confirm_panel = Panel(confirm_text, title="Confirmation", style="yellow") + confirm_panel = Panel(confirm_text, title="Confirmation", style=TUI_COLORS['accent_yellow']) self.console.print(confirm_panel) self.console.print() @@ -1075,18 +1125,18 @@ class ProgressionTUI: def main(): """Main entry point for the TUI application""" try: - # Check for updates first + # Check for updates first with new colors config = get_update_config() if config.get("check_on_startup", True): - console.print("[cyan]Checking for updates...[/cyan]") + console.print(f"[{TUI_COLORS['accent_cyan']}]Checking for updates...[/{TUI_COLORS['accent_cyan']}]") try: update_checker = UpdateChecker(config["current_version"]) release_info, error = update_checker.check_for_updates_sync() if release_info and update_checker.is_newer_version(release_info['version']): - console.print(f"[yellow]Update available: {release_info['version']}[/yellow]") + console.print(f"[{TUI_COLORS['accent_yellow']}]Update available: {release_info['version']}[/{TUI_COLORS['accent_yellow']}]") if config.get("updates_required", False): - console.print("[red]This update is required. Please update before continuing.[/red]") + console.print(f"[{TUI_COLORS['accent_red']}]This update is required. Please update before continuing.[/{TUI_COLORS['accent_red']}]") if Confirm.ask("Open download page?"): webbrowser.open(release_info['html_url']) return @@ -1100,9 +1150,9 @@ def main(): app.show_main_menu() except KeyboardInterrupt: - console.print("\n[yellow]Goodbye![/yellow]") + console.print(f"\n[{TUI_COLORS['accent_yellow']}]Goodbye![/{TUI_COLORS['accent_yellow']}]") except Exception as e: - console.print(f"[red]Fatal error: {e}[/red]") + console.print(f"[{TUI_COLORS['accent_red']}]Fatal error: {e}[/{TUI_COLORS['accent_red']}]") console.print("[dim]Please report this error to the developers.[/dim]") diff --git a/sB9nsdi.jpeg b/sB9nsdi.jpeg new file mode 100644 index 0000000..48ecb79 Binary files /dev/null and b/sB9nsdi.jpeg differ diff --git a/steam_workshop_gui.py b/steam_workshop_gui.py index 076f966..8384122 100644 --- a/steam_workshop_gui.py +++ b/steam_workshop_gui.py @@ -16,11 +16,20 @@ import webbrowser from update_checker import UpdateChecker from update_config import get_update_config import html +from ui_theme import ( + COLORS, + FONT_FAMILY, + create_striped_panel, + create_striped_texture, + load_cover_background, + style_text_button, + center_window, +) # Load environment variables load_dotenv() -# Set CustomTkinter appearance +# Set CustomTkinter appearance with new color scheme ctk.set_appearance_mode("dark") ctk.set_default_color_theme("dark-blue") @@ -138,16 +147,28 @@ class LoadingScreen: except Exception: return False - def get_font(self, size=10, weight='normal'): + def get_font(self, size=10, weight='normal', slant='roman'): """Get font tuple for UI elements""" + if weight in {'italic', 'roman'}: + slant = weight + weight = 'normal' + elif weight in {'bold italic', 'italic bold'}: + weight = 'bold' + slant = 'italic' + if self.custom_font_available and self.custom_font_family: try: - return font.Font(family=self.custom_font_family, size=size, weight=weight) + return font.Font( + family=self.custom_font_family, + size=size, + weight=weight, + slant=slant, + ) except Exception: pass # Fall back to system font - return font.Font(family='Arial', size=size, weight=weight) + return font.Font(family=FONT_FAMILY, size=size, weight=weight, slant=slant) def create_radial_glow_image(self, base_image): """Create a radial glow effect around the image with #ff8686 color""" @@ -212,28 +233,16 @@ class LoadingScreen: return ImageTk.PhotoImage(glow_bg) def load_background_image(self, width, height): - """Load and scale the Desolation background image to fit the given dimensions""" + """Load the Desolation art as the app backdrop.""" try: - bg_img = Image.open(get_resource_path("art/Desolation.png")) - # Scale to cover the entire area while maintaining aspect ratio - bg_ratio = bg_img.width / bg_img.height - target_ratio = width / height - - if bg_ratio > target_ratio: - # Image is wider, scale by height - new_height = height - new_width = int(height * bg_ratio) - else: - # Image is taller, scale by width - new_width = width - new_height = int(width / bg_ratio) - - bg_img = bg_img.resize((new_width, new_height), Image.Resampling.LANCZOS) - # Crop to exact size from center - left = (new_width - width) // 2 - top = (new_height - height) // 2 - bg_img = bg_img.crop((left, top, left + width, top + height)) - return ImageTk.PhotoImage(bg_img) + background = load_cover_background( + get_resource_path("art/Desolation.png"), + width, + height, + overlay_color=COLORS['bg_primary'], + overlay_alpha=130, + ) + return ImageTk.PhotoImage(background) except Exception as e: print(f"Error loading background image: {e}") return None @@ -327,7 +336,7 @@ class LoadingScreen: self.bg_canvas.create_image(0, 0, image=self.bg_image_tk, anchor='nw', tags='bg') else: print("Failed to load background image!") - self.bg_canvas.configure(bg='#2b2b2b') + self.bg_canvas.configure(bg=COLORS['bg_primary']) # Bind window state change events self.loading_window.bind('', self._on_window_map) @@ -345,8 +354,8 @@ class LoadingScreen: else: self.title_text_id = self.bg_canvas.create_text(width//2, height//2, text="Progression: Loader", - fill='white', - font=self.get_font(24, 'bold'), + fill=COLORS['text_highlight'], + font=self.get_font(24, 'bold', 'italic'), anchor='center', tags='title') # HR Systems logo (initially at bottom) @@ -357,8 +366,8 @@ class LoadingScreen: else: self.hr_logo_id = self.bg_canvas.create_text(width//2, int(height*0.95), text="Hudson Riggs Systems", - fill='#888888', - font=self.get_font(12), + fill=COLORS['text_muted'], + font=self.get_font(12, 'normal', 'italic'), anchor='center', tags='hr_logo') # Bind click events for HR logo @@ -374,26 +383,15 @@ class LoadingScreen: self.canvas_height = height def _load_and_scale_background(self, width, height): - """Load and scale the Desolation background image""" + """Load the Desolation art as the loading backdrop.""" try: - bg_img = Image.open(get_resource_path("art/Desolation.png")) - # Scale to cover the entire area while maintaining aspect ratio - bg_ratio = bg_img.width / bg_img.height - target_ratio = width / height - - if bg_ratio > target_ratio: - new_height = height - new_width = int(height * bg_ratio) - else: - new_width = width - new_height = int(width / bg_ratio) - - bg_img = bg_img.resize((new_width, new_height), Image.Resampling.LANCZOS) - # Crop to exact size from center - left = (new_width - width) // 2 - top = (new_height - height) // 2 - bg_img = bg_img.crop((left, top, left + width, top + height)) - return bg_img + return load_cover_background( + get_resource_path("art/Desolation.png"), + width, + height, + overlay_color=COLORS['bg_primary'], + overlay_alpha=135, + ) except Exception as e: print(f"Error loading background image: {e}") return None @@ -490,43 +488,55 @@ class LoadingScreen: if self.custom_font_available and self.custom_font_family: return ctk.CTkFont(family=self.custom_font_family, size=size, weight=weight if weight != 'normal' else 'normal') - return ctk.CTkFont(family='Arial', size=size, weight=weight if weight != 'normal' else 'normal') + return ctk.CTkFont(family=FONT_FAMILY, size=size, weight=weight if weight != 'normal' else 'normal') - def create_text_background(self, x, y, width, height, opacity=0.75, corner_radius=20): - """Create a semi-transparent rounded rectangle background for text - Width and height are automatically increased by 10%""" - # Increase size by 10% - width = int(width * 1.1) - height = int(height * 1.1) - - # Create a semi-transparent image with rounded corners - bg_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - draw = ImageDraw.Draw(bg_img) - - # Calculate alpha value (0-255) - default 75% opacity - alpha = int(255 * opacity) - fill_color = (43, 43, 43, alpha) # #2b2b2b with transparency - - # Draw rounded rectangle - draw.rounded_rectangle( - [(0, 0), (width - 1, height - 1)], - radius=corner_radius, - fill=fill_color - ) - - # Apply gaussian blur for soft edges - bg_img = bg_img.filter(ImageFilter.GaussianBlur(radius=3)) + def create_modern_card_background(self, x, y, width, height, card_type='info', corner_radius=12): + """Create a solid panel with a striped halo just outside its edges.""" + if card_type == 'error': + card_img = create_striped_panel( + width, + height, + panel_color=COLORS['bg_error'], + stripe_color=COLORS['stripe'], + stripe_width=18, + stripe_gap=18, + stripe_alpha=120, + halo_padding=26, + halo_alpha=150, + panel_alpha=238, + corner_radius=corner_radius, + border_color=COLORS['accent_red'], + border_width=2, + border_alpha=190, + ) + else: + card_img = create_striped_panel( + width, + height, + panel_color=COLORS['bg_card'], + stripe_color=COLORS['stripe_soft'], + stripe_width=18, + stripe_gap=18, + stripe_alpha=110, + halo_padding=22, + halo_alpha=135, + panel_alpha=238, + corner_radius=corner_radius, + border_color=COLORS['border_light'], + border_width=1, + border_alpha=170, + ) # Convert to PhotoImage - bg_tk = ImageTk.PhotoImage(bg_img) + card_tk = ImageTk.PhotoImage(card_img) # Store reference to prevent garbage collection - if not hasattr(self, '_text_bg_images'): - self._text_bg_images = [] - self._text_bg_images.append(bg_tk) + if not hasattr(self, '_modern_card_images'): + self._modern_card_images = [] + self._modern_card_images.append(card_tk) # Create canvas image - img_id = self.bg_canvas.create_image(x, y, image=bg_tk, anchor='center', tags='stage_content') + img_id = self.bg_canvas.create_image(x, y, image=card_tk, anchor='center', tags='stage_content') # Make sure it's behind text but above background self.bg_canvas.tag_lower(img_id, 'stage_content') @@ -660,8 +670,8 @@ class LoadingScreen: self.bg_canvas.delete('title') self.bg_canvas.create_text(window_width//2, int(current_y), text="Progression: Loader", - fill='white', - font=self.get_font(current_size, 'bold'), + fill=COLORS['text_highlight'], + font=self.get_font(current_size, 'bold', 'italic'), anchor='center', tags='title') self.loading_window.after(50, lambda: self.animate_text_step( @@ -692,8 +702,8 @@ class LoadingScreen: self.bg_canvas.delete('title') self.bg_canvas.create_text(window_width//2, int(window_height * 0.08), text="Progression: Loader", - fill='white', - font=self.get_font(16, 'bold'), + fill=COLORS['text_highlight'], + font=self.get_font(16, 'bold', 'italic'), anchor='center', tags='title') def stage_3_check_subscriptions(self): @@ -851,7 +861,7 @@ class LoadingScreen: return expansion_id def show_expansion_results(self): - """Show the results of expansion checking using canvas with proper backgrounds""" + """Show the results of expansion checking using new styled cards""" # Clear previous stage content self.bg_canvas.delete('stage_content') @@ -864,65 +874,106 @@ class LoadingScreen: missing_expansions = [name for name, owned in self.expansion_check_results.items() if not owned] if not missing_expansions: - # All expansions owned, proceed to RimWorld check - # Create background for success message - responsive sizing - success_bg_width = max(300, min(int(window_width * 0.5), 600)) - success_bg_height = max(80, min(int(window_height * 0.15), 150)) - self.create_text_background(window_width//2, int(window_height * 0.4), success_bg_width, success_bg_height) + # All expansions owned - use clean info card + card_width = max(400, min(int(window_width * 0.6), 700)) + card_height = max(150, min(int(window_height * 0.25), 200)) + self.create_modern_card_background(window_width//2, int(window_height * 0.4), + card_width, card_height, 'info') + + # Title in highlight color - bold italic + title_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=20, weight='bold', slant='italic') + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) - (card_height//2) + 40, + text="Expansion Check Complete", + fill=COLORS['text_highlight'], + font=title_font, + anchor='center', tags='stage_content') + + # Success message - bold + success_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=16, weight='bold') self.bg_canvas.create_text(window_width//2, int(window_height * 0.4), text="✓ All RimWorld expansions detected!", - fill='#00ff00', - font=self.get_font(16, 'bold'), + fill=COLORS['accent_green'], + font=success_font, + anchor='center', tags='stage_content') + + # Body text - regular + body_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=12) + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) + 40, + text="You can proceed with the complete Progression experience.", + fill=COLORS['text_body'], + font=body_font, anchor='center', tags='stage_content') self.loading_window.after(2000, self.stage_6_final_confirmation) else: - # Create background for error content - responsive sizing - error_bg_width = max(500, min(int(window_width * 0.7), 900)) - error_bg_height = max(300, min(int(window_height * 0.6), 600)) - self.create_text_background(window_width//2, int(window_height * 0.45), error_bg_width, error_bg_height) + # Missing expansions - use error card with diagonal lines + card_width = max(600, min(int(window_width * 0.8), 900)) + card_height = max(400, min(int(window_height * 0.7), 600)) - self.bg_canvas.create_text(window_width//2, int(window_height * 0.25), - text=f"You don't own {', '.join(missing_expansions)}!", - fill='#ff4444', - font=self.get_font(16, 'bold'), + self.create_modern_card_background(window_width//2, int(window_height * 0.45), + card_width, card_height, 'error') + + # Error title in highlight color - bold italic + error_title_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=24, weight='bold', slant='italic') + self.bg_canvas.create_text(window_width//2, int(window_height * 0.45) - (card_height//2) + 50, + text="ERROR", + fill=COLORS['text_highlight'], + font=error_title_font, anchor='center', tags='stage_content') - # Show expansion images side by side with individual backgrounds - img_y = int(window_height * 0.4) - img_spacing = max(120, min(160, int(window_width * 0.08))) # Responsive spacing + # Subtitle - bold + subtitle_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=16, weight='bold') + self.bg_canvas.create_text(window_width//2, int(window_height * 0.45) - (card_height//2) + 90, + text="Missing Required Expansions", + fill=COLORS['text_primary'], + font=subtitle_font, + anchor='center', tags='stage_content') + + # Body text explaining the issue - regular + body_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=12) + error_text = f"You don't own {', '.join(missing_expansions)}!\n\n" + error_text += "These expansions are required for the complete Progression experience. " + error_text += "Some mods may not function correctly without them. " + error_text += "Please purchase the missing expansions from Steam before continuing." + + self.bg_canvas.create_text(window_width//2, int(window_height * 0.45), + text=error_text, + fill=COLORS['text_body'], + font=body_font, + anchor='center', + width=card_width - 80, # Text wrapping + tags='stage_content') + + # Show expansion images side by side + img_y = int(window_height * 0.58) + img_spacing = max(120, min(160, int(window_width * 0.08))) total_width = len(missing_expansions) * img_spacing start_x = window_width//2 - (total_width - img_spacing) // 2 for i, expansion in enumerate(missing_expansions): img_x = start_x + i * img_spacing - # Create background for each expansion image - responsive size - img_bg_size = max(100, min(140, int(window_height * 0.15))) - self.create_text_background(img_x, img_y, img_bg_size, img_bg_size, opacity=0.6) - if expansion in self.expansion_images: self.bg_canvas.create_image(img_x, img_y, image=self.expansion_images[expansion], anchor='center', tags='stage_content') - # Cannot continue message - self.bg_canvas.create_text(window_width//2, int(window_height * 0.55), - text="Cannot continue!", - fill='#ff4444', - font=self.get_font(14, 'bold'), - anchor='center', tags='stage_content') - - # Exit button with transparent corners - self.create_canvas_button(window_width//2, int(window_height * 0.65), "Exit Application", - self.exit_application, '#ff4444', width=180) + # Exit button + self.create_canvas_button(window_width//2, int(window_height * 0.7), "Exit Application", + self.exit_application, COLORS['accent_red'], width=200) def show_subscription_stage(self): - """Show the subscription check stage using canvas""" + """Show the subscription check stage using new styled cards matching the design""" # Clear previous stage content and background images self.bg_canvas.delete('stage_content') - self._text_bg_images = [] # Clear old background images + self._modern_card_images = [] # Clear old background images # Track current stage for responsive updates self.current_stage_method = self.show_subscription_stage @@ -930,15 +981,36 @@ class LoadingScreen: window_width = self.canvas_width if hasattr(self, 'canvas_width') else self.loading_window.winfo_width() window_height = self.canvas_height if hasattr(self, 'canvas_height') else self.loading_window.winfo_height() - # Create semi-transparent background panel for content - responsive sizing - panel_width = max(400, min(int(window_width * 0.6), 800)) # 60% of width, min 400px, max 800px - panel_height = max(200, min(int(window_height * 0.35), 400)) # 35% of height, min 200px, max 400px - self.create_text_background(window_width//2, int(window_height * 0.35), panel_width, panel_height) + # Calculate card width based on content + 10% padding on each side + # Estimate text width for the longest line + temp_font = self.get_font(12) + max_text_width = max( + temp_font.measure("I have subscribed to Cosmetics Collection"), + temp_font.measure("Required for Complete Progression Experience"), + temp_font.measure("Steam Workshop Collections") + ) + card_width = int(max_text_width * 1.2) # 10% padding on each side + card_height = 320 # Increased height to accommodate title inside - self.bg_canvas.create_text(window_width//2, int(window_height * 0.18), + # Create modern card with fading edges + self.create_modern_card_background(window_width//2, int(window_height * 0.4), card_width, card_height, 'info') + + # Title INSIDE the card at the top (highlight color) - bold italic + title_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=18, weight='bold', slant='italic') + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) - (card_height//2) + 40, text="Steam Workshop Collections", - fill='white', - font=self.get_font(18, 'bold'), + fill=COLORS['text_highlight'], + font=title_font, + anchor='center', tags='stage_content') + + # Subtitle inside the card - regular + subtitle_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=12) + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) - (card_height//2) + 70, + text="Required for Complete Progression Experience", + fill=COLORS['text_body'], + font=subtitle_font, anchor='center', tags='stage_content') # Collection checkboxes @@ -950,24 +1022,26 @@ class LoadingScreen: self.subscription_vars = {} self.checkbox_ids = {} - y_pos = int(window_height * 0.28) + y_start = int(window_height * 0.4) - (card_height//2) + 110 # Start checkboxes inside card - for name, url in collections: + for i, (name, url) in enumerate(collections): var = tk.BooleanVar() self.subscription_vars[name] = var - # Create custom checkbox using canvas (no white background) - checkbox_x = window_width//2 - 300 + y_pos = y_start + (i * 35) + + # Create custom checkbox using canvas + checkbox_x = window_width//2 - (card_width//2) + 40 # Draw checkbox box box_id = self.bg_canvas.create_rectangle( - checkbox_x - 10, y_pos - 10, checkbox_x + 10, y_pos + 10, - outline='white', width=2, fill='', tags=('stage_content', f'checkbox_{name}') + checkbox_x - 8, y_pos - 8, checkbox_x + 8, y_pos + 8, + outline=COLORS['text_primary'], width=2, fill='', tags=('stage_content', f'checkbox_{name}') ) - # Create invisible clickable area around checkbox (larger for easier clicking) + # Create invisible clickable area around checkbox click_area = self.bg_canvas.create_rectangle( - checkbox_x - 20, y_pos - 20, checkbox_x + 20, y_pos + 20, + checkbox_x - 15, y_pos - 15, checkbox_x + 15, y_pos + 15, outline='', fill='', tags=('stage_content', f'checkbox_{name}') ) @@ -979,35 +1053,43 @@ class LoadingScreen: lambda e, n=name: self.toggle_checkbox(n)) # Create text label next to checkbox - text_x = checkbox_x + 25 + text_x = checkbox_x + 20 self.bg_canvas.create_text(text_x, y_pos, text="I have subscribed to ", - fill='white', + fill=COLORS['text_body'], font=self.get_font(12), anchor='w', tags='stage_content') # Calculate the width of the "I have subscribed to " text to position the link correctly - temp_font = self.get_font(12) text_width = temp_font.measure("I have subscribed to ") - # Create clickable link for collection name - positioned right after the text + # Create clickable link for collection name link_x = text_x + text_width link_id = self.bg_canvas.create_text(link_x, y_pos, text=name, - fill='#4da6ff', + fill=COLORS['accent_blue'], font=self.get_font(12, 'bold'), anchor='w', tags=('stage_content', f'link_{name}')) # Bind click and hover events to the text self.bg_canvas.tag_bind(f'link_{name}', '', lambda e, u=url: self.open_collection_url(u)) - self.bg_canvas.tag_bind(f'link_{name}', '', lambda e, lid=link_id: self.bg_canvas.itemconfig(lid, fill='#7ec8ff')) - self.bg_canvas.tag_bind(f'link_{name}', '', lambda e, lid=link_id: self.bg_canvas.itemconfig(lid, fill='#4da6ff')) - - y_pos += 40 + self.bg_canvas.tag_bind(f'link_{name}', '', lambda e, lid=link_id: self.bg_canvas.itemconfig(lid, fill=COLORS['text_highlight'])) + self.bg_canvas.tag_bind(f'link_{name}', '', lambda e, lid=link_id: self.bg_canvas.itemconfig(lid, fill=COLORS['accent_blue'])) - # Continue button - create custom button with transparent corners - self.create_canvas_button(window_width//2, y_pos + 40, "Continue", - self.check_all_subscribed, '#0078d4') + # Continue button - text only, no background, bold AND italic in success color + # Position it INSIDE the card at the bottom + continue_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=16, weight='bold', slant='italic') + continue_text_id = self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) + (card_height//2) - 30, + text="Continue", + fill=COLORS['accent_green'], + font=continue_font, + anchor='center', tags=('stage_content', 'continue_btn')) + + # Make continue button clickable + self.bg_canvas.tag_bind('continue_btn', '', lambda e: self.check_all_subscribed()) + self.bg_canvas.tag_bind('continue_btn', '', lambda e: self.bg_canvas.itemconfig(continue_text_id, fill=COLORS['text_highlight'])) + self.bg_canvas.tag_bind('continue_btn', '', lambda e: self.bg_canvas.itemconfig(continue_text_id, fill=COLORS['accent_green'])) # Ensure logo stays on top self._bring_logo_to_front() @@ -1018,16 +1100,18 @@ class LoadingScreen: var = info['var'] var.set(not var.get()) - checkbox_x = self.canvas_width//2 - 300 - y_pos = int(self.canvas_height * 0.28) + list(self.checkbox_ids.keys()).index(name) * 40 + # Get the checkbox position from the stored box coordinates + box_coords = self.bg_canvas.coords(info['box']) + checkbox_x = (box_coords[0] + box_coords[2]) / 2 # Center X of the box + checkbox_y = (box_coords[1] + box_coords[3]) / 2 # Center Y of the box if var.get(): - # Draw checkmark + # Draw checkmark INSIDE the checkbox if info['check'] is None: - check_id = self.bg_canvas.create_text(checkbox_x, y_pos, + check_id = self.bg_canvas.create_text(checkbox_x, checkbox_y, text="✓", - fill='#00ff00', - font=self.get_font(14, 'bold'), + fill=COLORS['accent_green'], + font=self.get_font(12, 'bold'), anchor='center', tags='stage_content') self.checkbox_ids[name]['check'] = check_id else: @@ -1037,46 +1121,74 @@ class LoadingScreen: self.checkbox_ids[name]['check'] = None def create_canvas_button(self, x, y, text, command, color, width=150, height=40): - """Create a button on canvas with transparent corners""" - # Create rounded rectangle button image - btn_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - draw = ImageDraw.Draw(btn_img) - - # Parse color - r = int(color[1:3], 16) - g = int(color[3:5], 16) - b = int(color[5:7], 16) - - # Draw rounded rectangle - draw.rounded_rectangle( - [(0, 0), (width - 1, height - 1)], - radius=10, - fill=(r, g, b, 255) - ) - - btn_tk = ImageTk.PhotoImage(btn_img) - - # Store reference - if not hasattr(self, '_button_images'): - self._button_images = [] - self._button_images.append(btn_tk) - - # Create button background - btn_id = self.bg_canvas.create_image(x, y, image=btn_tk, anchor='center', tags='stage_content') - - # Create safe tag name (remove special characters) + """Create a color-coded text action on the canvas.""" safe_tag = f'btn_{text.replace(" ", "_").replace("!", "").replace("?", "").replace(",", "")}' - - # Create button text - text_id = self.bg_canvas.create_text(x, y, text=text, fill='white', - font=self.get_font(12, 'bold'), - anchor='center', tags=('stage_content', safe_tag)) - - # Bind click event + + text_id = self.bg_canvas.create_text( + x, + y, + text=text, + fill=color, + font=self.get_font(14, 'bold', 'italic'), + anchor='center', + tags=('stage_content', safe_tag), + ) + + left, top, right, bottom = self.bg_canvas.bbox(text_id) + hit_area_id = self.bg_canvas.create_rectangle( + left - max(16, width // 6), + top - max(8, height // 5), + right + max(16, width // 6), + bottom + max(8, height // 5), + outline='', + fill='', + tags=('stage_content', safe_tag), + ) + self.bg_canvas.tag_lower(hit_area_id, text_id) + self.bg_canvas.tag_bind(safe_tag, '', lambda e: command()) - self.bg_canvas.tag_bind(btn_id, '', lambda e: command()) - - return btn_id + self.bg_canvas.tag_bind( + safe_tag, + '', + lambda e, item=text_id: self.bg_canvas.itemconfig(item, fill=COLORS['text_primary']), + ) + self.bg_canvas.tag_bind( + safe_tag, + '', + lambda e, item=text_id, button_color=color: self.bg_canvas.itemconfig(item, fill=button_color), + ) + + return text_id + + def create_text_background(self, x, y, width, height, opacity=0.82, corner_radius=20): + """Create a rounded dark panel with stripes extending just outside it.""" + alpha = max(0, min(255, int(255 * opacity))) + panel_image = create_striped_panel( + int(width * 1.05), + int(height * 1.05), + panel_color=COLORS['bg_card'], + stripe_color=COLORS['stripe_soft'], + stripe_width=18, + stripe_gap=18, + stripe_alpha=max(50, int(alpha * 0.3)), + halo_padding=22, + halo_alpha=max(90, int(alpha * 0.55)), + panel_alpha=max(220, alpha), + corner_radius=corner_radius, + border_color=COLORS['border_light'], + border_width=1, + border_alpha=180, + ) + + bg_tk = ImageTk.PhotoImage(panel_image) + + if not hasattr(self, '_text_bg_images'): + self._text_bg_images = [] + self._text_bg_images.append(bg_tk) + + img_id = self.bg_canvas.create_image(x, y, image=bg_tk, anchor='center', tags='stage_content') + self.bg_canvas.tag_lower(img_id, 'stage_content') + return img_id def check_all_subscribed(self): """Check if all collections are subscribed""" @@ -1108,7 +1220,7 @@ class LoadingScreen: # Create title as canvas text self.bg_canvas.create_text(window_width//2, int(window_height * 0.18), text="Checking RimWorld Expansions...", - fill='white', + fill=COLORS['text_highlight'], font=self.get_font(18, 'bold'), anchor='center', tags='stage_content') @@ -1117,10 +1229,9 @@ class LoadingScreen: def show_subscription_warning(self): """Show subscription warning in custom styled window""" - # Create warning window as CTkToplevel for transparent widgets warning_window = ctk.CTkToplevel(self.loading_window) warning_window.title("Progression: Loader - Subscription Required") - warning_window.configure(fg_color='#2b2b2b') + warning_window.configure(fg_color=COLORS['bg_card']) # Set window icon try: @@ -1142,7 +1253,7 @@ You must subscribe to all three collections before continuing: • Cosmetics Collection - Visual enhancements and aesthetics To subscribe to a collection: -1. Click on the collection name link (blue text) +1. Click on the collection name link 2. Click "Subscribe" on the Steam Workshop page 3. Wait for Steam to download the collection 4. Return here and check the subscription box @@ -1154,41 +1265,40 @@ All collections are required for the complete Progression experience.""" window_width = 650 window_height = 500 - # Set the calculated window size - warning_window.geometry(f"{window_width}x{window_height}") - - # Center the window on screen - warning_window.update_idletasks() - x = (warning_window.winfo_screenwidth() // 2) - (window_width // 2) - y = (warning_window.winfo_screenheight() // 2) - (window_height // 2) - warning_window.geometry(f"{window_width}x{window_height}+{x}+{y}") + center_window(warning_window, window_width, window_height, self.loading_window) # Title title_label = ctk.CTkLabel(warning_window, - text="⚠️ Subscription Required", - text_color='#ff8686', fg_color='transparent', - font=self.get_ctk_font(16, 'bold')) + text="Subscription Required", + text_color=COLORS['text_highlight'], + fg_color='transparent', + font=self.get_ctk_font(18, 'bold')) title_label.pack(pady=20) # Warning label with proper sizing warning_label = ctk.CTkLabel(warning_window, text=warning_text, - text_color='#cccccc', fg_color='transparent', + text_color=COLORS['text_body'], + fg_color='transparent', font=self.get_ctk_font(11), justify='left', wraplength=550) warning_label.pack(pady=20, padx=30, expand=True) - # Close button - close_btn = ctk.CTkButton(warning_window, - text="I Understand", - command=warning_window.destroy, - fg_color='#ff8686', text_color='white', - font=self.get_ctk_font(12, 'bold')) + close_btn = tk.Button( + warning_window, + text="Continue", + command=warning_window.destroy, + bg=COLORS['bg_card'], + font=self.get_font(13, 'bold', 'italic'), + padx=0, + pady=6, + ) + style_text_button(close_btn, COLORS['accent_green'], COLORS['bg_card']) close_btn.pack(pady=20) def stage_4_rimworld_check(self): - """Stage 4: Check RimWorld download and launch status using canvas""" + """Stage 4: Check RimWorld download and launch status using new styled cards""" # Clear previous stage content self.bg_canvas.delete('stage_content') @@ -1198,25 +1308,30 @@ All collections are required for the complete Progression experience.""" window_width = self.canvas_width if hasattr(self, 'canvas_width') else self.loading_window.winfo_width() window_height = self.canvas_height if hasattr(self, 'canvas_height') else self.loading_window.winfo_height() - # Create semi-transparent background panel for content - responsive sizing - panel_width = max(400, min(int(window_width * 0.6), 800)) # 60% of width, min 400px, max 800px - panel_height = max(250, min(int(window_height * 0.45), 500)) # 45% of height, min 250px, max 500px - self.create_text_background(window_width//2, int(window_height * 0.4), panel_width, panel_height) + # Create styled card for RimWorld setup info + card_width = max(500, min(int(window_width * 0.7), 800)) + card_height = max(350, min(int(window_height * 0.6), 550)) + self.create_modern_card_background(window_width//2, int(window_height * 0.4), card_width, card_height, 'info') - self.bg_canvas.create_text(window_width//2, int(window_height * 0.18), + # Title in highlight color - bold italic + title_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=20, weight='bold', slant='italic') + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) - (card_height//2) + 40, text="RimWorld Setup Verification", - fill='white', - font=self.get_font(18, 'bold'), + fill=COLORS['text_highlight'], + font=title_font, anchor='center', tags='stage_content') - # Add instructions - self.bg_canvas.create_text(window_width//2, int(window_height * 0.25), + # Subtitle - bold + subtitle_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=14, weight='bold') + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) - (card_height//2) + 80, text="Please ensure the following before continuing:", - fill='#cccccc', - font=self.get_font(14), + fill=COLORS['text_primary'], + font=subtitle_font, anchor='center', tags='stage_content') - # Checklist items + # Checklist items - bold for checkmarks checklist_items = [ "✓ RimWorld has finished downloading completely", "✓ You have launched RimWorld at least once", @@ -1224,29 +1339,44 @@ All collections are required for the complete Progression experience.""" "✓ All required DLCs are installed and activated" ] - y_pos = int(window_height * 0.32) + checklist_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=12, weight='bold') + y_pos = int(window_height * 0.4) - (card_height//2) + 120 for item in checklist_items: self.bg_canvas.create_text(window_width//2, y_pos, text=item, - fill='#00ff00', - font=self.get_font(12), + fill=COLORS['accent_green'], + font=checklist_font, anchor='center', tags='stage_content') y_pos += 30 - # Add confirmation message + # Confirmation message - bold italic + confirm_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=14, weight='bold', slant='italic') self.bg_canvas.create_text(window_width//2, y_pos + 30, text="Have you completed all the above requirements?", - fill='white', - font=self.get_font(14, 'bold'), + fill=COLORS['text_body'], + font=confirm_font, anchor='center', tags='stage_content') - # Yes Mother button - canvas button with transparent corners - self.create_canvas_button(window_width//2 - 80, y_pos + 80, "Yes Mother", - self.proceed_to_dlc_check, '#00aa00') - - # Not Yet button - canvas button with transparent corners - self.create_canvas_button(window_width//2 + 80, y_pos + 80, "Not Yet", - self.show_setup_instructions, '#aa6600') + # Only show one action pair here: proceed or read the setup help. + button_y = y_pos + 80 + self.create_canvas_button( + window_width//2 - 90, + button_y, + "Continue", + self.proceed_to_dlc_check, + COLORS['accent_green'], + width=140, + ) + self.create_canvas_button( + window_width//2 + 90, + button_y, + "Not Yet", + self.show_setup_instructions, + COLORS['accent_red'], + width=140, + ) # Ensure logo stays on top self._bring_logo_to_front() @@ -1261,10 +1391,9 @@ All collections are required for the complete Progression experience.""" def show_setup_instructions(self): """Show setup instructions if user is not ready""" - # Create instruction window as CTkToplevel for transparent widgets instruction_window = ctk.CTkToplevel(self.loading_window) instruction_window.title("Progression: Loader - Setup Instructions") - instruction_window.configure(fg_color='#2b2b2b') + instruction_window.configure(fg_color=COLORS['bg_card']) # Set window icon try: @@ -1298,47 +1427,46 @@ All collections are required for the complete Progression experience.""" • Check that all DLCs appear in the main menu • Ensure no error messages appear -Once completed, return to this screen and click "Yes Mother".""" +Once completed, return to this screen and click "Continue".""" # Calculate window size window_width = 650 window_height = 550 - # Set the calculated window size - instruction_window.geometry(f"{window_width}x{window_height}") - - # Center the window on screen - instruction_window.update_idletasks() - x = (instruction_window.winfo_screenwidth() // 2) - (window_width // 2) - y = (instruction_window.winfo_screenheight() // 2) - (window_height // 2) - instruction_window.geometry(f"{window_width}x{window_height}+{x}+{y}") + center_window(instruction_window, window_width, window_height, self.loading_window) # Title title_label = ctk.CTkLabel(instruction_window, text="Progression: Loader - Setup Instructions", - text_color='white', fg_color='transparent', + text_color=COLORS['text_highlight'], + fg_color='transparent', font=self.get_ctk_font(16, 'bold')) title_label.pack(pady=20) # Instructions label with proper sizing instructions_label = ctk.CTkLabel(instruction_window, text=instructions_text, - text_color='#cccccc', fg_color='transparent', + text_color=COLORS['text_body'], + fg_color='transparent', font=self.get_ctk_font(11), justify='left', wraplength=550) instructions_label.pack(pady=20, padx=30, expand=True) - # Close button - close_btn = ctk.CTkButton(instruction_window, - text="I Understand", - command=instruction_window.destroy, - fg_color='#0078d4', text_color='white', - font=self.get_ctk_font(12, 'bold')) + close_btn = tk.Button( + instruction_window, + text="Back", + command=instruction_window.destroy, + bg=COLORS['bg_card'], + font=self.get_font(13, 'bold', 'italic'), + padx=0, + pady=6, + ) + style_text_button(close_btn, COLORS['accent_red'], COLORS['bg_card']) close_btn.pack(pady=20) - def stage_6_final_confirmation(self): - """Stage 6: Final confirmation screen using canvas""" + def _legacy_stage_6_final_confirmation(self): + """Deprecated earlier version kept only for reference during cleanup.""" # Clear previous stage content self.bg_canvas.delete('stage_content') self._text_bg_images = [] # Clear old background images @@ -1358,24 +1486,24 @@ Once completed, return to this screen and click "Yes Mother".""" # Final message self.bg_canvas.create_text(window_width//2, int(window_height * 0.25), text="Ready to Create Mod List", - fill='white', - font=self.get_font(18, 'bold'), + fill=COLORS['text_highlight'], + font=self.get_font(18, 'bold', 'italic'), anchor='center', tags='stage_content') self.bg_canvas.create_text(window_width//2, int(window_height * 0.38), text="This tool creates a Mods list in your RimWorld folder\nwith the latest from the Steam Workshop collections for Progression.\n\nWould you like to continue?", - fill='#cccccc', + fill=COLORS['text_body'], font=self.get_font(14), justify='center', anchor='center', tags='stage_content') # Continue button with transparent corners self.create_canvas_button(window_width//2 - 80, int(window_height * 0.55), "Yes, Continue", - self.complete_loading, '#00aa00', width=150) + self.complete_loading, COLORS['accent_green'], width=150) # Exit button with transparent corners self.create_canvas_button(window_width//2 + 80, int(window_height * 0.55), "Exit", - self.exit_application, '#aa0000', width=100) + self.exit_application, COLORS['accent_red'], width=100) # Ensure logo stays on top self._bring_logo_to_front() @@ -1415,6 +1543,64 @@ Once completed, return to this screen and click "Yes Mother".""" self.loading_window.destroy() self.on_complete_callback() + def stage_6_final_confirmation(self): + """Final confirmation stage before proceeding to main application""" + # Clear previous stage content + self.bg_canvas.delete('stage_content') + + window_width = self.canvas_width if hasattr(self, 'canvas_width') else self.loading_window.winfo_width() + window_height = self.canvas_height if hasattr(self, 'canvas_height') else self.loading_window.winfo_height() + + # Create styled card for final confirmation + card_width = max(500, min(int(window_width * 0.7), 800)) + card_height = max(250, min(int(window_height * 0.4), 400)) + self.create_modern_card_background(window_width//2, int(window_height * 0.4), card_width, card_height, 'info') + + # Title in highlight color - bold italic + title_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=20, weight='bold', slant='italic') + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) - (card_height//2) + 60, + text="Ready to Proceed", + fill=COLORS['text_highlight'], + font=title_font, + anchor='center', tags='stage_content') + + # Body text - bold + body_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=14, weight='bold') + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) - 20, + text="All requirements have been verified.", + fill=COLORS['text_primary'], + font=body_font, + anchor='center', tags='stage_content') + + # Secondary text - regular + secondary_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=12) + self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) + 10, + text="You can now proceed with the Progression Loader.", + fill=COLORS['text_body'], + font=secondary_font, + anchor='center', tags='stage_content') + + # Continue button - bold italic + continue_font = font.Font(family=self.custom_font_family if self.custom_font_available else FONT_FAMILY, + size=16, weight='bold', slant='italic') + continue_text_id = self.bg_canvas.create_text(window_width//2, int(window_height * 0.4) + (card_height//2) - 40, + text="Launch Main Application", + fill=COLORS['accent_green'], + font=continue_font, + anchor='center', tags=('stage_content', 'launch_btn')) + + # Make launch button clickable + self.bg_canvas.tag_bind('launch_btn', '', lambda e: self.launch_main_application()) + self.bg_canvas.tag_bind('launch_btn', '', lambda e: self.bg_canvas.itemconfig(continue_text_id, fill=COLORS['text_highlight'])) + self.bg_canvas.tag_bind('launch_btn', '', lambda e: self.bg_canvas.itemconfig(continue_text_id, fill=COLORS['accent_green'])) + + def launch_main_application(self): + """Launch the main application""" + self.complete_loading() + def exit_application(self): """Exit the application""" self.root.quit() @@ -1428,7 +1614,7 @@ class SteamWorkshopGUI: self.root = root self.root.title("Progression: Loader") self.root.state('zoomed') # Make fullscreen on Windows - self.root.configure(bg='#2b2b2b') + self.root.configure(bg=COLORS['bg_primary']) # Set minimum window size for responsive design and enforce it always self.root.minsize(800, 600) @@ -1452,6 +1638,7 @@ class SteamWorkshopGUI: self.load_btn = None self.merge_btn = None self.is_rimworld_valid = False + self._panel_shells = [] # Store references for responsive layout self.left_frame = None @@ -1459,8 +1646,8 @@ class SteamWorkshopGUI: self.content_frame = None self.padded_frame = None - # Create canvas for background image - self.bg_canvas = tk.Canvas(root, highlightthickness=0, bg='#2b2b2b') + # Create canvas for background image with Steam styling + self.bg_canvas = tk.Canvas(root, highlightthickness=0, bg=COLORS['bg_primary']) self.bg_canvas.pack(fill=tk.BOTH, expand=True) # Load background image @@ -1470,19 +1657,19 @@ class SteamWorkshopGUI: # Bind resize event to update background and layout self.root.bind('', self._on_main_resize) - # Create main frame as canvas window - self.main_content_frame = tk.Frame(self.bg_canvas, bg='#2b2b2b') + # Create main frame as canvas window with Steam styling + self.main_content_frame = tk.Frame(self.bg_canvas, bg=COLORS['bg_primary']) - # Add padding frame with responsive padding - self.padded_frame = tk.Frame(self.main_content_frame, bg='#2b2b2b') - self.padded_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + # Add padding frame with responsive padding and Steam styling + self.padded_frame = tk.Frame(self.main_content_frame, bg=COLORS['bg_primary']) + self.padded_frame.pack(fill=tk.BOTH, expand=True, padx=18, pady=18) # Create header frame for progression logo self.create_header_frame(self.padded_frame) - # Create content frame (below header) with responsive layout - self.content_frame = tk.Frame(self.padded_frame, bg='#2b2b2b') - self.content_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0)) + # Create content frame (below header) with responsive layout and Steam styling + self.content_frame = tk.Frame(self.padded_frame, bg=COLORS['bg_primary']) + self.content_frame.pack(fill=tk.BOTH, expand=True, pady=(12, 0)) # Create responsive layout self._create_responsive_layout() @@ -1531,16 +1718,18 @@ class SteamWorkshopGUI: # Get current window dimensions window_width = self.root.winfo_width() window_height = self.root.winfo_height() - - # Determine layout based on window size and aspect ratio - aspect_ratio = window_width / window_height if window_height > 0 else 1.0 - - # For very wide screens (aspect ratio > 2.0) or large windows, use side-by-side layout - # For narrow screens or small windows, use stacked layout - if aspect_ratio > 1.5 and window_width > 1200: + + if self._should_use_side_by_side(window_width, window_height): + self._current_layout_mode = 'side_by_side' self._create_side_by_side_layout() else: + self._current_layout_mode = 'stacked' self._create_stacked_layout() + + def _should_use_side_by_side(self, window_width, window_height): + """Return True when the window can comfortably support the two-column layout.""" + aspect_ratio = window_width / window_height if window_height > 0 else 1.0 + return aspect_ratio > 1.5 and window_width > 1400 and window_height > 1220 def _create_side_by_side_layout(self): """Create side-by-side layout for wide screens""" @@ -1551,15 +1740,28 @@ class SteamWorkshopGUI: # Left panel for inputs - responsive width window_width = self.root.winfo_width() left_width = max(350, min(500, int(window_width * 0.35))) # 35% of width, min 350px, max 500px - - self.left_frame = tk.Frame(self.content_frame, bg='#2b2b2b', width=left_width) - self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) - self.left_frame.pack_propagate(False) - + + left_shell, self.left_frame = self.create_panel_shell( + self.content_frame, + panel_color=COLORS['bg_card'], + inner_padding=(14, 14), + halo_padding=18, + corner_radius=24, + ) + left_shell.configure(width=left_width) + left_shell.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 18)) + left_shell.pack_propagate(False) + # Right panel for output - responsive padding for HR logo hr_padding = max(80, min(150, int(window_width * 0.08))) # Responsive HR logo space - self.right_frame = tk.Frame(self.content_frame, bg='#2b2b2b') - self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(0, hr_padding)) + right_shell, self.right_frame = self.create_panel_shell( + self.content_frame, + panel_color=COLORS['bg_card'], + inner_padding=(16, 14), + halo_padding=18, + corner_radius=24, + ) + right_shell.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(0, hr_padding)) def _create_stacked_layout(self): """Create stacked layout for narrow screens""" @@ -1570,14 +1772,27 @@ class SteamWorkshopGUI: # Top panel for inputs - responsive height window_height = self.root.winfo_height() input_height = max(300, min(500, int(window_height * 0.45))) # 45% of height, min 300px, max 500px - - self.left_frame = tk.Frame(self.content_frame, bg='#2b2b2b', height=input_height) - self.left_frame.pack(side=tk.TOP, fill=tk.X, pady=(0, 10)) - self.left_frame.pack_propagate(False) - + + left_shell, self.left_frame = self.create_panel_shell( + self.content_frame, + panel_color=COLORS['bg_card'], + inner_padding=(14, 14), + halo_padding=18, + corner_radius=24, + ) + left_shell.configure(height=input_height) + left_shell.pack(side=tk.TOP, fill=tk.X, pady=(0, 18)) + left_shell.pack_propagate(False) + # Bottom panel for output - self.right_frame = tk.Frame(self.content_frame, bg='#2b2b2b') - self.right_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) + right_shell, self.right_frame = self.create_panel_shell( + self.content_frame, + panel_color=COLORS['bg_card'], + inner_padding=(16, 14), + halo_padding=18, + corner_radius=24, + ) + right_shell.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True) def _update_responsive_layout(self): """Update layout based on current window size""" @@ -1610,38 +1825,34 @@ class SteamWorkshopGUI: """Position the main content frame on the canvas with responsive sizing""" width = self.root.winfo_width() or 1920 height = self.root.winfo_height() or 1080 - - # Use responsive padding - padding_x = max(10, int(width * 0.01)) # 1% of width, minimum 10px - padding_y = max(10, int(height * 0.01)) # 1% of height, minimum 10px - + + content_width = max(760, min(width - 28, int(width * 0.95))) + content_height = max(560, min(height - 24, int(height * 0.96))) + x_pos = max(12, (width - content_width) // 2) + y_pos = max(12, (height - content_height) // 2) + self.bg_canvas.delete('main_content') - self.bg_canvas.create_window(padding_x, padding_y, window=self.main_content_frame, anchor='nw', - width=width - (2 * padding_x), height=height - (2 * padding_y), tags='main_content') + self.bg_canvas.create_window( + x_pos, + y_pos, + window=self.main_content_frame, + anchor='nw', + width=content_width, + height=content_height, + tags='main_content', + ) def load_background_image(self, width, height): - """Load and scale the Desolation background image to fit the given dimensions""" + """Load the Desolation art as the main-app backdrop.""" try: - bg_img = Image.open(get_resource_path("art/Desolation.png")) - # Scale to cover the entire area while maintaining aspect ratio - bg_ratio = bg_img.width / bg_img.height - target_ratio = width / height - - if bg_ratio > target_ratio: - # Image is wider, scale by height - new_height = height - new_width = int(height * bg_ratio) - else: - # Image is taller, scale by width - new_width = width - new_height = int(width / bg_ratio) - - bg_img = bg_img.resize((new_width, new_height), Image.Resampling.LANCZOS) - # Crop to exact size from center - left = (new_width - width) // 2 - top = (new_height - height) // 2 - bg_img = bg_img.crop((left, top, left + width, top + height)) - return ImageTk.PhotoImage(bg_img) + background = load_cover_background( + get_resource_path("art/Desolation.png"), + width, + height, + overlay_color=COLORS['bg_primary'], + overlay_alpha=130, + ) + return ImageTk.PhotoImage(background) except Exception as e: print(f"Error loading background image: {e}") return None @@ -1655,23 +1866,25 @@ class SteamWorkshopGUI: # Update background image self._update_main_background() - # Update responsive layout if window size changed significantly - if hasattr(self, '_last_window_size'): - last_width, last_height = self._last_window_size - current_width = self.root.winfo_width() - current_height = self.root.winfo_height() - - # Check if aspect ratio changed significantly (threshold of 0.3) - if current_width > 0 and current_height > 0 and last_width > 0 and last_height > 0: - last_ratio = last_width / last_height - current_ratio = current_width / current_height - - if abs(current_ratio - last_ratio) > 0.3: - # Significant aspect ratio change - update layout - self.root.after(100, self._update_responsive_layout) # Delay to avoid rapid updates + current_width = self.root.winfo_width() + current_height = self.root.winfo_height() + + if hasattr(self, 'header_canvas') and self.header_canvas.winfo_exists(): + header_height = max(70, min(102, int(current_height * 0.082))) + if int(self.header_canvas.cget('height')) != header_height: + self.header_canvas.configure(height=header_height) + self._render_header_canvas() + + desired_layout_mode = ( + 'side_by_side' + if self._should_use_side_by_side(current_width, current_height) + else 'stacked' + ) + if desired_layout_mode != getattr(self, '_current_layout_mode', None): + self.root.after(100, self._update_responsive_layout) # Store current window size - self._last_window_size = (self.root.winfo_width(), self.root.winfo_height()) + self._last_window_size = (current_width, current_height) # Update content positioning self._position_main_content() @@ -1708,54 +1921,120 @@ class SteamWorkshopGUI: self.root.after(200, self._update_responsive_layout) def create_header_frame(self, parent): - """Create header frame with progression logo - responsive sizing""" - # Responsive header height based on window size + """Create a striped header bar with a centered, scaled progression logo.""" window_height = self.root.winfo_height() or 1080 - header_height = max(80, min(120, int(window_height * 0.1))) # 10% of height, min 80px, max 120px - - header_frame = tk.Frame(parent, bg='#2b2b2b', height=header_height) - header_frame.pack(fill='x', pady=(0, 10)) - header_frame.pack_propagate(False) - - # Create custom frame with border for progression logo - logo_container = tk.Frame(header_frame, bg='#404040', relief='raised', bd=2) - logo_container.pack(expand=True) - - # Load and display progression logo (using game title as progression logo) + header_height = max(70, min(102, int(window_height * 0.082))) + self.header_canvas = tk.Canvas( + parent, + bg=COLORS['bg_primary'], + height=header_height, + highlightthickness=0, + bd=0, + relief='flat', + ) + self.header_canvas.pack(fill='x', pady=(0, 12)) + try: - # Load the game title image as progression logo - progression_img = Image.open(get_resource_path("art/GameTitle.png")) - # Responsive logo sizing - img_width, img_height = progression_img.size - logo_height = max(60, min(100, int(header_height * 0.7))) # 70% of header height - logo_width = int((logo_height / img_height) * img_width) - progression_img = progression_img.resize((logo_width, logo_height), Image.Resampling.LANCZOS) - self.progression_logo_tk = ImageTk.PhotoImage(progression_img) - - progression_label = tk.Label(logo_container, - image=self.progression_logo_tk, - bg='#404040') - progression_label.pack(padx=10, pady=10) - - except Exception as e: - # Fallback text if image loading fails - responsive font size - font_size = max(16, min(24, int(header_height * 0.25))) - progression_label = tk.Label(logo_container, - text="PROGRESSION PACK", - fg='white', bg='#404040', - font=self.get_font(font_size, 'bold')) - progression_label.pack(padx=20, pady=30) + self.progression_logo_source = Image.open( + get_resource_path("art/GameTitle.png") + ).convert("RGBA") + except Exception: + self.progression_logo_source = None + + self.header_canvas.bind('', self._on_header_canvas_configure) + self.root.after_idle(self._render_header_canvas) + + def _on_header_canvas_configure(self, event): + """Refresh the header texture and logo when the header size changes.""" + self._render_header_canvas() + + def _render_header_canvas(self): + """Render the striped header background and scale the logo to fit it.""" + if not hasattr(self, 'header_canvas') or not self.header_canvas.winfo_exists(): + return + + width = max(1, self.header_canvas.winfo_width()) + height = max(1, self.header_canvas.winfo_height()) + if width < 40 or height < 24: + return + + header_texture = create_striped_texture( + width, + height, + base_color=COLORS['bg_card'], + stripe_color=COLORS['stripe_soft'], + stripe_width=18, + stripe_gap=18, + stripe_alpha=118, + corner_radius=22, + border_color=COLORS['border_light'], + border_width=1, + border_alpha=185, + ) + self.header_bg_tk = ImageTk.PhotoImage(header_texture) + + if hasattr(self, 'header_bg_id'): + self.header_canvas.itemconfigure(self.header_bg_id, image=self.header_bg_tk) + else: + self.header_bg_id = self.header_canvas.create_image( + 0, + 0, + image=self.header_bg_tk, + anchor='nw', + ) + + self.header_canvas.delete('header_content') + + if self.progression_logo_source is not None: + available_width = max(120, width - 72) + available_height = max(24, height - 18) + scale = min( + available_width / self.progression_logo_source.width, + available_height / self.progression_logo_source.height, + ) + logo_width = max(1, int(self.progression_logo_source.width * scale)) + logo_height = max(1, int(self.progression_logo_source.height * scale)) + logo_image = self.progression_logo_source.resize( + (logo_width, logo_height), + Image.Resampling.LANCZOS, + ) + self.progression_logo_tk = ImageTk.PhotoImage(logo_image) + self.header_canvas.create_image( + width // 2, + height // 2, + image=self.progression_logo_tk, + anchor='center', + tags='header_content', + ) + else: + font_size = max(16, min(24, int(height * 0.32))) + self.header_canvas.create_text( + width // 2, + height // 2, + text="PROGRESSION PACK", + fill=COLORS['text_highlight'], + font=self.get_font(font_size, 'bold', 'italic'), + anchor='center', + tags='header_content', + ) def create_footer_frame(self, parent): """Create footer frame with HR Systems logo in bottom right - responsive sizing""" # Responsive footer height window_height = self.root.winfo_height() or 1080 - footer_height = max(80, min(120, int(window_height * 0.08))) # 8% of height, min 80px, max 120px - - self.footer_frame = tk.Frame(parent, bg='#2b2b2b', height=footer_height) - self.footer_frame.pack(fill='x', side=tk.BOTTOM, pady=(10, 0)) - self.footer_frame.pack_propagate(False) - + footer_height = max(62, min(82, int(window_height * 0.064))) + + footer_shell, self.footer_frame = self.create_panel_shell( + parent, + panel_color=COLORS['bg_card'], + inner_padding=(16, 8), + halo_padding=16, + corner_radius=22, + ) + footer_shell.configure(height=footer_height) + footer_shell.pack(fill='x', side=tk.BOTTOM, pady=(12, 0)) + footer_shell.pack_propagate(False) + # Create clickable HR Systems logo in bottom right self.create_hr_logo(self.footer_frame) @@ -1768,9 +2047,11 @@ class SteamWorkshopGUI: # Responsive padding based on window size padding_x = max(20, min(60, int(window_width * 0.03))) # 3% of width, min 20px, max 60px padding_y = max(15, min(30, int(window_height * 0.02))) # 2% of height, min 15px, max 30px - + + parent_bg = parent.cget('bg') + # Container for logo positioned in bottom right with extra space for glow - logo_frame = tk.Frame(parent, bg='#2b2b2b') + logo_frame = tk.Frame(parent, bg=parent_bg) logo_frame.pack(side=tk.RIGHT, anchor='se', padx=padding_x, pady=padding_y) try: @@ -1794,7 +2075,7 @@ class SteamWorkshopGUI: self.hr_logo_label = tk.Label(logo_frame, image=self.hr_logo_normal, - bg='#2b2b2b', + bg=parent_bg, cursor='hand2', width=label_width, height=label_height, @@ -1811,8 +2092,9 @@ class SteamWorkshopGUI: font_size = max(8, min(12, int(window_height * 0.012))) # Responsive font size self.hr_logo_label = tk.Label(logo_frame, text="Hudson Riggs Systems", - fg='#888888', bg='#2b2b2b', - font=self.get_font(font_size), + fg=COLORS['text_muted'], + bg=parent_bg, + font=self.get_font(font_size, 'normal', 'italic'), cursor='hand2') self.hr_logo_label.pack() @@ -1900,35 +2182,62 @@ class SteamWorkshopGUI: def on_hr_text_enter(self, event=None): """Handle mouse enter on HR text (glow effect)""" - self.hr_logo_label.configure(fg='#ff8686', bg='#404040') - + self.hr_logo_label.configure( + fg=COLORS['text_highlight'], + bg=self.hr_logo_label.master.cget('bg'), + ) + def on_hr_text_leave(self, event=None): """Handle mouse leave on HR text (remove glow effect)""" - self.hr_logo_label.configure(fg='#888888', bg='#2b2b2b') + self.hr_logo_label.configure( + fg=COLORS['text_muted'], + bg=self.hr_logo_label.master.cget('bg'), + ) def load_custom_font(self): - """Load the RimWorld font using Windows AddFontResourceEx with private flag""" + """Load all Georgia font variants using Windows AddFontResourceEx with private flag""" + self.custom_font_available = False + self.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: - font_path = get_resource_path(os.path.join("art", "georgia.ttf")) - if os.path.exists(font_path): - abs_font_path = os.path.abspath(font_path) - - # Use the Stack Overflow method with AddFontResourceEx - success = self._load_font_private(abs_font_path) - - if success: - # Use the actual font family name from the TTF file - self.custom_font_family = "Georgia" - self.custom_font_available = True - return + 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 = self._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: - self.custom_font_available = False - + print(f"Font file not found: {font_file}") + + # If at least one font was loaded successfully + if fonts_loaded > 0: + self.custom_font_family = "Georgia" + self.custom_font_available = True + print(f"Georgia font family loaded successfully ({fonts_loaded}/{len(georgia_fonts)} variants)") + return else: + print("No Georgia fonts could be loaded") self.custom_font_available = False except Exception as e: + print(f"Error loading Georgia fonts: {e}") self.custom_font_available = False # Initialize fallback values if font loading failed @@ -1966,16 +2275,28 @@ class SteamWorkshopGUI: except Exception: return False - def get_font(self, size=10, weight='normal'): + def get_font(self, size=10, weight='normal', slant='roman'): """Get font tuple for UI elements""" + if weight in {'italic', 'roman'}: + slant = weight + weight = 'normal' + elif weight in {'bold italic', 'italic bold'}: + weight = 'bold' + slant = 'italic' + if self.custom_font_available and self.custom_font_family: try: - return font.Font(family=self.custom_font_family, size=size, weight=weight) + return font.Font( + family=self.custom_font_family, + size=size, + weight=weight, + slant=slant, + ) except Exception: pass # Fall back to system font - return font.Font(family='Arial', size=size, weight=weight) + return font.Font(family=FONT_FAMILY, size=size, weight=weight, slant=slant) def get_ctk_font(self, size=10, weight='normal'): """Get CTkFont for UI elements""" @@ -1986,134 +2307,345 @@ class SteamWorkshopGUI: pass # Fall back to system font - return ctk.CTkFont(family='Arial', size=size, weight=weight) + return ctk.CTkFont(family=FONT_FAMILY, size=size, weight=weight) def setup_dark_theme(self): - """Configure dark theme colors""" + """Configure the shared charcoal theme for ttk widgets.""" style = ttk.Style() style.theme_use('clam') - # Configure colors for dark theme - style.configure('TLabel', background='#2b2b2b', foreground='#ffffff') - style.configure('TButton', background='#404040', foreground='#ffffff') - style.map('TButton', background=[('active', '#505050')]) - style.configure('TEntry', background='#404040', foreground='#ffffff', fieldbackground='#404040') + style.configure('TLabel', + background=COLORS['bg_primary'], + foreground=COLORS['text_primary']) + style.configure('TButton', + background=COLORS['bg_card'], + foreground=COLORS['text_primary'], + borderwidth=0, + focuscolor='none') + style.map('TButton', + background=[('active', COLORS['bg_hover']), + ('pressed', COLORS['bg_tertiary'])]) + style.configure('TEntry', + background=COLORS['bg_card'], + foreground=COLORS['text_primary'], + fieldbackground=COLORS['bg_card'], + borderwidth=1, + insertcolor=COLORS['text_primary']) + style.map('TEntry', + focuscolor=[('focus', COLORS['accent_blue'])]) + + def style_entry_widget(self, widget, *, muted=False, readonly=False): + """Apply the shared dark field styling to tk.Entry widgets.""" + widget.configure( + bg=COLORS['bg_tertiary'], + fg=COLORS['text_muted'] if muted else COLORS['text_primary'], + insertbackground=COLORS['text_primary'], + relief='flat', + bd=0, + highlightthickness=1, + highlightcolor=COLORS['accent_yellow'], + highlightbackground=COLORS['border_light'], + disabledbackground=COLORS['bg_tertiary'], + disabledforeground=COLORS['text_muted'], + selectbackground=COLORS['bg_hover'], + selectforeground=COLORS['text_primary'], + ) + if readonly: + widget.configure(readonlybackground=COLORS['bg_tertiary']) + + def style_action_button(self, button, accent_color): + """Apply the shared text-only action style to tk buttons.""" + style_text_button( + button, + accent_color, + button.cget('bg'), + hover_foreground=COLORS['text_primary'], + disabled_foreground=COLORS['text_muted'], + ) + + def _normalize_padding(self, padding): + """Normalize scalar or tuple padding values into x/y padding.""" + if isinstance(padding, (tuple, list)): + if len(padding) == 2: + return int(padding[0]), int(padding[1]) + if len(padding) == 1: + return int(padding[0]), int(padding[0]) + return int(padding), int(padding) + + def create_panel_shell( + self, + parent, + *, + panel_color=None, + inner_padding=18, + halo_padding=22, + corner_radius=24, + ): + """Create a striped halo shell with an inner content frame.""" + shell = tk.Frame(parent, bg=COLORS['bg_primary'], bd=0, highlightthickness=0) + canvas = tk.Canvas( + shell, + bg=COLORS['bg_primary'], + highlightthickness=0, + bd=0, + relief='flat', + ) + canvas.pack(fill='both', expand=True) + + pad_x, pad_y = self._normalize_padding(inner_padding) + inner_frame = tk.Frame(canvas, bg=panel_color or COLORS['bg_card'], bd=0, highlightthickness=0) + window_id = canvas.create_window(0, 0, window=inner_frame, anchor='nw') + + canvas._panel_options = { + 'panel_color': panel_color or COLORS['bg_card'], + 'halo_padding': int(halo_padding), + 'corner_radius': int(corner_radius), + 'inner_padding': (pad_x, pad_y), + } + canvas._panel_window_id = window_id + canvas.bind('', self._refresh_panel_shell) + self._panel_shells.append(canvas) + self.root.after_idle(lambda current_canvas=canvas: self._render_panel_shell(current_canvas)) + return shell, inner_frame + + def _refresh_panel_shell(self, event): + """Repaint a striped shell when its canvas changes size.""" + self._render_panel_shell(event.widget) + + def _render_panel_shell(self, canvas): + """Render the shared striped halo panel background onto a shell canvas.""" + if not canvas.winfo_exists() or not hasattr(canvas, '_panel_options'): + return + + width = max(1, canvas.winfo_width()) + height = max(1, canvas.winfo_height()) + options = canvas._panel_options + halo_padding = options['halo_padding'] + inner_pad_x, inner_pad_y = options['inner_padding'] + + minimum_size = (halo_padding * 2) + 6 + if width < minimum_size or height < minimum_size: + return + + panel_width = max(1, width - (halo_padding * 2)) + panel_height = max(1, height - (halo_padding * 2)) + panel_image = create_striped_panel( + panel_width, + panel_height, + panel_color=options['panel_color'], + stripe_color=COLORS['stripe_soft'], + stripe_width=18, + stripe_gap=18, + stripe_alpha=78, + halo_padding=halo_padding, + halo_alpha=120, + panel_alpha=238, + corner_radius=options['corner_radius'], + border_color=COLORS['border_light'], + border_width=1, + border_alpha=190, + ) + + canvas._panel_bg_tk = ImageTk.PhotoImage(panel_image) + if hasattr(canvas, '_panel_bg_id'): + canvas.itemconfigure(canvas._panel_bg_id, image=canvas._panel_bg_tk) + else: + canvas._panel_bg_id = canvas.create_image(0, 0, image=canvas._panel_bg_tk, anchor='nw') + canvas.tag_lower(canvas._panel_bg_id) + + content_width = max(1, width - (2 * (halo_padding + inner_pad_x))) + content_height = max(1, height - (2 * (halo_padding + inner_pad_y))) + canvas.coords(canvas._panel_window_id, halo_padding + inner_pad_x, halo_padding + inner_pad_y) + canvas.itemconfigure(canvas._panel_window_id, width=content_width, height=content_height) + + def create_steam_card(self, parent, title=None, padding=8): + """Create a striped charcoal card container.""" + card_frame = tk.Frame(parent, bg=COLORS['bg_card'], relief='flat', bd=0) + card_frame.configure(highlightbackground=COLORS['border_light'], + highlightcolor=COLORS['border_light'], + highlightthickness=1) + + if title: + title_frame = tk.Frame(card_frame, bg=COLORS['bg_card']) + title_frame.pack(fill='x', padx=padding, pady=(padding, 4)) + + title_label = tk.Label(title_frame, text=title, + font=self.get_font(12, 'bold', 'italic'), + bg=COLORS['bg_card'], + fg=COLORS['text_highlight']) + title_label.pack(anchor='w') + + # Add separator line + separator = tk.Frame(title_frame, bg=COLORS['border_light'], height=1) + separator.pack(fill='x', pady=(4, 0)) + + content_frame = tk.Frame(card_frame, bg=COLORS['bg_card']) + content_frame.pack(fill='both', expand=True, padx=padding, pady=padding) + + return card_frame, content_frame def create_input_section(self, parent): - """Create the left input section""" - title_label = tk.Label(parent, text="Progression: Loader", - font=self.get_font(14, 'bold'), bg='#2b2b2b', fg='#ffffff') - title_label.pack(pady=(0, 10)) + """Create the left input section with Steam Collections inspired design""" + parent_bg = parent.cget('bg') + + title_label = tk.Label( + parent, + text="Pack Configuration", + font=self.get_font(17, 'bold', 'italic'), + bg=parent_bg, + fg=COLORS['text_highlight'], + ) + title_label.pack(anchor='w', pady=(0, 4)) + + subtitle_label = tk.Label( + parent, + text="Point Progression at RimWorld, then review the workshop sources and actions below.", + font=self.get_font(10), + bg=parent_bg, + fg=COLORS['text_secondary'], + justify='left', + wraplength=390, + ) + subtitle_label.pack(anchor='w', pady=(0, 14)) - # RimWorld game folder input - rimworld_frame = tk.Frame(parent, bg='#2b2b2b') - rimworld_frame.pack(fill='x', pady=(0, 10)) + # RimWorld Path Card + rimworld_card, rimworld_content = self.create_steam_card(parent, "RimWorld Installation") + rimworld_card.pack(fill='x', pady=(0, 10)) - self.rimworld_label = tk.Label(rimworld_frame, text="RimWorld Game Folder:", - font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#00bfff') # Start with bright light blue - self.rimworld_label.pack(anchor='w') + self.rimworld_label = tk.Label(rimworld_content, text="Game Folder Path:", + font=self.get_font(10, 'bold'), + bg=COLORS['bg_card'], + fg=COLORS['text_secondary']) + self.rimworld_label.pack(anchor='w', pady=(0, 5)) - rimworld_help = tk.Label(rimworld_frame, - text="Right-click RimWorld in Steam > Manage > Browse local files, copy that path", - font=self.get_font(8), bg='#2b2b2b', fg='#888888', wraplength=380) - rimworld_help.pack(anchor='w', pady=(2, 5)) + rimworld_help = tk.Label(rimworld_content, + text="Right-click RimWorld in Steam → Manage → Browse local files, then copy that path", + font=self.get_font(8), + bg=COLORS['bg_card'], + fg=COLORS['text_muted'], + wraplength=380) + rimworld_help.pack(anchor='w', pady=(0, 10)) - rimworld_input_frame = tk.Frame(rimworld_frame, bg='#2b2b2b') - rimworld_input_frame.pack(fill='x') + rimworld_input_frame = tk.Frame(rimworld_content, bg=COLORS['bg_card']) + rimworld_input_frame.pack(fill='x', pady=(0, 5)) self.rimworld_var = tk.StringVar() self.rimworld_entry = tk.Entry(rimworld_input_frame, textvariable=self.rimworld_var, - font=self.get_font(9), bg='#404040', fg='#ffffff', - insertbackground='#ffffff', relief='flat', bd=5) - self.rimworld_entry.pack(side='left', fill='x', expand=True) + font=self.get_font(10)) + self.style_entry_widget(self.rimworld_entry) + self.rimworld_entry.pack(side='left', fill='x', expand=True, padx=(0, 10)) self.rimworld_entry.bind('', self.on_rimworld_path_change) - # Don't check initial state here - will be done after all GUI elements are created - browse_game_btn = tk.Button(rimworld_input_frame, text="Browse", command=self.browse_rimworld_folder, - bg='#505050', fg='white', font=self.get_font(8), - relief='flat', padx=10) - browse_game_btn.pack(side='right', padx=(5, 0)) + bg=COLORS['bg_card'], + font=self.get_font(10, 'bold', 'italic'), + padx=6, pady=2) + self.style_action_button(browse_game_btn, COLORS['accent_yellow']) + browse_game_btn.pack(side='right') - # Workshop folder display (derived from RimWorld path) - workshop_frame = tk.Frame(parent, bg='#2b2b2b') - workshop_frame.pack(fill='x', pady=(0, 10)) + # Workshop Path Card (Auto-derived) + workshop_card, workshop_content = self.create_steam_card(parent, "Workshop Content") + workshop_card.pack(fill='x', pady=(0, 10)) - workshop_label = tk.Label(workshop_frame, text="Workshop Folder (Auto-derived):", - font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff') - workshop_label.pack(anchor='w') + workshop_label = tk.Label(workshop_content, text="Workshop Folder (Auto-derived):", + font=self.get_font(10, 'bold'), + bg=COLORS['bg_card'], + fg=COLORS['text_secondary']) + workshop_label.pack(anchor='w', pady=(0, 5)) - self.workshop_var = tk.StringVar(value="Enter RimWorld path above") - self.workshop_display = tk.Entry(workshop_frame, textvariable=self.workshop_var, - font=self.get_font(8), bg='#404040', fg='#ffffff', - insertbackground='#ffffff', relief='flat', bd=5) - self.workshop_display.pack(fill='x', pady=(5, 0)) + self.workshop_var = tk.StringVar(value="Enter RimWorld path above to auto-detect") + self.workshop_display = tk.Entry(workshop_content, textvariable=self.workshop_var, + font=self.get_font(8), + state='readonly') + self.style_entry_widget(self.workshop_display, muted=True, readonly=True) + self.workshop_display.pack(fill='x', pady=(0, 5)) self.workshop_display.bind('', self.on_workshop_path_change) - # White line separator above ModsConfig section - separator_line = tk.Frame(parent, bg='#ffffff', height=1) - separator_line.pack(fill='x', pady=(15, 5)) + # Configuration Card + config_card, config_content = self.create_steam_card(parent, "Advanced Configuration") + config_card.pack(fill='x', pady=(0, 10)) - # Warning text above ModsConfig section - warning_label = tk.Label(parent, text="Don't edit anything below unless you know what you are doing", - font=self.get_font(8), bg='#2b2b2b', fg='#ffaa00', # Orange warning color - anchor='w') + # Warning text with Steam styling + warning_label = tk.Label(config_content, + text="⚠️ Advanced settings - modify only if you know what you're doing", + font=self.get_font(8, 'italic'), + bg=COLORS['bg_card'], + fg=COLORS['accent_yellow']) warning_label.pack(anchor='w', pady=(0, 10)) - # ModsConfig.xml folder display - modsconfig_frame = tk.Frame(parent, bg='#2b2b2b') - modsconfig_frame.pack(fill='x', pady=(0, 15)) - - modsconfig_label = tk.Label(modsconfig_frame, text="ModsConfig.xml Path:", - font=self.get_font(9, 'bold'), bg='#2b2b2b', fg='#ffffff') - modsconfig_label.pack(anchor='w') + # ModsConfig.xml path + modsconfig_label = tk.Label(config_content, text="ModsConfig.xml Path:", + font=self.get_font(9, 'bold'), + bg=COLORS['bg_card'], + fg=COLORS['text_secondary']) + modsconfig_label.pack(anchor='w', pady=(0, 5)) self.modsconfig_var = tk.StringVar() - self.modsconfig_display = tk.Entry(modsconfig_frame, textvariable=self.modsconfig_var, - font=self.get_font(8), bg='#404040', fg='#ffffff', - insertbackground='#ffffff', relief='flat', bd=5) - self.modsconfig_display.pack(fill='x', pady=(5, 0)) + self.modsconfig_display = tk.Entry(config_content, textvariable=self.modsconfig_var, + font=self.get_font(8)) + self.style_entry_widget(self.modsconfig_display) + self.modsconfig_display.pack(fill='x', pady=(0, 15)) self.modsconfig_display.bind('', self.on_modsconfig_path_change) # Initialize ModsConfig path self.find_modsconfig_path() + # Collections Card + collections_card, collections_content = self.create_steam_card(parent, "Steam Workshop Collections") + collections_card.pack(fill='x', pady=(0, 12)) + # Core collection core_url = os.getenv('CORE_COLLECTION_URL', 'https://steamcommunity.com/workshop/filedetails/?id=3521297585') - self.create_url_input(parent, "Core Collection:", core_url) + self.create_steam_url_input(collections_content, "Core Collection:", core_url) # Content collection content_url = os.getenv('CONTENT_COLLECTION_URL', 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3521319712') - self.create_url_input(parent, "Content Collection:", content_url) + self.create_steam_url_input(collections_content, "Content Collection:", content_url) # Cosmetics collection cosmetics_url = os.getenv('COSMETICS_COLLECTION_URL', 'steam://openurl/https://steamcommunity.com/sharedfiles/filedetails/?id=3637541646') - self.create_url_input(parent, "Cosmetics Collection:", cosmetics_url) + self.create_steam_url_input(collections_content, "Cosmetics Collection:", cosmetics_url) - # Load Progression Pack button - self.load_btn = tk.Button(parent, text="Load Progression Pack Complete", + # Action Buttons with Steam styling + buttons_frame = tk.Frame(parent, bg=parent_bg) + buttons_frame.pack(fill='x', pady=(6, 0)) + + # Load Progression Pack button (Primary Steam button) + self.load_btn = tk.Button(buttons_frame, text="Load Progression Pack Complete", command=self.load_progression_pack, - bg='#404040', fg='#888888', font=self.get_font(12, 'bold'), # Greyed out initially - relief='flat', padx=30, pady=12, state='disabled') - self.load_btn.pack(pady=30) + bg=parent_bg, + font=self.get_font(14, 'bold', 'italic'), + padx=0, pady=6, + state='disabled') + self.style_action_button(self.load_btn, COLORS['accent_green']) + self.load_btn.pack(pady=(0, 10)) - # Merge with Current Mods button (yellow) - self.merge_btn = tk.Button(parent, text="Merge with Current Mods Config", + # Merge with Current Mods button (Secondary Steam button) + self.merge_btn = tk.Button(buttons_frame, text="Merge with Current Mods Config", command=self.merge_with_current_mods, - bg='#404040', fg='#888888', font=self.get_font(12, 'bold'), # Greyed out initially - relief='flat', padx=30, pady=12, state='disabled') - self.merge_btn.pack(pady=(10, 30)) + bg=parent_bg, + font=self.get_font(12, 'bold', 'italic'), + padx=0, pady=4, + state='disabled') + self.style_action_button(self.merge_btn, COLORS['accent_yellow']) + self.merge_btn.pack() - def create_url_input(self, parent, label_text, default_url, label_font_size=10, entry_font_size=9): - """Create a labeled URL input field with responsive sizing""" - label = tk.Label(parent, text=label_text, font=self.get_font(label_font_size, 'bold'), - bg='#2b2b2b', fg='#ffffff') - label.pack(anchor='w', pady=(10, 5)) + def create_steam_url_input(self, parent, label_text, default_url): + """Create a Steam-styled URL input field""" + url_frame = tk.Frame(parent, bg=COLORS['bg_card']) + url_frame.pack(fill='x', pady=(0, 10)) - entry = tk.Entry(parent, font=self.get_font(entry_font_size), bg='#404040', fg='#ffffff', - insertbackground='#ffffff', relief='flat', bd=5) - entry.pack(fill='x', pady=(0, 10)) + label = tk.Label(url_frame, text=label_text, + font=self.get_font(9, 'bold'), + bg=COLORS['bg_card'], + fg=COLORS['text_secondary']) + label.pack(anchor='w', pady=(0, 3)) + + entry = tk.Entry(url_frame, font=self.get_font(8), + bg=COLORS['bg_tertiary']) + self.style_entry_widget(entry) + entry.pack(fill='x') entry.insert(0, default_url) # Store reference to entry widgets @@ -2124,31 +2656,65 @@ class SteamWorkshopGUI: self.url_entries[collection_name] = entry def create_output_section(self, parent): - """Create the right output section with responsive sizing""" + """Create the right output section with Steam Collections inspired styling""" # Responsive font sizes window_height = self.root.winfo_height() or 1080 title_font_size = max(12, min(16, int(window_height * 0.015))) text_font_size = max(8, min(12, int(window_height * 0.011))) - title_label = tk.Label(parent, text="Logs", - font=self.get_font(title_font_size, 'bold'), bg='#2b2b2b', fg='#ffffff') - title_label.pack(pady=(0, 10)) + # Create Steam-style card for logs + logs_card, logs_content = self.create_steam_card(parent, "System Logs") + logs_card.pack(fill='both', expand=True) - # Output text area with scrollbar - responsive sizing - self.output_text = scrolledtext.ScrolledText(parent, + # Output text area with Steam styling + self.output_text = scrolledtext.ScrolledText(logs_content, font=self.get_font(text_font_size), - bg='#1e1e1e', fg='#ffffff', - insertbackground='#ffffff', - selectbackground='#404040', - relief='flat', bd=5) + bg=COLORS['bg_tertiary'], + fg=COLORS['text_body'], + insertbackground=COLORS['text_primary'], + selectbackground=COLORS['bg_hover'], + selectforeground=COLORS['text_primary'], + relief='flat', bd=0, + highlightthickness=1, + highlightcolor=COLORS['accent_yellow'], + highlightbackground=COLORS['border_light']) self.output_text.pack(fill=tk.BOTH, expand=True) + + # Configure text tags for different log levels + self.output_text.tag_configure("success", foreground=COLORS['accent_green']) + self.output_text.tag_configure("error", foreground=COLORS['accent_red']) + self.output_text.tag_configure("warning", foreground=COLORS['accent_yellow']) + self.output_text.tag_configure("info", foreground=COLORS['accent_blue']) + self.output_text.tag_configure("highlight", foreground=COLORS['text_highlight']) - def log_to_output(self, message): - """Log a message to the output text area""" + def log_to_output(self, message, level="info"): + """Log a message to the output text area with Steam-style coloring""" if hasattr(self, 'output_text'): - self.output_text.insert(tk.END, message) + # Add timestamp + import datetime + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + + # Insert timestamp with muted color + self.output_text.insert(tk.END, f"[{timestamp}] ", "timestamp") + + # Insert message with appropriate color based on level + if "✓" in message or "success" in message.lower(): + self.output_text.insert(tk.END, message, "success") + elif "✗" in message or "error" in message.lower() or "failed" in message.lower(): + self.output_text.insert(tk.END, message, "error") + elif "⚠️" in message or "warning" in message.lower(): + self.output_text.insert(tk.END, message, "warning") + elif "🔍" in message or "info" in message.lower(): + self.output_text.insert(tk.END, message, "info") + else: + self.output_text.insert(tk.END, message) + self.output_text.see(tk.END) # Scroll to bottom + # Configure timestamp tag if not already configured + if hasattr(self, 'output_text') and not self.output_text.tag_cget("timestamp", "foreground"): + self.output_text.tag_configure("timestamp", foreground=COLORS['text_muted']) + def on_workshop_path_change(self, event=None): """Called when workshop path is manually changed""" self.workshop_folder = self.workshop_var.get().strip() @@ -2165,9 +2731,9 @@ class SteamWorkshopGUI: is_valid = self.validate_rimworld_path(rimworld_path) if is_valid: - # Stop blinking and set to normal color + # Stop blinking and set to Steam success color self.is_rimworld_valid = True - self.rimworld_label.configure(fg='#ffffff') # White for valid path + self.rimworld_label.configure(fg=COLORS['accent_green']) # Steam green for valid path self.enable_buttons() # Derive workshop path from RimWorld path @@ -2190,23 +2756,35 @@ class SteamWorkshopGUI: self.log_to_output(f"✓ Workshop folder derived: {workshop_path}\n") except (StopIteration, IndexError): - self.workshop_var.set("Invalid RimWorld path - should contain 'steamapps'") - self.log_to_output("⚠ Warning: RimWorld path should contain 'steamapps'\n") + self.log_to_output("⚠️ Could not auto-derive workshop path from RimWorld path\n") else: - self.workshop_var.set("Invalid RimWorld path - should contain 'steamapps'") - self.log_to_output("⚠ Warning: RimWorld path should contain 'steamapps'\n") + self.log_to_output("⚠️ RimWorld path doesn't appear to be in a Steam library\n") else: - # Invalid RimWorld path - keep blinking and buttons disabled + # Invalid path - start blinking and disable buttons self.is_rimworld_valid = False self.disable_buttons() - self.workshop_var.set("Invalid RimWorld path") - self.log_to_output(f"✗ Invalid RimWorld installation at: {rimworld_path}\n") - self.log_to_output(" Please ensure the path contains RimWorld.exe (or RimWorldWin64.exe) and Data folder\n") + self.start_rimworld_label_blink() + self.log_to_output("✗ Invalid RimWorld installation path\n") else: - # Empty path - keep blinking and buttons disabled + # Empty path - disable buttons and stop blinking self.is_rimworld_valid = False self.disable_buttons() - self.workshop_var.set("Enter RimWorld path above") + if hasattr(self, 'rimworld_label'): + self.rimworld_label.configure(fg=COLORS['text_secondary']) # Reset to normal color + + def enable_buttons(self): + """Enable the primary actions once the RimWorld path is valid.""" + if hasattr(self, 'load_btn'): + self.load_btn.configure(state='normal') + if hasattr(self, 'merge_btn'): + self.merge_btn.configure(state='normal') + + def disable_buttons(self): + """Disable the primary actions until the RimWorld path is valid.""" + if hasattr(self, 'load_btn'): + self.load_btn.configure(state='disabled') + if hasattr(self, 'merge_btn'): + self.merge_btn.configure(state='disabled') def validate_rimworld_path(self, path): """Validate if the given path is a valid RimWorld installation""" @@ -2235,30 +2813,14 @@ class SteamWorkshopGUI: """Start the blinking animation for RimWorld label""" if self.rimworld_label: if not self.is_rimworld_valid: - # Toggle between bright light blue and dark grey for maximum contrast if self.blink_state: - self.rimworld_label.configure(fg='#00bfff') # Bright light blue (DeepSkyBlue) + self.rimworld_label.configure(fg=COLORS['accent_yellow']) else: - self.rimworld_label.configure(fg='#555555') # Even darker grey for more contrast + self.rimworld_label.configure(fg=COLORS['text_muted']) self.blink_state = not self.blink_state - # Continue blinking until valid path is provided (even faster blink rate) - self.root.after(500, self.start_rimworld_label_blink) # Even faster blink (500ms) - - def enable_buttons(self): - """Enable the buttons when RimWorld path is valid""" - if self.load_btn: - self.load_btn.configure(state='normal', bg='#0078d4', fg='white') - if self.merge_btn: - self.merge_btn.configure(state='normal', bg='#ffcc00', fg='black') - - def disable_buttons(self): - """Disable the buttons when RimWorld path is invalid""" - if self.load_btn: - self.load_btn.configure(state='disabled', bg='#404040', fg='#888888') - if self.merge_btn: - self.merge_btn.configure(state='disabled', bg='#404040', fg='#888888') + self.root.after(500, self.start_rimworld_label_blink) def browse_rimworld_folder(self): """Allow user to browse for RimWorld game folder""" @@ -3182,24 +3744,24 @@ class SteamWorkshopGUI: width = int(width * 1.1) height = int(height * 1.1) - # Create a semi-transparent image with rounded corners - bg_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - draw = ImageDraw.Draw(bg_img) - - # Calculate alpha value (0-255) - default 75% opacity - alpha = int(255 * opacity) - fill_color = (43, 43, 43, alpha) # #2b2b2b with transparency - - # Draw rounded rectangle - draw.rounded_rectangle( - [(0, 0), (width - 1, height - 1)], - radius=corner_radius, - fill=fill_color + alpha = max(0, min(255, int(255 * opacity))) + bg_img = create_striped_panel( + width, + height, + panel_color=COLORS['bg_card'], + stripe_color=COLORS['stripe_soft'], + stripe_width=18, + stripe_gap=18, + stripe_alpha=max(50, int(alpha * 0.3)), + halo_padding=24, + halo_alpha=max(90, int(alpha * 0.55)), + panel_alpha=max(220, alpha), + corner_radius=corner_radius, + border_color=COLORS['border_light'], + border_width=1, + border_alpha=180, ) - # Apply gaussian blur for soft edges - bg_img = bg_img.filter(ImageFilter.GaussianBlur(radius=3)) - # Convert to PhotoImage bg_tk = ImageTk.PhotoImage(bg_img) @@ -3225,7 +3787,7 @@ class SteamWorkshopGUI: # Create overlay window for success animation self.success_window = tk.Toplevel() self.success_window.title("Progression: Loader - Success!") - self.success_window.configure(bg='#2b2b2b') + self.success_window.configure(bg=COLORS['bg_primary']) # Set window icon try: @@ -3251,7 +3813,7 @@ class SteamWorkshopGUI: if self.success_bg_image_tk: self.success_bg_canvas.create_image(0, 0, image=self.success_bg_image_tk, anchor='nw', tags='bg') else: - self.success_bg_canvas.configure(bg='#2b2b2b') + self.success_bg_canvas.configure(bg=COLORS['bg_primary']) # Bind resize event self.success_window.bind('', self._on_success_resize) @@ -3360,8 +3922,8 @@ class SteamWorkshopGUI: self.success_bg_canvas.create_text( window_width // 2, window_height // 2 - 150 - int(window_height * 0.03), text="Success!", - fill='#00ff00', - font=self.get_font(24, 'bold'), + fill=COLORS['text_highlight'], + font=self.get_font(24, 'bold', 'italic'), tags='success_text' ) @@ -3401,8 +3963,8 @@ class SteamWorkshopGUI: self.success_bg_canvas.create_text( center_x, y_pos, text="Mods list created successfully!", - fill='#00ff00', - font=self.get_font(20, 'bold'), + fill=COLORS['text_highlight'], + font=self.get_font(20, 'bold', 'italic'), tags='success_text' ) y_pos += 50 @@ -3411,7 +3973,7 @@ class SteamWorkshopGUI: self.success_bg_canvas.create_text( center_x, y_pos, text=f"It is named: {mod_list_name.lower()}", - fill='white', + fill=COLORS['text_primary'], font=self.get_font(16), tags='success_text' ) @@ -3439,7 +4001,7 @@ You must auto sort after this!""" self.success_bg_canvas.create_text( center_x, y_pos + 80, text=instructions_text, - fill='#cccccc', + fill=COLORS['text_body'], font=self.get_font(14), justify='center', tags='success_text' @@ -3448,24 +4010,23 @@ You must auto sort after this!""" # Auto sort warning (more prominent for homebrew) if mod_list_name == "ProgressionHomebrew": - warning_color = '#ff4444' + warning_color = COLORS['accent_red'] warning_msg = "⚠️ Critical: auto sort after loading! Check for mod conflicts! ⚠️" else: - warning_color = '#ff8686' + warning_color = COLORS['accent_red'] warning_msg = "⚠️ Important: auto sort after loading! ⚠️" self.success_bg_canvas.create_text( center_x, y_pos, text=warning_msg, fill=warning_color, - font=self.get_font(16, 'bold'), + font=self.get_font(16, 'bold', 'italic'), tags='success_text' ) y_pos += 60 - # Close button with transparent corners self._create_success_canvas_button(center_x, y_pos + 30, "got it!", - self.close_success_animation, '#00aa00', width=150, height=50) + self.close_success_animation, COLORS['accent_green'], width=150, height=50) # Ensure logo stays on top of all elements self._bring_success_logo_to_front() @@ -3478,46 +4039,44 @@ You must auto sort after this!""" self.success_bg_canvas.tag_raise('logo') def _create_success_canvas_button(self, x, y, text, command, color, width=150, height=40): - """Create a button on success canvas with transparent corners""" - # Create rounded rectangle button image - btn_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) - draw = ImageDraw.Draw(btn_img) - - # Parse color - r = int(color[1:3], 16) - g = int(color[3:5], 16) - b = int(color[5:7], 16) - - # Draw rounded rectangle - draw.rounded_rectangle( - [(0, 0), (width - 1, height - 1)], - radius=10, - fill=(r, g, b, 255) - ) - - btn_tk = ImageTk.PhotoImage(btn_img) - - # Store reference - if not hasattr(self, '_success_button_images'): - self._success_button_images = [] - self._success_button_images.append(btn_tk) - - # Create button background - btn_id = self.success_bg_canvas.create_image(x, y, image=btn_tk, anchor='center', tags='success_text') - - # Create safe tag name (remove special characters) + """Create a text-only action for the success screen.""" safe_tag = f'btn_{text.replace(" ", "_").replace("!", "").replace("?", "").replace(",", "")}' - - # Create button text - text_id = self.success_bg_canvas.create_text(x, y, text=text, fill='white', - font=self.get_font(14, 'bold'), - anchor='center', tags=('success_text', safe_tag)) - - # Bind click event + + text_id = self.success_bg_canvas.create_text( + x, + y, + text=text, + fill=color, + font=self.get_font(14, 'bold', 'italic'), + anchor='center', + tags=('success_text', safe_tag), + ) + + left, top, right, bottom = self.success_bg_canvas.bbox(text_id) + hit_area_id = self.success_bg_canvas.create_rectangle( + left - max(16, width // 6), + top - max(8, height // 5), + right + max(16, width // 6), + bottom + max(8, height // 5), + outline='', + fill='', + tags=('success_text', safe_tag), + ) + self.success_bg_canvas.tag_lower(hit_area_id, text_id) + self.success_bg_canvas.tag_bind(safe_tag, '', lambda e: command()) - self.success_bg_canvas.tag_bind(btn_id, '', lambda e: command()) - - return btn_id + self.success_bg_canvas.tag_bind( + safe_tag, + '', + lambda e, item=text_id: self.success_bg_canvas.itemconfig(item, fill=COLORS['text_primary']), + ) + self.success_bg_canvas.tag_bind( + safe_tag, + '', + lambda e, item=text_id, button_color=color: self.success_bg_canvas.itemconfig(item, fill=button_color), + ) + + return text_id def close_success_animation(self): """Close the success animation window and exit the application""" @@ -3534,10 +4093,10 @@ You must auto sort after this!""" update_btn = tk.Button(self.footer_frame, text="Check for Updates", command=self.manual_update_check, - bg='#404040', fg='white', - font=('Arial', 10), - padx=15, pady=5, - cursor='hand2') + bg=COLORS['bg_card'], + font=self.get_font(11, 'bold', 'italic'), + padx=10, pady=4) + self.style_action_button(update_btn, COLORS['accent_yellow']) update_btn.pack(side='left', padx=10, pady=10) except Exception as e: print(f"Could not add update button: {e}") @@ -3624,7 +4183,7 @@ def show_blocking_update_dialog(root, update_checker, release_info): # Create a new window for the update dialog update_window = tk.Toplevel(root) update_window.title("Update Required - Progression Loader") - update_window.configure(bg='#2b2b2b') + update_window.configure(bg=COLORS['bg_primary']) update_window.resizable(False, False) update_window.attributes('-topmost', True) @@ -3640,72 +4199,78 @@ def show_blocking_update_dialog(root, update_checker, release_info): except: pass - # Calculate window size and center it window_width = 600 - window_height = 600 # Increased height to accommodate buttons - - # Get screen dimensions - screen_width = update_window.winfo_screenwidth() - screen_height = update_window.winfo_screenheight() - - # Calculate center position - x = (screen_width - window_width) // 2 - y = (screen_height - window_height) // 2 - - update_window.geometry(f"{window_width}x{window_height}+{x}+{y}") - - # Title - title_label = tk.Label(update_window, - text="🔄 Update Required", - fg='#ff6b6b', bg='#2b2b2b', - font=('Arial', 20, 'bold')) + window_height = 600 + center_window(update_window, window_width, window_height, root) + + panel = tk.Frame( + update_window, + bg=COLORS['bg_card'], + highlightbackground=COLORS['border_light'], + highlightcolor=COLORS['border_light'], + highlightthickness=1, + ) + panel.pack(fill='both', expand=True, padx=18, pady=18) + + title_label = tk.Label( + panel, + text="Update Required", + fg=COLORS['text_highlight'], + bg=COLORS['bg_card'], + font=(FONT_FAMILY, 20, 'bold italic'), + ) title_label.pack(pady=20) - # Message - message_label = tk.Label(update_window, + message_label = tk.Label(panel, text="A new version is available and required to continue.", - fg='white', bg='#2b2b2b', - font=('Arial', 14)) + fg=COLORS['text_primary'], bg=COLORS['bg_card'], + font=(FONT_FAMILY, 14)) message_label.pack(pady=10) - # Version info - version_frame = tk.Frame(update_window, bg='#2b2b2b') + version_frame = tk.Frame(panel, bg=COLORS['bg_card']) version_frame.pack(pady=15) current_label = tk.Label(version_frame, text=f"Current Version: {update_checker.current_version}", - fg='#cccccc', bg='#2b2b2b', - font=('Arial', 12)) + fg=COLORS['text_body'], bg=COLORS['bg_card'], + font=(FONT_FAMILY, 12)) current_label.pack() latest_label = tk.Label(version_frame, text=f"Required Version: {release_info['version']}", - fg='#4ecdc4', bg='#2b2b2b', - font=('Arial', 12, 'bold')) + fg=COLORS['accent_green'], bg=COLORS['bg_card'], + font=(FONT_FAMILY, 12, 'bold')) latest_label.pack(pady=5) # Release notes if release_info.get('body'): - notes_label = tk.Label(update_window, + notes_label = tk.Label(panel, text="What's New:", - fg='white', bg='#2b2b2b', - font=('Arial', 14, 'bold')) + fg=COLORS['text_primary'], bg=COLORS['bg_card'], + font=(FONT_FAMILY, 14, 'bold italic')) notes_label.pack(pady=(20, 10)) - # Create scrollable text widget for release notes - notes_frame = tk.Frame(update_window, bg='#2b2b2b') + notes_frame = tk.Frame(panel, bg=COLORS['bg_card']) notes_frame.pack(fill='both', expand=True, padx=40, pady=(0, 10)) notes_text = tk.Text(notes_frame, - height=6, # Reduced height to make room for buttons - bg='#404040', fg='#ffffff', - font=('Arial', 11), + height=6, + bg=COLORS['bg_tertiary'], fg=COLORS['text_body'], + font=(FONT_FAMILY, 11), wrap=tk.WORD, state='disabled', relief='flat', - bd=0) + bd=0, + insertbackground=COLORS['text_primary'], + highlightthickness=1, + highlightbackground=COLORS['border_light']) - scrollbar = tk.Scrollbar(notes_frame, bg='#404040') + 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) @@ -3717,9 +4282,8 @@ def show_blocking_update_dialog(root, update_checker, release_info): notes_text.insert('1.0', release_info['body']) notes_text.config(state='disabled') - # Buttons - fixed at bottom - button_frame = tk.Frame(update_window, bg='#2b2b2b') - button_frame.pack(side='bottom', pady=20) # Pack at bottom with padding + button_frame = tk.Frame(panel, bg=COLORS['bg_card']) + button_frame.pack(side='bottom', pady=20) def auto_update(): """Download and install the update automatically""" @@ -3876,30 +4440,28 @@ del "%~f0" auto_btn = tk.Button(button_frame, text="Auto Update", command=auto_update, - bg='#4ecdc4', fg='white', - font=('Arial', 14, 'bold'), - padx=30, pady=10, - cursor='hand2') + bg=COLORS['bg_card'], + font=(FONT_FAMILY, 14, 'bold italic'), + padx=0, pady=6) + style_text_button(auto_btn, COLORS['accent_green'], COLORS['bg_card']) auto_btn.pack(side='left', padx=10) - # Download Page button (secondary action) download_btn = tk.Button(button_frame, text="Download Page", command=download_page, - bg='#45b7d1', fg='white', - font=('Arial', 12), - padx=20, pady=10, - cursor='hand2') + bg=COLORS['bg_card'], + font=(FONT_FAMILY, 12, 'bold italic'), + padx=0, pady=6) + style_text_button(download_btn, COLORS['accent_yellow'], COLORS['bg_card']) download_btn.pack(side='left', padx=10) - # Exit button (tertiary action) exit_btn = tk.Button(button_frame, text="Exit Application", command=exit_app, - bg='#666666', fg='white', - font=('Arial', 12), - padx=20, pady=10, - cursor='hand2') + bg=COLORS['bg_card'], + font=(FONT_FAMILY, 12, 'bold italic'), + padx=0, pady=6) + style_text_button(exit_btn, COLORS['accent_red'], COLORS['bg_card']) exit_btn.pack(side='left', padx=10) # Handle window close (same as exit) diff --git a/ui_theme.py b/ui_theme.py new file mode 100644 index 0000000..66ee958 --- /dev/null +++ b/ui_theme.py @@ -0,0 +1,250 @@ +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", +} + + +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 = Image.open(image_path).convert("RGBA") + 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.LANCZOS) + 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)}") diff --git a/update_checker.py b/update_checker.py index 65415b9..2ccfb4f 100644 --- a/update_checker.py +++ b/update_checker.py @@ -16,6 +16,7 @@ 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): @@ -170,6 +171,57 @@ class UpdateChecker: 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""" @@ -184,7 +236,7 @@ class UpdateChecker: # Create update dialog dialog = tk.Toplevel(parent_window) dialog.title("Update Available - Progression Loader") - dialog.configure(bg='#2b2b2b') + dialog.configure(bg=COLORS['bg_primary']) dialog.resizable(False, False) dialog.attributes('-topmost', True) @@ -195,58 +247,74 @@ class UpdateChecker: 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')) + 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(dialog, bg='#2b2b2b') + 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='#cccccc', bg='#2b2b2b', - font=('Arial', 12)) + 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='#00ff00', bg='#2b2b2b', - font=('Arial', 12, 'bold')) + 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(dialog, + notes_label = tk.Label(panel, text="Release Notes:", - fg='#cccccc', bg='#2b2b2b', - font=('Arial', 12, 'bold')) + 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(dialog, bg='#2b2b2b') + 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='#404040', fg='#ffffff', - font=('Arial', 10), + bg=COLORS['bg_tertiary'], fg=COLORS['text_body'], + font=(FONT_FAMILY, 10), wrap=tk.WORD, - state='disabled') + state='disabled', + relief='flat', + bd=0, + insertbackground=COLORS['text_primary'], + highlightthickness=1, + highlightbackground=COLORS['border_light']) - scrollbar = tk.Scrollbar(notes_frame) + 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) @@ -259,7 +327,7 @@ class UpdateChecker: notes_text.config(state='disabled') # Buttons - button_frame = tk.Frame(dialog, bg='#2b2b2b') + button_frame = tk.Frame(panel, bg=COLORS['bg_card']) button_frame.pack(pady=20) def download_update(): @@ -302,9 +370,10 @@ class UpdateChecker: download_btn = tk.Button(button_frame, text="Download Update", command=download_update, - bg='#00aa00', fg='white', - font=('Arial', 12, 'bold'), - padx=20, pady=5) + 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 @@ -312,25 +381,28 @@ class UpdateChecker: later_btn = tk.Button(button_frame, text="Remind Later", command=remind_later, - bg='#0078d4', fg='white', - font=('Arial', 12), - padx=20, pady=5) + 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='#666666', fg='white', - font=('Arial', 12), - padx=20, pady=5) + 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='#666666', fg='white', - font=('Arial', 12), - padx=20, pady=5) + 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 @@ -355,15 +427,21 @@ class UpdateChecker: """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) + 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: - messagebox.showinfo("No Updates", - "Could not retrieve release information.", - parent=parent_window) + self._show_notice_dialog( + parent_window, + "No Updates", + "Could not retrieve release information.", + COLORS['accent_yellow'], + ) return latest_version = release_info['version'] @@ -371,33 +449,44 @@ class UpdateChecker: 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) + 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='#2b2b2b') + checking_dialog.configure(bg=COLORS['bg_primary']) 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)) + + 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() - checking_dialog.destroy() - check_complete(release_info, error) + 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() @@ -452,9 +541,10 @@ def integrate_update_checker_with_gui(gui_class): 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) + 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}") @@ -484,4 +574,4 @@ def integrate_update_checker_with_gui(gui_class): gui_class.manual_update_check = manual_update_check gui_class.__init__ = new_init - return gui_class \ No newline at end of file + return gui_class