902 lines
35 KiB
Python
902 lines
35 KiB
Python
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)
|