Files
Warlock-Studio-Universal/warlock_preferences.py
T

902 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
# GUI Imports
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
# Hardware Info Imports
import psutil
import requests
from packaging import version as pkg_version
# Optional Imports Handling
try:
import wmi
except ImportError:
wmi = None
try:
import GPUtil
except ImportError:
GPUtil = None
# =============================================================================
# CONSTANTES Y TEMA VISUAL (WARLOCK IDENTITY)
# =============================================================================
CONFIG_FILE = "warlock_config.json"
THEME = {
"bg_main": "#121212", # Fondo principal muy oscuro
"bg_sidebar": "#1E1E1E", # Fondo barra lateral
"card_bg": "#252525", # Fondo de tarjetas
"card_hover": "#2F2F2F", # Hover tarjetas
"text_main": "#FFFFFF", # Texto blanco puro
"text_dim": "#A0A0A0", # Texto secundario
"accent": "#E63946", # Rojo Warlock Vibrante
"accent_hover": "#C92A35", # Rojo oscuro hover
"gold": "#FDEF2F", # Dorado (Highlights)
"success": "#00E676", # Verde éxito
"error": "#CF6679", # Rojo error
"border": "#333333", # Bordes sutiles
"input_bg": "#151515" # Fondo de inputs
}
FONTS = {
"header": ("Segoe UI", 24, "bold"),
"subheader": ("Segoe UI", 16, "bold"),
"label": ("Segoe UI", 12),
"label_bold": ("Segoe UI", 12, "bold"),
"small": ("Segoe UI", 10),
"mono": ("Consolas", 11)
}
# =============================================================================
# GESTOR DE CONFIGURACIÓN (CENTRALIZADO)
# =============================================================================
class ConfigManager:
# Configuración por defecto expandida
DEFAULT_CONFIG = {
# General
"app_theme": "Dark",
"ui_scaling": 1.0,
"window_opacity": 1.0,
"keep_window_on_top": False,
"check_updates_on_startup": True,
# Rendimiento
"process_priority": "Normal",
"onnx_provider_preference": "Auto", # Auto, CUDA, DirectML, CPU
"auto_close_on_finish": False,
"max_cpu_threads": 0, # 0 = Auto
# Integraciones (Rutas manuales)
"custom_ffmpeg_path": "",
"custom_exiftool_path": "",
# Logs
"extended_logging": False
}
@staticmethod
def get_base_path():
"""Obtiene la ruta base de instalación (funciona en .py y .exe)."""
if getattr(sys, 'frozen', False):
# Si es un ejecutable (PyInstaller), usa la carpeta del .exe
return os.path.dirname(sys.executable)
else:
# Si es script, usa la carpeta del script
return os.path.dirname(os.path.abspath(__file__))
@staticmethod
def get_assets_path():
"""Obtiene la ruta de assets compatible con PyInstaller --onefile"""
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(), 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)
# Merge seguro
for k, v in data.items():
# Permitimos nuevas claves si el usuario edita manualmente,
# pero priorizamos la estructura existente
config[k] = v
except Exception as e:
print(f"[Config] Error loading 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()
# =============================================================================
# HARDWARE SCANNER (NEO ENGINE OPTIMIZED)
# =============================================================================
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": "Unknown",
"vram": 0
}
try:
# RAM
mem = psutil.virtual_memory()
info["ram_total"] = round(mem.total / (1024**3), 2)
info["ram_used"] = round(mem.used / (1024**3), 2)
# CPU Name Cleanup
if wmi and platform.system() == "Windows":
try:
c = wmi.WMI()
cpu = c.Win32_Processor()[0]
info["cpu"] = cpu.Name.strip()
except:
pass
elif platform.system() == "Darwin":
info["cpu"] = subprocess.check_output(
["sysctl", "-n", "machdep.cpu.brand_string"]).strip().decode('utf-8')
# GPU (Prioridad: NVIDIA > AMD/Intel Dedicada > Integrada)
gpu_found = False
# 1. GPUtil (NVIDIA)
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
# 2. WMI (Fallback Windows)
if not gpu_found and wmi and platform.system() == "Windows":
try:
c = wmi.WMI()
best_vram = -1
for g in c.Win32_VideoController():
v_bytes = 0
try:
# Algunas implementaciones WMI devuelven enteros negativos para valores altos
v_bytes = int(g.AdapterRAM)
if v_bytes < 0:
v_bytes += 2**32
except:
continue
v_gb = round(v_bytes / (1024**3), 2)
# Buscamos la GPU con más VRAM (generalmente la dedicada)
if v_gb > best_vram:
best_vram = v_gb
info["gpu"] = g.Name
info["vram"] = v_gb
except:
pass
except Exception as e:
print(f"Hardware scan error: {e}")
info["os"] = f"Error scanning: {str(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")
# Comparación semántica de versiones
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 (WIDGETS)
# =============================================================================
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=8,
font=FONTS["label_bold"], command=command)
class SettingRow(ctk.CTkFrame):
def __init__(self, master, title, desc=None):
super().__init__(master, fg_color="transparent")
self.pack(fill="x", pady=10)
lbl_frame = ctk.CTkFrame(self, fg_color="transparent")
lbl_frame.pack(side="left", fill="both", expand=True)
ctk.CTkLabel(lbl_frame, text=title, font=FONTS["label_bold"],
text_color=THEME["text_main"], anchor="w").pack(fill="x")
if desc:
ctk.CTkLabel(lbl_frame, text=desc, font=FONTS["small"],
text_color=THEME["text_dim"], anchor="w").pack(fill="x")
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)
# Configuración Ventana
self.title("Preferences")
self.geometry("750x500")
self.minsize(850, 600)
self.configure(fg_color=THEME["bg_main"])
# Intentar centrar la ventana respecto al padre
try:
self.after(10, self.lift)
self.after(10, self.focus_force)
except:
pass
# Datos
self.config = ConfigManager.load_config()
self.version = version
self.owner = owner
self.repo = repo
self.cached_hw_info = None # Cache para no escanear constantemente
# Layout Principal (Grid 2 columnas: Sidebar | Content)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
self.setup_sidebar()
self.setup_content_area()
# Cargar pestaña inicial
self.show_page("general")
# Icono (Intentar cargar)
try:
assets = ConfigManager.get_assets_path()
icon_path = os.path.join(assets, "logo.ico")
if os.path.exists(icon_path):
self.iconbitmap(icon_path)
except:
pass
def setup_sidebar(self):
self.sidebar = ctk.CTkFrame(
self, width=240, corner_radius=0, fg_color=THEME["bg_sidebar"])
self.sidebar.grid(row=0, column=0, sticky="nsew")
self.sidebar.grid_propagate(False)
# Logo / Título
title_frame = ctk.CTkFrame(self.sidebar, fg_color="transparent")
title_frame.pack(pady=30, padx=20, fill="x")
ctk.CTkLabel(title_frame, text="Warlock-Studio", font=("Impact", 28),
text_color=THEME["accent"]).pack(anchor="w")
ctk.CTkLabel(title_frame, text="CONFIG", font=("Arial", 10, "bold"),
text_color=THEME["text_dim"]).pack(anchor="w", pady=(0, 10))
# Separador
ctk.CTkFrame(self.sidebar, height=2, fg_color=THEME["border"]).pack(
fill="x", padx=20, pady=(0, 20))
# Botones de Navegación
self.nav_btns = {}
pages = [
("general", "General", "⚙"),
("performance", "Performance & AI", "🚀"),
("integrations", "Paths & Tools", "🔗"),
("logs", "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=5)
self.nav_btns[pid] = btn
# Footer Sidebar
footer = ctk.CTkFrame(self.sidebar, fg_color="transparent")
footer.pack(side="bottom", pady=20, fill="x")
ctk.CTkLabel(footer, text=f"v{self.version}", font=FONTS["small"],
text_color=THEME["text_dim"]).pack()
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=20, pady=20)
def show_page(self, page_id):
# Actualizar estado botones sidebar
for pid, btn in self.nav_btns.items():
is_active = (pid == page_id)
btn.configure(fg_color=THEME["card_bg"] if is_active else "transparent",
text_color=THEME["accent"] if is_active else THEME["text_dim"])
# Limpiar contenido
for widget in self.content.winfo_children():
widget.destroy()
# Construir página
if page_id == "general":
self.build_general()
elif page_id == "performance":
self.build_performance()
elif page_id == "integrations":
self.build_integrations()
elif page_id == "logs":
self.build_logs()
elif page_id == "about":
self.build_about()
# =========================================================================
# CONSTRUCTORES DE PÁGINAS
# =========================================================================
def header(self, text):
ctk.CTkLabel(self.content, text=text, font=FONTS["header"],
text_color=THEME["text_main"]).pack(anchor="w", pady=(0, 20))
def build_general(self):
self.header("General Settings")
# UI Appearance
grp = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
grp.pack(fill="x", pady=10, padx=5)
# Theme
r1 = SettingRow(grp, "App Theme",
"Select the visual style of the application")
om = ctk.CTkOptionMenu(grp, 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=150)
om.set(self.config.get("app_theme", "Dark"))
r1.add_widget(om)
# UI Scaling
r_scale = SettingRow(
grp, "UI Scaling", "Adjust the size of the interface elements")
om_scale = ctk.CTkOptionMenu(grp, values=["80%", "90%", "100%", "110%", "125%"],
command=self.set_ui_scaling,
fg_color=THEME["input_bg"], button_color=THEME["accent"], width=150)
current_scale = f"{int(self.config.get('ui_scaling', 1.0) * 100)}%"
om_scale.set(current_scale)
r_scale.add_widget(om_scale)
# Opacity
r2 = SettingRow(grp, "Window Opacity",
"Adjust transparency (0.5 - 1.0)")
def update_opacity(val):
# Convertir a float porque el slider devuelve float pero a veces muchos decimales
v = float(val)
self.master.attributes("-alpha", v) # Aplica al padre
self.attributes("-alpha", v) # Aplica a esta ventana también
self.config["window_opacity"] = v
sl = ctk.CTkSlider(grp, from_=0.5, to=1.0, number_of_steps=50,
command=update_opacity,
progress_color=THEME["accent"], button_color=THEME["gold"])
sl.set(self.config.get("window_opacity", 1.0))
# Bind release para guardar configuración y no saturar disco
sl.bind("<ButtonRelease-1>",
lambda event: ConfigManager.save_config(self.config))
r2.add_widget(sl)
# Toggles
# Usamos winfo_toplevel() para asegurar que obtenemos la ventana raíz
root_win = self.master.winfo_toplevel()
r3 = SettingRow(grp, "Always on Top",
"Keep the main window above others")
sw1 = ctk.CTkSwitch(grp, text="", command=lambda: self.save_bool("keep_window_on_top", sw1, lambda: root_win.attributes("-topmost", sw1.get())),
progress_color=THEME["accent"], button_color="white")
if self.config.get("keep_window_on_top", False):
sw1.select()
r3.add_widget(sw1)
r4 = SettingRow(grp, "Check Updates on Startup",
"Automatically check Github for new versions")
sw2 = ctk.CTkSwitch(grp, text="", command=lambda: self.save_bool("check_updates_on_startup", sw2),
progress_color=THEME["accent"])
if self.config.get("check_updates_on_startup", True):
sw2.select()
r4.add_widget(sw2)
r5 = SettingRow(grp, "Auto-Close on Finish",
"Close app automatically when processing ends")
sw3 = ctk.CTkSwitch(grp, text="", command=lambda: self.save_bool("auto_close_on_finish", sw3),
progress_color=THEME["accent"])
if self.config.get("auto_close_on_finish", False):
sw3.select()
r5.add_widget(sw3)
def set_ui_scaling(self, value):
scale_map = {"80%": 0.8, "90%": 0.9,
"100%": 1.0, "110%": 1.1, "125%": 1.2}
new_scale = scale_map.get(value, 1.0)
ctk.set_widget_scaling(new_scale)
self.save("ui_scaling", new_scale)
def build_performance(self):
self.header("Performance & AI")
# Hardware Info Card
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))
# Loading Label
self.lbl_hw_loading = ctk.CTkLabel(
self.hw_frame, text="Scanning Hardware...", text_color=THEME["text_dim"])
self.lbl_hw_loading.pack(pady=20)
# Iniciar scan en hilo para no congelar
if self.cached_hw_info:
self._display_hw_info(self.cached_hw_info)
else:
threading.Thread(target=self._run_hw_scan, daemon=True).start()
# Settings
grp = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
grp.pack(fill="x")
# ONNX Provider
r1 = SettingRow(grp, "ONNX Provider",
"Force specific AI execution backend (Restart Required)")
om1 = ctk.CTkOptionMenu(grp, values=["Auto", "CUDA", "DirectML", "CPU"],
command=lambda v: self.save(
"onnx_provider_preference", v),
fg_color=THEME["input_bg"], button_color=THEME["accent"])
om1.set(self.config.get("onnx_provider_preference", "Auto"))
r1.add_widget(om1)
# Process Priority
r2 = SettingRow(grp, "Process Priority",
"System priority for FFmpeg and AI threads")
om2 = ctk.CTkOptionMenu(grp, values=["Normal", "Above Normal", "High"],
command=lambda v: self.save(
"process_priority", v),
fg_color=THEME["input_bg"], button_color=THEME["accent"])
om2.set(self.config.get("process_priority", "Normal"))
r2.add_widget(om2)
def _run_hw_scan(self):
self.cached_hw_info = HardwareScanner.get_system_info()
# Actualizar UI en main thread
self.after(0, lambda: self._display_hw_info(self.cached_hw_info))
def _display_hw_info(self, hw):
# Seguridad: evitar error cuando el frame ya no existe
if not hasattr(self, "hw_frame"):
return
if not str(self.hw_frame) or not self.hw_frame.winfo_exists():
return
for w in self.hw_frame.winfo_children():
w.destroy()
lbl_grid = ctk.CTkFrame(self.hw_frame, fg_color="transparent")
lbl_grid.pack(padx=20, pady=15, fill="x")
items = [
("OS:", hw['os']),
("CPU Model:", hw['cpu']),
("RAM Usage:", f"{hw['ram_used']} GB / {hw['ram_total']} GB"),
("GPU Model:", hw['gpu']),
("VRAM:", f"{hw['vram']} GB")
]
for i, (k, v) in enumerate(items):
ctk.CTkLabel(lbl_grid, text=k, font=FONTS["label_bold"], text_color=THEME["text_dim"]).grid(
row=i//2, column=(i % 2)*2, sticky="w", padx=(0, 10), pady=2)
ctk.CTkLabel(lbl_grid, text=v, font=FONTS["label"], text_color=THEME["text_main"]).grid(
row=i//2, column=(i % 2)*2+1, sticky="w", padx=(0, 40), pady=2)
def build_integrations(self):
self.header("Paths & Tools")
desc = ctk.CTkLabel(self.content, text="Manually configure paths for external tools if auto-detection fails. Leave empty to use built-in/default paths.",
text_color=THEME["text_dim"], font=FONTS["small"], wraplength=500, justify="left")
desc.pack(anchor="w", pady=(0, 20))
grp = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
grp.pack(fill="x")
# FFmpeg
self.create_path_selector(
grp, "FFmpeg Executable", "custom_ffmpeg_path", "ffmpeg.exe")
# ExifTool
self.create_path_selector(
grp, "ExifTool Executable", "custom_exiftool_path", "exiftool.exe")
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=FONTS["label_bold"]).pack(anchor="w")
row = ctk.CTkFrame(f, fg_color="transparent")
row.pack(fill="x", pady=(5, 0))
entry = ctk.CTkEntry(
row, fg_color=THEME["input_bg"], border_color=THEME["border"])
entry.pack(side="left", fill="x", expand=True, padx=(0, 10))
current_val = self.config.get(config_key, "")
entry.insert(0, current_val)
# Validación visual simple
if current_val and not os.path.exists(current_val):
entry.configure(text_color=THEME["error"])
def pick():
path = filedialog.askopenfilename(
filetypes=[("Executables", "*.exe"), ("All Files", "*.*")])
if path:
entry.delete(0, "end")
entry.insert(0, path)
entry.configure(text_color=THEME["text_main"])
self.save(config_key, path)
def clear():
entry.delete(0, "end")
self.save(config_key, "")
entry.configure(text_color=THEME["text_main"])
btn_pick = ctk.CTkButton(
row, text="Browse", width=80, fg_color=THEME["border"], command=pick)
btn_pick.pack(side="left", padx=5)
btn_clear = ctk.CTkButton(
row, text="✖", width=30, fg_color=THEME["bg_main"], hover_color=THEME["accent"], command=clear)
btn_clear.pack(side="left", padx=(5, 0))
def build_logs(self):
self.header("Logs & Maintenance")
grp = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
grp.pack(fill="x", pady=10)
# Actions Row
act_row = ctk.CTkFrame(grp, fg_color="transparent")
act_row.pack(fill="x", padx=10, pady=10)
ctk.CTkButton(act_row, text="Open Logs Folder", fg_color=THEME["border"],
command=self.open_logs_dir).pack(side="left", padx=5)
ctk.CTkButton(act_row, text="Clean Temp Files", fg_color=THEME["accent"],
command=self.clean_temp_files).pack(side="left", padx=5)
ctk.CTkButton(act_row, text="Export Debug Zip", fg_color="#444",
command=self.export_debug_info).pack(side="left", padx=5)
ctk.CTkButton(act_row, text="Reset Config", fg_color="transparent", border_color="#FF0000", border_width=1, text_color="#FF0000",
command=self.reset_all).pack(side="right", padx=5)
# Log Viewer
ctk.CTkLabel(grp, text="Latest Log Preview:",
font=FONTS["label_bold"]).pack(anchor="w", padx=10, pady=(10, 0))
log_box = ctk.CTkTextbox(
grp, height=300, fg_color=THEME["input_bg"], font=FONTS["mono"])
log_box.pack(fill="x", padx=10, pady=10)
# Load Log Content
base = ConfigManager.get_base_path()
log_folder_pattern = os.path.join(base, "*_Logs")
log_folders = glob.glob(log_folder_pattern)
if log_folders:
log_folder = log_folders[0]
log_path_pattern = os.path.join(log_folder, "*.log")
files = glob.glob(log_path_pattern)
if files:
latest = max(files, key=os.path.getmtime)
try:
with open(latest, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
log_box.insert("0.0", content)
log_box.see("end") # Auto scroll al final
except Exception as e:
log_box.insert("0.0", f"Error reading log file: {e}")
else:
log_box.insert(
"0.0", "Log folder found, but no .log files present.")
else:
log_box.insert("0.0", "No logs folder found.")
def build_about(self):
self.header("About")
card = ctk.CTkFrame(self.content, fg_color=THEME["card_bg"])
card.pack(fill="x", pady=10)
ctk.CTkLabel(card, text="Warlock-Studio", font=("Impact", 48),
text_color=THEME["accent"]).pack(pady=(30, 5))
ctk.CTkLabel(
card, text=f"Version {self.version}", font=FONTS["label_bold"]).pack()
ctk.CTkLabel(card, text="Developed by Ivan-Ayub97",
font=FONTS["small"], text_color=THEME["text_dim"]).pack(pady=(0, 20))
# Buttons
btn_row = ctk.CTkFrame(card, fg_color="transparent")
btn_row.pack(pady=20)
ctk.CTkButton(btn_row, text="GitHub Repository", fg_color="#333", width=140,
command=lambda: webbrowser.open(f"https://github.com/{self.owner}/{self.repo}")).pack(side="left", padx=10)
# License View
lbl_lic = ctk.CTkLabel(card, text="LICENSE AGREEMENT",
font=FONTS["label_bold"], text_color=THEME["text_dim"])
lbl_lic.pack(pady=(10, 5))
# Intento robusto de cargar licencia
l_txt = "License file not found."
try:
# Opción 1: Assets/license.txt
assets_path = ConfigManager.get_assets_path()
lic_path = os.path.join(assets_path, "license.txt")
if not os.path.exists(lic_path):
# Opción 2: Raíz/license.txt
lic_path = os.path.join(
ConfigManager.get_base_path(), "license.txt")
if os.path.exists(lic_path):
with open(lic_path, "r", encoding="utf-8") as f:
l_txt = f.read()
except:
pass
ld = ctk.CTkTextbox(card, height=150, fg_color="#111111",
text_color=THEME["text_dim"], font=("Consolas", 10))
ld.pack(fill="x", pady=10, padx=20)
ld.insert("0.0", l_txt)
ld.configure(state="disabled")
# Update Checker
upd_frame = ctk.CTkFrame(self.content, fg_color="transparent")
upd_frame.pack(fill="x", pady=20)
self.btn_upd = ctk.CTkButton(upd_frame, text="Check for Updates", height=50, font=FONTS["subheader"],
fg_color=THEME["border"], hover_color=THEME["success"], command=self.check_updates)
self.btn_upd.pack(fill="x")
# =========================================================================
# LOGICA DE ACCIONES
# =========================================================================
def save(self, key, value, callback=None):
self.config[key] = value
ConfigManager.save_config(self.config)
if callback:
callback()
def save_bool(self, key, switch_widget, callback=None):
val = bool(switch_widget.get())
self.save(key, val, callback)
def open_logs_dir(self):
base = ConfigManager.get_base_path()
log_dirs = glob.glob(os.path.join(base, "*_Logs"))
target = log_dirs[0] if log_dirs else base
try:
if platform.system() == "Windows":
os.startfile(target)
else:
webbrowser.open(target)
except Exception as e:
messagebox.showerror("Error", f"Could not open folder: {e}")
def clean_temp_files(self):
if not messagebox.askyesno("Confirm Cleanup", "Are you sure you want to delete all temporary and intermediate files? This action cannot be undone."):
return
base = ConfigManager.get_base_path()
patterns = ["*.tmp", "*.checkpoint", "*.part", "temp_*",
"*_frames.txt", "Pipeline_temp.json", "warlock_config.json.bak"]
count = 0
for p in patterns:
for f in glob.glob(os.path.join(base, p)):
try:
os.remove(f)
count += 1
except:
pass
messagebox.showinfo("Cleanup", f"Deleted {count} temporary files.")
def export_debug_info(self):
"""Genera un ZIP con logs y config para facilitar reporte de bugs."""
save_path = filedialog.asksaveasfilename(defaultextension=".zip",
initialfile=f"Warlock_Debug_{datetime.now().strftime('%Y%m%d')}.zip")
if not save_path:
return
try:
base = ConfigManager.get_base_path()
with zipfile.ZipFile(save_path, 'w', zipfile.ZIP_DEFLATED) as zf:
# Agregar Config
cfg_path = os.path.join(base, CONFIG_FILE)
if os.path.exists(cfg_path):
zf.write(cfg_path, CONFIG_FILE)
# Agregar Logs recientes
log_dirs = glob.glob(os.path.join(base, "*_Logs"))
if log_dirs:
for f in glob.glob(os.path.join(log_dirs[0], "*.log")):
zf.write(f, os.path.join("Logs", os.path.basename(f)))
messagebox.showinfo("Export Successful",
f"Debug info saved to:\n{save_path}")
except Exception as e:
messagebox.showerror("Export Failed", str(e))
def reset_all(self):
if messagebox.askyesno("Reset", "Are you sure you want to reset ALL preferences to default? The application will restart."):
ConfigManager.reset_config()
messagebox.showinfo(
"Reset", "Configuration reset. The application will now close.")
self.master.destroy()
sys.exit(0)
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, body = um.check()
def ui_cb():
self.btn_upd.configure(state="normal")
if is_new is True:
self.btn_upd.configure(
text=f"Update Available: {tag}", fg_color=THEME["success"], hover_color="#00C853")
if messagebox.askyesno("Update Found", f"Version {tag} is available.\n\nOpen download page?"):
webbrowser.open(url)
elif is_new is False:
self.btn_upd.configure(
text="You are up to date", fg_color=THEME["border"], hover_color=THEME["card_hover"])
else:
self.btn_upd.configure(
text=f"Error checking updates: {tag}", fg_color=THEME["accent"], hover_color=THEME["accent_hover"])
self.after(0, ui_cb)
# =============================================================================
# BOTÓN DE ACCESO (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=40, height=28,
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_data = (repo_owner, repo_name)
# Aplicar configuraciones iniciales
# Usamos after para asegurar que el widget esté cargado en la jerarquía
self.after(100, self._apply_startup)
def open_window(self):
# Evitar múltiples ventanas
for w in self.winfo_toplevel().winfo_children():
if isinstance(w, PreferencesWindow):
w.lift()
w.focus_force()
return
# Pasamos el toplevel actual como master
PreferencesWindow(self.winfo_toplevel(), self.ver, *self.repo_data)
def _apply_startup(self):
"""Aplica la configuración al iniciar la app de forma segura."""
c = ConfigManager.load_config()
# Obtener la ventana raíz real (no el frame donde está este botón)
root = self.winfo_toplevel()
# Tema
try:
ctk.set_appearance_mode(c.get("app_theme", "Dark"))
except:
pass
# Escala
try:
ctk.set_widget_scaling(c.get("ui_scaling", 1.0))
except:
pass
# Opacidad
alpha = c.get("window_opacity", 1.0)
if 0.1 <= alpha <= 1.0:
root.attributes("-alpha", alpha)
# Siempre Visible
top = c.get("keep_window_on_top", False)
root.attributes("-topmost", top)