import glob import json import os import platform import shutil import subprocess import sys import threading import time import webbrowser import zipfile from datetime import datetime from tkinter import filedialog, messagebox from typing import Any, Dict, List, Optional, Tuple import customtkinter as ctk import psutil import requests from packaging import version as pkg_version # Integración con Hardware (Opcional) try: import wmi except ImportError: wmi = None try: import GPUtil except ImportError: GPUtil = None # ----------------------------------------------------------------------------- # IMPORTAR TEMA Y PUENTE DE COMPATIBILIDAD (FIX FINAL) # ----------------------------------------------------------------------------- from warlock_theme import * # --- THEME BRIDGE --- # Mapea los nombres que usa Preferences a los nombres que existen en WarlockTheme _aliases = { "bg_sidebar": "widget_bg", "bg_main": "bg", "card_bg": "widget_bg", "card_hover": "btn_hover", "text_dim": "text_sec", "input_bg": "entry_bg", "gold": "accent", "border": "widget_border" # <--- AGREGADO: Esto soluciona tu error actual } # Aplicar los alias de forma segura for _new_key, _existing_key in _aliases.items(): if _new_key not in THEME: # Si la clave original tampoco existe, usamos un gris oscuro por seguridad THEME[_new_key] = THEME.get(_existing_key, "#333333") # Aseguramos compatibilidad inversa extra si el tema es muy antiguo if "info" not in THEME: THEME["info"] = "#0277BD" if "success" not in THEME: THEME["success"] = "#00C853" if "error" not in THEME: THEME["error"] = "#B71C1C" # ============================================================================= # UTILIDADES DE UI (TOOLTIPS) # ============================================================================= class ToolTip: """Muestra un mensaje flotante al pasar el mouse sobre un widget.""" def __init__(self, widget, text, delay=500): self.widget = widget self.text = text self.delay = delay self.id = None self.tw = None self.widget.bind("", self.enter) self.widget.bind("", self.leave) self.widget.bind("", self.leave) def enter(self, event=None): self.id = self.widget.after(self.delay, self.show) def leave(self, event=None): if self.id: self.widget.after_cancel(self.id) self.id = None self.hide() def show(self): if self.tw: return x, y, cx, cy = self.widget.bbox("insert") x += self.widget.winfo_rootx() + 25 y += self.widget.winfo_rooty() + 20 self.tw = ctk.CTkToplevel(self.widget) self.tw.wm_overrideredirect(True) self.tw.wm_geometry(f"+{x}+{y}") # Estilo del Tooltip label = ctk.CTkLabel( self.tw, text=self.text, justify='left', bg_color="#2B2B2B", fg_color="#2B2B2B", text_color="#FFFFFF", corner_radius=4, font=("Segoe UI", 10), padx=8, pady=4 ) label.pack() # Borde sutil frame = ctk.CTkFrame(self.tw, width=0, height=0, border_width=1, border_color="#555") frame.pack() def hide(self): if self.tw: self.tw.destroy() self.tw = None # ============================================================================= # GESTOR DE CONFIGURACIÓN (CENTRALIZADO & EXPANDIDO) # ============================================================================= class ConfigManager: # Configuración Maestra con todas las opciones nuevas DEFAULT_CONFIG = { # --- GENERAL --- "app_theme": "Dark", # Dark, Light, System "ui_scaling": 1.0, # 0.8 - 1.25 "window_opacity": 1.0, # 0.5 - 1.0 "keep_window_on_top": False, "check_updates_on_startup": True, "start_minimized": False, # Nuevo "notifications_enabled": True, # Nuevo "play_sounds": True, # Nuevo # --- APARIENCIA AVANZADA --- "corner_radius": 10, # Nuevo: Redondez de ventanas "font_size_offset": 0, # Nuevo: Ajuste global de tamaño letra # --- RENDIMIENTO --- "process_priority": "Normal", "onnx_provider_preference": "Auto", "auto_close_on_finish": False, "max_cpu_threads": 0, # 0 = Auto "gpu_memory_limit": 0, # 0 = No limit (Nuevo) # --- SALIDA Y ARCHIVOS (NUEVO) --- "default_output_dir": "", # Vacío = Carpeta del proyecto "filename_pattern": "Warlock_Output_{date}_{time}", # Patrón de nombre "auto_save_logs": True, "backup_count": 5, # --- RUTAS MANUALES --- "custom_ffmpeg_path": "", "custom_exiftool_path": "", # --- DEBUG --- "extended_logging": False } @staticmethod def get_base_path(): if getattr(sys, 'frozen', False): return os.path.dirname(sys.executable) else: return os.path.dirname(os.path.abspath(__file__)) @staticmethod def get_assets_path(): base_dir = getattr(sys, '_MEIPASS', os.path.dirname( os.path.abspath(__file__))) return os.path.join(base_dir, "Assets") @staticmethod def get_config_path(): return os.path.join(ConfigManager.get_base_path(), "warlock_config.json") @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(): config[k] = v except Exception as e: print(f"[Config] Warn: {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: {e}") @staticmethod def reset_config(): ConfigManager.save_config(ConfigManager.DEFAULT_CONFIG) return ConfigManager.DEFAULT_CONFIG.copy() # ============================================================================= # HARDWARE SCANNER # ============================================================================= class HardwareScanner: @staticmethod def get_system_info() -> Dict[str, Any]: info = { "os": f"{platform.system()} {platform.release()}", "cpu": platform.processor(), "ram_total": 0, "ram_used": 0, "gpu": "Integrated / Unknown", "vram": 0 } try: mem = psutil.virtual_memory() info["ram_total"] = round(mem.total / (1024**3), 2) info["ram_used"] = round(mem.used / (1024**3), 2) if wmi and platform.system() == "Windows": try: c = wmi.WMI() info["cpu"] = c.Win32_Processor()[0].Name.strip() except: pass # Detección GPU mejorada gpu_found = False if GPUtil: try: gpus = GPUtil.getGPUs() if gpus: info["gpu"] = gpus[0].name info["vram"] = round(gpus[0].memoryTotal / 1024, 2) gpu_found = True except: pass if not gpu_found and wmi and platform.system() == "Windows": try: c = wmi.WMI() best_vram = -1 for g in c.Win32_VideoController(): try: v_bytes = int(g.AdapterRAM) if g.AdapterRAM else 0 if v_bytes < 0: v_bytes += 2**32 v_gb = round(v_bytes / (1024**3), 2) if v_gb > best_vram: best_vram = v_gb info["gpu"] = g.Name info["vram"] = v_gb except: continue except: pass except Exception as e: info["os"] = f"Error: {e}" return info # ============================================================================= # UPDATE MANAGER # ============================================================================= class UpdateManager: def __init__(self, owner, repo, version): self.url = f"https://api.github.com/repos/{owner}/{repo}/releases/latest" self.current = version def check(self): try: r = requests.get(self.url, timeout=5) 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): return True, tag, data.get("html_url"), data.get("body") return False, self.current, None, None except Exception as e: return None, str(e), None, None # ============================================================================= # COMPONENTES UI # ============================================================================= class SidebarButton(ctk.CTkButton): def __init__(self, master, text, icon_char, command, is_selected=False): color = THEME["card_bg"] if is_selected else "transparent" text_col = THEME["accent"] if is_selected else THEME["text_dim"] super().__init__( master, text=f" {icon_char} {text}", anchor="w", fg_color=color, hover_color=THEME["card_hover"], text_color=text_col, height=45, corner_radius=6, font=("Segoe UI", 12, "bold"), command=command, border_width=0 ) class SettingRow(ctk.CTkFrame): def __init__(self, master, title, desc=None, tooltip_text=None): super().__init__(master, fg_color="transparent") self.pack(fill="x", pady=8) lbl_frame = ctk.CTkFrame(self, fg_color="transparent") lbl_frame.pack(side="left", fill="both", expand=True, padx=(5, 0)) t_lbl = ctk.CTkLabel(lbl_frame, text=title, font=("Segoe UI", 12, "bold"), text_color=THEME["text_main"], anchor="w") t_lbl.pack(fill="x") if tooltip_text: ToolTip(t_lbl, tooltip_text) if desc: d_lbl = ctk.CTkLabel(lbl_frame, text=desc, font=("Segoe UI", 10), text_color=THEME["text_dim"], anchor="w") d_lbl.pack(fill="x") if tooltip_text and not getattr(t_lbl, "tw", None): ToolTip(d_lbl, tooltip_text) self.control_area = ctk.CTkFrame(self, fg_color="transparent") self.control_area.pack(side="right") def add_widget(self, widget): widget.pack(in_=self.control_area) # ============================================================================= # VENTANA PRINCIPAL DE PREFERENCIAS # ============================================================================= class PreferencesWindow(ctk.CTkToplevel): def __init__(self, master, version, owner, repo): super().__init__(master) # Config Inicial self.title("Warlock Preferences") self.geometry("850x600") self.minsize(850, 600) self.configure(fg_color=THEME["bg_main"]) # Cargar datos self.config = ConfigManager.load_config() self.version = version self.owner = owner self.repo = repo self.cached_hw_info = None # Grid Principal self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(0, weight=1) self.setup_sidebar() self.setup_content_area() self.show_page("general") # Intentar poner al frente self.after(200, lambda: self.attributes('-topmost', True)) self.lift() self.focus_force() def setup_sidebar(self): self.sidebar = ctk.CTkFrame( self, width=250, corner_radius=0, fg_color=THEME["bg_sidebar"]) self.sidebar.grid(row=0, column=0, sticky="nsew") self.sidebar.grid_propagate(False) # Header Sidebar head = ctk.CTkFrame(self.sidebar, fg_color="transparent") head.pack(pady=30, padx=20, fill="x") ctk.CTkLabel(head, text="PREFERENCES", font=("Impact", 24), text_color=THEME["accent"]).pack(anchor="w") ctk.CTkLabel(head, text="Warlock Studio Control Center", font=( "Segoe UI", 10), text_color=THEME["text_dim"]).pack(anchor="w") # Botones Navegación self.nav_btns = {} pages = [ ("general", "General & UI", "⚙"), ("output", "Output & Files", "📂"), ("performance", "Performance / AI", "🚀"), ("integrations", "Tools & Paths", "🔗"), ("logs", "Maintenance", "🛠"), ("about", "About & Update", "ℹ") ] for pid, text, icon in pages: btn = SidebarButton(self.sidebar, text, icon, lambda p=pid: self.show_page(p)) btn.pack(fill="x", padx=15, pady=4) self.nav_btns[pid] = btn def setup_content_area(self): self.content = ctk.CTkScrollableFrame( self, fg_color="transparent", corner_radius=0) self.content.grid(row=0, column=1, sticky="nsew", padx=25, pady=25) # Scrollbar styling hack try: self.content._scrollbar.configure( width=12, fg_color=THEME["bg_main"]) except: pass def show_page(self, page_id): # Actualizar estilo botones for pid, btn in self.nav_btns.items(): btn.configure(fg_color=THEME["card_bg"] if pid == page_id else "transparent", text_color=THEME["accent"] if pid == page_id else THEME["text_dim"]) # Limpiar frame for widget in self.content.winfo_children(): widget.destroy() # Renderizar method = getattr(self, f"build_{page_id}", None) if method: # Animación simple de entrada (fade in simulado por orden de pack) title = page_id.replace("_", " ").title() if page_id == "output": title = "Output Management" self.header(title) method() def header(self, text): ctk.CTkLabel(self.content, text=text, font=("Segoe UI", 26, "bold"), text_color=THEME["text_main"]).pack(anchor="w", pady=(0, 25)) ctk.CTkFrame(self.content, height=2, fg_color=THEME["border"]).pack( fill="x", pady=(0, 20)) # ========================================================================= # BUILDERS (PÁGINAS) # ========================================================================= def build_general(self): # --- SECCIÓN VISUAL --- self.sub_header("Visual Appearance") grp_vis = self.create_group() # Theme r_theme = SettingRow(grp_vis, "Application Theme", "Light or Dark mode preference") om_theme = ctk.CTkOptionMenu(grp_vis, values=["Dark", "Light", "System"], command=lambda v: self.save( "app_theme", v, lambda: ctk.set_appearance_mode(v)), fg_color=THEME["input_bg"], button_color=THEME["accent"], width=140) om_theme.set(self.config.get("app_theme", "Dark")) r_theme.add_widget(om_theme) # Corner Radius r_rad = SettingRow(grp_vis, "Window Corner Radius", "Roundness of interface elements (Restart required)") sl_rad = ctk.CTkSlider(grp_vis, from_=0, to=20, number_of_steps=20, command=lambda v: self.save_debounced( "corner_radius", int(v)), progress_color=THEME["accent"], button_color=THEME["gold"]) sl_rad.set(self.config.get("corner_radius", 10)) r_rad.add_widget(sl_rad) # Opacidad r_op = SettingRow(grp_vis, "Window Opacity", "Transparency level of the main window") sl_op = ctk.CTkSlider(grp_vis, from_=0.5, to=1.0, number_of_steps=50, command=lambda v: self.update_opacity(v), progress_color=THEME["accent"], button_color=THEME["gold"]) sl_op.set(self.config.get("window_opacity", 1.0)) sl_op.bind("", lambda e: ConfigManager.save_config(self.config)) r_op.add_widget(sl_op) # Scaling r_sca = SettingRow(grp_vis, "UI Scaling", "Increase size for 4K monitors") om_sca = ctk.CTkOptionMenu(grp_vis, values=["80%", "90%", "100%", "110%", "125%", "150%"], command=self.set_ui_scaling, fg_color=THEME["input_bg"], button_color=THEME["accent"], width=140) om_sca.set(f"{int(self.config.get('ui_scaling', 1.0)*100)}%") r_sca.add_widget(om_sca) # --- SECCIÓN COMPORTAMIENTO --- self.sub_header("Behavior & System", pady=20) grp_beh = self.create_group() self.add_switch(grp_beh, "Always on Top", "keep_window_on_top", cmd=lambda v: self.master.winfo_toplevel().attributes("-topmost", v)) self.add_switch(grp_beh, "Notifications Enabled", "notifications_enabled", "Show toast messages on finish") self.add_switch(grp_beh, "Play Sounds", "play_sounds", "Audio feedback for actions") self.add_switch(grp_beh, "Check Updates on Startup", "check_updates_on_startup") def build_output(self): # --- GESTIÓN DE ARCHIVOS --- self.sub_header("File Management") grp = self.create_group() # Default Directory ctk.CTkLabel(grp, text="Default Output Directory", font=( "Segoe UI", 12, "bold")).pack(anchor="w", padx=10, pady=(10, 0)) dir_row = ctk.CTkFrame(grp, fg_color="transparent") dir_row.pack(fill="x", padx=10, pady=5) self.ent_dir = ctk.CTkEntry( dir_row, fg_color=THEME["input_bg"], border_color=THEME["border"]) self.ent_dir.pack(side="left", fill="x", expand=True, padx=(0, 10)) self.ent_dir.insert(0, self.config.get("default_output_dir", "")) self.ent_dir.configure(state="readonly") ctk.CTkButton(dir_row, text="Browse...", width=100, fg_color=THEME["accent"], text_color="#000", command=self.browse_output_dir).pack(side="right") ctk.CTkButton(dir_row, text="Reset", width=60, fg_color=THEME["bg_main"], command=lambda: self.save_dir("")).pack(side="right", padx=5) # Filename Pattern r_pat = SettingRow(grp, "Filename Pattern", "Use {date}, {time}, {original} as placeholders") ent_pat = ctk.CTkEntry( grp, fg_color=THEME["input_bg"], border_color=THEME["border"], width=250) ent_pat.insert(0, self.config.get( "filename_pattern", "Warlock_{date}_{time}")) ent_pat.bind("", lambda e: self.save( "filename_pattern", ent_pat.get())) r_pat.add_widget(ent_pat) # Options self.add_switch(grp, "Auto-Save Processing Logs", "auto_save_logs", "Save a .txt log with each export") def build_performance(self): # --- HARDWARE INFO --- self.hw_frame = ctk.CTkFrame( self.content, fg_color=THEME["card_bg"], border_color=THEME["accent"], border_width=1) self.hw_frame.pack(fill="x", pady=(0, 20)) if self.cached_hw_info: self._display_hw_info(self.cached_hw_info) else: ctk.CTkLabel(self.hw_frame, text="Scanning System Hardware...", text_color=THEME["accent"]).pack(pady=20) threading.Thread(target=self._run_hw_scan, daemon=True).start() # --- AI SETTINGS --- self.sub_header("Artificial Intelligence Engine") grp = self.create_group() # Provider r_prov = SettingRow(grp, "Inference Backend", "Hardware acceleration provider (Requires Restart)", tooltip_text="CUDA: NVIDIA GPUs (Fastest)\nDirectML: AMD/Intel/NVIDIA (Compatible)\nCPU: Slowest, universal compatibility.") om_prov = ctk.CTkOptionMenu(grp, values=["Auto", "CUDA", "DirectML", "CPU", "OpenVINO"], command=lambda v: self.save( "onnx_provider_preference", v), fg_color=THEME["input_bg"], button_color=THEME["accent"]) om_prov.set(self.config.get("onnx_provider_preference", "Auto")) r_prov.add_widget(om_prov) # Priority r_prio = SettingRow(grp, "Process Priority", "OS CPU scheduling priority") om_prio = ctk.CTkOptionMenu(grp, values=["Normal", "Above Normal", "High", "Realtime"], command=lambda v: self.save( "process_priority", v), fg_color=THEME["input_bg"], button_color=THEME["accent"]) om_prio.set(self.config.get("process_priority", "Normal")) r_prio.add_widget(om_prio) self.add_switch(grp, "Auto-Close After Task", "auto_close_on_finish") def build_integrations(self): ctk.CTkLabel(self.content, text="External Tools Configuration", font=( "Segoe UI", 12), text_color=THEME["text_dim"]).pack(anchor="w") grp = self.create_group() self.create_path_selector( grp, "FFmpeg Binary", "custom_ffmpeg_path", "ffmpeg.exe") self.create_path_selector( grp, "ExifTool Binary", "custom_exiftool_path", "exiftool.exe") def build_logs(self): # --- MAINTENANCE --- grp = self.create_group() # Tools btn_row = ctk.CTkFrame(grp, fg_color="transparent") btn_row.pack(fill="x", padx=10, pady=10) self.create_tool_btn(btn_row, "Open Logs Folder", self.open_logs_dir, THEME["border"]) self.create_tool_btn(btn_row, "Clean Temp Files", self.clean_temp_files, THEME["info"]) self.create_tool_btn(btn_row, "Export Debug Zip", self.export_debug_info, THEME["accent"], text_col="#000") # Reset Danger Zone ctk.CTkFrame(grp, height=1, fg_color="#444").pack( fill="x", pady=15, padx=10) dz_row = ctk.CTkFrame(grp, fg_color="transparent") dz_row.pack(fill="x", padx=10, pady=(0, 10)) ctk.CTkLabel(dz_row, text="Danger Zone:", text_color=THEME["error"], font=( "Segoe UI", 12, "bold")).pack(side="left") ctk.CTkButton(dz_row, text="Factory Reset", fg_color="transparent", border_color=THEME["error"], border_width=1, text_color=THEME["error"], hover_color=THEME["error"], command=self.reset_all).pack(side="right") # Log Viewer ctk.CTkLabel(self.content, text="Latest Log Preview", font=( "Segoe UI", 14, "bold"), pady=10).pack(anchor="w") log_box = ctk.CTkTextbox( self.content, height=250, fg_color="#111", text_color="#DDD", font=("Consolas", 10)) log_box.pack(fill="x") self.load_log_preview(log_box) def build_about(self): card = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"]) card.pack(fill="x", pady=10) # Hero ctk.CTkLabel(card, text="WARLOCK STUDIO", font=( "Impact", 42), text_color=THEME["accent"]).pack(pady=(30, 0)) ctk.CTkLabel(card, text="Professional Media Suite", font=( "Segoe UI", 12, "bold"), text_color=THEME["text_main"]).pack() ctk.CTkLabel(card, text=f"v{self.version} | Build 2024", font=( "Consolas", 10), text_color=THEME["text_dim"]).pack(pady=5) ctk.CTkButton(card, text="Visit GitHub", fg_color="#24292e", hover_color="#000", width=120, command=lambda: webbrowser.open(f"https://github.com/{self.owner}/{self.repo}")).pack(pady=20) # Update self.btn_upd = ctk.CTkButton(self.content, text="Check for Updates", height=45, fg_color=THEME["border"], hover_color=THEME["success"], command=self.check_updates) self.btn_upd.pack(fill="x", pady=10) # ========================================================================= # HELPERS & LOGIC # ========================================================================= def sub_header(self, text, pady=5): ctk.CTkLabel(self.content, text=text, font=("Segoe UI", 14, "bold"), text_color=THEME["accent"]).pack(anchor="w", pady=(pady, 5)) def create_group(self): f = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"]) f.pack(fill="x", pady=(0, 20)) return f def add_switch(self, parent, text, key, desc=None, cmd=None): r = SettingRow(parent, text, desc) sw = ctk.CTkSwitch( parent, text="", progress_color=THEME["accent"], onvalue=True, offvalue=False) def callback(): val = sw.get() self.config[key] = bool(val) ConfigManager.save_config(self.config) if cmd: cmd(val) sw.configure(command=callback) if self.config.get(key, False): sw.select() r.add_widget(sw) def create_tool_btn(self, parent, text, cmd, color, text_col="#FFF"): ctk.CTkButton(parent, text=text, command=cmd, fg_color=color, text_color=text_col, width=120).pack(side="left", padx=5) def update_opacity(self, val): v = float(val) self.master.winfo_toplevel().attributes("-alpha", v) self.attributes("-alpha", v) self.config["window_opacity"] = v def set_ui_scaling(self, value): scale_map = {"80%": 0.8, "90%": 0.9, "100%": 1.0, "110%": 1.1, "125%": 1.2, "150%": 1.5} new = scale_map.get(value, 1.0) ctk.set_widget_scaling(new) self.save("ui_scaling", new) def save(self, key, value, callback=None): self.config[key] = value ConfigManager.save_config(self.config) if callback: callback() def save_debounced(self, key, value): self.config[key] = value def save_dir(self, path): self.config["default_output_dir"] = path ConfigManager.save_config(self.config) self.ent_dir.configure(state="normal") self.ent_dir.delete(0, "end") self.ent_dir.insert(0, path) self.ent_dir.configure(state="readonly") def browse_output_dir(self): p = filedialog.askdirectory() if p: self.save_dir(p) def create_path_selector(self, parent, title, config_key, file_filter): f = ctk.CTkFrame(parent, fg_color="transparent") f.pack(fill="x", padx=10, pady=10) ctk.CTkLabel(f, text=title, font=( "Segoe UI", 12, "bold")).pack(anchor="w") row = ctk.CTkFrame(f, fg_color="transparent") row.pack(fill="x", pady=(5, 0)) ent = ctk.CTkEntry( row, fg_color=THEME["input_bg"], border_color=THEME["border"]) ent.pack(side="left", fill="x", expand=True, padx=(0, 5)) ent.insert(0, self.config.get(config_key, "")) def pick(): p = filedialog.askopenfilename( filetypes=[("Executables", "*.exe"), ("All", "*.*")]) if p: ent.delete(0, "end") ent.insert(0, p) self.save(config_key, p) ctk.CTkButton(row, text="Browse", width=70, fg_color=THEME["border"], command=pick).pack(side="left") # --- SYSTEM INFO & LOGS --- def _run_hw_scan(self): self.cached_hw_info = HardwareScanner.get_system_info() self.after(0, lambda: self._display_hw_info(self.cached_hw_info)) def _display_hw_info(self, hw): if not hasattr(self, "hw_frame") or not self.hw_frame.winfo_exists(): return for w in self.hw_frame.winfo_children(): w.destroy() grid = ctk.CTkFrame(self.hw_frame, fg_color="transparent") grid.pack(padx=15, pady=10, fill="x") data = [("OS", hw['os']), ("CPU", hw['cpu']), ("RAM", f"{hw['ram_used']} / {hw['ram_total']} GB"), ("GPU", hw['gpu']), ("VRAM", f"{hw['vram']} GB")] for i, (k, v) in enumerate(data): ctk.CTkLabel(grid, text=k+":", font=("Segoe UI", 11, "bold"), text_color=THEME["text_dim"]).grid(row=i//2, column=(i % 2)*2, sticky="w", padx=(0, 5)) ctk.CTkLabel(grid, text=v, font=("Segoe UI", 11), text_color=THEME["text_main"]).grid( row=i//2, column=(i % 2)*2+1, sticky="w", padx=(0, 25)) def load_log_preview(self, textbox): base = ConfigManager.get_base_path() logs = glob.glob(os.path.join(base, "*_Logs", "*.log")) if logs: latest = max(logs, key=os.path.getmtime) try: with open(latest, "r", encoding="utf-8", errors="ignore") as f: textbox.insert("0.0", f.read()) except: textbox.insert("0.0", "Error reading log.") else: textbox.insert("0.0", "No logs found.") # --- ACTIONS --- def open_logs_dir(self): base = ConfigManager.get_base_path() d = glob.glob(os.path.join(base, "*_Logs")) if d: os.startfile(d[0]) if platform.system( ) == "Windows" else webbrowser.open(d[0]) else: messagebox.showinfo("Info", "No log folder yet.") def clean_temp_files(self): if messagebox.askyesno("Clean", "Delete temporary files?"): c = 0 for p in ["*.tmp", "*.part", "temp_*"]: for f in glob.glob(os.path.join(ConfigManager.get_base_path(), p)): try: os.remove(f) c += 1 except: pass messagebox.showinfo("Done", f"Removed {c} files.") def export_debug_info(self): path = filedialog.asksaveasfilename( defaultextension=".zip", initialfile=f"Debug_{datetime.now().strftime('%Y%m%d')}.zip") if path: with zipfile.ZipFile(path, 'w') as z: cfg = ConfigManager.get_config_path() if os.path.exists(cfg): z.write(cfg, "config.json") messagebox.showinfo("Export", "Debug info saved.") def reset_all(self): if messagebox.askyesno("RESET", "Reset ALL settings to default? App will close."): ConfigManager.reset_config() self.master.destroy() sys.exit() def check_updates(self): self.btn_upd.configure(text="Checking...", state="disabled") threading.Thread(target=self._update_thread, daemon=True).start() def _update_thread(self): um = UpdateManager(self.owner, self.repo, self.version) is_new, tag, url, _ = um.check() def cb(): self.btn_upd.configure(state="normal") if is_new: self.btn_upd.configure( text=f"Update Available: {tag}", fg_color=THEME["success"]) if messagebox.askyesno("Update", "Open download page?"): webbrowser.open(url) else: self.btn_upd.configure( text="Up to date", fg_color=THEME["border"]) self.after(0, cb) # ============================================================================= # BOTÓN DE INTEGRACIÓN (MAIN UI) # ============================================================================= class PreferencesButton(ctk.CTkButton): def __init__(self, master, current_version, repo_owner="Ivan-Ayub97", repo_name="Warlock-Studio", **kwargs): super().__init__(master, text="⚙", width=40, height=30, fg_color=THEME["bg_sidebar"], border_color=THEME["border"], border_width=1, hover_color=THEME["accent"], text_color=THEME["text_main"], command=self.open_window, **kwargs) self.ver = current_version self.repo = (repo_owner, repo_name) self.after(500, self._apply_startup) def open_window(self): for w in self.winfo_toplevel().winfo_children(): if isinstance(w, PreferencesWindow): w.lift() w.focus_force() return PreferencesWindow(self.winfo_toplevel(), self.ver, *self.repo) def _apply_startup(self): c = ConfigManager.load_config() root = self.winfo_toplevel() # Tema y Escala try: ctk.set_appearance_mode(c.get("app_theme", "Dark")) except: pass try: ctk.set_widget_scaling(c.get("ui_scaling", 1.0)) except: pass # Atributos de Ventana # CORRECCIÓN: Aplicamos el valor directamente (sea True o False) # Esto fuerza a la ventana a bajar si la opción está desactivada, # limpiando el estado heredado del Splash Screen. should_be_topmost = c.get("keep_window_on_top", False) root.attributes("-topmost", should_be_topmost) # Opacidad if 0.5 <= c.get("window_opacity", 1.0) <= 1.0: root.attributes("-alpha", c["window_opacity"])