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