import ctypes import glob import json import os import platform import shutil import socket import subprocess import sys import threading import tkinter as tk import webbrowser import zipfile from datetime import datetime from tkinter import filedialog, messagebox from typing import Any, Dict, List, Optional import customtkinter as ctk # --- IMPORTACIONES DEL MOTOR NEO (Hardware Info) --- import psutil import requests from packaging import version as pkg_version try: import wmi except ImportError: wmi = None try: import GPUtil except ImportError: GPUtil = None # ----------------------------------------------------------------------------- # CONSTANTES DE ESTILO & TEMAS # ----------------------------------------------------------------------------- THEME = { "bg": "#1B1818", "widget_bg": "#2A2727", "card_bg": "#212121", "text": "#FFFFFF", "text_sec": "#CAC9C9", "accent": "#FDEF2F", # Amarillo Dorado (Warlock) "accent_hover": "#D4C428", "title": "#FF3232", # Rojo Warlock "hover": "#D41C1C", # Rojo oscuro "border": "#E2340D", "success": "#00E676", "error": "#B00020", "warning": "#FFA000", "info_bg": "#1E3A8A", "scroll_bg": "#181818" } CONFIG_FILE = "warlock_config.json" # ----------------------------------------------------------------------------- # UTILIDADES DE UI # ----------------------------------------------------------------------------- def get_font(size: int, weight: str = "normal", family: str = "Segoe UI") -> tuple: """Genera una fuente base.""" return (family, size, weight) # ----------------------------------------------------------------------------- # GESTOR DE CONFIGURACIÓN # ----------------------------------------------------------------------------- class ConfigManager: DEFAULT_CONFIG = { "check_updates_on_startup": True, "keep_window_on_top": False, "ui_scaling": 1.0, "font_scale": 1.0, "window_opacity": 1.0, "app_theme": "Dark", "process_priority": "Normal", "auto_close_on_finish": False, "auto_clean_temp": False, "notifications_enabled": True, "last_gpu_index": "Auto" } @staticmethod def get_config_path(): if getattr(sys, 'frozen', False): base_dir = os.path.dirname(sys.executable) else: base_dir = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base_dir, CONFIG_FILE) @staticmethod def load_config() -> Dict[str, Any]: path = ConfigManager.get_config_path() config = ConfigManager.DEFAULT_CONFIG.copy() if os.path.exists(path): try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) for k, v in data.items(): if k in config: if isinstance(v, type(config[k])) or (isinstance(config[k], float) and isinstance(v, int)): config[k] = v except Exception as e: print(f"[Config] Error loading/validating config: {e}") return config @staticmethod def save_config(config_data: Dict[str, Any]): try: path = ConfigManager.get_config_path() with open(path, "w", encoding="utf-8") as f: json.dump(config_data, f, indent=4) except Exception as e: print(f"[Config] Error saving config: {e}") @staticmethod def reset_config(): ConfigManager.save_config(ConfigManager.DEFAULT_CONFIG) return ConfigManager.DEFAULT_CONFIG.copy() # ----------------------------------------------------------------------------- # ESCÁNER DE HARDWARE (INTEGRACIÓN MOTOR NEO) # ----------------------------------------------------------------------------- class HardwareScanner: @staticmethod def get_specs_detailed() -> Dict[str, Any]: """ Utiliza el motor de detección de NEO (WMI/PSUTIL/GPUTIL) adaptado para devolver el diccionario que espera Warlock. """ specs = { "os": f"{platform.system()} {platform.release()} ({platform.architecture()[0]})", "cpu_name": platform.processor(), "cpu_cores": os.cpu_count() or 4, "ram_total": 0.0, "ram_used": 0.0, "gpu_name": "Integrated / Unknown", "gpu_vram": 0, "disk_free": 0.0, } try: # 1. CPU y SO (Motor NEO: WMI Processor) if wmi: try: c = wmi.WMI() # SO Info detallada specs["os"] = f"{platform.system()} {platform.release()} {platform.machine()}" # CPU Info detallada proc_obj = c.Win32_Processor()[0] specs["cpu_name"] = proc_obj.Name.strip() specs["cpu_cores"] = proc_obj.NumberOfLogicalProcessors except Exception: pass # Fallback a los valores por defecto de platform # 2. RAM (Motor NEO: Psutil + WMI logic adaptation) mem = psutil.virtual_memory() specs["ram_total"] = round(mem.total / (1024**3), 2) specs["ram_used"] = round(mem.used / (1024**3), 2) # 3. Almacenamiento (Motor NEO logic) total_free_space = 0 for p in psutil.disk_partitions(): try: usage = psutil.disk_usage(p.mountpoint) total_free_space += usage.free except PermissionError: continue specs["disk_free"] = round(total_free_space / (1024**3), 2) # 4. GPU (Motor NEO: GPUtil Priority -> WMI Fallback) gpu_found = False # Intento A: GPUtil (NVIDIA) if GPUtil: try: gpus = GPUtil.getGPUs() if gpus: best_gpu = gpus[0] specs["gpu_name"] = f"[NVIDIA] {best_gpu.name}" specs["gpu_vram"] = round( best_gpu.memoryTotal / 1024, 2) # GPUtil returns MB gpu_found = True except Exception: pass # Intento B: WMI (AMD / Intel / Fallback) if not gpu_found and wmi: try: c = wmi.WMI() for gpu in c.Win32_VideoController(): # Lógica simple para elegir la GPU dedicada si hay varias name = gpu.Name vram_bytes = 0 try: # AdapterRAM suele devolver bytes, pero a veces es negativo en int32 overflow vram_bytes = int(gpu.AdapterRAM) if vram_bytes < 0: vram_bytes += 2**32 except: vram_bytes = 0 vram_gb = round(vram_bytes / (1024**3), 2) # Preferir GPU con más VRAM o que no sea "Intel" si ya tenemos una integrada current_best = specs["gpu_vram"] # Si encontramos una con más VRAM o es la primera dedicada que vemos if vram_gb > current_best or (vram_gb > 0 and "Intel" not in name and specs["gpu_name"] == "Integrated / Unknown"): specs["gpu_name"] = name specs["gpu_vram"] = vram_gb except Exception: pass except Exception as e: print(f"[HardwareScanner] Error using NEO Engine: {e}") return specs @staticmethod def get_smart_recommendations(specs: Dict) -> Dict: rec = {} vram = specs.get("gpu_vram", 0) ram = specs.get("ram_total", 0) # Cálculo de seguridad Warlock safe_vram = max(0.5, vram - 1.5) rec_tiles = int(safe_vram * 3.5) if rec_tiles < 2: rec_tiles = 2 rec["Recommended Tiles"] = f"{rec_tiles}" rec["Safe VRAM Limit"] = f"{safe_vram:.1f} GB" if vram >= 8 and ram >= 16: rec["AI Model Class"] = "High End (RealESRGANx4 / BSRGANx4)" rec["Multithreading"] = "2 - 4 Threads" elif vram >= 4: rec["AI Model Class"] = "Mid Range (RealESR_Animex4)" rec["Multithreading"] = "2 Threads" else: rec["AI Model Class"] = "Low End (RealESR_Gx4 / RIFE Lite)" rec["Multithreading"] = "OFF (1 Thread)" return rec # ----------------------------------------------------------------------------- # UPDATE MANAGER # ----------------------------------------------------------------------------- class UpdateManager: def __init__(self, owner, repo, version): self.api_url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" self.current_ver = version def check_update(self): try: r = requests.get(self.api_url, timeout=10) if r.status_code == 200: data = r.json() tag = data.get("tag_name", "v0.0").lstrip("v") if pkg_version.parse(tag) > pkg_version.parse(self.current_ver): return True, tag, data.get("assets", []), data.get("body", "No changelog provided.") return False, self.current_ver, None, None except Exception as e: return None, str(e), None, None def download(self, url, path, progress_callback): try: with requests.get(url, stream=True, timeout=30) as r: r.raise_for_status() total = int(r.headers.get('content-length', 0)) downloaded = 0 with open(path, 'wb') as f: for chunk in r.iter_content(8192): f.write(chunk) downloaded += len(chunk) if progress_callback and total > 0: progress_callback(downloaded / total) return path except Exception as e: raise e # ----------------------------------------------------------------------------- # COMPONENTES UI PERSONALIZADOS (Blender Style) # ----------------------------------------------------------------------------- class SettingCard(ctk.CTkFrame): def __init__(self, master, title, **kwargs): super().__init__(master, fg_color="transparent", **kwargs) self.pack(fill="x", pady=(5, 0), padx=5) h = ctk.CTkFrame(self, fg_color="transparent") h.pack(fill="x", pady=(5, 5)) ctk.CTkLabel(h, text="—", text_color=THEME["border"], font=( "Arial", 12, "bold")).pack(side="left", padx=(0, 5)) ctk.CTkLabel(h, text=title.upper(), text_color=THEME["text_sec"], font=get_font( 11, "bold")).pack(side="left") ctk.CTkFrame(self, height=1, fg_color=THEME["widget_bg"]).pack( fill="x", padx=0, pady=(0, 5)) class CollapsibleMenu(ctk.CTkFrame): def __init__(self, master, title, **kwargs): super().__init__(master, fg_color=THEME["card_bg"], border_color=THEME["border"], border_width=1, **kwargs) self.pack(fill="x", pady=8, padx=15) self.grid_columnconfigure(0, weight=1) self.header_frame = ctk.CTkFrame( self, fg_color=THEME["widget_bg"], height=40, corner_radius=0) self.header_frame.grid(row=0, column=0, sticky="ew") self.header_frame.grid_columnconfigure(0, weight=1) ctk.CTkLabel(self.header_frame, text=title.upper(), font=get_font( 13, "bold"), text_color=THEME["accent"], padx=15, anchor="w").grid(row=0, column=0, sticky="ew", pady=5) self.is_expanded = True self.toggle_button = ctk.CTkButton(self.header_frame, text="▼", width=30, height=30, fg_color=THEME["widget_bg"], hover_color=THEME["hover"], text_color=THEME["text"], command=self.toggle) self.toggle_button.grid(row=0, column=1, padx=10, pady=5) self.content_frame = ctk.CTkFrame( self, fg_color=THEME["card_bg"], corner_radius=0) self.content_frame.grid( row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) self.content_frame.grid_columnconfigure(0, weight=1) def toggle(self): if self.is_expanded: self.content_frame.grid_remove() self.toggle_button.configure(text="▶") else: self.content_frame.grid( row=1, column=0, sticky="ew", padx=15, pady=(0, 15)) self.toggle_button.configure(text="▼") self.is_expanded = not self.is_expanded class DownloadWindow(ctk.CTkToplevel): def __init__(self, master, filename): super().__init__(master) self.title("Update Downloader") self.geometry("400x150") self.attributes("-topmost", True) self.configure(fg_color=THEME["bg"]) try: x = master.winfo_x() + (master.winfo_width()//2) - 200 y = master.winfo_y() + (master.winfo_height()//2) - 75 self.geometry(f"+{x}+{y}") except: pass ctk.CTkLabel(self, text=f"Downloading {filename}...", text_color=THEME["accent"], font=get_font( 12)).pack(pady=(30, 10)) self.prog = ctk.CTkProgressBar( self, width=300, progress_color=THEME["success"]) self.prog.pack(pady=10) self.prog.set(0) self.lbl = ctk.CTkLabel(self, text="0%", text_color=THEME["text_sec"]) self.lbl.pack() def update_progress(self, val): self.prog.set(val) self.lbl.configure(text=f"{int(val*100)}%") self.update_idletasks() # ----------------------------------------------------------------------------- # VENTANA PRINCIPAL DE PREFERENCIAS # ----------------------------------------------------------------------------- class PreferencesWindow(ctk.CTkToplevel): def __init__(self, master, version, owner, repo): super().__init__(master) self.title("Warlock-Studio - Preferences") self.geometry("650x750") self.configure(fg_color=THEME["bg"]) self.attributes("-topmost", True) try: if getattr(sys, 'frozen', False): base_dir = os.path.dirname(sys.executable) else: base_dir = os.path.dirname(os.path.abspath(__file__)) icon_path = os.path.join(base_dir, "Assets", "logo.ico") self.iconbitmap(icon_path) except: pass self.curr_ver = version self.upd_mgr = UpdateManager(owner, repo, version) self.config = ConfigManager.load_config() self.create_ui() self.after(100, lambda: self.focus_force()) def create_ui(self): # HEADER head = ctk.CTkFrame(self, fg_color="transparent", height=80) head.pack(fill="x", padx=25, pady=15) t_frame = ctk.CTkFrame(head, fg_color="transparent") t_frame.pack(side="left") ctk.CTkLabel(t_frame, text="WARLOCK", font=("Impact", 36), text_color=THEME["title"]).pack(side="left") ctk.CTkLabel(t_frame, text="- STUDIO", font=("Impact", 36), text_color=THEME["text"]).pack(side="left") ver_badge = ctk.CTkLabel(head, text=f"v{self.curr_ver}", fg_color=THEME["widget_bg"], text_color=THEME["accent"], corner_radius=6, padx=12, font=get_font(12, "bold")) ver_badge.pack(side="right", padx=10) self.status_lbl = ctk.CTkLabel( head, text="", font=get_font(11, "bold")) self.status_lbl.pack(side="right", padx=10) # SCROLLABLE CONTENT self.scroll_frame = ctk.CTkScrollableFrame(self, fg_color=THEME["bg"], scrollbar_button_color=THEME["border"], label_text="SETTINGS MENU", label_text_color=THEME["text_sec"], label_font=get_font(10, "bold")) self.scroll_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20)) self.scroll_frame.grid_columnconfigure(0, weight=1) self._build_general_menu() self._build_system_menu() self._build_updates_menu() self._build_logs_menu() self._build_about_menu() self.after(500, self._run_scan) # --- MENUS --- def _build_general_menu(self): menu = CollapsibleMenu(self.scroll_frame, "General Settings") frame = menu.content_frame c1 = SettingCard(frame, "USER INTERFACE") self._add_dropdown(frame, "Theme Mode", "app_theme", ["Dark", "Light", "System"], lambda v: self._update("app_theme", v, lambda: ctk.set_appearance_mode(v))) self._add_slider(frame, "Window Opacity", "window_opacity", 0.5, 1.0, 50, lambda v: self._update("window_opacity", v, lambda: self.master.attributes("-alpha", v))) self._add_slider(frame, "UI Scaling", "ui_scaling", 0.8, 1.5, 7, lambda v: self._update("ui_scaling", v, lambda: ctk.set_widget_scaling(v))) c2 = SettingCard(frame, "BEHAVIOR") self._add_switch(frame, "Keep Window on Top", "keep_window_on_top", lambda: self.master.attributes("-topmost", self.config["keep_window_on_top"])) self._add_switch(frame, "Check Updates on Startup", "check_updates_on_startup", lambda: self._update("check_updates_on_startup", self.config["check_updates_on_startup"])) self._add_switch(frame, "Auto-Close App when Task Finishes", "auto_close_on_finish", lambda: self._update("auto_close_on_finish", self.config["auto_close_on_finish"])) c3 = SettingCard(frame, "MAINTENANCE") row = ctk.CTkFrame(frame, fg_color="transparent") row.pack(fill="x", padx=5, pady=10) ctk.CTkButton(row, text="Clean Temporary Files", fg_color="#4A0000", hover_color=THEME["error"], border_color=THEME["error"], border_width=1, command=self._clean_temp).pack(side="left", expand=True, fill="x", padx=(0, 5)) ctk.CTkButton(row, text="Reset All Settings", fg_color="transparent", border_color=THEME["warning"], border_width=1, text_color=THEME["warning"], hover_color="#332200", command=self._reset_settings).pack(side="right", expand=True, fill="x", padx=(5, 0)) def _build_system_menu(self): self.hw_menu = CollapsibleMenu( self.scroll_frame, "System & AI Configuration") frame = self.hw_menu.content_frame SettingCard(frame, "HARDWARE DIAGNOSTICS (NEO ENGINE)") self.hw_content = ctk.CTkFrame(frame, fg_color="transparent") self.hw_content.pack(fill="both", padx=10, pady=5) self.btn_scan = ctk.CTkButton(frame, text="Run Hardware Scan", fg_color=THEME["border"], hover_color=THEME["hover"], command=self._run_scan, height=35) self.btn_scan.pack(pady=15, padx=10) SettingCard(frame, "PROCESSING PRIORITY") self._add_dropdown(frame, "FFmpeg/AI Priority", "process_priority", ["Normal", "Above Normal", "High"], lambda v: self._update("process_priority", v)) def _run_scan(self): self.btn_scan.configure(state="disabled", text="Scanning System...") for widget in self.hw_content.winfo_children(): widget.destroy() ctk.CTkLabel(self.hw_content, text="Scanning with NEO Engine... Please wait.", text_color=THEME["text_sec"]).pack(pady=20) threading.Thread(target=self._thread_scan, daemon=True).start() def _thread_scan(self): specs = HardwareScanner.get_specs_detailed() recs = HardwareScanner.get_smart_recommendations(specs) if self.winfo_exists(): self.after(0, lambda: self._render_specs(specs, recs)) def _render_specs(self, specs, recs): if not self.winfo_exists(): return self.btn_scan.configure(state="normal", text="Refresh Hardware Info") for widget in self.hw_content.winfo_children(): widget.destroy() grid = ctk.CTkFrame(self.hw_content, fg_color="transparent") grid.pack(fill="x") grid.grid_columnconfigure(1, weight=1) items = [("CPU", specs['cpu_name']), ("Cores", f"{specs['cpu_cores']} Logical Cores"), ("RAM", f"{specs['ram_total']} GB (Used: {specs['ram_used']} GB)"), ("GPU", specs['gpu_name']), ("VRAM", f"{specs['gpu_vram']} GB"), ("OS", specs['os'])] for i, (label, val) in enumerate(items): ctk.CTkLabel(grid, text=label, font=get_font(11, "bold"), text_color=THEME["text_sec"], anchor="w").grid( row=i, column=0, sticky="w", pady=4, padx=5) ctk.CTkLabel(grid, text=str(val), font=get_font(11), text_color=THEME["accent"], anchor="w").grid( row=i, column=1, sticky="w", padx=20, pady=4) rec_frame = ctk.CTkFrame( self.hw_content, fg_color="#1A1A1A", corner_radius=6) rec_frame.pack(fill="x", padx=10, pady=20) ctk.CTkLabel(rec_frame, text="RECOMMENDED AI SETTINGS", text_color=THEME["success"], font=get_font(11, "bold")).pack(pady=(10, 10)) rec_grid = ctk.CTkFrame(rec_frame, fg_color="transparent") rec_grid.pack(pady=(0, 10), padx=10) r_items = [("Tiles Resolution", recs.get("Recommended Tiles", "N/A")), ("VRAM Limit", recs.get("Safe VRAM Limit", "N/A")), ("AI Threads", recs.get("Multithreading", "OFF"))] for i, (k, v) in enumerate(r_items): ctk.CTkLabel(rec_grid, text=k + ": ", text_color=THEME["text_sec"], anchor="e").grid( row=i, column=0, padx=(10, 5), pady=2, sticky="e") ctk.CTkLabel(rec_grid, text=v, text_color="#FFFFFF", font=get_font( 12, "bold"), anchor="w").grid(row=i, column=1, padx=(0, 10), pady=2, sticky="w") def _build_updates_menu(self): menu = CollapsibleMenu(self.scroll_frame, "Updates & Changelog") frame = menu.content_frame v_frame = ctk.CTkFrame(frame, fg_color="transparent") v_frame.pack(pady=15) ctk.CTkLabel(v_frame, text="CURRENT VERSION", font=get_font( 10), text_color=THEME["text_sec"]).pack() ctk.CTkLabel(v_frame, text=f"v{self.curr_ver}", font=( "Impact", 42), text_color=THEME["accent"]).pack() self.btn_upd = ctk.CTkButton(frame, text="Check for Updates", height=45, fg_color=THEME["border"], hover_color=THEME["hover"], font=get_font(14, "bold"), command=self._check_update) self.btn_upd.pack(fill="x", padx=60, pady=(0, 20)) ctk.CTkLabel(frame, text="CHANGELOG / STATUS:", font=get_font(11, "bold"), text_color=THEME["text_sec"], anchor="w").pack(fill="x", pady=(10, 5), padx=5) self.upd_log = ctk.CTkTextbox(frame, height=200, fg_color="#111111", text_color=THEME["text"], font=( "Consolas", 11), border_width=1, border_color=THEME["widget_bg"]) self.upd_log.pack(fill="x", expand=True, pady=5, padx=5) self.upd_log.insert( "0.0", "Click 'Check for Updates' to connect to GitHub repository...") def _check_update(self): self.btn_upd.configure(state="disabled", text="Checking GitHub...") self.upd_log.delete("0.0", "end") self.upd_log.insert("end", "Connecting to repository...\n") threading.Thread(target=self._thread_upd, daemon=True).start() def _thread_upd(self): res = self.upd_mgr.check_update() if self.winfo_exists(): self.after(0, lambda: self._res_upd(res)) def _res_upd(self, res): is_new, tag, assets, body = res self.btn_upd.configure(state="normal", text="Check Again") self.upd_log.delete("0.0", "end") if is_new is None: self.upd_log.insert("end", f"Connection Error: {tag}\n") elif is_new: self.upd_log.insert( "end", f"NEW UPDATE AVAILABLE: v{tag}\n\n--- CHANGELOG ---\n\n{body}\n") if messagebox.askyesno("Update Found", f"Version v{tag} available. Download?"): target = next( (a for a in assets if a['name'].endswith('.exe')), None) if target: self._start_download( target['browser_download_url'], target['name']) else: webbrowser.open( "https://github.com/Ivan-Ayub97/Warlock-Studio/releases") else: self.upd_log.insert( "end", f"You are using the latest version (v{tag}).") def _start_download(self, url, name): dw = DownloadWindow(self, name) def t(): try: tmp = os.path.join(os.getenv('TEMP'), name) self.upd_mgr.download(url, tmp, dw.update_progress) dw.destroy() if messagebox.askyesno("Install", "Download complete. Install now?"): os.startfile(tmp) sys.exit(0) except Exception as e: dw.destroy() messagebox.showerror("Download Error", str(e)) threading.Thread(target=t, daemon=True).start() def _build_logs_menu(self): menu = CollapsibleMenu(self.scroll_frame, "Log Management") frame = menu.content_frame ctrl = ctk.CTkFrame(frame, fg_color="transparent") ctrl.pack(fill="x", padx=5, pady=5) ctk.CTkButton(ctrl, text="Refresh Logs", width=120, fg_color=THEME["widget_bg"], command=self._load_logs).pack(side="left", padx=5) ctk.CTkButton(ctrl, text="Export All Logs (.zip)", width=160, fg_color=THEME["info_bg"], command=self._export_logs).pack(side="right", padx=5) self.log_text = ctk.CTkTextbox(frame, font=("Consolas", 11), fg_color="#111111", text_color="#DDDDDD", wrap="none", border_width=1, border_color=THEME["widget_bg"], height=200) self.log_text.pack(fill="x", expand=True, padx=5, pady=5) self._load_logs() def _load_logs(self): self.log_text.delete("0.0", "end") log_dir = os.path.join(os.path.expanduser('~'), 'Documents') patterns = [os.path.join(log_dir, "Warlock*Logs", "*.log"), os.path.join(log_dir, "Warlock*Logs", "*.txt")] found_files = [] for p in patterns: found_files.extend(glob.glob(p)) found_files.sort(key=os.path.getmtime, reverse=True) if not found_files: self.log_text.insert("end", "No log files found.") return latest = found_files[0] self.log_text.insert( "end", f"--- LOADING LOG: {os.path.basename(latest)} ---\n\n") try: with open(latest, "r", encoding="utf-8", errors='replace') as f: self.log_text.insert("end", f.read()) except Exception as e: self.log_text.insert("end", f"Error reading log: {e}") def _export_logs(self): try: save_path = filedialog.asksaveasfilename( defaultextension=".zip", filetypes=[("ZIP", "*.zip")]) if not save_path: return log_dir = os.path.join(os.path.expanduser('~'), 'Documents') folders = glob.glob(os.path.join(log_dir, "Warlock*Logs")) if not folders: return with zipfile.ZipFile(save_path, 'w') as zf: for folder in folders: for root, _, files in os.walk(folder): for file in files: zf.write(os.path.join(root, file), arcname=file) messagebox.showinfo("Export", "Logs exported successfully!") except Exception as e: messagebox.showerror("Error", str(e)) def _build_about_menu(self): menu = CollapsibleMenu(self.scroll_frame, "About Warlock-Studio") frame = menu.content_frame ctk.CTkLabel(frame, text="WARLOCK-STUDIO", font=("Impact", 40), text_color=THEME["title"]).pack(pady=(20, 5)) ctk.CTkLabel(frame, text=f"Version {self.curr_ver}", font=get_font( 12), text_color=THEME["accent"]).pack(pady=(0, 20)) SettingCard(frame, "DEVELOPER & PROJECT") ctk.CTkLabel(frame, text="Developed by Ivan-Ayub97", font=get_font(12, "bold")).pack(pady=5) link_frame = ctk.CTkFrame(frame, fg_color="transparent") link_frame.pack(pady=10) ctk.CTkButton(link_frame, text="GitHub Repository", fg_color="#24292e", width=140, command=lambda: webbrowser.open( "https://github.com/Ivan-Ayub97/Warlock-Studio")).pack(side="left", padx=10) ctk.CTkButton(link_frame, text="SourceForge", fg_color="#EE7600", width=140, command=lambda: webbrowser.open( "https://sourceforge.net/projects/warlock-studio/")).pack(side="left", padx=10) SettingCard(frame, "LICENSES") base_dir = getattr(sys, '_MEIPASS', os.path.dirname( os.path.abspath(__file__))) license_path = os.path.join(base_dir, "Assets", "license.txt") try: with open(license_path, "r", encoding="utf-8") as f: l_txt = f.read() except: l_txt = "License file not found." ld = ctk.CTkTextbox(frame, height=200, fg_color="#111111", text_color=THEME["text"], font=("Consolas", 10)) ld.pack(fill="x", expand=True, pady=10, padx=20) ld.insert("0.0", l_txt) ld.configure(state="disabled") # --- HELPERS --- def _update(self, key, val, callback=None): if isinstance(val, float): val = round(val, 2) self.config[key] = val ConfigManager.save_config(self.config) if callback: callback() self.status_lbl.configure(text="Saved ✓", text_color=THEME["success"]) self.after(2000, lambda: self.status_lbl.configure(text="")) def _add_slider(self, parent, text, key, min_v, max_v, steps, cmd): f = ctk.CTkFrame(parent, fg_color="transparent") f.pack(fill="x", padx=5, pady=5) ctk.CTkLabel(f, text=text, width=140, anchor="w", font=get_font(11)).pack(side="left") val_lbl = ctk.CTkLabel( f, text=f"{self.config.get(key, min_v):.2f}", width=40, text_color=THEME["accent"]) val_lbl.pack(side="right") def internal_cmd(v): val_lbl.configure(text=f"{v:.2f}") cmd(v) s = ctk.CTkSlider(f, from_=min_v, to=max_v, number_of_steps=steps, command=internal_cmd, progress_color=THEME["accent"], button_color=THEME["accent"], button_hover_color=THEME["accent_hover"]) s.set(self.config.get(key, min_v)) s.pack(side="right", fill="x", expand=True, padx=10) def _add_switch(self, parent, text, key, cmd): def internal_cmd(): self.config[key] = bool(sw.get()) ConfigManager.save_config(self.config) if cmd: cmd() self.status_lbl.configure( text="Saved ✓", text_color=THEME["success"]) self.after(1500, lambda: self.status_lbl.configure(text="")) sw = ctk.CTkSwitch(parent, text=text, command=internal_cmd, font=get_font(11), progress_color=THEME["accent"], button_color="#FFFFFF", button_hover_color="#EEEEEE") if self.config.get(key, False): sw.select() sw.pack(anchor="w", padx=10, pady=8) def _add_dropdown(self, parent, text, key, values, cmd): f = ctk.CTkFrame(parent, fg_color="transparent") f.pack(fill="x", padx=5, pady=5) ctk.CTkLabel(f, text=text, width=140, anchor="w", font=get_font(11)).pack(side="left") om = ctk.CTkOptionMenu(f, values=values, command=cmd, font=get_font(11), fg_color=THEME["widget_bg"], button_color=THEME["accent"], button_hover_color=THEME["accent_hover"], text_color=THEME["bg"]) om.set(self.config.get(key, values[0])) om.pack(side="right", fill="x", expand=True, padx=10) def _clean_temp(self): n = 0 cwd = os.getcwd() patterns = ["*.tmp", "*.checkpoint", "*.part", "temp_*", "*_frames.txt"] for p in patterns: for f in glob.glob(os.path.join(cwd, p)): try: os.remove(f) n += 1 except: pass messagebox.showinfo( "Clean", f"Cleanup complete.\nRemoved {n} temporary files.") def _reset_settings(self): if messagebox.askyesno("Reset", "Restore default settings?"): ConfigManager.reset_config() self.destroy() # ----------------------------------------------------------------------------- # BOTÓN PRINCIPAL (INTEGRACIÓN) # ----------------------------------------------------------------------------- class PreferencesButton(ctk.CTkButton): def __init__(self, master, current_version, repo_owner="Ivan-Ayub97", repo_name="Warlock-Studio", **kwargs): super().__init__(master, text="⚙", width=100, height=28, fg_color=THEME["widget_bg"], border_color=THEME["border"], border_width=1, hover_color=THEME["hover"], text_color=THEME["text"], command=self.open_window, **kwargs) self.ver = current_version self.repo = (repo_owner, repo_name) self._apply_startup_config() def open_window(self): for w in self.master.winfo_children(): if isinstance(w, PreferencesWindow): w.lift() w.focus_force() return PreferencesWindow(self.master, self.ver, *self.repo) def _apply_startup_config(self): c = ConfigManager.load_config() try: ctk.set_appearance_mode(c.get("app_theme", "Dark")) ctk.set_widget_scaling(c.get("ui_scaling", 1.0)) except: pass if c.get("window_opacity", 1.0) < 1.0: self.master.attributes("-alpha", c["window_opacity"]) if c.get("keep_window_on_top", False): self.master.attributes("-topmost", True)