Files
Warlock-Studio-Universal/warlock_preferences.py
T
Iván Eduardo Chavez Ayub 08a897e38f Update 5.1.1
2025-12-31 23:47:43 -06:00

886 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("<Enter>", self.enter)
self.widget.bind("<Leave>", self.leave)
self.widget.bind("<ButtonPress>", 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("<ButtonRelease-1>",
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("<FocusOut>", 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"])