diff --git a/art/Desolation.png b/art/Desolation.png new file mode 100644 index 0000000..ce50937 Binary files /dev/null and b/art/Desolation.png differ diff --git a/art/georgia.ttf b/art/georgia.ttf new file mode 100644 index 0000000..43672d8 Binary files /dev/null and b/art/georgia.ttf differ diff --git a/steam_workshop_gui.py b/steam_workshop_gui.py index 0f1fe2c..661bdea 100644 --- a/steam_workshop_gui.py +++ b/steam_workshop_gui.py @@ -1,5 +1,6 @@ import tkinter as tk from tkinter import ttk, scrolledtext, filedialog, messagebox, font +import customtkinter as ctk import requests import re from urllib.parse import urlparse, parse_qs @@ -19,6 +20,10 @@ import html # Load environment variables load_dotenv() +# Set CustomTkinter appearance +ctk.set_appearance_mode("dark") +ctk.set_default_color_theme("dark-blue") + def get_resource_path(relative_path): """Get absolute path to resource, works for dev and for PyInstaller""" try: @@ -39,14 +44,13 @@ class LoadingScreen: # Load custom font first self.load_custom_font() - # Create fullscreen loading window - self.loading_window = tk.Toplevel(root) + # Create fullscreen loading window using CTkToplevel + self.loading_window = ctk.CTkToplevel(root) self.loading_window.title("Progression: Loader") - self.loading_window.configure(bg='#2b2b2b') # Set window icon try: - icon_path = get_resource_path(get_resource_path(os.path.join("art", "Progression.ico"))) + icon_path = get_resource_path(os.path.join("art", "Progression.ico")) if os.path.exists(icon_path): self.loading_window.iconbitmap(icon_path) except Exception as e: @@ -54,8 +58,15 @@ class LoadingScreen: self.loading_window.attributes('-topmost', True) - # Make it fullscreen + # Make it fullscreen initially, but allow free resizing self.loading_window.state('zoomed') + + # Set minimum size for loading window too + self.loading_window.minsize(800, 600) + + # Bind to enforce minimum size for loading window + self.loading_window.bind('', self._enforce_loading_minimum_size) + self.loading_window.protocol("WM_DELETE_WINDOW", self.on_close) # Load images @@ -200,12 +211,43 @@ 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""" + 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) + except Exception as e: + print(f"Error loading background image: {e}") + return None + def load_images(self): """Load all required images""" try: # Load game title self.game_title_img = Image.open(get_resource_path("art/GameTitle.png")) self.game_title_tk = ImageTk.PhotoImage(self.game_title_img) + # Create CTkImage version for use with CTkLabel + self.game_title_ctk = ctk.CTkImage(light_image=self.game_title_img, + dark_image=self.game_title_img, + size=self.game_title_img.size) # Load HR Systems logo self.hr_logo_img = Image.open(get_resource_path("art/hudsonriggssystems.png")) @@ -215,6 +257,10 @@ class LoadingScreen: new_hr_width = int((new_hr_height / hr_height) * hr_width) self.hr_logo_img = self.hr_logo_img.resize((new_hr_width, new_hr_height), Image.Resampling.LANCZOS) self.hr_logo_tk = ImageTk.PhotoImage(self.hr_logo_img) + # Create CTkImage version + self.hr_logo_ctk = ctk.CTkImage(light_image=self.hr_logo_img, + dark_image=self.hr_logo_img, + size=(new_hr_width, new_hr_height)) # Create radial glow effect version for loading screen self.hr_logo_glow_tk = self.create_radial_glow_image(self.hr_logo_img) @@ -242,7 +288,9 @@ class LoadingScreen: print(f"Error loading images: {e}") # Create placeholder images if loading fails self.game_title_tk = None + self.game_title_ctk = None self.hr_logo_tk = None + self.hr_logo_ctk = None self.expansion_images = {} # Create placeholder images if loading fails self.game_title_tk = None @@ -250,59 +298,259 @@ class LoadingScreen: self.expansion_images = {} def create_loading_ui(self): - """Create the loading screen UI""" - # Main container - self.main_frame = tk.Frame(self.loading_window, bg='#2b2b2b') - self.main_frame.pack(fill=tk.BOTH, expand=True) + """Create the loading screen UI with background image using Canvas for true transparency""" + # Force window to update and get proper dimensions + self.loading_window.update() + width = self.loading_window.winfo_width() + height = self.loading_window.winfo_height() + # Fallback to screen size if window size not available + if width < 100 or height < 100: + width = self.loading_window.winfo_screenwidth() + height = self.loading_window.winfo_screenheight() + + print(f"Loading UI with dimensions: {width}x{height}") + + # Store canvas dimensions for later use + self.canvas_width = width + self.canvas_height = height + + # Create canvas for background and all content + self.bg_canvas = tk.Canvas(self.loading_window, highlightthickness=0) + self.bg_canvas.pack(fill=tk.BOTH, expand=True) + + # Load and display background image + self.bg_image_pil = self._load_and_scale_background(width, height) + if self.bg_image_pil: + print(f"Background image loaded: {self.bg_image_pil.size}") + self.bg_image_tk = ImageTk.PhotoImage(self.bg_image_pil) + 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') + + # Bind window state change events + self.loading_window.bind('', self._on_window_map) + self.loading_window.bind('', self._on_window_unmap) + + # Bind resize event to update background (minimum size enforcement is handled separately) + # Note: _enforce_loading_minimum_size calls _on_loading_resize after size correction + + # Place widgets directly on the canvas # Game title (initially large and centered) - self.title_label = tk.Label(self.main_frame, bg='#2b2b2b') - if self.game_title_tk: - self.title_label.configure(image=self.game_title_tk) + if hasattr(self, 'game_title_tk') and self.game_title_tk: + self.title_image_id = self.bg_canvas.create_image(width//2, height//2, + image=self.game_title_tk, + anchor='center', tags='title') else: - self.title_label.configure(text="Progression: Loader", - fg='white', font=self.get_font(24, 'bold')) - self.title_label.pack(expand=True) + self.title_text_id = self.bg_canvas.create_text(width//2, height//2, + text="Progression: Loader", + fill='white', + font=self.get_font(24, 'bold'), + anchor='center', tags='title') - # HR Systems logo (initially at bottom) with space for glow effect - hr_container = tk.Frame(self.main_frame, bg='#2b2b2b') - hr_container.pack(side=tk.BOTTOM, pady=40) # Increased padding for glow space + # HR Systems logo (initially at bottom) + if hasattr(self, 'hr_logo_tk') and self.hr_logo_tk: + self.hr_logo_id = self.bg_canvas.create_image(width//2, int(height*0.95), + image=self.hr_logo_tk, + anchor='center', tags='hr_logo') + 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), + anchor='center', tags='hr_logo') - self.hr_logo_label = tk.Label(hr_container, bg='#2b2b2b', cursor='hand2') - if self.hr_logo_tk: - # Calculate fixed size for the label to accommodate glow - glow_padding = 15 - # Get original image size (60x height from load_images) - base_width = 60 * 3 # Approximate width based on typical logo proportions - base_height = 60 - label_width = base_width + (glow_padding * 2) - label_height = base_height + (glow_padding * 2) + # Bind click events for HR logo + self.bg_canvas.tag_bind('hr_logo', '', self.open_hr_website) + self.bg_canvas.tag_bind('hr_logo', '', self.on_loading_hr_enter) + self.bg_canvas.tag_bind('hr_logo', '', self.on_loading_hr_leave) + + # Create a reference for compatibility + self.main_frame = self.loading_window + + # Store canvas dimensions for later use + self.canvas_width = width + self.canvas_height = height + + def _load_and_scale_background(self, width, height): + """Load and scale the Desolation background image""" + 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 - self.hr_logo_label.configure(image=self.hr_logo_tk, - width=label_width, - height=label_height, - compound='center') - else: - self.hr_logo_label.configure(text="Hudson Riggs Systems", - fg='#888888', font=self.get_font(12)) + 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 + except Exception as e: + print(f"Error loading background image: {e}") + return None + + def _enforce_loading_minimum_size(self, event): + """Enforce minimum window size for loading screen""" + if event.widget == self.loading_window: + width = self.loading_window.winfo_width() + height = self.loading_window.winfo_height() + + # Check if window is smaller than minimum + if width < 800 or height < 600: + # Resize to minimum if too small + new_width = max(width, 800) + new_height = max(height, 600) + self.loading_window.geometry(f"{new_width}x{new_height}") + return # Don't process resize if we're correcting size + + # Continue with normal resize handling + self._on_loading_resize(event) + + def _on_loading_resize(self, event): + """Handle window resize to update background image and reposition elements responsively""" + if event.widget == self.loading_window and hasattr(self, 'bg_canvas'): + width = event.width + height = event.height + + # Skip if dimensions are too small (handled by minimum size enforcement) + if width < 800 or height < 600: + return + + # Update canvas dimensions for responsive scaling + self.canvas_width = width + self.canvas_height = height + + # Update background image with new dimensions + self.bg_image_pil = self._load_and_scale_background(width, height) + if self.bg_image_pil: + self.bg_image_tk = ImageTk.PhotoImage(self.bg_image_pil) + self.bg_canvas.delete('bg') + self.bg_canvas.create_image(0, 0, image=self.bg_image_tk, anchor='nw', tags='bg') + self.bg_canvas.tag_lower('bg') + + # Reposition elements responsively based on new dimensions + self._reposition_loading_elements_responsive(width, height) + + # Ensure logo stays on top after repositioning + self._bring_logo_to_front() + + # If we're in a stage, refresh the stage content to reposition elements + if hasattr(self, 'current_stage_method'): + # Add a small delay to avoid too frequent updates during resize + if hasattr(self, '_refresh_timer'): + self.loading_window.after_cancel(self._refresh_timer) + self._refresh_timer = self.loading_window.after(150, self._refresh_current_stage) + + def _reposition_loading_elements_responsive(self, width, height): + """Reposition loading elements responsively based on window size""" + # Calculate responsive positions as percentages of window size + title_y = max(60, int(height * 0.08)) # 8% from top, minimum 60px + hr_logo_y = max(height - 80, int(height * 0.95)) # 5% from bottom, minimum 80px from bottom - # Make HR logo clickable in loading screen - self.hr_logo_label.bind('', self.open_hr_website) - self.hr_logo_label.bind('', self.on_loading_hr_enter) - self.hr_logo_label.bind('', self.on_loading_hr_leave) - self.hr_logo_label.pack() + # Reposition title to maintain relative position + title_items = self.bg_canvas.find_withtag('title') + for item in title_items: + self.bg_canvas.coords(item, width//2, title_y) - # Content frame (initially hidden) - self.content_frame = tk.Frame(self.main_frame, bg='#2b2b2b') + # Reposition HR logo to maintain relative position + hr_items = self.bg_canvas.find_withtag('hr_logo') + for item in hr_items: + self.bg_canvas.coords(item, width//2, hr_logo_y) + + def _refresh_current_stage(self): + """Refresh the current stage content after window resize""" + # This will be called to reposition stage elements after resize + if hasattr(self, 'current_stage_method') and self.current_stage_method: + # Re-call the current stage method to reposition elements + try: + self.current_stage_method() + except Exception as e: + print(f"Error refreshing stage: {e}") + # If refresh fails, just continue without crashing + + def _on_window_map(self, event): + """Handle window map event (window becomes visible)""" + pass + + def _on_window_unmap(self, event): + """Handle window unmap event (window becomes hidden)""" + pass + + def get_ctk_font(self, size=10, weight='normal'): + """Get CTkFont for UI elements""" + 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') + + 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) - # Expansion check frame - self.expansion_frame = tk.Frame(self.content_frame, bg='#2b2b2b') + # Create a semi-transparent image with rounded corners + bg_img = Image.new('RGBA', (width, height), (0, 0, 0, 0)) + draw = ImageDraw.Draw(bg_img) - # Collection subscription frame - self.subscription_frame = tk.Frame(self.content_frame, bg='#2b2b2b') + # Calculate alpha value (0-255) - default 75% opacity + alpha = int(255 * opacity) + fill_color = (43, 43, 43, alpha) # #2b2b2b with transparency - # Final confirmation frame - self.final_frame = tk.Frame(self.content_frame, bg='#2b2b2b') + # 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)) + + # Convert to PhotoImage + bg_tk = ImageTk.PhotoImage(bg_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) + + # Create canvas image + img_id = self.bg_canvas.create_image(x, y, image=bg_tk, anchor='center', tags='stage_content') + + # Make sure it's behind text but above background + self.bg_canvas.tag_lower(img_id, 'stage_content') + + # Ensure logo stays on top + self._bring_logo_to_front() + + return img_id + + def _bring_logo_to_front(self): + """Ensure the game logo stays on top of all other elements""" + # Bring title/logo to the very front + title_items = self.bg_canvas.find_withtag('title') + for item in title_items: + self.bg_canvas.tag_raise(item) + + # Also bring HR logo to front (but below title) + hr_items = self.bg_canvas.find_withtag('hr_logo') + for item in hr_items: + self.bg_canvas.tag_raise(item) + + # Make sure title is above HR logo + for item in title_items: + self.bg_canvas.tag_raise(item) def start_loading_sequence(self): """Start the loading animation sequence""" @@ -314,26 +562,22 @@ class LoadingScreen: # Start the smooth animation to move title to top self.animate_title_to_top_smooth() - # After animation completes, check expansions - self.loading_window.after(1500, self.stage_3_check_subscriptions) # Updated stage number + # After animation completes, check subscriptions + self.loading_window.after(1500, self.stage_3_check_subscriptions) def animate_title_to_top_smooth(self): - """Smoothly animate the title moving to the top of the screen""" + """Smoothly animate the title moving to the top of the screen using canvas""" # Get current window dimensions - window_width = self.loading_window.winfo_width() - window_height = self.loading_window.winfo_height() + 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() # If window dimensions aren't available yet, wait and try again if window_width <= 1 or window_height <= 1: self.loading_window.after(100, self.animate_title_to_top_smooth) return - # Remove from pack layout and switch to place for animation - self.title_label.pack_forget() - # Get current title image dimensions - if hasattr(self, 'game_title_tk') and self.game_title_tk: - # Calculate current and target sizes + if hasattr(self, 'game_title_img') and self.game_title_img: current_img = self.game_title_img current_width, current_height = current_img.size @@ -341,31 +585,27 @@ class LoadingScreen: target_height = 80 target_width = int((target_height / current_height) * current_width) - # Animation parameters - start_x = window_width // 2 + # Animation parameters (using pixel positions) start_y = window_height // 2 - target_x = window_width // 2 - target_y = 60 # Top position + target_y = int(window_height * 0.08) # Start the smooth animation - self.animate_title_step(0, 30, start_x, start_y, target_x, target_y, + self.animate_title_step(0, 30, start_y, target_y, current_width, current_height, target_width, target_height) else: # Fallback for text version self.animate_text_title_smooth() - def animate_title_step(self, step, total_steps, start_x, start_y, target_x, target_y, + def animate_title_step(self, step, total_steps, start_y, target_y, start_width, start_height, target_width, target_height): - """Perform one step of the title animation""" + """Perform one step of the title animation using canvas""" if step <= total_steps: # Calculate progress (0.0 to 1.0) progress = step / total_steps - # Use easing function for smooth animation (ease-out) eased_progress = 1 - (1 - progress) ** 3 # Calculate current position - current_x = start_x + (target_x - start_x) * eased_progress current_y = start_y + (target_y - start_y) * eased_progress # Calculate current size @@ -378,53 +618,54 @@ class LoadingScreen: (int(current_width), int(current_height)), Image.Resampling.LANCZOS ) - current_tk_img = ImageTk.PhotoImage(resized_img) - self.title_label.configure(image=current_tk_img) - # Keep reference to prevent garbage collection - self.title_label.image = current_tk_img - - # Position the label - self.title_label.place(x=current_x, y=current_y, anchor='center') + self.current_title_tk = ImageTk.PhotoImage(resized_img) + + # Update canvas image + window_width = self.canvas_width if hasattr(self, 'canvas_width') else self.loading_window.winfo_width() + self.bg_canvas.delete('title') + self.bg_canvas.create_image(window_width//2, int(current_y), + image=self.current_title_tk, + anchor='center', tags='title') # Schedule next step self.loading_window.after(50, lambda: self.animate_title_step( - step + 1, total_steps, start_x, start_y, target_x, target_y, + step + 1, total_steps, start_y, target_y, start_width, start_height, target_width, target_height )) else: - # Animation complete - finalize position and switch back to pack + # Animation complete - finalize position self.finalize_title_position() def animate_text_title_smooth(self): """Animate text title if image is not available""" - window_width = self.loading_window.winfo_width() - window_height = self.loading_window.winfo_height() - - start_x = window_width // 2 + window_height = self.canvas_height if hasattr(self, 'canvas_height') else self.loading_window.winfo_height() start_y = window_height // 2 - target_x = window_width // 2 - target_y = 60 + target_y = int(window_height * 0.08) # Animate text title position and size - self.animate_text_step(0, 30, start_x, start_y, target_x, target_y, 24, 16) + self.animate_text_step(0, 30, start_y, target_y, 24, 16) - def animate_text_step(self, step, total_steps, start_x, start_y, target_x, target_y, + def animate_text_step(self, step, total_steps, start_y, target_y, start_size, target_size): - """Animate text title step by step""" + """Animate text title step by step using canvas""" if step <= total_steps: progress = step / total_steps eased_progress = 1 - (1 - progress) ** 3 - current_x = start_x + (target_x - start_x) * eased_progress current_y = start_y + (target_y - start_y) * eased_progress - current_size = start_size + (target_size - start_size) * eased_progress + current_size = int(start_size + (target_size - start_size) * eased_progress) - # Update font size - self.title_label.configure(font=self.get_font(int(current_size), 'bold')) - self.title_label.place(x=current_x, y=current_y, anchor='center') + # Update canvas text + window_width = self.canvas_width if hasattr(self, 'canvas_width') else self.loading_window.winfo_width() + 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'), + anchor='center', tags='title') self.loading_window.after(50, lambda: self.animate_text_step( - step + 1, total_steps, start_x, start_y, target_x, target_y, + step + 1, total_steps, start_y, target_y, start_size, target_size )) else: @@ -432,39 +673,33 @@ class LoadingScreen: def finalize_title_position(self): """Finalize the title position after animation and show content""" - # Remove from place and switch back to pack layout - self.title_label.place_forget() + 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() # Set final image size - if hasattr(self, 'game_title_img'): + if hasattr(self, 'game_title_img') and self.game_title_img: final_height = 80 final_width = int((final_height / self.game_title_img.size[1]) * self.game_title_img.size[0]) final_img = self.game_title_img.resize((final_width, final_height), Image.Resampling.LANCZOS) self.small_title_tk = ImageTk.PhotoImage(final_img) - self.title_label.configure(image=self.small_title_tk) + + # Position title at top center + self.bg_canvas.delete('title') + self.bg_canvas.create_image(window_width//2, int(window_height * 0.08), + image=self.small_title_tk, + anchor='center', tags='title') else: - self.title_label.configure(font=self.get_font(16, 'bold')) - - # Pack at top - self.title_label.pack(side=tk.TOP, pady=20) - - # Show content frame - self.content_frame.pack(fill=tk.BOTH, expand=True, padx=50, pady=20) + 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'), + anchor='center', tags='title') def stage_3_check_subscriptions(self): - """Stage 3: Check Steam Workshop subscriptions (renamed from stage_4)""" - # Show expansion check frame - self.expansion_frame.pack(fill=tk.BOTH, expand=True) - - # Add title - check_title = tk.Label(self.expansion_frame, - text="Checking RimWorld Expansions...", - fg='white', bg='#2b2b2b', - font=self.get_font(18, 'bold')) - check_title.pack(pady=20) - - # Check expansions in a separate thread - threading.Thread(target=self.check_expansions_thread, daemon=True).start() + """Stage 3: Show Steam Workshop subscriptions directly""" + # Skip expansion check and go directly to subscription stage + self.show_subscription_stage() def check_expansions_thread(self): """Check which expansions are owned""" @@ -616,68 +851,95 @@ class LoadingScreen: return expansion_id def show_expansion_results(self): - """Show the results of expansion checking""" - # Clear expansion frame - for widget in self.expansion_frame.winfo_children(): - widget.destroy() + """Show the results of expansion checking using canvas with proper backgrounds""" + # Clear previous stage content + self.bg_canvas.delete('stage_content') + + # Track current stage for responsive updates + self.current_stage_method = self.show_expansion_results + + 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() missing_expansions = [name for name, owned in self.expansion_check_results.items() if not owned] if not missing_expansions: - # All expansions owned, proceed to subscription check - success_label = tk.Label(self.expansion_frame, - text="✓ All RimWorld expansions detected!", - fg='#00ff00', bg='#2b2b2b', - font=self.get_font(16, 'bold')) - success_label.pack(pady=20) + # 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) + + 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'), + anchor='center', tags='stage_content') self.loading_window.after(2000, self.stage_6_final_confirmation) else: - # Show missing expansions - error_title = tk.Label(self.expansion_frame, - text=f"You don't own {', '.join(missing_expansions)}!", - fg='#ff4444', bg='#2b2b2b', - font=self.get_font(16, 'bold')) - error_title.pack(pady=20) + # 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) - # Show expansion images side by side - images_frame = tk.Frame(self.expansion_frame, bg='#2b2b2b') - images_frame.pack(pady=20) + 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'), + anchor='center', tags='stage_content') - for expansion in missing_expansions: + # 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 + 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: - img_label = tk.Label(images_frame, - image=self.expansion_images[expansion], - bg='#2b2b2b') - img_label.pack(side=tk.LEFT, padx=10) + self.bg_canvas.create_image(img_x, img_y, + image=self.expansion_images[expansion], + anchor='center', tags='stage_content') # Cannot continue message - cannot_continue = tk.Label(self.expansion_frame, - text="Cannot continue!", - fg='#ff4444', bg='#2b2b2b', - font=self.get_font(14, 'bold')) - cannot_continue.pack(pady=20) + 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 - exit_btn = tk.Button(self.expansion_frame, - text="Exit Application", - command=self.exit_application, - bg='#ff4444', fg='white', - font=self.get_font(12, 'bold'), - padx=30, pady=10) - exit_btn.pack(pady=20) + # 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) - def stage_3_check_subscriptions(self): - """Stage 3: Check Steam Workshop subscriptions""" - # Show subscription check frame - self.subscription_frame.pack(fill=tk.BOTH, expand=True) + def show_subscription_stage(self): + """Show the subscription check stage using canvas""" + # Clear previous stage content and background images + self.bg_canvas.delete('stage_content') + self._text_bg_images = [] # Clear old background images - # Add title - sub_title = tk.Label(self.subscription_frame, - text="Steam Workshop Collections", - fg='white', bg='#2b2b2b', - font=self.get_font(18, 'bold')) - sub_title.pack(pady=20) + # Track current stage for responsive updates + self.current_stage_method = self.show_subscription_stage + + 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) + + self.bg_canvas.create_text(window_width//2, int(window_height * 0.18), + text="Steam Workshop Collections", + fill='white', + font=self.get_font(18, 'bold'), + anchor='center', tags='stage_content') # Collection checkboxes collections = [ @@ -687,50 +949,134 @@ class LoadingScreen: ] self.subscription_vars = {} + self.checkbox_ids = {} + y_pos = int(window_height * 0.28) for name, url in collections: - frame = tk.Frame(self.subscription_frame, bg='#2b2b2b') - frame.pack(pady=10, padx=50, fill='x') - var = tk.BooleanVar() self.subscription_vars[name] = var - # Create a frame for checkbox and link - checkbox_frame = tk.Frame(frame, bg='#2b2b2b') - checkbox_frame.pack(fill='x') + # Create custom checkbox using canvas (no white background) + checkbox_x = window_width//2 - 300 - checkbox = tk.Checkbutton(checkbox_frame, - text=f"I have subscribed to the ", - variable=var, - fg='white', bg='#2b2b2b', - selectcolor='#404040', - font=self.get_font(12), - activebackground='#2b2b2b', - activeforeground='white') - checkbox.pack(side='left', anchor='w') + # 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}') + ) - # Create clickable link for collection name - collection_link = tk.Label(checkbox_frame, - text=name, - fg='#4da6ff', # Light blue color for links - bg='#2b2b2b', - font=self.get_font(12, 'bold'), - cursor='hand2') - collection_link.pack(side='left') + # Create invisible clickable area around checkbox (larger for easier clicking) + click_area = self.bg_canvas.create_rectangle( + checkbox_x - 20, y_pos - 20, checkbox_x + 20, y_pos + 20, + outline='', fill='', tags=('stage_content', f'checkbox_{name}') + ) - # Bind click event to open collection URL - collection_link.bind('', lambda e, url=url: self.open_collection_url(url)) - collection_link.bind('', lambda e, label=collection_link: self.on_link_enter(label)) - collection_link.bind('', lambda e, label=collection_link: self.on_link_leave(label)) + # Store checkbox info for click handling + self.checkbox_ids[name] = {'box': box_id, 'check': None, 'var': var} + + # Bind click event to toggle checkbox + self.bg_canvas.tag_bind(f'checkbox_{name}', '', + lambda e, n=name: self.toggle_checkbox(n)) + + # Create text label next to checkbox + text_x = checkbox_x + 25 + self.bg_canvas.create_text(text_x, y_pos, + text="I have subscribed to ", + fill='white', + 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 + link_x = text_x + text_width + link_id = self.bg_canvas.create_text(link_x, y_pos, + text=name, + fill='#4da6ff', + 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 - # Continue button - continue_btn = tk.Button(self.subscription_frame, - text="Continue", - command=self.check_all_subscribed, - bg='#0078d4', fg='white', - font=self.get_font(12, 'bold'), - padx=30, pady=10) - continue_btn.pack(pady=30) + # Continue button - create custom button with transparent corners + self.create_canvas_button(window_width//2, y_pos + 40, "Continue", + self.check_all_subscribed, '#0078d4') + + # Ensure logo stays on top + self._bring_logo_to_front() + + def toggle_checkbox(self, name): + """Toggle a checkbox state""" + info = self.checkbox_ids[name] + 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 + + if var.get(): + # Draw checkmark + if info['check'] is None: + check_id = self.bg_canvas.create_text(checkbox_x, y_pos, + text="✓", + fill='#00ff00', + font=self.get_font(14, 'bold'), + anchor='center', tags='stage_content') + self.checkbox_ids[name]['check'] = check_id + else: + # Remove checkmark + if info['check'] is not None: + self.bg_canvas.delete(info['check']) + 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) + 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 + self.bg_canvas.tag_bind(safe_tag, '', lambda e: command()) + self.bg_canvas.tag_bind(btn_id, '', lambda e: command()) + + return btn_id def check_all_subscribed(self): """Check if all collections are subscribed""" @@ -743,18 +1089,44 @@ class LoadingScreen: # All subscribed, proceed to RimWorld download check self.stage_4_rimworld_check() + def stage_5_check_expansions(self): + """Stage 5: Check RimWorld Expansions after RimWorld setup verification""" + # Clear previous stage content + self.bg_canvas.delete('stage_content') + + # Track current stage for responsive updates + self.current_stage_method = self.stage_5_check_expansions + + 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.4), 400)) # 40% of height, min 200px, max 400px + self.create_text_background(window_width//2, int(window_height * 0.4), panel_width, panel_height) + + # Create title as canvas text + self.bg_canvas.create_text(window_width//2, int(window_height * 0.18), + text="Checking RimWorld Expansions...", + fill='white', + font=self.get_font(18, 'bold'), + anchor='center', tags='stage_content') + + # Check expansions in a separate thread + threading.Thread(target=self.check_expansions_thread, daemon=True).start() + def show_subscription_warning(self): """Show subscription warning in custom styled window""" - # Create warning window - warning_window = tk.Toplevel(self.loading_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(bg='#2b2b2b') + warning_window.configure(fg_color='#2b2b2b') # Set window icon try: icon_path = get_resource_path(os.path.join("art", "Progression.ico")) if os.path.exists(icon_path): - warning_window.iconbitmap(icon_path) + warning_window.after(200, lambda: warning_window.iconbitmap(icon_path)) except Exception: pass # Ignore icon loading errors for popup windows @@ -778,24 +1150,9 @@ To subscribe to a collection: All collections are required for the complete Progression experience.""" - # Create a temporary label to measure text size - temp_label = tk.Label(warning_window, - text=warning_text, - font=self.get_font(11), - justify='left', - wraplength=550) - temp_label.update_idletasks() - - # Get the required text dimensions - text_width = temp_label.winfo_reqwidth() - text_height = temp_label.winfo_reqheight() - - # Destroy temporary label - temp_label.destroy() - - # Calculate window size (25% larger than text + padding for title and button) - window_width = int(text_width * 1.25) + 60 # 25% larger + padding - window_height = int(text_height * 1.25) + 140 # 25% larger + space for title and button + # Calculate window size + window_width = 650 + window_height = 500 # Set the calculated window size warning_window.geometry(f"{window_width}x{window_height}") @@ -807,57 +1164,59 @@ All collections are required for the complete Progression experience.""" warning_window.geometry(f"{window_width}x{window_height}+{x}+{y}") # Title - title_label = tk.Label(warning_window, + title_label = ctk.CTkLabel(warning_window, text="⚠️ Subscription Required", - fg='#ff8686', bg='#2b2b2b', - font=self.get_font(16, 'bold')) + text_color='#ff8686', fg_color='transparent', + font=self.get_ctk_font(16, 'bold')) title_label.pack(pady=20) # Warning label with proper sizing - warning_label = tk.Label(warning_window, + warning_label = ctk.CTkLabel(warning_window, text=warning_text, - fg='#cccccc', bg='#2b2b2b', - font=self.get_font(11), + text_color='#cccccc', fg_color='transparent', + font=self.get_ctk_font(11), justify='left', - wraplength=text_width) # Use measured width + wraplength=550) warning_label.pack(pady=20, padx=30, expand=True) # Close button - close_btn = tk.Button(warning_window, + close_btn = ctk.CTkButton(warning_window, text="I Understand", command=warning_window.destroy, - bg='#ff8686', fg='white', - font=self.get_font(12, 'bold'), - padx=30, pady=10) + fg_color='#ff8686', text_color='white', + font=self.get_ctk_font(12, 'bold')) close_btn.pack(pady=20) def stage_4_rimworld_check(self): - """Stage 4: Check RimWorld download and launch status""" - # Hide subscription frame, show RimWorld check frame - self.subscription_frame.pack_forget() + """Stage 4: Check RimWorld download and launch status using canvas""" + # Clear previous stage content + self.bg_canvas.delete('stage_content') - # Create RimWorld check frame - self.rimworld_check_frame = tk.Frame(self.content_frame, bg='#2b2b2b') - self.rimworld_check_frame.pack(fill=tk.BOTH, expand=True) + # Track current stage for responsive updates + self.current_stage_method = self.stage_4_rimworld_check - # Add title - check_title = tk.Label(self.rimworld_check_frame, - text="RimWorld Setup Verification", - fg='white', bg='#2b2b2b', - font=self.get_font(18, 'bold')) - check_title.pack(pady=30) + 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) + + self.bg_canvas.create_text(window_width//2, int(window_height * 0.18), + text="RimWorld Setup Verification", + fill='white', + font=self.get_font(18, 'bold'), + anchor='center', tags='stage_content') # Add instructions - instructions = tk.Label(self.rimworld_check_frame, - text="Please ensure the following before continuing:", - fg='#cccccc', bg='#2b2b2b', - font=self.get_font(14)) - instructions.pack(pady=10) - - # Create checklist - checklist_frame = tk.Frame(self.rimworld_check_frame, bg='#2b2b2b') - checklist_frame.pack(pady=20) + self.bg_canvas.create_text(window_width//2, int(window_height * 0.25), + text="Please ensure the following before continuing:", + fill='#cccccc', + font=self.get_font(14), + anchor='center', tags='stage_content') + # Checklist items checklist_items = [ "✓ RimWorld has finished downloading completely", "✓ You have launched RimWorld at least once", @@ -865,61 +1224,53 @@ All collections are required for the complete Progression experience.""" "✓ All required DLCs are installed and activated" ] + y_pos = int(window_height * 0.32) for item in checklist_items: - item_label = tk.Label(checklist_frame, - text=item, - fg='#00ff00', bg='#2b2b2b', - font=self.get_font(12), - anchor='w') - item_label.pack(fill='x', pady=5, padx=50) + self.bg_canvas.create_text(window_width//2, y_pos, + text=item, + fill='#00ff00', + font=self.get_font(12), + anchor='center', tags='stage_content') + y_pos += 30 # Add confirmation message - confirm_message = tk.Label(self.rimworld_check_frame, - text="Have you completed all the above requirements?", - fg='white', bg='#2b2b2b', - font=self.get_font(14, 'bold')) - confirm_message.pack(pady=30) + 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'), + anchor='center', tags='stage_content') - # Buttons frame - buttons_frame = tk.Frame(self.rimworld_check_frame, bg='#2b2b2b') - buttons_frame.pack(pady=20) + # 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') - # Yes Mother button - yes_btn = tk.Button(buttons_frame, - text="Yes Mother", - command=self.proceed_to_dlc_check, - bg='#00aa00', fg='white', - font=self.get_font(12, 'bold'), - padx=30, pady=10) - yes_btn.pack(side=tk.LEFT, padx=10) + # 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') - # Not Yet button - not_yet_btn = tk.Button(buttons_frame, - text="Not Yet", - command=self.show_setup_instructions, - bg='#aa6600', fg='white', - font=self.get_font(12, 'bold'), - padx=30, pady=10) - not_yet_btn.pack(side=tk.LEFT, padx=10) + # Ensure logo stays on top + self._bring_logo_to_front() def proceed_to_dlc_check(self): """Proceed to DLC check after RimWorld verification""" - # Hide RimWorld check frame and proceed to expansion check - self.rimworld_check_frame.pack_forget() + # Clear stage content and proceed to expansion check + self.bg_canvas.delete('stage_content') + self._text_bg_images = [] + self._button_images = [] self.stage_5_check_expansions() def show_setup_instructions(self): """Show setup instructions if user is not ready""" - # Create instruction window - instruction_window = tk.Toplevel(self.loading_window) + # 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(bg='#2b2b2b') + instruction_window.configure(fg_color='#2b2b2b') # Set window icon try: icon_path = get_resource_path(os.path.join("art", "Progression.ico")) if os.path.exists(icon_path): - instruction_window.iconbitmap(icon_path) + instruction_window.after(200, lambda: instruction_window.iconbitmap(icon_path)) except Exception: pass # Ignore icon loading errors for popup windows @@ -949,24 +1300,9 @@ All collections are required for the complete Progression experience.""" Once completed, return to this screen and click "Yes Mother".""" - # Create a temporary label to measure text size - temp_label = tk.Label(instruction_window, - text=instructions_text, - font=self.get_font(11), - justify='left', - wraplength=550) - temp_label.update_idletasks() - - # Get the required text dimensions - text_width = temp_label.winfo_reqwidth() - text_height = temp_label.winfo_reqheight() - - # Destroy temporary label - temp_label.destroy() - - # Calculate window size (25% larger than text + padding for title and button) - window_width = int(text_width * 1.25) + 60 # 25% larger + padding - window_height = int(text_height * 1.25) + 140 # 25% larger + space for title and button + # Calculate window size + window_width = 650 + window_height = 550 # Set the calculated window size instruction_window.geometry(f"{window_width}x{window_height}") @@ -978,86 +1314,71 @@ Once completed, return to this screen and click "Yes Mother".""" instruction_window.geometry(f"{window_width}x{window_height}+{x}+{y}") # Title - title_label = tk.Label(instruction_window, + title_label = ctk.CTkLabel(instruction_window, text="Progression: Loader - Setup Instructions", - fg='white', bg='#2b2b2b', - font=self.get_font(16, 'bold')) + text_color='white', fg_color='transparent', + font=self.get_ctk_font(16, 'bold')) title_label.pack(pady=20) # Instructions label with proper sizing - instructions_label = tk.Label(instruction_window, + instructions_label = ctk.CTkLabel(instruction_window, text=instructions_text, - fg='#cccccc', bg='#2b2b2b', - font=self.get_font(11), + text_color='#cccccc', fg_color='transparent', + font=self.get_ctk_font(11), justify='left', - wraplength=text_width) # Use measured width + wraplength=550) instructions_label.pack(pady=20, padx=30, expand=True) # Close button - close_btn = tk.Button(instruction_window, + close_btn = ctk.CTkButton(instruction_window, text="I Understand", command=instruction_window.destroy, - bg='#0078d4', fg='white', - font=self.get_font(12, 'bold'), - padx=30, pady=10) + fg_color='#0078d4', text_color='white', + font=self.get_ctk_font(12, 'bold')) close_btn.pack(pady=20) - def stage_5_check_expansions(self): - """Stage 5: Check for owned expansions (renamed from stage_3)""" - # Show expansion check frame - self.expansion_frame.pack(fill=tk.BOTH, expand=True) - - # Add title - check_title = tk.Label(self.expansion_frame, - text="Checking RimWorld Expansions...", - fg='white', bg='#2b2b2b', - font=self.get_font(18, 'bold')) - check_title.pack(pady=20) - - # Check expansions in a separate thread - threading.Thread(target=self.check_expansions_thread, daemon=True).start() - def stage_6_final_confirmation(self): - """Stage 6: Final confirmation screen (renamed from stage_5)""" - # Hide subscription frame, show final frame - self.subscription_frame.pack_forget() - self.final_frame.pack(fill=tk.BOTH, expand=True) + """Stage 6: Final confirmation screen using canvas""" + # Clear previous stage content + self.bg_canvas.delete('stage_content') + self._text_bg_images = [] # Clear old background images + self._button_images = [] + + # Track current stage for responsive updates + self.current_stage_method = self.stage_6_final_confirmation + + 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.4), panel_width, panel_height) # Final message - final_title = tk.Label(self.final_frame, - text="Ready to Create Mod List", - fg='white', bg='#2b2b2b', - font=self.get_font(18, 'bold')) - final_title.pack(pady=30) + 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'), + anchor='center', tags='stage_content') - final_message = tk.Label(self.final_frame, - 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?", - fg='#cccccc', bg='#2b2b2b', - font=self.get_font(14), - justify='center') - final_message.pack(pady=20) + 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', + font=self.get_font(14), + justify='center', + anchor='center', tags='stage_content') - # Buttons frame - buttons_frame = tk.Frame(self.final_frame, bg='#2b2b2b') - buttons_frame.pack(pady=30) + # 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) - # Continue button - continue_btn = tk.Button(buttons_frame, - text="Yes, Continue", - command=self.complete_loading, - bg='#00aa00', fg='white', - font=self.get_font(12, 'bold'), - padx=30, pady=10) - continue_btn.pack(side=tk.LEFT, padx=10) + # 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) - # Exit button - exit_btn = tk.Button(buttons_frame, - text="No, Exit", - command=self.exit_application, - bg='#aa0000', fg='white', - font=self.get_font(12, 'bold'), - padx=30, pady=10) - exit_btn.pack(side=tk.LEFT, padx=10) + # Ensure logo stays on top + self._bring_logo_to_front() def open_hr_website(self, event=None): """Open Hudson Riggs Systems website""" @@ -1066,17 +1387,12 @@ Once completed, return to this screen and click "Yes Mother".""" def on_loading_hr_enter(self, event=None): """Handle mouse enter on HR logo in loading screen""" - if hasattr(self, 'hr_logo_glow_tk'): - self.hr_logo_label.configure(image=self.hr_logo_glow_tk) - else: - self.hr_logo_label.configure(fg='#ff8686', bg='#404040') + # Change cursor to hand and highlight + self.bg_canvas.config(cursor='hand2') def on_loading_hr_leave(self, event=None): """Handle mouse leave on HR logo in loading screen""" - if hasattr(self, 'hr_logo_tk'): - self.hr_logo_label.configure(image=self.hr_logo_tk) - else: - self.hr_logo_label.configure(fg='#888888', bg='#2b2b2b') + self.bg_canvas.config(cursor='') def open_collection_url(self, url): """Open Steam Workshop collection URL in browser""" @@ -1114,6 +1430,12 @@ class SteamWorkshopGUI: self.root.state('zoomed') # Make fullscreen on Windows self.root.configure(bg='#2b2b2b') + # Set minimum window size for responsive design and enforce it always + self.root.minsize(800, 600) + + # Bind to enforce minimum size continuously + self.root.bind('', self._enforce_minimum_size) + # Load custom font self.load_custom_font() @@ -1131,31 +1453,48 @@ class SteamWorkshopGUI: self.merge_btn = None self.is_rimworld_valid = False - # Create main frame - main_frame = tk.Frame(root, bg='#2b2b2b') - main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + # Store references for responsive layout + self.left_frame = None + self.right_frame = None + self.content_frame = None + self.padded_frame = None + + # Create canvas for background image + self.bg_canvas = tk.Canvas(root, highlightthickness=0, bg='#2b2b2b') + self.bg_canvas.pack(fill=tk.BOTH, expand=True) + + # Load background image + self.root.update_idletasks() + self._update_main_background() + + # 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') + + # 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) # Create header frame for progression logo - self.create_header_frame(main_frame) + self.create_header_frame(self.padded_frame) - # Create content frame (below header) - content_frame = tk.Frame(main_frame, bg='#2b2b2b') - content_frame.pack(fill=tk.BOTH, expand=True, pady=(10, 0)) + # 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)) - # Left panel for inputs - left_frame = tk.Frame(content_frame, bg='#2b2b2b', width=400) - left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) - left_frame.pack_propagate(False) - - # Right panel for output - reduced width to accommodate HR logo - right_frame = tk.Frame(content_frame, bg='#2b2b2b') - right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(0, 120)) # Added right padding for logo space + # Create responsive layout + self._create_responsive_layout() # Create footer frame for HR Systems logo - self.create_footer_frame(main_frame) + self.create_footer_frame(self.padded_frame) - self.create_input_section(left_frame) - self.create_output_section(right_frame) + self.create_input_section(self.left_frame) + self.create_output_section(self.right_frame) + + # Place main content on canvas + self._position_main_content() # Log initial message self.log_to_output("🔍 Please provide a valid RimWorld installation path to continue.\n") @@ -1173,10 +1512,208 @@ class SteamWorkshopGUI: # Start blinking animation for RimWorld label (with slight delay to ensure GUI is ready) self.root.after(1000, self.start_rimworld_label_blink) + + def _enforce_minimum_size(self, event): + """Enforce minimum window size at all times""" + if event.widget == self.root: + width = self.root.winfo_width() + height = self.root.winfo_height() + + # Check if window is smaller than minimum + if width < 800 or height < 600: + # Resize to minimum if too small + new_width = max(width, 800) + new_height = max(height, 600) + self.root.geometry(f"{new_width}x{new_height}") + + def _create_responsive_layout(self): + """Create responsive layout that adapts to window size""" + # 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: + self._create_side_by_side_layout() + else: + self._create_stacked_layout() + + def _create_side_by_side_layout(self): + """Create side-by-side layout for wide screens""" + # Clear existing layout + for widget in self.content_frame.winfo_children(): + widget.destroy() + + # 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) + + # 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)) + + def _create_stacked_layout(self): + """Create stacked layout for narrow screens""" + # Clear existing layout + for widget in self.content_frame.winfo_children(): + widget.destroy() + + # 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) + + # 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) + + def _update_responsive_layout(self): + """Update layout based on current window size""" + if hasattr(self, 'content_frame') and self.content_frame.winfo_exists(): + # Store current content before recreating layout + input_content = None + output_content = None + + # Recreate responsive layout + self._create_responsive_layout() + + # Recreate content in new layout + if hasattr(self, 'left_frame') and self.left_frame: + self.create_input_section(self.left_frame) + if hasattr(self, 'right_frame') and self.right_frame: + self.create_output_section(self.right_frame) + + def _update_main_background(self): + """Update the main window background image""" + width = self.root.winfo_width() or 1920 + height = self.root.winfo_height() or 1080 + if width > 1 and height > 1: + self.bg_image_tk = self.load_background_image(width, height) + if self.bg_image_tk: + self.bg_canvas.delete('bg') + self.bg_canvas.create_image(0, 0, image=self.bg_image_tk, anchor='nw', tags='bg') + self.bg_canvas.tag_lower('bg') + + def _position_main_content(self): + """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 + + 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') + + def load_background_image(self, width, height): + """Load and scale the Desolation background image to fit the given dimensions""" + 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) + except Exception as e: + print(f"Error loading background image: {e}") + return None + + def _on_main_resize(self, event): + """Handle window resize to update background image and responsive layout""" + if event.widget == self.root and hasattr(self, 'bg_canvas'): + # First enforce minimum size + self._enforce_minimum_size(event) + + # 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 + + # Store current window size + self._last_window_size = (self.root.winfo_width(), self.root.winfo_height()) + + # Update content positioning + self._position_main_content() + + def _on_window_map(self, event): + """Handle window map event (window becomes visible)""" + if event.widget == self.root: + # Window is now visible - update layout if needed + self.root.after(100, self._check_window_state) + + def _on_window_unmap(self, event): + """Handle window unmap event (window becomes hidden)""" + pass + + def _check_window_state(self): + """Check and update window state tracking""" + try: + # Check if window is maximized or fullscreen + state = self.root.state() + if state == 'zoomed': + if not getattr(self, '_is_maximized', False): + self._is_maximized = True + self._on_window_state_change('maximized') + elif state == 'normal': + if getattr(self, '_is_maximized', False): + self._is_maximized = False + self._on_window_state_change('normal') + except Exception: + pass # Ignore state check errors + + def _on_window_state_change(self, new_state): + """Handle window state changes (maximized, normal, etc.)""" + # Update responsive layout when window state changes + self.root.after(200, self._update_responsive_layout) def create_header_frame(self, parent): - """Create header frame with progression logo""" - header_frame = tk.Frame(parent, bg='#2b2b2b', height=100) + """Create header frame with progression logo - responsive sizing""" + # Responsive header height based on window size + 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) @@ -1188,11 +1725,11 @@ class SteamWorkshopGUI: try: # Load the game title image as progression logo progression_img = Image.open(get_resource_path("art/GameTitle.png")) - # Resize to fit header + # Responsive logo sizing img_width, img_height = progression_img.size - new_height = 80 - new_width = int((new_height / img_height) * img_width) - progression_img = progression_img.resize((new_width, new_height), Image.Resampling.LANCZOS) + 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, @@ -1201,16 +1738,21 @@ class SteamWorkshopGUI: progression_label.pack(padx=10, pady=10) except Exception as e: - # Fallback text if image loading fails + # 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(20, 'bold')) + font=self.get_font(font_size, 'bold')) progression_label.pack(padx=20, pady=30) def create_footer_frame(self, parent): - """Create footer frame with HR Systems logo in bottom right""" - self.footer_frame = tk.Frame(parent, bg='#2b2b2b', height=100) # Increased height for better visibility + """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) @@ -1218,19 +1760,27 @@ class SteamWorkshopGUI: self.create_hr_logo(self.footer_frame) def create_hr_logo(self, parent): - """Create clickable HR Systems logo with radial glow hover effects""" + """Create clickable HR Systems logo with radial glow hover effects - responsive sizing""" + # Responsive positioning and sizing + window_width = self.root.winfo_width() or 1920 + window_height = self.root.winfo_height() or 1080 + + # 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 + # Container for logo positioned in bottom right with extra space for glow logo_frame = tk.Frame(parent, bg='#2b2b2b') - logo_frame.pack(side=tk.RIGHT, anchor='se', padx=40, pady=20) # Increased padding for better visibility + logo_frame.pack(side=tk.RIGHT, anchor='se', padx=padding_x, pady=padding_y) try: # Load HR Systems logo hr_img = Image.open(get_resource_path("art/hudsonriggssystems.png")) - # Resize to appropriate size for footer - slightly larger for better visibility + # Responsive logo sizing img_width, img_height = hr_img.size - new_height = 60 # Increased from 50 to 60 - new_width = int((new_height / img_height) * img_width) - hr_img = hr_img.resize((new_width, new_height), Image.Resampling.LANCZOS) + logo_height = max(40, min(80, int(window_height * 0.06))) # 6% of height, min 40px, max 80px + logo_width = int((logo_height / img_height) * img_width) + hr_img = hr_img.resize((logo_width, logo_height), Image.Resampling.LANCZOS) self.hr_logo_normal = ImageTk.PhotoImage(hr_img) # Create radial glow effect version @@ -1238,9 +1788,9 @@ class SteamWorkshopGUI: # Create clickable label with fixed size to prevent position changes # Calculate the glow image size to set fixed dimensions - glow_padding = 20 - label_width = new_width + (glow_padding * 2) - label_height = new_height + (glow_padding * 2) + glow_padding = max(15, min(25, int(logo_height * 0.4))) # Responsive glow padding + label_width = logo_width + (glow_padding * 2) + label_height = logo_height + (glow_padding * 2) self.hr_logo_label = tk.Label(logo_frame, image=self.hr_logo_normal, @@ -1257,11 +1807,12 @@ class SteamWorkshopGUI: self.hr_logo_label.bind('', self.on_hr_logo_leave) except Exception as e: - # Fallback text link if image loading fails + # Fallback text link if image loading fails - responsive font size + 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(10), + font=self.get_font(font_size), cursor='hand2') self.hr_logo_label.pack() @@ -1425,6 +1976,17 @@ class SteamWorkshopGUI: # Fall back to system font return font.Font(family='Arial', size=size, weight=weight) + + def get_ctk_font(self, size=10, weight='normal'): + """Get CTkFont for UI elements""" + if self.custom_font_available and self.custom_font_family: + try: + return ctk.CTkFont(family=self.custom_font_family, size=size, weight=weight) + except Exception: + pass + + # Fall back to system font + return ctk.CTkFont(family='Arial', size=size, weight=weight) def setup_dark_theme(self): """Configure dark theme colors""" @@ -1543,13 +2105,13 @@ class SteamWorkshopGUI: relief='flat', padx=30, pady=12, state='disabled') self.merge_btn.pack(pady=(10, 30)) - def create_url_input(self, parent, label_text, default_url): - """Create a labeled URL input field""" - label = tk.Label(parent, text=label_text, font=self.get_font(10, 'bold'), + 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)) - entry = tk.Entry(parent, font=self.get_font(9), bg='#404040', fg='#ffffff', + 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)) entry.insert(0, default_url) @@ -1562,14 +2124,19 @@ class SteamWorkshopGUI: self.url_entries[collection_name] = entry def create_output_section(self, parent): - """Create the right output section""" + """Create the right output section with responsive sizing""" + # 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(14, 'bold'), bg='#2b2b2b', fg='#ffffff') + font=self.get_font(title_font_size, 'bold'), bg='#2b2b2b', fg='#ffffff') title_label.pack(pady=(0, 10)) - # Output text area with scrollbar + # Output text area with scrollbar - responsive sizing self.output_text = scrolledtext.ScrolledText(parent, - font=self.get_font(10), + font=self.get_font(text_font_size), bg='#1e1e1e', fg='#ffffff', insertbackground='#ffffff', selectbackground='#404040', @@ -2524,6 +3091,50 @@ class SteamWorkshopGUI: self._safe_update_output(f"Error fetching collection: {str(e)}\n") return [] + def _create_success_text_background(self, x, y, width, height, opacity=0.75, corner_radius=20): + """Create a semi-transparent rounded rectangle background for success text + Width and height are automatically increased by 10%""" + print(f"Creating success background at {x}, {y} with size {width}x{height}") + + # 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)) + + # Convert to PhotoImage + bg_tk = ImageTk.PhotoImage(bg_img) + + # Store reference to prevent garbage collection + if not hasattr(self, '_success_bg_images'): + self._success_bg_images = [] + self._success_bg_images.append(bg_tk) + + # Create canvas image + img_id = self.success_bg_canvas.create_image(x, y, image=bg_tk, anchor='center', tags='success_text') + + # Make sure it's behind text but above background + self.success_bg_canvas.tag_lower(img_id, 'success_text') + + print(f"Created success background with ID: {img_id}") + return img_id + def show_success_animation(self, mod_list_name): """Show success animation with logo returning to center and instructions""" # Close the main creator window @@ -2546,9 +3157,25 @@ class SteamWorkshopGUI: self.success_window.state('zoomed') # Fullscreen self.success_window.protocol("WM_DELETE_WINDOW", self.close_success_animation) - # Create main container - self.success_main_frame = tk.Frame(self.success_window, bg='#2b2b2b') - self.success_main_frame.pack(fill=tk.BOTH, expand=True) + # Create canvas for background image and all content + self.success_bg_canvas = tk.Canvas(self.success_window, highlightthickness=0) + self.success_bg_canvas.pack(fill=tk.BOTH, expand=True) + + # Load background image + self.success_window.update_idletasks() + width = self.success_window.winfo_width() or 1920 + height = self.success_window.winfo_height() or 1080 + self.success_bg_image_tk = self.load_background_image(width, height) + 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') + + # Bind resize event + self.success_window.bind('', self._on_success_resize) + + # Store mod list name for later use + self.success_mod_list_name = mod_list_name # Load and prepare logo for animation try: @@ -2563,15 +3190,26 @@ class SteamWorkshopGUI: target_width = int((target_height / success_logo_img.size[1]) * success_logo_img.size[0]) self.success_logo_img = success_logo_img - self.success_logo_label = tk.Label(self.success_main_frame, bg='#2b2b2b') - # Start animation from top to center + # Start animation from top to center using canvas image self.animate_success_logo(0, 30, start_width, start_height, target_width, target_height, mod_list_name) except Exception as e: # Fallback if image loading fails self.show_success_text_only(mod_list_name) + def _on_success_resize(self, event): + """Handle success window resize to update background image""" + if event.widget == self.success_window and hasattr(self, 'success_bg_canvas'): + width = event.width + height = event.height + if width > 1 and height > 1: + self.success_bg_image_tk = self.load_background_image(width, height) + if self.success_bg_image_tk: + self.success_bg_canvas.delete('bg') + self.success_bg_canvas.create_image(0, 0, image=self.success_bg_image_tk, anchor='nw', tags='bg') + self.success_bg_canvas.tag_lower('bg') + def animate_success_logo(self, step, total_steps, start_width, start_height, target_width, target_height, mod_list_name): """Animate the logo moving from top to center with size increase""" @@ -2591,11 +3229,11 @@ class SteamWorkshopGUI: target_width, target_height, mod_list_name)) return - # Calculate positions + # Calculate positions - move logo up by 3% start_x = window_width // 2 start_y = 60 # Top position (header position) target_x = window_width // 2 - target_y = window_height // 2 - 50 # Slightly above center + target_y = window_height // 2 - 150 - int(window_height * 0.03) # Higher up by 3% + room for text current_x = start_x + (target_x - start_x) * eased_progress current_y = start_y + (target_y - start_y) * eased_progress @@ -2604,17 +3242,16 @@ class SteamWorkshopGUI: current_width = start_width + (target_width - start_width) * eased_progress current_height = start_height + (target_height - start_height) * eased_progress - # Resize and display logo + # Resize and display logo using canvas image resized_img = self.success_logo_img.resize( (int(current_width), int(current_height)), Image.Resampling.LANCZOS ) - current_tk_img = ImageTk.PhotoImage(resized_img) - self.success_logo_label.configure(image=current_tk_img) - self.success_logo_label.image = current_tk_img # Keep reference + self.success_logo_tk = ImageTk.PhotoImage(resized_img) - # Position the label - self.success_logo_label.place(x=current_x, y=current_y, anchor='center') + # Delete old logo and create new one + self.success_bg_canvas.delete('logo') + self.success_bg_canvas.create_image(current_x, current_y, image=self.success_logo_tk, anchor='center', tags='logo') # Schedule next step self.success_window.after(50, lambda: self.animate_success_logo( @@ -2627,30 +3264,76 @@ class SteamWorkshopGUI: def show_success_text_only(self, mod_list_name): """Show success message without logo animation (fallback)""" - # Create centered text - success_title = tk.Label(self.success_main_frame, - text="success!", - fg='#00ff00', bg='#2b2b2b', - font=self.get_font(24, 'bold')) - success_title.pack(expand=True) + # Ensure window is updated and get proper dimensions + self.success_window.update_idletasks() + window_width = self.success_window.winfo_width() + window_height = self.success_window.winfo_height() - self.show_success_message(mod_list_name) + # Fallback to default dimensions if window size not available + if window_width <= 1 or window_height <= 1: + window_width = 1920 + window_height = 1080 + + # Create centered text using canvas - moved up by 3% + 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'), + tags='success_text' + ) + + self.show_success_message(mod_list_name, create_background=False) - def show_success_message(self, mod_list_name): - """Show the success message and instructions""" + def show_success_message(self, mod_list_name, create_background=True): + """Show the success message and instructions using canvas text""" + # Ensure window is updated and get proper dimensions + self.success_window.update_idletasks() + window_width = self.success_window.winfo_width() + window_height = self.success_window.winfo_height() + + # Fallback to default dimensions if window size not available + if window_width <= 1 or window_height <= 1: + window_width = 1920 + window_height = 1080 + + center_x = window_width // 2 + + # Create semi-transparent background panel for text content IMMEDIATELY + if create_background: + panel_width = int(window_width * 0.6) + panel_height = int(window_height * 0.45) + # Force window update before creating background + self.success_window.update() + self._create_success_text_background(center_x, int(window_height * 0.55), panel_width, panel_height) + # Force another update to ensure background is drawn + self.success_window.update() + + # Ensure logo stays on top of the background + self.success_bg_canvas.tag_raise('logo') + + # Starting Y position (below the logo) + y_pos = window_height // 2 - 50 + # Success message - success_text = tk.Label(self.success_main_frame, - text="mods list created successfully!", - fg='#00ff00', bg='#2b2b2b', - font=self.get_font(20, 'bold')) - success_text.pack(pady=20) + self.success_bg_canvas.create_text( + center_x, y_pos, + text="mods list created successfully!", + fill='#00ff00', + font=self.get_font(20, 'bold'), + tags='success_text' + ) + y_pos += 50 # Mod list name - name_text = tk.Label(self.success_main_frame, - text=f"it is named: {mod_list_name.lower()}", - fg='white', bg='#2b2b2b', - font=self.get_font(16)) - name_text.pack(pady=10) + self.success_bg_canvas.create_text( + center_x, y_pos, + text=f"it is named: {mod_list_name.lower()}", + fill='white', + font=self.get_font(16), + tags='success_text' + ) + y_pos += 50 # Instructions (different for homebrew vs vanilla) if mod_list_name == "ProgressionHomebrew": @@ -2671,37 +3354,89 @@ some mods may be incompatible - check for errors!""" you must auto sort after this!""" - instructions_label = tk.Label(self.success_main_frame, - text=instructions_text, - fg='#cccccc', bg='#2b2b2b', - font=self.get_font(14), - justify='center') - instructions_label.pack(pady=30) + self.success_bg_canvas.create_text( + center_x, y_pos + 80, + text=instructions_text, + fill='#cccccc', + font=self.get_font(14), + justify='center', + tags='success_text' + ) + y_pos += 200 # Auto sort warning (more prominent for homebrew) if mod_list_name == "ProgressionHomebrew": - warning_text = tk.Label(self.success_main_frame, - text="⚠️ critical: auto sort after loading! check for mod conflicts! ⚠️", - fg='#ff4444', bg='#2b2b2b', - font=self.get_font(16, 'bold')) + warning_color = '#ff4444' + warning_msg = "⚠️ critical: auto sort after loading! check for mod conflicts! ⚠️" else: - warning_text = tk.Label(self.success_main_frame, - text="⚠️ important: auto sort after loading! ⚠️", - fg='#ff8686', bg='#2b2b2b', - font=self.get_font(16, 'bold')) - warning_text.pack(pady=20) + warning_color = '#ff8686' + warning_msg = "⚠️ important: auto sort after loading! ⚠️" - # Close button - close_btn = tk.Button(self.success_main_frame, - text="got it!", - command=self.close_success_animation, - bg='#00aa00', fg='white', - font=self.get_font(14, 'bold'), - padx=40, pady=15) - close_btn.pack(pady=30) + self.success_bg_canvas.create_text( + center_x, y_pos, + text=warning_msg, + fill=warning_color, + font=self.get_font(16, 'bold'), + 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) + + # Ensure logo stays on top of all elements + self._bring_success_logo_to_front() # No auto-close - user must manually close + def _bring_success_logo_to_front(self): + """Ensure the success logo stays on top of all other elements""" + # Bring logo to the very front + 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) + 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 + 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 + def close_success_animation(self): """Close the success animation window and exit the application""" if hasattr(self, 'success_window'):