From 8cc4fd81af71baca1277b231ef2faacdea2451e3 Mon Sep 17 00:00:00 2001 From: Mahan Date: Tue, 1 Sep 2026 17:12:10 +0330 Subject: [PATCH] Uploaded Launcher --- launcher.py | 885 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 885 insertions(+) create mode 100644 launcher.py diff --git a/launcher.py b/launcher.py new file mode 100644 index 0000000..070ad94 --- /dev/null +++ b/launcher.py @@ -0,0 +1,885 @@ +""" +Steam-like Python Game Launcher +Install dependencies: + python -m pip install customtkinter pillow +Run: + python launcher.py +""" + +import customtkinter as ctk +import tkinter as tk +from tkinter import filedialog, messagebox + +import json +import os +import sys +import uuid +import subprocess + +from PIL import Image, ImageDraw, ImageFont + + +# ------------------------------------------------------------ +# DATA FILE +# ------------------------------------------------------------ +DATA_FILE = os.path.join(os.path.expanduser("~"), ".steam_like_launcher.json") + + +# ------------------------------------------------------------ +# STEAM-LIKE COLORS +# ------------------------------------------------------------ +BG = "#1b2838" +SIDEBAR = "#171a21" +TOPBAR = "#141920" +PANEL = "#2a475e" +HOVER = "#3b5068" +ENTRY_BG = "#232a35" + +TEXT = "#c7d5e0" +MUTED = "#8f98a0" +ACCENT = "#66c0f4" + +GREEN = "#5c7e10" +GREEN_HOVER = "#6f9a12" + +DANGER = "#a63d3d" +DANGER_HOVER = "#bf4c4c" + + +ctk.set_appearance_mode("dark") +ctk.set_default_color_theme("blue") + + +# ------------------------------------------------------------ +# IMAGE HELPERS +# ------------------------------------------------------------ +def get_font(size): + """Try to load a decent font for placeholder icons.""" + candidates = [ + "arialbd.ttf", + "arial.ttf", + "segoeui.ttf", + "DejaVuSans-Bold.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "Helvetica.ttc", + ] + + for candidate in candidates: + try: + return ImageFont.truetype(candidate, size) + except Exception: + pass + + try: + return ImageFont.load_default() + except Exception: + return None + + +def make_placeholder(text, size=(180, 180)): + """Create a placeholder icon image with initials.""" + img = Image.new("RGBA", size, (23, 26, 33, 255)) + draw = ImageDraw.Draw(img) + + initials = "".join(part[0] for part in text.split() if part)[:2].upper() or "G" + font = get_font(int(min(size) * 0.32)) + + try: + draw.text( + (size[0] / 2, size[1] / 2), + initials, + fill=(102, 192, 244, 255), + font=font, + anchor="mm", + ) + except Exception: + draw.text((10, 10), initials, fill=(102, 192, 244, 255), font=font) + + return img + + +def load_image(path, size, fallback_text="?"): + """Load icon and center it on a transparent square canvas.""" + try: + if not path or not os.path.isfile(path): + raise FileNotFoundError("No icon path provided.") + + img = Image.open(path).convert("RGBA") + + resample = Image.Resampling.LANCZOS if hasattr(Image, "Resampling") else Image.LANCZOS + img.thumbnail(size, resample) + + canvas = Image.new("RGBA", size, (0, 0, 0, 0)) + offset = ((size[0] - img.width) // 2, (size[1] - img.height) // 2) + canvas.paste(img, offset, img) + + return canvas + + except Exception: + return make_placeholder(fallback_text, size) + + +# ------------------------------------------------------------ +# ADD GAME DIALOG +# ------------------------------------------------------------ +class AddGameDialog(ctk.CTkToplevel): + def __init__(self, master, on_save): + super().__init__(master) + + self.on_save = on_save + + self.title("Add Game") + self.geometry("580x360") + self.resizable(False, False) + self.configure(fg_color=BG) + + self.transient(master) + self.after(20, self.center_window) + self.after(20, self.grab_set) + self.after(20, self.lift) + + pad = ctk.CTkFrame(self, fg_color="transparent") + pad.pack(fill="both", expand=True, padx=22, pady=20) + + ctk.CTkLabel( + pad, + text="Add a Game", + font=ctk.CTkFont(size=24, weight="bold"), + text_color=TEXT, + ).pack(anchor="w", pady=(0, 14)) + + self.name_entry = self._field(pad, "Game Name") + + self.exe_entry, _ = self._field_with_browse( + pad, + "Executable", + self.browse_exe, + ) + + self.icon_entry, _ = self._field_with_browse( + pad, + "Icon Image (.ico, .png, .jpg)", + self.browse_icon, + ) + + buttons = ctk.CTkFrame(pad, fg_color="transparent") + buttons.pack(fill="x", pady=(18, 0)) + + ctk.CTkButton( + buttons, + text="Cancel", + width=110, + height=38, + fg_color=PANEL, + hover_color=HOVER, + text_color=TEXT, + command=self.destroy, + ).pack(side="right") + + ctk.CTkButton( + buttons, + text="Add Game", + width=125, + height=38, + fg_color=GREEN, + hover_color=GREEN_HOVER, + text_color="white", + font=ctk.CTkFont(weight="bold"), + command=self.save, + ).pack(side="right", padx=(0, 10)) + + def center_window(self): + try: + self.update_idletasks() + + master = self.master + x = master.winfo_rootx() + (master.winfo_width() - self.winfo_width()) // 2 + y = master.winfo_rooty() + (master.winfo_height() - self.winfo_height()) // 2 + + self.geometry(f"+{max(x, 0)}+{max(y, 0)}") + except Exception: + pass + + def _field(self, parent, label): + block = ctk.CTkFrame(parent, fg_color="transparent") + block.pack(fill="x", pady=7) + + ctk.CTkLabel( + block, + text=label, + text_color=MUTED, + anchor="w", + ).pack(fill="x") + + entry = ctk.CTkEntry( + block, + height=36, + fg_color=ENTRY_BG, + border_color="#39434f", + text_color=TEXT, + ) + entry.pack(fill="x", pady=(4, 0)) + + return entry + + def _field_with_browse(self, parent, label, command): + block = ctk.CTkFrame(parent, fg_color="transparent") + block.pack(fill="x", pady=7) + + ctk.CTkLabel( + block, + text=label, + text_color=MUTED, + anchor="w", + ).pack(fill="x") + + row = ctk.CTkFrame(block, fg_color="transparent") + row.pack(fill="x", pady=(4, 0)) + + entry = ctk.CTkEntry( + row, + height=36, + fg_color=ENTRY_BG, + border_color="#39434f", + text_color=TEXT, + ) + entry.pack(side="left", fill="x", expand=True, padx=(0, 8)) + + button = ctk.CTkButton( + row, + text="Browse", + width=90, + height=36, + fg_color=PANEL, + hover_color=HOVER, + text_color=TEXT, + command=command, + ) + button.pack(side="right") + + return entry, button + + def browse_exe(self): + if sys.platform.startswith("win"): + filetypes = [ + ("Executable", "*.exe *.bat *.cmd *.lnk"), + ("All files", "*.*"), + ] + else: + filetypes = [ + ("All files", "*"), + ("Executable", "*"), + ] + + path = filedialog.askopenfilename( + title="Select Game Executable", + filetypes=filetypes, + ) + + if path: + self.exe_entry.delete(0, "end") + self.exe_entry.insert(0, path) + + def browse_icon(self): + path = filedialog.askopenfilename( + title="Select Icon Image", + filetypes=[ + ("Images", "*.ico *.png *.jpg *.jpeg *.bmp"), + ("All files", "*.*"), + ], + ) + + if path: + self.icon_entry.delete(0, "end") + self.icon_entry.insert(0, path) + + def save(self): + name = self.name_entry.get().strip() + exe = self.exe_entry.get().strip() + icon = self.icon_entry.get().strip() + + if not name: + messagebox.showwarning( + "Missing Name", + "Please enter a game name.", + parent=self, + ) + return + + if not exe: + messagebox.showwarning( + "Missing Executable", + "Please choose the game executable.", + parent=self, + ) + return + + self.on_save(name, exe, icon) + self.destroy() + + +# ------------------------------------------------------------ +# MAIN LAUNCHER WINDOW +# ------------------------------------------------------------ +class Launcher(ctk.CTk): + def __init__(self): + super().__init__() + + self.title("Game Library") + self.geometry("1180x700") + self.minsize(980, 600) + self.configure(fg_color=BG) + + self.games = self.load_games() + self.selected_id = None + self.detail_refs = [] + + # Root grid + self.grid_columnconfigure(0, weight=0) + self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) + + # ------------------------------------------------------------ + # SIDEBAR + # ------------------------------------------------------------ + sidebar = ctk.CTkFrame(self, corner_radius=0, fg_color=SIDEBAR, width=290) + sidebar.grid(row=0, column=0, sticky="nsew") + sidebar.grid_propagate(False) + + side_head = ctk.CTkFrame(sidebar, fg_color="transparent") + side_head.pack(fill="x", padx=16, pady=(18, 10)) + + ctk.CTkLabel( + side_head, + text="LIBRARY", + font=ctk.CTkFont(size=25, weight="bold"), + text_color=TEXT, + ).pack(anchor="w") + + ctk.CTkButton( + side_head, + text="+ Add Game", + height=38, + corner_radius=8, + fg_color=ACCENT, + hover_color="#4fa3d1", + text_color="#08121c", + font=ctk.CTkFont(size=14, weight="bold"), + command=self.open_add_dialog, + ).pack(fill="x", pady=(14, 0)) + + self.search = ctk.CTkEntry( + sidebar, + placeholder_text="Search", + height=36, + corner_radius=8, + fg_color=ENTRY_BG, + border_color="#39434f", + text_color=TEXT, + ) + self.search.pack(fill="x", padx=16, pady=(0, 10)) + self.search.bind("", lambda e: self.render_list()) + + self.list_frame = ctk.CTkScrollableFrame(sidebar, fg_color="transparent") + self.list_frame.pack(fill="both", expand=True, padx=10, pady=(0, 14)) + + self.list_content = ctk.CTkFrame(self.list_frame, fg_color="transparent") + self.list_content.pack(fill="both", expand=True) + + # ------------------------------------------------------------ + # MAIN AREA + # ------------------------------------------------------------ + main = ctk.CTkFrame(self, corner_radius=0, fg_color=BG) + main.grid(row=0, column=1, sticky="nsew") + main.grid_columnconfigure(0, weight=1) + main.grid_rowconfigure(1, weight=1) + + topbar = ctk.CTkFrame(main, corner_radius=0, fg_color=TOPBAR, height=68) + topbar.grid(row=0, column=0, sticky="ew") + topbar.grid_propagate(False) + + self.top_title = ctk.CTkLabel( + topbar, + text="Library", + font=ctk.CTkFont(size=20, weight="bold"), + text_color=TEXT, + ) + self.top_title.pack(side="left", padx=26) + + self.top_status = ctk.CTkLabel( + topbar, + text="", + text_color=MUTED, + ) + self.top_status.pack(side="right", padx=26) + + self.details = ctk.CTkScrollableFrame(main, fg_color="transparent") + self.details.grid(row=1, column=0, sticky="nsew", padx=26, pady=22) + + self.details_content = ctk.CTkFrame(self.details, fg_color="transparent") + self.details_content.pack(fill="both", expand=True) + + # Initial render + self.render_list() + + if self.games: + first = sorted(self.games, key=lambda g: g.get("name", "").lower())[0] + self.select_game(first["id"]) + else: + self.show_home() + + # ------------------------------------------------------------ + # DATA + # ------------------------------------------------------------ + def load_games(self): + try: + with open(DATA_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + + if isinstance(data, list): + cleaned = [] + + for item in data: + if not isinstance(item, dict): + continue + + if not item.get("name"): + continue + + if not item.get("id"): + item["id"] = str(uuid.uuid4()) + + cleaned.append(item) + + return cleaned + + except Exception: + pass + + return [] + + def save_games(self): + try: + with open(DATA_FILE, "w", encoding="utf-8") as f: + json.dump(self.games, f, indent=2) + except Exception as e: + messagebox.showerror( + "Save Error", + f"Could not save library:\n{e}", + parent=self, + ) + + def get_game(self, game_id): + for game in self.games: + if game.get("id") == game_id: + return game + return None + + # ------------------------------------------------------------ + # SIDEBAR LIST + # ------------------------------------------------------------ + def render_list(self): + for child in self.list_content.winfo_children(): + child.destroy() + + query = self.search.get().strip().lower() + games = sorted(self.games, key=lambda g: g.get("name", "").lower()) + + shown = 0 + + for game in games: + if query and query not in game.get("name", "").lower(): + continue + + selected = game.get("id") == self.selected_id + + button = ctk.CTkButton( + self.list_content, + text=game.get("name", "Unknown"), + anchor="w", + height=40, + corner_radius=8, + fg_color=PANEL if selected else "transparent", + hover_color=HOVER, + text_color="#ffffff" if selected else TEXT, + font=ctk.CTkFont(size=14), + command=lambda gid=game.get("id"): self.select_game(gid), + ) + + button.pack(fill="x", padx=4, pady=2) + button.bind( + "", + lambda e, g=game: self.show_context_menu(e, g), + ) + + shown += 1 + + if shown == 0: + ctk.CTkLabel( + self.list_content, + text="No games found", + text_color=MUTED, + ).pack(pady=18) + + self.top_status.configure(text=f"{len(self.games)} games") + + def show_context_menu(self, event, game): + menu = tk.Menu( + self, + tearoff=0, + bg="#202630", + fg=TEXT, + activebackground=HOVER, + activeforeground="white", + relief="flat", + ) + + menu.add_command( + label="Play", + command=lambda: self.launch_game(game), + ) + + menu.add_command( + label="Open Folder", + command=lambda: self.open_folder(game), + ) + + menu.add_separator() + + menu.add_command( + label="Delete", + foreground="#ff7b7b", + activeforeground="#ff7b7b", + command=lambda: self.delete_game(game), + ) + + try: + menu.tk_popup(event.x_root, event.y_root) + finally: + menu.grab_release() + + # ------------------------------------------------------------ + # DETAILS AREA + # ------------------------------------------------------------ + def clear_details(self): + for child in self.details_content.winfo_children(): + child.destroy() + + self.detail_refs.clear() + + def show_home(self): + self.selected_id = None + self.top_title.configure(text="Library") + self.clear_details() + + wrap = ctk.CTkFrame(self.details_content, fg_color="transparent") + wrap.pack(expand=True, pady=80) + + if self.games: + ctk.CTkLabel( + wrap, + text="Select a game from the library.", + font=ctk.CTkFont(size=24), + text_color=MUTED, + ).pack() + else: + ctk.CTkLabel( + wrap, + text="Your library is empty", + font=ctk.CTkFont(size=30, weight="bold"), + text_color=TEXT, + ).pack(pady=(0, 10)) + + ctk.CTkLabel( + wrap, + text="Click + Add Game to add your first game.", + text_color=MUTED, + ).pack(pady=(0, 18)) + + ctk.CTkButton( + wrap, + text="+ Add Game", + width=160, + height=42, + fg_color=ACCENT, + hover_color="#4fa3d1", + text_color="#08121c", + font=ctk.CTkFont(weight="bold"), + command=self.open_add_dialog, + ).pack() + + def show_game_details(self, game): + self.clear_details() + self.top_title.configure(text=game.get("name", "Game")) + + # Hero panel + hero = ctk.CTkFrame(self.details_content, corner_radius=14, fg_color=PANEL) + hero.pack(fill="x") + hero.grid_columnconfigure(1, weight=1) + + # Icon + image = load_image( + game.get("icon", ""), + (180, 180), + game.get("name", "G"), + ) + + ctk_image = ctk.CTkImage( + light_image=image, + dark_image=image, + size=(180, 180), + ) + + # Keep reference so image does not disappear + self.detail_refs.append(ctk_image) + + icon_label = ctk.CTkLabel(hero, image=ctk_image, text="") + icon_label.grid(row=0, column=0, rowspan=2, padx=30, pady=30, sticky="w") + + # Info + info = ctk.CTkFrame(hero, fg_color="transparent") + info.grid(row=0, column=1, sticky="nsew", padx=(8, 30), pady=(30, 8)) + + ctk.CTkLabel( + info, + text=game.get("name", "Unknown"), + anchor="w", + font=ctk.CTkFont(size=36, weight="bold"), + text_color="#f2f6fa", + ).pack(anchor="w") + + ctk.CTkLabel( + info, + text="Installed game", + anchor="w", + text_color=ACCENT, + font=ctk.CTkFont(size=13), + ).pack(anchor="w", pady=(2, 16)) + + exe = game.get("exe", "") + ctk.CTkLabel( + info, + text="Executable", + anchor="w", + text_color=MUTED, + ).pack(anchor="w") + + ctk.CTkLabel( + info, + text=exe if exe else "Not set", + anchor="w", + text_color=TEXT, + wraplength=560, + justify="left", + ).pack(anchor="w", pady=(0, 10)) + + icon = game.get("icon", "") + ctk.CTkLabel( + info, + text="Icon", + anchor="w", + text_color=MUTED, + ).pack(anchor="w") + + ctk.CTkLabel( + info, + text=icon if icon else "Default placeholder", + anchor="w", + text_color=TEXT, + wraplength=560, + justify="left", + ).pack(anchor="w") + + # Actions + actions = ctk.CTkFrame(hero, fg_color="transparent") + actions.grid(row=1, column=1, sticky="nsew", padx=(8, 30), pady=(8, 30)) + + ctk.CTkButton( + actions, + text="Play", + width=170, + height=52, + corner_radius=10, + fg_color=GREEN, + hover_color=GREEN_HOVER, + text_color="white", + font=ctk.CTkFont(size=20, weight="bold"), + command=lambda: self.launch_game(game), + ).pack(side="left") + + ctk.CTkButton( + actions, + text="Open Folder", + width=140, + height=52, + corner_radius=10, + fg_color="#3a4858", + hover_color=HOVER, + text_color=TEXT, + command=lambda: self.open_folder(game), + ).pack(side="left", padx=12) + + ctk.CTkButton( + actions, + text="Delete", + width=110, + height=52, + corner_radius=10, + fg_color=DANGER, + hover_color=DANGER_HOVER, + text_color="white", + command=lambda: self.delete_game(game), + ).pack(side="left") + + # Extra card to make it feel more like a library page + card = ctk.CTkFrame(self.details_content, corner_radius=14, fg_color="#20303f") + card.pack(fill="x", pady=(18, 0)) + + ctk.CTkLabel( + card, + text="Quick Notes", + anchor="w", + font=ctk.CTkFont(size=18, weight="bold"), + text_color=TEXT, + ).pack(anchor="w", padx=22, pady=(18, 6)) + + ctk.CTkLabel( + card, + text=( + "Right-click a game in the left list for quick actions.\n" + "Your library is saved locally in a JSON file." + ), + anchor="w", + text_color=MUTED, + justify="left", + ).pack(anchor="w", padx=22, pady=(0, 20)) + + # ------------------------------------------------------------ + # GAME ACTIONS + # ------------------------------------------------------------ + def select_game(self, game_id): + game = self.get_game(game_id) + if not game: + return + + self.selected_id = game_id + self.render_list() + self.show_game_details(game) + + def open_add_dialog(self): + AddGameDialog(self, self.add_game) + + def add_game(self, name, exe, icon): + game = { + "id": str(uuid.uuid4()), + "name": name, + "exe": exe, + "icon": icon, + } + + self.games.append(game) + self.save_games() + + self.selected_id = game["id"] + self.render_list() + self.show_game_details(game) + + def delete_game(self, game): + name = game.get("name", "this game") + + confirmed = messagebox.askyesno( + "Delete Game", + f"Remove '{name}' from the library?\n\n" + "This only removes the shortcut, not the game files.", + parent=self, + ) + + if not confirmed: + return + + self.games = [g for g in self.games if g.get("id") != game.get("id")] + self.save_games() + + if self.selected_id == game.get("id"): + self.selected_id = None + + self.render_list() + + if self.games: + first = sorted(self.games, key=lambda g: g.get("name", "").lower())[0] + self.select_game(first["id"]) + else: + self.show_home() + + def launch_game(self, game): + exe = os.path.normpath(game.get("exe", "")) + + if not exe or not os.path.isfile(exe): + messagebox.showerror( + "Executable not found", + f"Cannot find:\n{exe}\n\nUpdate or restore the game executable.", + parent=self, + ) + return + + cwd = os.path.dirname(exe) + + try: + if sys.platform.startswith("win"): + ext = os.path.splitext(exe)[1].lower() + + if ext == ".exe": + subprocess.Popen([exe], cwd=cwd) + else: + # Handles .lnk, .bat, .cmd, etc. + os.startfile(exe) # type: ignore[attr-defined] + + elif sys.platform == "darwin": + subprocess.Popen(["open", exe], cwd=cwd) + + else: + subprocess.Popen(["xdg-open", exe], cwd=cwd) + + except Exception as e: + messagebox.showerror( + "Launch Failed", + f"Could not start game:\n{e}", + parent=self, + ) + + def open_folder(self, game): + exe = os.path.normpath(game.get("exe", "")) + folder = os.path.dirname(exe) if exe else os.path.expanduser("~") + + if not os.path.isdir(folder): + messagebox.showerror( + "Folder not found", + f"Cannot find folder:\n{folder}", + parent=self, + ) + return + + try: + if sys.platform.startswith("win"): + os.startfile(folder) # type: ignore[attr-defined] + + elif sys.platform == "darwin": + subprocess.Popen(["open", folder]) + + else: + subprocess.Popen(["xdg-open", folder]) + + except Exception as e: + messagebox.showerror( + "Open Folder Failed", + f"Could not open folder:\n{e}", + parent=self, + ) + + +# ------------------------------------------------------------ +# RUN +# ------------------------------------------------------------ +if __name__ == "__main__": + app = Launcher() + app.mainloop() \ No newline at end of file